diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..fcd1e9e
Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ
diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc
new file mode 100644
index 0000000..e9ec818
Binary files /dev/null and b/app/__pycache__/main.cpython-312.pyc differ
diff --git a/app/core/__pycache__/__init__.cpython-312.pyc b/app/core/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..256a2ec
Binary files /dev/null and b/app/core/__pycache__/__init__.cpython-312.pyc differ
diff --git a/app/core/__pycache__/config.cpython-312.pyc b/app/core/__pycache__/config.cpython-312.pyc
new file mode 100644
index 0000000..bcb5242
Binary files /dev/null and b/app/core/__pycache__/config.cpython-312.pyc differ
diff --git a/app/db/__pycache__/__init__.cpython-312.pyc b/app/db/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..81f22c3
Binary files /dev/null and b/app/db/__pycache__/__init__.cpython-312.pyc differ
diff --git a/app/db/__pycache__/database.cpython-312.pyc b/app/db/__pycache__/database.cpython-312.pyc
new file mode 100644
index 0000000..d59f820
Binary files /dev/null and b/app/db/__pycache__/database.cpython-312.pyc differ
diff --git a/app/main.py b/app/main.py
index 5bd191b..88c29fa 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1,4 +1,4 @@
-from fastapi import FastAPI, Depends, HTTPException, status
+from fastapi import FastAPI, Depends, HTTPException, status, Form
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from typing import List
@@ -13,10 +13,28 @@
# Create tables
Base.metadata.create_all(bind=engine)
+import os
+from fastapi.responses import FileResponse
+from fastapi.staticfiles import StaticFiles
+
app = FastAPI(title="Agent Suite", version="0.1.0")
security = HTTPBearer()
settings = get_settings()
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+STATIC_DIR = os.path.join(BASE_DIR, "static")
+
+os.makedirs(STATIC_DIR, exist_ok=True)
+app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
+
+@app.get("/inbox")
+def get_inbox_page():
+ return FileResponse(os.path.join(STATIC_DIR, "inbox.html"))
+
+@app.get("/compose")
+def get_compose_page():
+ return FileResponse(os.path.join(STATIC_DIR, "compose.html"))
+
def get_inbox_by_api_key(api_key: str, db: Session):
return db.query(models.Inbox).filter(
@@ -143,12 +161,12 @@ def list_messages(
@app.post("/v1/webhooks/mailgun")
def mailgun_webhook(
- sender: str,
- recipient: str,
- subject: str = "",
- body_plain: str = "",
- body_html: str = "",
- message_id: str = "",
+ sender: str = Form(...),
+ recipient: str = Form(...),
+ subject: str = Form(""),
+ body_plain: str = Form(""),
+ body_html: str = Form(""),
+ message_id: str = Form(""),
db: Session = Depends(get_db)
):
"""Receive incoming email from Mailgun."""
diff --git a/app/models/__pycache__/__init__.cpython-312.pyc b/app/models/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..3d4a31e
Binary files /dev/null and b/app/models/__pycache__/__init__.cpython-312.pyc differ
diff --git a/app/models/__pycache__/models.cpython-312.pyc b/app/models/__pycache__/models.cpython-312.pyc
new file mode 100644
index 0000000..a1986c8
Binary files /dev/null and b/app/models/__pycache__/models.cpython-312.pyc differ
diff --git a/app/models/models.py b/app/models/models.py
index 7a21b6f..14b02b6 100644
--- a/app/models/models.py
+++ b/app/models/models.py
@@ -1,7 +1,6 @@
import uuid
from datetime import datetime
-from sqlalchemy import Column, String, DateTime, Text, ForeignKey, Boolean
-from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy import Column, String, DateTime, Text, ForeignKey, Boolean, Uuid
from app.db.database import Base
@@ -12,7 +11,7 @@ def generate_api_key():
class Inbox(Base):
__tablename__ = "inboxes"
- id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
+ id = Column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
email_address = Column(String(255), unique=True, index=True, nullable=False)
api_key = Column(String(255), unique=True, index=True, default=generate_api_key)
created_at = Column(DateTime, default=datetime.utcnow)
@@ -22,8 +21,8 @@ class Inbox(Base):
class Message(Base):
__tablename__ = "messages"
- id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
- inbox_id = Column(UUID(as_uuid=True), ForeignKey("inboxes.id"), index=True)
+ id = Column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
+ inbox_id = Column(Uuid(as_uuid=True), ForeignKey("inboxes.id"), index=True)
sender = Column(String(255), nullable=False)
recipient = Column(String(255), nullable=False)
subject = Column(String(500))
diff --git a/app/schemas/__pycache__/__init__.cpython-312.pyc b/app/schemas/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..d86b5ae
Binary files /dev/null and b/app/schemas/__pycache__/__init__.cpython-312.pyc differ
diff --git a/app/schemas/__pycache__/schemas.cpython-312.pyc b/app/schemas/__pycache__/schemas.cpython-312.pyc
new file mode 100644
index 0000000..1e2b049
Binary files /dev/null and b/app/schemas/__pycache__/schemas.cpython-312.pyc differ
diff --git a/requirements.txt b/requirements.txt
index 7633720..ab3f7f3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,4 @@ boto3==1.34.0
python-multipart==0.0.6
pytest==7.4.4
httpx==0.26.0
+email-validator==2.2.0
diff --git a/static/compose.html b/static/compose.html
new file mode 100644
index 0000000..d6237ab
--- /dev/null
+++ b/static/compose.html
@@ -0,0 +1,122 @@
+
+
+
+
+
+ Compose - Agent Suite
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/inbox.html b/static/inbox.html
new file mode 100644
index 0000000..5de38c8
--- /dev/null
+++ b/static/inbox.html
@@ -0,0 +1,119 @@
+
+
+
+
+
+ Inbox - Agent Suite
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test.db b/test.db
new file mode 100644
index 0000000..ce4e5c6
Binary files /dev/null and b/test.db differ
diff --git a/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..6c8ef7e
Binary files /dev/null and b/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/__pycache__/test_api.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_api.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..528366a
Binary files /dev/null and b/tests/__pycache__/test_api.cpython-312-pytest-7.4.4.pyc differ
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..60e957c
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,4 @@
+import os
+
+# Force tests onto sqlite before app modules import settings/engine.
+os.environ.setdefault("DATABASE_URL", "sqlite:///./test.db")
diff --git a/tests/test_api.py b/tests/test_api.py
index 08f4976..91245b7 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -97,3 +97,16 @@ def test_list_messages(setup_db):
data = response.json()
assert data["total"] == 1
assert data["messages"][0]["subject"] == "Test Subject"
+
+
+def test_inbox_page_served():
+ response = client.get("/inbox")
+ assert response.status_code == 200
+ assert "Agent Suite - Inbox" in response.text
+
+
+
+def test_compose_page_served():
+ response = client.get("/compose")
+ assert response.status_code == 200
+ assert "Agent Suite - Compose" in response.text
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/__pycache__/jp.cpython-312.pyc b/venv/bin/__pycache__/jp.cpython-312.pyc
new file mode 100644
index 0000000..034e4e3
Binary files /dev/null and b/venv/bin/__pycache__/jp.cpython-312.pyc differ
diff --git a/venv/bin/activate b/venv/bin/activate
new file mode 100644
index 0000000..246d850
--- /dev/null
+++ b/venv/bin/activate
@@ -0,0 +1,70 @@
+# 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
+
+ # Call hash to forget past commands. Without forgetting
+ # past commands the $PATH changes we made may not be respected
+ hash -r 2> /dev/null
+
+ 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
+
+# on Windows, a path can contain colons and backslashes and has to be converted:
+if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
+ # transform D:\path\to\venv to /d/path/to/venv on MSYS
+ # and to /cygdrive/d/path/to/venv on Cygwin
+ export VIRTUAL_ENV=$(cygpath /home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv)
+else
+ # use the path as-is
+ export VIRTUAL_ENV=/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv
+fi
+
+_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
+
+# Call hash to forget past commands. Without forgetting
+# past commands the $PATH changes we made may not be respected
+hash -r 2> /dev/null
diff --git a/venv/bin/activate.csh b/venv/bin/activate.csh
new file mode 100644
index 0000000..bfe9cdb
--- /dev/null
+++ b/venv/bin/activate.csh
@@ -0,0 +1,27 @@
+# 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/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/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..0708c90
--- /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/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/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/alembic b/venv/bin/alembic
new file mode 100755
index 0000000..db976a9
--- /dev/null
+++ b/venv/bin/alembic
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from alembic.config 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/dotenv b/venv/bin/dotenv
new file mode 100755
index 0000000..c6923f3
--- /dev/null
+++ b/venv/bin/dotenv
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/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/email_validator b/venv/bin/email_validator
new file mode 100755
index 0000000..5fe7e44
--- /dev/null
+++ b/venv/bin/email_validator
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from email_validator.__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/httpx b/venv/bin/httpx
new file mode 100755
index 0000000..0ac68d1
--- /dev/null
+++ b/venv/bin/httpx
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from httpx 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/jp.py b/venv/bin/jp.py
new file mode 100755
index 0000000..9d05845
--- /dev/null
+++ b/venv/bin/jp.py
@@ -0,0 +1,54 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+
+import sys
+import json
+import argparse
+from pprint import pformat
+
+import jmespath
+from jmespath import exceptions
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('expression')
+ parser.add_argument('-f', '--filename',
+ help=('The filename containing the input data. '
+ 'If a filename is not given then data is '
+ 'read from stdin.'))
+ parser.add_argument('--ast', action='store_true',
+ help=('Pretty print the AST, do not search the data.'))
+ args = parser.parse_args()
+ expression = args.expression
+ if args.ast:
+ # Only print the AST
+ expression = jmespath.compile(args.expression)
+ sys.stdout.write(pformat(expression.parsed))
+ sys.stdout.write('\n')
+ return 0
+ if args.filename:
+ with open(args.filename, 'r') as f:
+ data = json.load(f)
+ else:
+ data = sys.stdin.read()
+ data = json.loads(data)
+ try:
+ sys.stdout.write(json.dumps(
+ jmespath.search(expression, data), indent=4, ensure_ascii=False))
+ sys.stdout.write('\n')
+ except exceptions.ArityError as e:
+ sys.stderr.write("invalid-arity: %s\n" % e)
+ return 1
+ except exceptions.JMESPathTypeError as e:
+ sys.stderr.write("invalid-type: %s\n" % e)
+ return 1
+ except exceptions.UnknownFunctionError as e:
+ sys.stderr.write("unknown-function: %s\n" % e)
+ return 1
+ except exceptions.ParseError as e:
+ sys.stderr.write("syntax-error: %s\n" % e)
+ return 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/venv/bin/mako-render b/venv/bin/mako-render
new file mode 100755
index 0000000..d6f95db
--- /dev/null
+++ b/venv/bin/mako-render
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from mako.cmd import cmdline
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(cmdline())
diff --git a/venv/bin/pip b/venv/bin/pip
new file mode 100755
index 0000000..79b56d2
--- /dev/null
+++ b/venv/bin/pip
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/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..79b56d2
--- /dev/null
+++ b/venv/bin/pip3
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/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.12 b/venv/bin/pip3.12
new file mode 100755
index 0000000..79b56d2
--- /dev/null
+++ b/venv/bin/pip3.12
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/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/py.test b/venv/bin/py.test
new file mode 100755
index 0000000..dc119ab
--- /dev/null
+++ b/venv/bin/py.test
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pytest import console_main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(console_main())
diff --git a/venv/bin/pytest b/venv/bin/pytest
new file mode 100755
index 0000000..dc119ab
--- /dev/null
+++ b/venv/bin/pytest
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pytest import console_main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(console_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.12 b/venv/bin/python3.12
new file mode 120000
index 0000000..b8a0adb
--- /dev/null
+++ b/venv/bin/python3.12
@@ -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..8fd81d5
--- /dev/null
+++ b/venv/bin/uvicorn
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/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/bin/watchfiles b/venv/bin/watchfiles
new file mode 100755
index 0000000..f638bf7
--- /dev/null
+++ b/venv/bin/watchfiles
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from watchfiles.cli 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/websockets b/venv/bin/websockets
new file mode 100755
index 0000000..1738240
--- /dev/null
+++ b/venv/bin/websockets
@@ -0,0 +1,8 @@
+#!/home/wayman_al/.openclaw/workspace/operation_mint/agentwork-infrastructure/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from websockets.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/include/site/python3.12/greenlet/greenlet.h b/venv/include/site/python3.12/greenlet/greenlet.h
new file mode 100644
index 0000000..d02a16e
--- /dev/null
+++ b/venv/include/site/python3.12/greenlet/greenlet.h
@@ -0,0 +1,164 @@
+/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
+
+/* Greenlet object interface */
+
+#ifndef Py_GREENLETOBJECT_H
+#define Py_GREENLETOBJECT_H
+
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* This is deprecated and undocumented. It does not change. */
+#define GREENLET_VERSION "1.0.0"
+
+#ifndef GREENLET_MODULE
+#define implementation_ptr_t void*
+#endif
+
+typedef struct _greenlet {
+ PyObject_HEAD
+ PyObject* weakreflist;
+ PyObject* dict;
+ implementation_ptr_t pimpl;
+} PyGreenlet;
+
+#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
+
+
+/* C API functions */
+
+/* Total number of symbols that are exported */
+#define PyGreenlet_API_pointers 12
+
+#define PyGreenlet_Type_NUM 0
+#define PyExc_GreenletError_NUM 1
+#define PyExc_GreenletExit_NUM 2
+
+#define PyGreenlet_New_NUM 3
+#define PyGreenlet_GetCurrent_NUM 4
+#define PyGreenlet_Throw_NUM 5
+#define PyGreenlet_Switch_NUM 6
+#define PyGreenlet_SetParent_NUM 7
+
+#define PyGreenlet_MAIN_NUM 8
+#define PyGreenlet_STARTED_NUM 9
+#define PyGreenlet_ACTIVE_NUM 10
+#define PyGreenlet_GET_PARENT_NUM 11
+
+#ifndef GREENLET_MODULE
+/* This section is used by modules that uses the greenlet C API */
+static void** _PyGreenlet_API = NULL;
+
+# define PyGreenlet_Type \
+ (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
+
+# define PyExc_GreenletError \
+ ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
+
+# define PyExc_GreenletExit \
+ ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
+
+/*
+ * PyGreenlet_New(PyObject *args)
+ *
+ * greenlet.greenlet(run, parent=None)
+ */
+# define PyGreenlet_New \
+ (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
+ _PyGreenlet_API[PyGreenlet_New_NUM])
+
+/*
+ * PyGreenlet_GetCurrent(void)
+ *
+ * greenlet.getcurrent()
+ */
+# define PyGreenlet_GetCurrent \
+ (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
+
+/*
+ * PyGreenlet_Throw(
+ * PyGreenlet *greenlet,
+ * PyObject *typ,
+ * PyObject *val,
+ * PyObject *tb)
+ *
+ * g.throw(...)
+ */
+# define PyGreenlet_Throw \
+ (*(PyObject * (*)(PyGreenlet * self, \
+ PyObject * typ, \
+ PyObject * val, \
+ PyObject * tb)) \
+ _PyGreenlet_API[PyGreenlet_Throw_NUM])
+
+/*
+ * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
+ *
+ * g.switch(*args, **kwargs)
+ */
+# define PyGreenlet_Switch \
+ (*(PyObject * \
+ (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
+ _PyGreenlet_API[PyGreenlet_Switch_NUM])
+
+/*
+ * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
+ *
+ * g.parent = new_parent
+ */
+# define PyGreenlet_SetParent \
+ (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
+ _PyGreenlet_API[PyGreenlet_SetParent_NUM])
+
+/*
+ * PyGreenlet_GetParent(PyObject* greenlet)
+ *
+ * return greenlet.parent;
+ *
+ * This could return NULL even if there is no exception active.
+ * If it does not return NULL, you are responsible for decrementing the
+ * reference count.
+ */
+# define PyGreenlet_GetParent \
+ (*(PyGreenlet* (*)(PyGreenlet*)) \
+ _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
+
+/*
+ * deprecated, undocumented alias.
+ */
+# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
+
+# define PyGreenlet_MAIN \
+ (*(int (*)(PyGreenlet*)) \
+ _PyGreenlet_API[PyGreenlet_MAIN_NUM])
+
+# define PyGreenlet_STARTED \
+ (*(int (*)(PyGreenlet*)) \
+ _PyGreenlet_API[PyGreenlet_STARTED_NUM])
+
+# define PyGreenlet_ACTIVE \
+ (*(int (*)(PyGreenlet*)) \
+ _PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
+
+
+
+
+/* Macro that imports greenlet and initializes C API */
+/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
+ keep the older definition to be sure older code that might have a copy of
+ the header still works. */
+# define PyGreenlet_Import() \
+ { \
+ _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
+ }
+
+#endif /* GREENLET_MODULE */
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* !Py_GREENLETOBJECT_H */
diff --git a/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/LICENSE b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/LICENSE
new file mode 100644
index 0000000..967cdc5
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2005-2024 SQLAlchemy authors and 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.12/site-packages/SQLAlchemy-2.0.25.dist-info/METADATA b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/METADATA
new file mode 100644
index 0000000..ac36a34
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/METADATA
@@ -0,0 +1,242 @@
+Metadata-Version: 2.1
+Name: SQLAlchemy
+Version: 2.0.25
+Summary: Database Abstraction Library
+Home-page: https://www.sqlalchemy.org
+Author: Mike Bayer
+Author-email: mike_mp@zzzcomputing.com
+License: MIT
+Project-URL: Documentation, https://docs.sqlalchemy.org
+Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+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 :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Database :: Front-Ends
+Requires-Python: >=3.7
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Requires-Dist: typing-extensions >=4.6.0
+Requires-Dist: greenlet !=0.4.17 ; platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32")))))
+Requires-Dist: importlib-metadata ; python_version < "3.8"
+Provides-Extra: aiomysql
+Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql'
+Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql'
+Provides-Extra: aioodbc
+Requires-Dist: greenlet !=0.4.17 ; extra == 'aioodbc'
+Requires-Dist: aioodbc ; extra == 'aioodbc'
+Provides-Extra: aiosqlite
+Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite'
+Requires-Dist: aiosqlite ; extra == 'aiosqlite'
+Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite'
+Provides-Extra: asyncio
+Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio'
+Provides-Extra: asyncmy
+Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy'
+Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy'
+Provides-Extra: mariadb_connector
+Requires-Dist: mariadb !=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector'
+Provides-Extra: mssql
+Requires-Dist: pyodbc ; extra == 'mssql'
+Provides-Extra: mssql_pymssql
+Requires-Dist: pymssql ; extra == 'mssql_pymssql'
+Provides-Extra: mssql_pyodbc
+Requires-Dist: pyodbc ; extra == 'mssql_pyodbc'
+Provides-Extra: mypy
+Requires-Dist: mypy >=0.910 ; extra == 'mypy'
+Provides-Extra: mysql
+Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql'
+Provides-Extra: mysql_connector
+Requires-Dist: mysql-connector-python ; extra == 'mysql_connector'
+Provides-Extra: oracle
+Requires-Dist: cx-oracle >=8 ; extra == 'oracle'
+Provides-Extra: oracle_oracledb
+Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb'
+Provides-Extra: postgresql
+Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql'
+Provides-Extra: postgresql_asyncpg
+Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg'
+Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg'
+Provides-Extra: postgresql_pg8000
+Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000'
+Provides-Extra: postgresql_psycopg
+Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg'
+Provides-Extra: postgresql_psycopg2binary
+Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary'
+Provides-Extra: postgresql_psycopg2cffi
+Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi'
+Provides-Extra: postgresql_psycopgbinary
+Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary'
+Provides-Extra: pymysql
+Requires-Dist: pymysql ; extra == 'pymysql'
+Provides-Extra: sqlcipher
+Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher'
+
+SQLAlchemy
+==========
+
+|PyPI| |Python| |Downloads|
+
+.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy
+ :target: https://pypi.org/project/sqlalchemy
+ :alt: PyPI
+
+.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy
+ :target: https://pypi.org/project/sqlalchemy
+ :alt: PyPI - Python Version
+
+.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month
+ :target: https://pepy.tech/project/sqlalchemy
+ :alt: PyPI - Downloads
+
+
+The Python SQL Toolkit and Object Relational Mapper
+
+Introduction
+-------------
+
+SQLAlchemy is the Python SQL toolkit and Object Relational Mapper
+that gives application developers the full power and
+flexibility of SQL. SQLAlchemy provides a full suite
+of well known enterprise-level persistence patterns,
+designed for efficient and high-performing database
+access, adapted into a simple and Pythonic domain
+language.
+
+Major SQLAlchemy features include:
+
+* An industrial strength ORM, built
+ from the core on the identity map, unit of work,
+ and data mapper patterns. These patterns
+ allow transparent persistence of objects
+ using a declarative configuration system.
+ Domain models
+ can be constructed and manipulated naturally,
+ and changes are synchronized with the
+ current transaction automatically.
+* A relationally-oriented query system, exposing
+ the full range of SQL's capabilities
+ explicitly, including joins, subqueries,
+ correlation, and most everything else,
+ in terms of the object model.
+ Writing queries with the ORM uses the same
+ techniques of relational composition you use
+ when writing SQL. While you can drop into
+ literal SQL at any time, it's virtually never
+ needed.
+* A comprehensive and flexible system
+ of eager loading for related collections and objects.
+ Collections are cached within a session,
+ and can be loaded on individual access, all
+ at once using joins, or by query per collection
+ across the full result set.
+* A Core SQL construction system and DBAPI
+ interaction layer. The SQLAlchemy Core is
+ separate from the ORM and is a full database
+ abstraction layer in its own right, and includes
+ an extensible Python-based SQL expression
+ language, schema metadata, connection pooling,
+ type coercion, and custom types.
+* All primary and foreign key constraints are
+ assumed to be composite and natural. Surrogate
+ integer primary keys are of course still the
+ norm, but SQLAlchemy never assumes or hardcodes
+ to this model.
+* Database introspection and generation. Database
+ schemas can be "reflected" in one step into
+ Python structures representing database metadata;
+ those same structures can then generate
+ CREATE statements right back out - all within
+ the Core, independent of the ORM.
+
+SQLAlchemy's philosophy:
+
+* SQL databases behave less and less like object
+ collections the more size and performance start to
+ matter; object collections behave less and less like
+ tables and rows the more abstraction starts to matter.
+ SQLAlchemy aims to accommodate both of these
+ principles.
+* An ORM doesn't need to hide the "R". A relational
+ database provides rich, set-based functionality
+ that should be fully exposed. SQLAlchemy's
+ ORM provides an open-ended set of patterns
+ that allow a developer to construct a custom
+ mediation layer between a domain model and
+ a relational schema, turning the so-called
+ "object relational impedance" issue into
+ a distant memory.
+* The developer, in all cases, makes all decisions
+ regarding the design, structure, and naming conventions
+ of both the object model as well as the relational
+ schema. SQLAlchemy only provides the means
+ to automate the execution of these decisions.
+* With SQLAlchemy, there's no such thing as
+ "the ORM generated a bad query" - you
+ retain full control over the structure of
+ queries, including how joins are organized,
+ how subqueries and correlation is used, what
+ columns are requested. Everything SQLAlchemy
+ does is ultimately the result of a developer-initiated
+ decision.
+* Don't use an ORM if the problem doesn't need one.
+ SQLAlchemy consists of a Core and separate ORM
+ component. The Core offers a full SQL expression
+ language that allows Pythonic construction
+ of SQL constructs that render directly to SQL
+ strings for a target database, returning
+ result sets that are essentially enhanced DBAPI
+ cursors.
+* Transactions should be the norm. With SQLAlchemy's
+ ORM, nothing goes to permanent storage until
+ commit() is called. SQLAlchemy encourages applications
+ to create a consistent means of delineating
+ the start and end of a series of operations.
+* Never render a literal value in a SQL statement.
+ Bound parameters are used to the greatest degree
+ possible, allowing query optimizers to cache
+ query plans effectively and making SQL injection
+ attacks a non-issue.
+
+Documentation
+-------------
+
+Latest documentation is at:
+
+https://www.sqlalchemy.org/docs/
+
+Installation / Requirements
+---------------------------
+
+Full documentation for installation is at
+`Installation `_.
+
+Getting Help / Development / Bug reporting
+------------------------------------------
+
+Please refer to the `SQLAlchemy Community Guide `_.
+
+Code of Conduct
+---------------
+
+Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
+constructive communication between users and developers.
+Please see our current Code of Conduct at
+`Code of Conduct `_.
+
+License
+-------
+
+SQLAlchemy is distributed under the `MIT license
+`_.
+
diff --git a/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/RECORD b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/RECORD
new file mode 100644
index 0000000..824b416
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/RECORD
@@ -0,0 +1,530 @@
+SQLAlchemy-2.0.25.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+SQLAlchemy-2.0.25.dist-info/LICENSE,sha256=PA9Zq4h9BB3mpOUv_j6e212VIt6Qn66abNettue-MpM,1100
+SQLAlchemy-2.0.25.dist-info/METADATA,sha256=e57J_l66lNZ5LyXmLMbAGiL02rqx7HxQu_Ci5m_5Y8U,9602
+SQLAlchemy-2.0.25.dist-info/RECORD,,
+SQLAlchemy-2.0.25.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+SQLAlchemy-2.0.25.dist-info/WHEEL,sha256=vJMp7mUkE-fMIYyE5xJ9Q2cYPnWVgHf20clVdwMSXAg,152
+SQLAlchemy-2.0.25.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
+sqlalchemy/__init__.py,sha256=Tu8hhzZF610d9j59ruCV2IROKp-u1Y9i-Plhc6Nf50c,13033
+sqlalchemy/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/__pycache__/events.cpython-312.pyc,,
+sqlalchemy/__pycache__/exc.cpython-312.pyc,,
+sqlalchemy/__pycache__/inspection.cpython-312.pyc,,
+sqlalchemy/__pycache__/log.cpython-312.pyc,,
+sqlalchemy/__pycache__/schema.cpython-312.pyc,,
+sqlalchemy/__pycache__/types.cpython-312.pyc,,
+sqlalchemy/connectors/__init__.py,sha256=PzXPqZqi3BzEnrs1eW0DcsR4lyknAzhhN9rWcQ97hb4,476
+sqlalchemy/connectors/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/connectors/__pycache__/aioodbc.cpython-312.pyc,,
+sqlalchemy/connectors/__pycache__/asyncio.cpython-312.pyc,,
+sqlalchemy/connectors/__pycache__/pyodbc.cpython-312.pyc,,
+sqlalchemy/connectors/aioodbc.py,sha256=GSTiNMO9h0qjPxgqaxDwWZ8HvhWMFNVR6MJQnN1oc40,5288
+sqlalchemy/connectors/asyncio.py,sha256=6s4hDYfuMjJ9KbJ4s7bF1fp5DmcgV77ozgZ5-bwZ0wc,5955
+sqlalchemy/connectors/pyodbc.py,sha256=PZC86t3poFmhgW9_tjDJH8o1Ua0OyiCdfrP7GRX5Gxc,8453
+sqlalchemy/cyextension/__init__.py,sha256=GzhhN8cjMnDTE0qerlUlpbrNmFPHQWCZ4Gk74OAxl04,244
+sqlalchemy/cyextension/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/cyextension/collections.cpython-312-x86_64-linux-gnu.so,sha256=w52mPyvsvh9eI8jjcKdy6CkeoaAQqQgyA1fsoIxbyNw,1945776
+sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571
+sqlalchemy/cyextension/immutabledict.cpython-312-x86_64-linux-gnu.so,sha256=DdLUYRGwSoHPvjZVH4IVg4juCsUDopZS8uaUFB6sgy8,811416
+sqlalchemy/cyextension/immutabledict.pxd,sha256=3x3-rXG5eRQ7bBnktZ-OJ9-6ft8zToPmTDOd92iXpB0,291
+sqlalchemy/cyextension/immutabledict.pyx,sha256=KfDTYbTfebstE8xuqAtuXsHNAK0_b5q_ymUiinUe_xs,3535
+sqlalchemy/cyextension/processors.cpython-312-x86_64-linux-gnu.so,sha256=e4g_ijRARjeUcb6O4mHKD6Si6o5syeTmmgxeYbheilY,534296
+sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792
+sqlalchemy/cyextension/resultproxy.cpython-312-x86_64-linux-gnu.so,sha256=hyEJquVScOr748nm3DgxYuOl2-hia-4K0i6VjvWhAzw,626328
+sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725
+sqlalchemy/cyextension/util.cpython-312-x86_64-linux-gnu.so,sha256=wlYybCnldGKuyWaAF2i2-vcvCIfcq0NSleB1oeiuXlw,958968
+sqlalchemy/cyextension/util.pyx,sha256=B85orxa9LddLuQEaDoVSq1XmAXIbLKxrxpvuB8ogV_o,2530
+sqlalchemy/dialects/__init__.py,sha256=Kos9Gf5JZg1Vg6GWaCqEbD6e0r1jCwCmcnJIfcxDdcY,1770
+sqlalchemy/dialects/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/dialects/__pycache__/_typing.cpython-312.pyc,,
+sqlalchemy/dialects/_typing.py,sha256=hyv0nKucX2gI8ispB1IsvaUgrEPn9zEcq9hS7kfstEw,888
+sqlalchemy/dialects/mssql/__init__.py,sha256=r5t8wFRNtBQoiUWh0WfIEWzXZW6f3D0uDt6NZTW_7Cc,1880
+sqlalchemy/dialects/mssql/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/__pycache__/aioodbc.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/__pycache__/information_schema.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/__pycache__/json.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/__pycache__/provision.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/__pycache__/pymssql.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/__pycache__/pyodbc.cpython-312.pyc,,
+sqlalchemy/dialects/mssql/aioodbc.py,sha256=UQd9ecSMIML713TDnLAviuBVJle7P7i1FtqGZZePk2Y,2022
+sqlalchemy/dialects/mssql/base.py,sha256=lkOGhA8Kg3aximjmbAOZcShXlSWLZWEe94-KdfzxkMo,133650
+sqlalchemy/dialects/mssql/information_schema.py,sha256=ZmFLZ7d4qlguBTm5pIAe3XfnCOr8qZfXPDFK5DE7in8,8083
+sqlalchemy/dialects/mssql/json.py,sha256=evUACW2O62TAPq8B7QIPagz7jfc664ql9ms68JqiYzg,4816
+sqlalchemy/dialects/mssql/provision.py,sha256=RTVbgYLFAHzEnpVQDJroU8ji_10MqBTiZfyP9_-QNT4,5362
+sqlalchemy/dialects/mssql/pymssql.py,sha256=eZRLz7HGt3SdoZUjFBmA9BS43N7AhIASw7VPBPEJuG0,4038
+sqlalchemy/dialects/mssql/pyodbc.py,sha256=GqWKptZfVMKZJ7RXQyuXL4pRJlfnOrZtRYchEeeHl3o,27057
+sqlalchemy/dialects/mysql/__init__.py,sha256=bxbi4hkysUK2OOVvr1F49akUj1cky27kKb07tgFzI9U,2153
+sqlalchemy/dialects/mysql/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/aiomysql.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/asyncmy.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/cymysql.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/dml.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/enumerated.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/expression.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/json.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/mariadb.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/mariadbconnector.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/mysqlconnector.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/mysqldb.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/provision.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/pymysql.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/pyodbc.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/reflection.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/reserved_words.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/__pycache__/types.cpython-312.pyc,,
+sqlalchemy/dialects/mysql/aiomysql.py,sha256=tGe7R8lfaRhdRNMJHQg-AocWu5UTkstLg1C_yIKFBl8,9759
+sqlalchemy/dialects/mysql/asyncmy.py,sha256=sAz0ctNETtEc_8vl03xroLTsEWS6CLNKcqjesm4Ne6Q,9828
+sqlalchemy/dialects/mysql/base.py,sha256=Zy_ZCzuMUeeFmaOPSjmVDP5oxBEw9898QTuxppQCoq8,120698
+sqlalchemy/dialects/mysql/cymysql.py,sha256=eXT1ry0w_qRxjiO24M980c-8PZ9qSsbhqBHntjEiKB0,2300
+sqlalchemy/dialects/mysql/dml.py,sha256=HXJMAvimJsqvhj3UZO4vW_6LkF5RqaKbHvklAjor7yU,7645
+sqlalchemy/dialects/mysql/enumerated.py,sha256=ipEPPQqoXfFwcywNdcLlZCEzHBtnitHRah1Gn6nItcg,8448
+sqlalchemy/dialects/mysql/expression.py,sha256=lsmQCHKwfPezUnt27d2kR6ohk4IRFCA64KBS16kx5dc,4097
+sqlalchemy/dialects/mysql/json.py,sha256=l6MEZ0qp8FgiRrIQvOMhyEJq0q6OqiEnvDTx5Cbt9uQ,2269
+sqlalchemy/dialects/mysql/mariadb.py,sha256=kTfBLioLKk4JFFst4TY_iWqPtnvvQXFHknLfm89H2N8,853
+sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=VVRwKLb6GzDmitOM4wLNvmZw6RdhnIwkLl7IZfAmUy8,8734
+sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=qiQdfLPze3QHuASAZ9iqRzD0hDW8FbKoQnfAEQCF7tM,5675
+sqlalchemy/dialects/mysql/mysqldb.py,sha256=9x_JiY4hj4tykG1ckuEGPyH4jCtsh4fgBhNukVnjUos,9658
+sqlalchemy/dialects/mysql/provision.py,sha256=4oGkClQ8jC3YLPF54sB4kCjFc8HRTwf5zl5zftAAXGo,3474
+sqlalchemy/dialects/mysql/pymysql.py,sha256=GUnSHd2M2uKjmN46Hheymtm26g7phEgwYOXrX0zLY8M,4083
+sqlalchemy/dialects/mysql/pyodbc.py,sha256=072crI4qVyPhajYvHnsfFeSrNjLFVPIjBQKo5uyz5yk,4297
+sqlalchemy/dialects/mysql/reflection.py,sha256=TsRocAsRbAisEgu5NWSND7DZg9OS9ZqwHyaxLzlEgnU,22565
+sqlalchemy/dialects/mysql/reserved_words.py,sha256=Dm7FINIAkrKLoXmdu26SpE6V8LDCGyp734nmHV2tMd0,9154
+sqlalchemy/dialects/mysql/types.py,sha256=aPzx7hqqZ21aGwByEC-yWZUl6OpMvkbxwTqdN3OUGGI,24267
+sqlalchemy/dialects/oracle/__init__.py,sha256=p4-2gw7TT0bX_MoJXTGD4i8WHctYsK9kCRbkpzykBrc,1493
+sqlalchemy/dialects/oracle/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/dialects/oracle/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/dialects/oracle/__pycache__/cx_oracle.cpython-312.pyc,,
+sqlalchemy/dialects/oracle/__pycache__/dictionary.cpython-312.pyc,,
+sqlalchemy/dialects/oracle/__pycache__/oracledb.cpython-312.pyc,,
+sqlalchemy/dialects/oracle/__pycache__/provision.cpython-312.pyc,,
+sqlalchemy/dialects/oracle/__pycache__/types.cpython-312.pyc,,
+sqlalchemy/dialects/oracle/base.py,sha256=Ng_T-Xl1rOHJO7mcnTbINnTVEQUjeLIKBl1DutaBRdY,118045
+sqlalchemy/dialects/oracle/cx_oracle.py,sha256=21xgnOZ8kCKrIfC9XlZM7R2zKZa86nsBHx4cv0Iw3x8,55290
+sqlalchemy/dialects/oracle/dictionary.py,sha256=7WMrbPkqo8ZdGjaEZyQr-5f2pajSOF1OTGb8P97z8-g,19519
+sqlalchemy/dialects/oracle/oracledb.py,sha256=vDKUuy4DExEMnXeidZL0wjuggJAqTkhXtSvxkvaEjbs,9485
+sqlalchemy/dialects/oracle/provision.py,sha256=O9ZpF4OG6Cx4mMzLRfZwhs8dZjrJETWR402n9c7726A,8304
+sqlalchemy/dialects/oracle/types.py,sha256=QK3hJvWzKnnCe3oD3rItwEEIwcoBze8qGg7VFOvVlIk,8231
+sqlalchemy/dialects/postgresql/__init__.py,sha256=rlGgUbemHlQWvsDjqWufCU-CioMPtSHRDgCd8G0v10E,3743
+sqlalchemy/dialects/postgresql/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/_psycopg_common.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/array.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/asyncpg.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/dml.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/ext.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/hstore.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/json.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/named_types.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/operators.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/pg8000.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/pg_catalog.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/provision.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/psycopg.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/psycopg2.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/psycopg2cffi.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/ranges.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/__pycache__/types.cpython-312.pyc,,
+sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=7TudtgsPiSB8O5kX8W8KxcNYR8t5h_UHb86b_ChL0P8,5696
+sqlalchemy/dialects/postgresql/array.py,sha256=3EWWhFJbw2xJfie1RAqtscecCIXSGZM4qmOipLYc1T0,13691
+sqlalchemy/dialects/postgresql/asyncpg.py,sha256=KGBdzxbHnqFpWOe6usS5yxuw2KK_KBqSpp6RA7F0Ua8,40232
+sqlalchemy/dialects/postgresql/base.py,sha256=SVALrrOKYJfrlelcsUM50neHQzVAWcwA6RxQG84YM24,175627
+sqlalchemy/dialects/postgresql/dml.py,sha256=L3G3bVL41DXirham5XRBXYOM4eefhqyGCzMyn8zCdI0,11212
+sqlalchemy/dialects/postgresql/ext.py,sha256=1bZ--iNh2O9ym7l2gXZX48yP3yMO4dqb9RpYro2Mj2Q,16262
+sqlalchemy/dialects/postgresql/hstore.py,sha256=otAx-RTDfpi_tcXkMuQV0JOIXtYgevgnsikLKKOkI6U,11541
+sqlalchemy/dialects/postgresql/json.py,sha256=-ffnp85fQBOyt0Bjb7XAupmOxloUdzFZZgixUG3Wj5w,11212
+sqlalchemy/dialects/postgresql/named_types.py,sha256=i2GwHI8V83AA2Gr87yyVCNBsm9mF99as54UlyOBs7IY,17101
+sqlalchemy/dialects/postgresql/operators.py,sha256=NsAaWun_tL3d_be0fs9YL6T4LPKK6crnmFxxIJHgyeY,2808
+sqlalchemy/dialects/postgresql/pg8000.py,sha256=qnMSG3brW6XiygF-vXr1JguSSNpd-xlHJRkHHCDWs-k,18637
+sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=nAKavWTE_4cqxiDKDTdo-ivkCxxRIlzD5GO9Wl1yrG4,8884
+sqlalchemy/dialects/postgresql/provision.py,sha256=yqyx-aDFO9l2YcL9f4T5HBP_Lnt5dHsMjpuXUG8mi7A,5762
+sqlalchemy/dialects/postgresql/psycopg.py,sha256=zOM1PfiGU1I-XnkF5N6pBEDvTLP0qZZg6YSWWNgd-Xw,22320
+sqlalchemy/dialects/postgresql/psycopg2.py,sha256=f6vuQ4BStwcdLRV2iVYwyqjksYSV-c_uJvCWawU6cfg,31601
+sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=M7wAYSL6Pvt-4nbfacAHGyyw4XMKJ_bQZ1tc1pBtIdg,1756
+sqlalchemy/dialects/postgresql/ranges.py,sha256=mPsXfEz3Ot0QebOqp5dt9mmy_SATwdDQ7wVe-Q5Bqqc,30252
+sqlalchemy/dialects/postgresql/types.py,sha256=yoBV_hSq6m93mtXmWPX8LoOORNqWyLC1zyckgu656BI,7323
+sqlalchemy/dialects/sqlite/__init__.py,sha256=lp9DIggNn349M-7IYhUA8et8--e8FRExWD2V_r1LJk4,1182
+sqlalchemy/dialects/sqlite/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/__pycache__/aiosqlite.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/__pycache__/dml.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/__pycache__/json.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/__pycache__/provision.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/__pycache__/pysqlcipher.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/__pycache__/pysqlite.cpython-312.pyc,,
+sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=OMvxP2eWyqk5beF-sHhzxRmjzO4VCQp55q7NH2XPVTE,12305
+sqlalchemy/dialects/sqlite/base.py,sha256=irgfivk--Pf7nj2tIZHALGOWjeK4CDnTtvRg9vN0QjY,96794
+sqlalchemy/dialects/sqlite/dml.py,sha256=ZZ6RiyflrhtPwrgNQSYUCdUWobDnuXPN9yop0gJTm9c,8443
+sqlalchemy/dialects/sqlite/json.py,sha256=Eoplbb_4dYlfrtmQaI8Xddd2suAIHA-IdbDQYM-LIhs,2777
+sqlalchemy/dialects/sqlite/provision.py,sha256=UCpmwxf4IWlrpb2eLHGbPTpCFVbdI_KAh2mKtjiLYao,5632
+sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=OL2S_05DK9kllZj6DOz7QtEl7jI7syxjW6woS725ii4,5356
+sqlalchemy/dialects/sqlite/pysqlite.py,sha256=TAOqsHIjhbUZOF_Qk7UooiekkVZNhYJNduxlGQjokeA,27900
+sqlalchemy/dialects/type_migration_guidelines.txt,sha256=-uHNdmYFGB7bzUNT6i8M5nb4j6j9YUKAtW4lcBZqsMg,8239
+sqlalchemy/engine/__init__.py,sha256=Stb2oV6l8w65JvqEo6J4qtKoApcmOpXy3AAxQud4C1o,2818
+sqlalchemy/engine/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/_py_processors.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/_py_row.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/_py_util.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/characteristics.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/create.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/cursor.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/default.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/events.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/interfaces.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/mock.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/processors.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/reflection.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/result.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/row.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/strategies.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/url.cpython-312.pyc,,
+sqlalchemy/engine/__pycache__/util.cpython-312.pyc,,
+sqlalchemy/engine/_py_processors.py,sha256=j9i_lcYYQOYJMcsDerPxI0sVFBIlX5sqoYMdMJlgWPI,3744
+sqlalchemy/engine/_py_row.py,sha256=wSqoUFzLOJ1f89kgDb6sJm9LUrF5LMFpXPcK1vUsKcs,3787
+sqlalchemy/engine/_py_util.py,sha256=f2DI3AN1kv6EplelowesCVpwS8hSXNufRkZoQmJtSH8,2484
+sqlalchemy/engine/base.py,sha256=5n8SHmQh5Cr9DxGeRis12-_VQO4Cl7sM_FOnCBy2zD0,122207
+sqlalchemy/engine/characteristics.py,sha256=Qbvt4CPrggJ3GfxHl0hOAxopjnCQy-W_pjtwLIe-Q1g,2590
+sqlalchemy/engine/create.py,sha256=Lua52hd3e5H0a68rCVzrbwWZCnNKCef2Ew7FRchJK-c,32888
+sqlalchemy/engine/cursor.py,sha256=ErLMvqRMT8HJm-5RQeniC8-429cvXf8Sq3JSReLeVfI,74442
+sqlalchemy/engine/default.py,sha256=rZdc8JvEZCM4LtaWeJY3VsRrWMo_GDvfayK96nmGkvc,84065
+sqlalchemy/engine/events.py,sha256=c0unNFFiHzTAvkUtXoJaxzMFMDwurBkHiiUhuN8qluc,37381
+sqlalchemy/engine/interfaces.py,sha256=WE50MbuYGYhhMgF71GbfLVJAn2DDYEXiOOt6M3r-5z0,112814
+sqlalchemy/engine/mock.py,sha256=yvpxgFmRw5G4QsHeF-ZwQGHKES-HqQOucTxFtN1uzdk,4179
+sqlalchemy/engine/processors.py,sha256=XyfINKbo-2fjN-mW55YybvFyQMOil50_kVqsunahkNs,2379
+sqlalchemy/engine/reflection.py,sha256=FlT5kPpKm7Lah50GNt5XcnlJWojTL3LD_x0SoCF9kfY,75127
+sqlalchemy/engine/result.py,sha256=tGQX_zP5wlalOXXAV5BfljHd6bpSPePRXlju099TEe4,77756
+sqlalchemy/engine/row.py,sha256=S4d_WWD292B6_AnucKGG7E_6KFjEqP-BZAInrxGgicw,12080
+sqlalchemy/engine/strategies.py,sha256=DqFSWaXJPL-29Omot9O0aOcuGL8KmCGyOvnPGDkAJoE,442
+sqlalchemy/engine/url.py,sha256=dlbRISW9lMJ-Co_p1-TCXCBLMXGyrv7ROgll26FTe74,30558
+sqlalchemy/engine/util.py,sha256=hkEql1t19WHl6uzR55-F-Fs_VMCJ7p02KKQVNUDSXTk,5667
+sqlalchemy/event/__init__.py,sha256=KBrp622xojnC3FFquxa2JsMamwAbfkvzfv6Op0NKiYc,997
+sqlalchemy/event/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/event/__pycache__/api.cpython-312.pyc,,
+sqlalchemy/event/__pycache__/attr.cpython-312.pyc,,
+sqlalchemy/event/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/event/__pycache__/legacy.cpython-312.pyc,,
+sqlalchemy/event/__pycache__/registry.cpython-312.pyc,,
+sqlalchemy/event/api.py,sha256=BUTAZjSlzvq4Hn2v2pihP_P1yo3lvCVDczK8lV_XJ80,8227
+sqlalchemy/event/attr.py,sha256=h8pFjHgyvLTeai6_LRQYk6ii-6M3PGgXFeZlPe-OJgo,20767
+sqlalchemy/event/base.py,sha256=LFcvzFaop51Im1IP1vz3BxFqQWXA90F2b4CMEV3JyKI,14980
+sqlalchemy/event/legacy.py,sha256=I5e9JLpRybIVlRi_ArEwymwbfx8vNns52v9QWaqVtA4,8211
+sqlalchemy/event/registry.py,sha256=LKbNsF5quf0DAGCw8jaGLmSf7p9ejWjT2y_Oz7FoDNo,10833
+sqlalchemy/events.py,sha256=k-ZD38aSPD29LYhED7CBqttp5MDVVx_YSaWC2-cu9ec,525
+sqlalchemy/exc.py,sha256=GRMcfOg64pRXX7nGsDt5iXfwqUdiMILstYse87vzmLI,24000
+sqlalchemy/ext/__init__.py,sha256=S1fGKAbycnQDV01gs-JWGaFQ9GCD4QHwKcU2wnugg_o,322
+sqlalchemy/ext/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/associationproxy.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/automap.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/baked.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/compiler.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/horizontal_shard.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/hybrid.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/indexable.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/instrumentation.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/mutable.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/orderinglist.cpython-312.pyc,,
+sqlalchemy/ext/__pycache__/serializer.cpython-312.pyc,,
+sqlalchemy/ext/associationproxy.py,sha256=BPYsvBlh8KVxKPLhHc31_JTatk7bhrSJTqO50b3OLcA,65960
+sqlalchemy/ext/asyncio/__init__.py,sha256=1OqSxEyIUn7RWLGyO12F-jAUIvk1I6DXlVy80-Gvkds,1317
+sqlalchemy/ext/asyncio/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/ext/asyncio/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/ext/asyncio/__pycache__/engine.cpython-312.pyc,,
+sqlalchemy/ext/asyncio/__pycache__/exc.cpython-312.pyc,,
+sqlalchemy/ext/asyncio/__pycache__/result.cpython-312.pyc,,
+sqlalchemy/ext/asyncio/__pycache__/scoping.cpython-312.pyc,,
+sqlalchemy/ext/asyncio/__pycache__/session.cpython-312.pyc,,
+sqlalchemy/ext/asyncio/base.py,sha256=HVpevdn2vcCrhWyeSDdP0JFm2SdEIitmBlXv82Yywbo,8937
+sqlalchemy/ext/asyncio/engine.py,sha256=vy8_HiMFP4HWXvqLSES0AMQIWWNUmrIxPIeVHa6QbEw,48058
+sqlalchemy/ext/asyncio/exc.py,sha256=8sII7VMXzs2TrhizhFQMzSfcroRtiesq8o3UwLfXSgQ,639
+sqlalchemy/ext/asyncio/result.py,sha256=pVBeJym7zjT4eMcduU3X2_g5qtnaKuy2kIJWVJGTv_o,30554
+sqlalchemy/ext/asyncio/scoping.py,sha256=xLdjNJ1VnlTmi5YAsZKQo3XfBzKpviKpLG_HeSSbv2g,52685
+sqlalchemy/ext/asyncio/session.py,sha256=sc9SKwqEgPiTIjlIyVa679F-Q2CWp6_8ucJOis_hYaQ,62998
+sqlalchemy/ext/automap.py,sha256=MTvMs97xALDugdgyY3JqiVeusrcKruN0BR7iSFhNZOg,61431
+sqlalchemy/ext/baked.py,sha256=H6T1il7GY84BhzPFj49UECSpZh_eBuiHomA-QIsYOYQ,17807
+sqlalchemy/ext/compiler.py,sha256=ONPoxoKD2yUS9R2-oOhmPsA7efm-Bs0BXo7HE1dGlsU,20391
+sqlalchemy/ext/declarative/__init__.py,sha256=20psLdFQbbOWfpdXHZ0CTY6I1k4UqXvKemNVu1LvPOI,1818
+sqlalchemy/ext/declarative/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/ext/declarative/__pycache__/extensions.cpython-312.pyc,,
+sqlalchemy/ext/declarative/extensions.py,sha256=uCjN1GisQt54AjqYnKYzJdUjnGd2pZBW47WWdPlS7FE,19547
+sqlalchemy/ext/horizontal_shard.py,sha256=ITc2MU4pVc6t_HLR-T6tTMvYbwrl8cM9Bi02jI4e7FM,16766
+sqlalchemy/ext/hybrid.py,sha256=iU2GCE-PDHWa6hp3-nv9fNjuk46hzmRPJKxHEs7smxY,52514
+sqlalchemy/ext/indexable.py,sha256=UkTelbydKCdKelzbv3HWFFavoET9WocKaGRPGEOVfN8,11032
+sqlalchemy/ext/instrumentation.py,sha256=7908PLZGlD9tiq0nWS0A7iNNax0iGNsIchhJ_OoxOCg,15723
+sqlalchemy/ext/mutable.py,sha256=IX_H7vCBG834gFtCQzopuwgpmEDspBQBdb_nK-Dfrfo,37427
+sqlalchemy/ext/mypy/__init__.py,sha256=0WebDIZmqBD0OTq5JLtd_PmfF9JGxe4d4Qv3Ml3PKUg,241
+sqlalchemy/ext/mypy/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/ext/mypy/__pycache__/apply.cpython-312.pyc,,
+sqlalchemy/ext/mypy/__pycache__/decl_class.cpython-312.pyc,,
+sqlalchemy/ext/mypy/__pycache__/infer.cpython-312.pyc,,
+sqlalchemy/ext/mypy/__pycache__/names.cpython-312.pyc,,
+sqlalchemy/ext/mypy/__pycache__/plugin.cpython-312.pyc,,
+sqlalchemy/ext/mypy/__pycache__/util.cpython-312.pyc,,
+sqlalchemy/ext/mypy/apply.py,sha256=KZP0RAsQd65hyoS9bOBZ0UtgA1sZSd1wH9S0jyc_v08,10508
+sqlalchemy/ext/mypy/decl_class.py,sha256=gQOIMZOpKKdFW1TQjnEVM2RxGJbZzz5dFeMWRaIoFHc,17382
+sqlalchemy/ext/mypy/infer.py,sha256=KVnmLFEVS33Al8pUKI7MJbJQu3KeveBUMl78EluBORw,19369
+sqlalchemy/ext/mypy/names.py,sha256=IQ16GLZFqKxfYxIZxkbTurBqOUYbUV-64V_DSRns1tc,10630
+sqlalchemy/ext/mypy/plugin.py,sha256=74ML8LI9xar0V86oCxnPFv5FQGEEfUzK64vOay4BKFs,9750
+sqlalchemy/ext/mypy/util.py,sha256=DLvKhM38mZk_1vvxbpenq-krTwdqdrZbg-o072TodCs,9408
+sqlalchemy/ext/orderinglist.py,sha256=TGYbsGH72wEZcFNQDYDsZg9OSPuzf__P8YX8_2HtYUo,14384
+sqlalchemy/ext/serializer.py,sha256=YemanWdeMVUDweHCnQc-iMO6mVVXNo2qQ5NK0Eb2_Es,6178
+sqlalchemy/future/__init__.py,sha256=q2mw-gxk_xoxJLEvRoyMha3vO1xSRHrslcExOHZwmPA,512
+sqlalchemy/future/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/future/__pycache__/engine.cpython-312.pyc,,
+sqlalchemy/future/engine.py,sha256=AgIw6vMsef8W6tynOTkxsjd6o_OQDwGjLdbpoMD8ue8,495
+sqlalchemy/inspection.py,sha256=fLQyZnIS7irH7dRX3AaUHCkCArjIxMboaJj0rjM3cnA,5137
+sqlalchemy/log.py,sha256=KPxnFg6D2kj2_-3k6njiwxqbqyj51lid-O8ZWZN3HIg,8623
+sqlalchemy/orm/__init__.py,sha256=ZYys5nL3RFUDCMOLFDBrRI52F6er3S1U1OY9TeORuKs,8463
+sqlalchemy/orm/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/_orm_constructors.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/_typing.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/attributes.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/bulk_persistence.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/clsregistry.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/collections.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/context.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/decl_api.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/decl_base.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/dependency.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/descriptor_props.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/dynamic.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/evaluator.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/events.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/exc.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/identity.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/instrumentation.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/interfaces.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/loading.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/mapped_collection.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/mapper.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/path_registry.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/persistence.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/properties.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/query.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/relationships.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/scoping.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/session.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/state.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/state_changes.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/strategies.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/strategy_options.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/sync.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/unitofwork.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/util.cpython-312.pyc,,
+sqlalchemy/orm/__pycache__/writeonly.cpython-312.pyc,,
+sqlalchemy/orm/_orm_constructors.py,sha256=hhvHmDbY2cbMedLbLRYVAh14XO1U1Co513qzIke0THY,99370
+sqlalchemy/orm/_typing.py,sha256=RhN8pyEBjpYzGaxezyZCPRNV82U--BMRTIDdIiogsyo,5024
+sqlalchemy/orm/attributes.py,sha256=2R9p508nfGDwj_qr4Hxbv_g2QDDQSp0-n_GCHcjqb2U,92578
+sqlalchemy/orm/base.py,sha256=4-AzRGLdOTCOpgg8larSWb6s-YOqbMLrgQPhw__51ys,27700
+sqlalchemy/orm/bulk_persistence.py,sha256=vjx8DSvbeyTzDiuwaBonZ8TDc6kfUtBIEaXJZEuY6ac,69878
+sqlalchemy/orm/clsregistry.py,sha256=W9Su2JwNCvJuJiAyOT49ALUj59G36z4ugJimkKId530,17962
+sqlalchemy/orm/collections.py,sha256=t184F_YbXxZdwLOyv85l4M32gbJ-EQtbdMewnsmfdHg,52159
+sqlalchemy/orm/context.py,sha256=esaaJ7pYE4-SzXqeEE-Pv7BZLtnghwMY2dasW9N1vnY,111893
+sqlalchemy/orm/decl_api.py,sha256=YrN1zaL7yFAXFtzJYkzC6DUMaqfEenCdQnBGSK1Xn-E,63882
+sqlalchemy/orm/decl_base.py,sha256=WfKe0wyxrYr1-mmeiQgfIhRD_PHlFK-xDNsvYJ3R0T0,81621
+sqlalchemy/orm/dependency.py,sha256=lrTu8yfqLbz7U0iOHULR_Yk4C0z-2VDtFpuH7TaeynA,47583
+sqlalchemy/orm/descriptor_props.py,sha256=RennfXQ7bdnANgNF2SlB_-W_GlaZ26SsdhI4Yb_orYE,37176
+sqlalchemy/orm/dynamic.py,sha256=toSmHi9AF9nnbZmvLSMOSGR0NaG0YpszlLQx8KnxMbk,9798
+sqlalchemy/orm/evaluator.py,sha256=q292K5vdpP69G7Z9y1RqI5GFAk2diUPwnsXE8De_Wgw,11925
+sqlalchemy/orm/events.py,sha256=USrIP-2JlcIbmssvCkea1veL3eIIWC7WH7KDmTzqa-Q,127601
+sqlalchemy/orm/exc.py,sha256=w7MZkJMGGlu5J6jOFSmi9XXzc02ctnTv34jrEWpI-eM,7356
+sqlalchemy/orm/identity.py,sha256=jHdCxCpCyda_8mFOfGmN_Pr0XZdKiU-2hFZshlNxbHs,9249
+sqlalchemy/orm/instrumentation.py,sha256=wHdGTYpzND7nhgbpmiryLOXuWLIzCVii6jpfVWAi2RQ,24337
+sqlalchemy/orm/interfaces.py,sha256=0cK5udFIGusQ3cW697zXEElIPVafxW5UD5KL_6oNi8w,48404
+sqlalchemy/orm/loading.py,sha256=GKvLzmFklYgS89enTt_b2fvyDnu9rUAlYLL-Oa9eAeM,57417
+sqlalchemy/orm/mapped_collection.py,sha256=IsSSxYWuR396Qep4MYCe2VDmFr5sU_3AuDrhXDf_mVA,19704
+sqlalchemy/orm/mapper.py,sha256=j5r6ezZKW_WOcp_SIz0neMHMXiAMriVMdXbgvIhusfg,170969
+sqlalchemy/orm/path_registry.py,sha256=uZULekFBpy292nYuE0ON6vGcgM0Szs6a0iN7Wyya0_A,25938
+sqlalchemy/orm/persistence.py,sha256=2nQZpi9Mi-uKlJ-cwLodOMu-9gs4ZpYcUseIk4T210M,60989
+sqlalchemy/orm/properties.py,sha256=1gaf8QaGunBN2K2nEvMcucD4U1cOntJgsqJafLtHi7w,29095
+sqlalchemy/orm/query.py,sha256=QwUV1vm-6gyriYiZ4GIoNV3rdwdaCFhfdChTvVq00Oo,117714
+sqlalchemy/orm/relationships.py,sha256=NRQBABfdAWWqtRzhyE8Xq_uDeFPLxruSBNK646yv-vo,127619
+sqlalchemy/orm/scoping.py,sha256=Aeu34zEhcxcbS8NCzfgHzDBhlSXnlay5Ln8SPczdh9k,78821
+sqlalchemy/orm/session.py,sha256=bJTTXE7yB4qD6JSe0lvJv8QASf2OoUOoHQplt1ODHJk,193265
+sqlalchemy/orm/state.py,sha256=M60-bI0R0dzGHc-fBNGXX1V8osNTSC_WwtmlhcErSBM,37536
+sqlalchemy/orm/state_changes.py,sha256=qKYg7NxwrDkuUY3EPygAztym6oAVUFcP2wXn7QD3Mz4,6815
+sqlalchemy/orm/strategies.py,sha256=u6d7F5cAi2TC4NNYzFecoMYEUownpQcght5hIXVPJ7M,114052
+sqlalchemy/orm/strategy_options.py,sha256=267SGNfWJGlWjrdBffX8cQYrN7Ilk3Xk4kL4J150P7U,84161
+sqlalchemy/orm/sync.py,sha256=g7iZfSge1HgxMk9SKRgUgtHEbpbZ1kP_CBqOIdTOXqc,5779
+sqlalchemy/orm/unitofwork.py,sha256=fiVaqcymbDDHRa1NjS90N9Z466nd5pkJOEi1dHO6QLY,27033
+sqlalchemy/orm/util.py,sha256=j6BLkPtzZs88tINO21h-Dv3w2N1mlRZNabB_Q27U9ac,80340
+sqlalchemy/orm/writeonly.py,sha256=xh-KN8CiykLNQ_L9HE8QM1A822CJfiYopG9snSbWsz0,22329
+sqlalchemy/pool/__init__.py,sha256=qiDdq4r4FFAoDrK6ncugF_i6usi_X1LeJt-CuBHey0s,1804
+sqlalchemy/pool/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/pool/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/pool/__pycache__/events.cpython-312.pyc,,
+sqlalchemy/pool/__pycache__/impl.cpython-312.pyc,,
+sqlalchemy/pool/base.py,sha256=2c614izCvoBw0xxu0LOhtziVB_G_MGns0jZu6ts9Bn8,52243
+sqlalchemy/pool/events.py,sha256=12gjivfkZkROTFYoIUS0kkvt3Ftj5ogBGavP9oSFPl4,13137
+sqlalchemy/pool/impl.py,sha256=2gdX23oZPLEqf3phI8yJod4ElB1BwFWkZCAtEXjXqbM,17718
+sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+sqlalchemy/schema.py,sha256=dKiWmgHYjcKQ4TiiD6vD0UMmIsD8u0Fsor1M9AAeGUs,3194
+sqlalchemy/sql/__init__.py,sha256=UNa9EUiYWoPayf-FzNcwVgQvpsBdInPZfpJesAStN9o,5820
+sqlalchemy/sql/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/_dml_constructors.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/_elements_constructors.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/_orm_types.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/_py_util.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/_selectable_constructors.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/_typing.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/annotation.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/cache_key.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/coercions.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/compiler.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/crud.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/ddl.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/default_comparator.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/dml.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/elements.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/events.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/expression.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/functions.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/lambdas.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/naming.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/operators.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/roles.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/schema.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/selectable.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/sqltypes.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/traversals.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/type_api.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/util.cpython-312.pyc,,
+sqlalchemy/sql/__pycache__/visitors.cpython-312.pyc,,
+sqlalchemy/sql/_dml_constructors.py,sha256=YdBJex0MCVACv4q2nl_ii3uhxzwU6aDB8zAsratX5UQ,3867
+sqlalchemy/sql/_elements_constructors.py,sha256=oCro-h7QqOIbq4abduZP8EIw88HbHhPWPdyN8fzMuas,62558
+sqlalchemy/sql/_orm_types.py,sha256=T-vjcry4C1y0GToFKVxQCnmly_-Zsq4IO4SHN6bvUF4,625
+sqlalchemy/sql/_py_util.py,sha256=hiM9ePbRSGs60bAMxPFuJCIC_p9SQ1VzqXGiPchiYwE,2173
+sqlalchemy/sql/_selectable_constructors.py,sha256=kHm45Q6t2QCHIj2CS0SEZvD0MwKmK59lmg55NQMMKK8,18812
+sqlalchemy/sql/_typing.py,sha256=XDr2i-6GxPLBvtJC3ybzffQubUv_h3I2xRDUBlQLg30,12613
+sqlalchemy/sql/annotation.py,sha256=fEeyIJYId-xVHxp4VuHUzOnosa3iKLAHExnb8swq8v0,18271
+sqlalchemy/sql/base.py,sha256=vnkJFb9yJwTFX9Fd0CsISiP9m3PBfNQiBfMCmfUt69k,73928
+sqlalchemy/sql/cache_key.py,sha256=mTIa7UfTiUpeXDcVqn5vDBf-soj81fXOIHLPNjoQzhg,33124
+sqlalchemy/sql/coercions.py,sha256=BKj_pkSS4zBmc3tiS8wWmNhGWeyr66sK4QHJudqy1Lg,40489
+sqlalchemy/sql/compiler.py,sha256=WgnzXhjGvHxEC4VhIXgoeCoyKDFjRGfIwv0rH4Ug6vw,269842
+sqlalchemy/sql/crud.py,sha256=Xp3rX7N-YuecL14StUEc959a6oj61EG_fiyoiVYGAqY,56457
+sqlalchemy/sql/ddl.py,sha256=GJfH800KdwMhBbhp1f6FFyDVx-RjtG2WRGa8UTVaZI4,45542
+sqlalchemy/sql/default_comparator.py,sha256=1OiYbEojh6Vq8gy_jqY3b19dYZ0DvjFbyV5QeFiAs_o,16646
+sqlalchemy/sql/dml.py,sha256=BXUkqWPhnELKrhRKhfWZH4YZXKCCDCoHemVnfaew8Us,65728
+sqlalchemy/sql/elements.py,sha256=sv1D2nLZO0rmnmcrPuQcMAp2YCkTWv_r4GBG_xDlSbY,172784
+sqlalchemy/sql/events.py,sha256=iC_Q1Htm1Aobt5tOYxWfHHqNpoytrULORmUKcusH_-E,18290
+sqlalchemy/sql/expression.py,sha256=VMX-dLpsZYnVRJpYNDozDUgaj7iQ0HuewUKVefD57PE,7586
+sqlalchemy/sql/functions.py,sha256=e_29NAfsMBltrVVlgVUDt2CizHXrtS8dVBzvqnXgMJg,64248
+sqlalchemy/sql/lambdas.py,sha256=bG3D175kcQ3-9OWMLABSa6-diJyOfMf0ajr2w4mFdfM,49281
+sqlalchemy/sql/naming.py,sha256=ZHs1qSV3ou8TYmZ92uvU3sfdklUQlIz4uhe330n05SU,6858
+sqlalchemy/sql/operators.py,sha256=eCADkzisFq3PPWUSN28HYe8sgpxFQe6kJUATPn-XsuM,76193
+sqlalchemy/sql/roles.py,sha256=VwwJacCiopxIHABAeNgLApDxo-xHPhJl7UfdLebMJGw,7686
+sqlalchemy/sql/schema.py,sha256=XO0fxfaBbnyJjlunZln_Xi8ecPM_l7WZowgyi-btAek,228211
+sqlalchemy/sql/selectable.py,sha256=_Bu-nSxEC3kSUosAsDsZZn7S7BC_HtbFainREIQK6D8,233041
+sqlalchemy/sql/sqltypes.py,sha256=Bv_xlg27fsYN0xPQ81LaQ-snVm2JqsX_Rz1IpkUBqpw,126509
+sqlalchemy/sql/traversals.py,sha256=Tvt6DFCGIqsQHSCfKzk2kAbi_bnEX3jh_8ZRdtOFoYE,33521
+sqlalchemy/sql/type_api.py,sha256=PhMBSDza_dT5RXSPNkpTJUqGj2ojI_2vSkOSWl5OuRQ,83883
+sqlalchemy/sql/util.py,sha256=LVLjLqpZuJ_DT6oUeE7WJbuCkIMEA8ZUEbO204Gp06U,48187
+sqlalchemy/sql/visitors.py,sha256=St-h4A5ZMVMGu9hFhbGZJUxa9KaEg7e2KaCMihVTU64,36427
+sqlalchemy/testing/__init__.py,sha256=VsrEHrORpAF5n7Vfl43YQgABh6EP1xBx_gHxs7pSXeE,3126
+sqlalchemy/testing/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/assertions.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/assertsql.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/asyncio.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/config.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/engines.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/entities.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/exclusions.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/pickleable.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/profiling.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/provision.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/requirements.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/schema.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/util.cpython-312.pyc,,
+sqlalchemy/testing/__pycache__/warnings.cpython-312.pyc,,
+sqlalchemy/testing/assertions.py,sha256=gL0rA7CCZJbcVgvWOPV91tTZTRwQc1_Ta0-ykBn83Ew,31439
+sqlalchemy/testing/assertsql.py,sha256=xwo0ZuCN69Y0ElCZys3lnmPxHdmJ34E2Cns4V3g8MA0,16817
+sqlalchemy/testing/asyncio.py,sha256=fkdRz-E37d5OrQKw5hdjmglOTJyXGnJzaJpvNXOBLxg,3728
+sqlalchemy/testing/config.py,sha256=9HWOgvPLSRJIjRWa0wauo3klYafV9oEhp8qhjptvlVw,12030
+sqlalchemy/testing/engines.py,sha256=iB3dLHhSBLPbTB5lSYnnppnfB66IP2DmfW56mhH4sjI,13355
+sqlalchemy/testing/entities.py,sha256=IphFegPKbff3Un47jY6bi7_MQXy6qkx_50jX2tHZJR4,3354
+sqlalchemy/testing/exclusions.py,sha256=vZlqF8Jy_PpXc9e-yPAI8-UHFVM-UiRfMwI-WrWS-nU,12444
+sqlalchemy/testing/fixtures/__init__.py,sha256=dMClrIoxqlYIFpk2ia4RZpkbfxsS_3EBigr9QsPJ66g,1198
+sqlalchemy/testing/fixtures/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/testing/fixtures/__pycache__/base.cpython-312.pyc,,
+sqlalchemy/testing/fixtures/__pycache__/mypy.cpython-312.pyc,,
+sqlalchemy/testing/fixtures/__pycache__/orm.cpython-312.pyc,,
+sqlalchemy/testing/fixtures/__pycache__/sql.cpython-312.pyc,,
+sqlalchemy/testing/fixtures/base.py,sha256=9r_J2ksiTzClpUxW0TczICHrWR7Ny8PV8IsBz6TsGFI,12256
+sqlalchemy/testing/fixtures/mypy.py,sha256=nrfgQnzIZoRFJ47F-7IZpouvAq6mSQHb8A-TbiDxv5I,11845
+sqlalchemy/testing/fixtures/orm.py,sha256=8EFbnaBbXX_Bf4FcCzBUaAHgyVpsLGBHX16SGLqE3Fg,6095
+sqlalchemy/testing/fixtures/sql.py,sha256=MFOuYBUyPIpHJzjRCHL9vU-IT4bD6LXGGMvsp0v1FY8,15704
+sqlalchemy/testing/pickleable.py,sha256=U9mIqk-zaxq9Xfy7HErP7UrKgTov-A3QFnhZh-NiOjI,2833
+sqlalchemy/testing/plugin/__init__.py,sha256=79F--BIY_NTBzVRIlJGgAY5LNJJ3cD19XvrAo4X0W9A,247
+sqlalchemy/testing/plugin/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/testing/plugin/__pycache__/bootstrap.cpython-312.pyc,,
+sqlalchemy/testing/plugin/__pycache__/plugin_base.cpython-312.pyc,,
+sqlalchemy/testing/plugin/__pycache__/pytestplugin.cpython-312.pyc,,
+sqlalchemy/testing/plugin/bootstrap.py,sha256=oYScMbEW4pCnWlPEAq1insFruCXFQeEVBwo__i4McpU,1685
+sqlalchemy/testing/plugin/plugin_base.py,sha256=IR2tLVvW7dbAqagFYwUjsc2X1oVIRVp1GM_b6tZTQkw,21581
+sqlalchemy/testing/plugin/pytestplugin.py,sha256=xasjXXEMsT4RtKR7WzmEqzYXPxz83KSRaM2CwJQEQK8,27546
+sqlalchemy/testing/profiling.py,sha256=PbuPhRFbauFilUONeY3tV_Y_5lBkD7iCa8VVyH2Sk9Y,10148
+sqlalchemy/testing/provision.py,sha256=zXsw2D2Xpmw_chmYLsE1GXQqKQ-so3V8xU_joTcKan0,14619
+sqlalchemy/testing/requirements.py,sha256=N9pSj7z2wVMkBif-DQfPVa_cl9k6p9g_J5FY1OsWtrY,51817
+sqlalchemy/testing/schema.py,sha256=lr4GkGrGwagaHMuSGzWdzkMaj3HnS7dgfLLWfxt__-U,6513
+sqlalchemy/testing/suite/__init__.py,sha256=Y5DRNG0Yl1u3ypt9zVF0Z9suPZeuO_UQGLl-wRgvTjU,722
+sqlalchemy/testing/suite/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_cte.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_ddl.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_deprecations.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_dialect.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_insert.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_reflection.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_results.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_rowcount.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_select.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_sequence.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_types.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_unicode_ddl.cpython-312.pyc,,
+sqlalchemy/testing/suite/__pycache__/test_update_delete.cpython-312.pyc,,
+sqlalchemy/testing/suite/test_cte.py,sha256=6zBC3W2OwX1Xs-HedzchcKN2S7EaLNkgkvV_JSZ_Pq0,6451
+sqlalchemy/testing/suite/test_ddl.py,sha256=1Npkf0C_4UNxphthAGjG078n0vPEgnSIHpDu5MfokxQ,12031
+sqlalchemy/testing/suite/test_deprecations.py,sha256=BcJxZTcjYqeOAENVElCg3hVvU6fkGEW3KGBMfnW8bng,5337
+sqlalchemy/testing/suite/test_dialect.py,sha256=EH4ZQWbnGdtjmx5amZtTyhYmrkXJCvW1SQoLahoE7uk,22923
+sqlalchemy/testing/suite/test_insert.py,sha256=8ASo87s2pvaFkYZbmx5zYDGsRloyGpI9Zo5ut4ttM9g,18557
+sqlalchemy/testing/suite/test_reflection.py,sha256=_44jrB9iQ0PbQ3Aj2nz45eJIkDmCGJ5I8r4Q2-Km9Ww,106458
+sqlalchemy/testing/suite/test_results.py,sha256=NQ23m8FDVd0ub751jN4PswGoAhk5nrqvjHvpYULZXnc,15937
+sqlalchemy/testing/suite/test_rowcount.py,sha256=Ozu9NmGrsMWROGNdtE-KKOaa_WMHnvSy-qcnAYpG20Y,7903
+sqlalchemy/testing/suite/test_select.py,sha256=FvMFYQW9IJpDWGYZiJk46is6YrtmdSghBdTjZCG8T0Y,58574
+sqlalchemy/testing/suite/test_sequence.py,sha256=66bCoy4xo99GBSaX6Hxb88foANAykLGRz1YEKbvpfuA,9923
+sqlalchemy/testing/suite/test_types.py,sha256=rFmTOg6XuMch9L2-XthfLJRCTTwpZbMfrNss2g09gmc,65677
+sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=c3_eIxLyORuSOhNDP0jWKxPyUf3SwMFpdalxtquwqlM,6141
+sqlalchemy/testing/suite/test_update_delete.py,sha256=3aaM9LtV_qC8NzMcOn2BZHxnQ5eWJnFKOx6iQpGArSw,3914
+sqlalchemy/testing/util.py,sha256=BFiSp3CEX95Dr-vv4l_7ZRu5vjZi9hjjnp-JKNfuS5E,14080
+sqlalchemy/testing/warnings.py,sha256=fJ-QJUY2zY2PPxZJKv9medW-BKKbCNbA4Ns_V3YwFXM,1546
+sqlalchemy/types.py,sha256=cQFM-hFRmaf1GErun1qqgEs6QxufvzMuwKqj9tuMPpE,3168
+sqlalchemy/util/__init__.py,sha256=B3bedg-LSQEscwqgmYYU-VENUX8_zAE3q9vb7tkfJNY,8277
+sqlalchemy/util/__pycache__/__init__.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/_collections.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/_concurrency_py3k.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/_has_cy.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/_py_collections.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/compat.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/concurrency.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/deprecations.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/langhelpers.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/preloaded.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/queue.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/tool_support.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/topological.cpython-312.pyc,,
+sqlalchemy/util/__pycache__/typing.cpython-312.pyc,,
+sqlalchemy/util/_collections.py,sha256=py6nwBAgPC_pmbghrobaCVZZxmiVuLgqrbjdgqP_3jU,20111
+sqlalchemy/util/_concurrency_py3k.py,sha256=3jieKUZN8sS-RS6L0a2BUCUm_pqQMldbuFCxZ7xr1-Q,8602
+sqlalchemy/util/_has_cy.py,sha256=wCQmeSjT3jaH_oxfCEtGk-1g0gbSpt5MCK5UcWdMWqk,1247
+sqlalchemy/util/_py_collections.py,sha256=AZEigbk8jfiKjH3Rqy6BBmRwe8csZuRZ5lz2DYS-Cls,16738
+sqlalchemy/util/compat.py,sha256=R6bpBydldtbr6h7oJePihQxFb7jKiI-YDsK465MSOzk,8714
+sqlalchemy/util/concurrency.py,sha256=mhwHm0utriD14DRqxTBWgIW7QuwdSEiLgLiJdUjiR3w,2427
+sqlalchemy/util/deprecations.py,sha256=YBwvvYhSB8LhasIZRKvg_-WNoVhPUcaYI1ZrnjDn868,11971
+sqlalchemy/util/langhelpers.py,sha256=HEdM_PcZxoSXH9qlcvf5Vp1k4xKU2XJBwn0Aq9RVmeU,64980
+sqlalchemy/util/preloaded.py,sha256=az7NmLJLsqs0mtM9uBkIu10-841RYDq8wOyqJ7xXvqE,5904
+sqlalchemy/util/queue.py,sha256=_5qHVIvWluQQt0jiDHnxuT2FhUxnxH2cKDxCmwmTFe0,10205
+sqlalchemy/util/tool_support.py,sha256=9braZyidaiNrZVsWtGmkSmus50-byhuYrlAqvhjcmnA,6135
+sqlalchemy/util/topological.py,sha256=N3M3Le7KzGHCmqPGg0ZBqixTDGwmFLhOZvBtc4rHL_g,3458
+sqlalchemy/util/typing.py,sha256=-YKRlPyicpM94qhFAUj9eYnv2hnKHkhv0Gxq7jNPAvE,16255
diff --git a/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/REQUESTED b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/WHEEL b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/WHEEL
new file mode 100644
index 0000000..bd099b7
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/WHEEL
@@ -0,0 +1,6 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.42.0)
+Root-Is-Purelib: false
+Tag: cp312-cp312-manylinux_2_17_x86_64
+Tag: cp312-cp312-manylinux2014_x86_64
+
diff --git a/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/top_level.txt
new file mode 100644
index 0000000..39fb2be
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/SQLAlchemy-2.0.25.dist-info/top_level.txt
@@ -0,0 +1 @@
+sqlalchemy
diff --git a/venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc b/venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc
new file mode 100644
index 0000000..4491930
Binary files /dev/null and b/venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/__pycache__/six.cpython-312.pyc b/venv/lib/python3.12/site-packages/__pycache__/six.cpython-312.pyc
new file mode 100644
index 0000000..570556c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/__pycache__/six.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc b/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc
new file mode 100644
index 0000000..c4f4611
Binary files /dev/null and b/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__init__.py b/venv/lib/python3.12/site-packages/_pytest/__init__.py
new file mode 100644
index 0000000..8a406c5
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/__init__.py
@@ -0,0 +1,9 @@
+__all__ = ["__version__", "version_tuple"]
+
+try:
+ from ._version import version as __version__, version_tuple
+except ImportError: # pragma: no cover
+ # broken installation, we don't even try
+ # unknown only works because we do poor mans version compare
+ __version__ = "unknown"
+ version_tuple = (0, 0, "unknown") # type:ignore[assignment]
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..767fe45
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/_argcomplete.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/_argcomplete.cpython-312.pyc
new file mode 100644
index 0000000..62a710c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/_argcomplete.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/_version.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/_version.cpython-312.pyc
new file mode 100644
index 0000000..27232d1
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/_version.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/cacheprovider.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/cacheprovider.cpython-312.pyc
new file mode 100644
index 0000000..fb8c0c9
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/cacheprovider.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/capture.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/capture.cpython-312.pyc
new file mode 100644
index 0000000..43bcfc4
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/capture.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/compat.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/compat.cpython-312.pyc
new file mode 100644
index 0000000..313fb67
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/compat.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/debugging.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/debugging.cpython-312.pyc
new file mode 100644
index 0000000..91a6d85
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/debugging.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/deprecated.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/deprecated.cpython-312.pyc
new file mode 100644
index 0000000..087bbc1
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/deprecated.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/doctest.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/doctest.cpython-312.pyc
new file mode 100644
index 0000000..e9a1134
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/doctest.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/faulthandler.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/faulthandler.cpython-312.pyc
new file mode 100644
index 0000000..721c8c5
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/faulthandler.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/fixtures.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/fixtures.cpython-312.pyc
new file mode 100644
index 0000000..9ea95a2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/fixtures.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/freeze_support.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/freeze_support.cpython-312.pyc
new file mode 100644
index 0000000..6d45f42
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/freeze_support.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/helpconfig.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/helpconfig.cpython-312.pyc
new file mode 100644
index 0000000..c307fd9
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/helpconfig.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/hookspec.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/hookspec.cpython-312.pyc
new file mode 100644
index 0000000..dcbdf63
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/hookspec.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/junitxml.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/junitxml.cpython-312.pyc
new file mode 100644
index 0000000..c5536fe
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/junitxml.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/legacypath.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/legacypath.cpython-312.pyc
new file mode 100644
index 0000000..4e52410
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/legacypath.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/logging.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/logging.cpython-312.pyc
new file mode 100644
index 0000000..d20a82c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/logging.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/main.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/main.cpython-312.pyc
new file mode 100644
index 0000000..2ad548c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/main.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/monkeypatch.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/monkeypatch.cpython-312.pyc
new file mode 100644
index 0000000..defecd5
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/monkeypatch.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/nodes.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/nodes.cpython-312.pyc
new file mode 100644
index 0000000..e5b920b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/nodes.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/nose.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/nose.cpython-312.pyc
new file mode 100644
index 0000000..66a5600
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/nose.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/outcomes.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/outcomes.cpython-312.pyc
new file mode 100644
index 0000000..b7d53fb
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/outcomes.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/pastebin.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pastebin.cpython-312.pyc
new file mode 100644
index 0000000..552819e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pastebin.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/pathlib.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pathlib.cpython-312.pyc
new file mode 100644
index 0000000..cdd3060
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pathlib.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester.cpython-312.pyc
new file mode 100644
index 0000000..db8e13b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester_assertions.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester_assertions.cpython-312.pyc
new file mode 100644
index 0000000..1477e2f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/pytester_assertions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/python.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/python.cpython-312.pyc
new file mode 100644
index 0000000..f7e3d27
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/python.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_api.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_api.cpython-312.pyc
new file mode 100644
index 0000000..18fd6dd
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_api.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_path.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_path.cpython-312.pyc
new file mode 100644
index 0000000..19e40ab
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/python_path.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/recwarn.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/recwarn.cpython-312.pyc
new file mode 100644
index 0000000..8f43897
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/recwarn.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/reports.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/reports.cpython-312.pyc
new file mode 100644
index 0000000..e71814a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/reports.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/runner.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/runner.cpython-312.pyc
new file mode 100644
index 0000000..b7c548b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/runner.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/scope.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/scope.cpython-312.pyc
new file mode 100644
index 0000000..e0503a6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/scope.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/setuponly.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/setuponly.cpython-312.pyc
new file mode 100644
index 0000000..fe75747
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/setuponly.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/setupplan.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/setupplan.cpython-312.pyc
new file mode 100644
index 0000000..fe93efc
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/setupplan.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/skipping.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/skipping.cpython-312.pyc
new file mode 100644
index 0000000..1cc4820
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/skipping.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/stash.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/stash.cpython-312.pyc
new file mode 100644
index 0000000..d66b93f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/stash.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/stepwise.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/stepwise.cpython-312.pyc
new file mode 100644
index 0000000..42c6271
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/stepwise.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/terminal.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/terminal.cpython-312.pyc
new file mode 100644
index 0000000..6b2f65e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/terminal.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/threadexception.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/threadexception.cpython-312.pyc
new file mode 100644
index 0000000..d67981a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/threadexception.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/timing.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/timing.cpython-312.pyc
new file mode 100644
index 0000000..a91cd67
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/timing.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/tmpdir.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/tmpdir.cpython-312.pyc
new file mode 100644
index 0000000..652df09
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/tmpdir.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/unittest.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/unittest.cpython-312.pyc
new file mode 100644
index 0000000..06a0820
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/unittest.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/unraisableexception.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/unraisableexception.cpython-312.pyc
new file mode 100644
index 0000000..45d2d9d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/unraisableexception.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/warning_types.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/warning_types.cpython-312.pyc
new file mode 100644
index 0000000..0e987c7
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/warning_types.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/__pycache__/warnings.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/__pycache__/warnings.cpython-312.pyc
new file mode 100644
index 0000000..d0e36f9
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/__pycache__/warnings.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_argcomplete.py b/venv/lib/python3.12/site-packages/_pytest/_argcomplete.py
new file mode 100644
index 0000000..6a80837
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_argcomplete.py
@@ -0,0 +1,116 @@
+"""Allow bash-completion for argparse with argcomplete if installed.
+
+Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
+to find the magic string, so _ARGCOMPLETE env. var is never set, and
+this does not need special code).
+
+Function try_argcomplete(parser) should be called directly before
+the call to ArgumentParser.parse_args().
+
+The filescompleter is what you normally would use on the positional
+arguments specification, in order to get "dirname/" after "dirn"
+instead of the default "dirname ":
+
+ optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
+
+Other, application specific, completers should go in the file
+doing the add_argument calls as they need to be specified as .completer
+attributes as well. (If argcomplete is not installed, the function the
+attribute points to will not be used).
+
+SPEEDUP
+=======
+
+The generic argcomplete script for bash-completion
+(/etc/bash_completion.d/python-argcomplete.sh)
+uses a python program to determine startup script generated by pip.
+You can speed up completion somewhat by changing this script to include
+ # PYTHON_ARGCOMPLETE_OK
+so the python-argcomplete-check-easy-install-script does not
+need to be called to find the entry point of the code and see if that is
+marked with PYTHON_ARGCOMPLETE_OK.
+
+INSTALL/DEBUGGING
+=================
+
+To include this support in another application that has setup.py generated
+scripts:
+
+- Add the line:
+ # PYTHON_ARGCOMPLETE_OK
+ near the top of the main python entry point.
+
+- Include in the file calling parse_args():
+ from _argcomplete import try_argcomplete, filescompleter
+ Call try_argcomplete just before parse_args(), and optionally add
+ filescompleter to the positional arguments' add_argument().
+
+If things do not work right away:
+
+- Switch on argcomplete debugging with (also helpful when doing custom
+ completers):
+ export _ARC_DEBUG=1
+
+- Run:
+ python-argcomplete-check-easy-install-script $(which appname)
+ echo $?
+ will echo 0 if the magic line has been found, 1 if not.
+
+- Sometimes it helps to find early on errors using:
+ _ARGCOMPLETE=1 _ARC_DEBUG=1 appname
+ which should throw a KeyError: 'COMPLINE' (which is properly set by the
+ global argcomplete script).
+"""
+import argparse
+import os
+import sys
+from glob import glob
+from typing import Any
+from typing import List
+from typing import Optional
+
+
+class FastFilesCompleter:
+ """Fast file completer class."""
+
+ def __init__(self, directories: bool = True) -> None:
+ self.directories = directories
+
+ def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
+ # Only called on non option completions.
+ if os.sep in prefix[1:]:
+ prefix_dir = len(os.path.dirname(prefix) + os.sep)
+ else:
+ prefix_dir = 0
+ completion = []
+ globbed = []
+ if "*" not in prefix and "?" not in prefix:
+ # We are on unix, otherwise no bash.
+ if not prefix or prefix[-1] == os.sep:
+ globbed.extend(glob(prefix + ".*"))
+ prefix += "*"
+ globbed.extend(glob(prefix))
+ for x in sorted(globbed):
+ if os.path.isdir(x):
+ x += "/"
+ # Append stripping the prefix (like bash, not like compgen).
+ completion.append(x[prefix_dir:])
+ return completion
+
+
+if os.environ.get("_ARGCOMPLETE"):
+ try:
+ import argcomplete.completers
+ except ImportError:
+ sys.exit(-1)
+ filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter()
+
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
+ argcomplete.autocomplete(parser, always_complete_options=False)
+
+else:
+
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
+ pass
+
+ filescompleter = None
diff --git a/venv/lib/python3.12/site-packages/_pytest/_code/__init__.py b/venv/lib/python3.12/site-packages/_pytest/_code/__init__.py
new file mode 100644
index 0000000..511d0dd
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_code/__init__.py
@@ -0,0 +1,22 @@
+"""Python inspection/code generation API."""
+from .code import Code
+from .code import ExceptionInfo
+from .code import filter_traceback
+from .code import Frame
+from .code import getfslineno
+from .code import Traceback
+from .code import TracebackEntry
+from .source import getrawcode
+from .source import Source
+
+__all__ = [
+ "Code",
+ "ExceptionInfo",
+ "filter_traceback",
+ "Frame",
+ "getfslineno",
+ "getrawcode",
+ "Traceback",
+ "TracebackEntry",
+ "Source",
+]
diff --git a/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..08a99da
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/code.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/code.cpython-312.pyc
new file mode 100644
index 0000000..5e5c5dc
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/code.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/source.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/source.cpython-312.pyc
new file mode 100644
index 0000000..e40447b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_code/__pycache__/source.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_code/code.py b/venv/lib/python3.12/site-packages/_pytest/_code/code.py
new file mode 100644
index 0000000..9b05133
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_code/code.py
@@ -0,0 +1,1337 @@
+import ast
+import dataclasses
+import inspect
+import os
+import re
+import sys
+import traceback
+from inspect import CO_VARARGS
+from inspect import CO_VARKEYWORDS
+from io import StringIO
+from pathlib import Path
+from traceback import format_exception_only
+from types import CodeType
+from types import FrameType
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import ClassVar
+from typing import Dict
+from typing import Generic
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Pattern
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+import pluggy
+
+import _pytest
+from _pytest._code.source import findsource
+from _pytest._code.source import getrawcode
+from _pytest._code.source import getstatementrange_ast
+from _pytest._code.source import Source
+from _pytest._io import TerminalWriter
+from _pytest._io.saferepr import safeformat
+from _pytest._io.saferepr import saferepr
+from _pytest.compat import final
+from _pytest.compat import get_real_func
+from _pytest.deprecated import check_ispytest
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+ from typing_extensions import Literal
+ from typing_extensions import SupportsIndex
+
+ _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
+
+if sys.version_info[:2] < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+
+class Code:
+ """Wrapper around Python code objects."""
+
+ __slots__ = ("raw",)
+
+ def __init__(self, obj: CodeType) -> None:
+ self.raw = obj
+
+ @classmethod
+ def from_function(cls, obj: object) -> "Code":
+ return cls(getrawcode(obj))
+
+ def __eq__(self, other):
+ return self.raw == other.raw
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @property
+ def firstlineno(self) -> int:
+ return self.raw.co_firstlineno - 1
+
+ @property
+ def name(self) -> str:
+ return self.raw.co_name
+
+ @property
+ def path(self) -> Union[Path, str]:
+ """Return a path object pointing to source code, or an ``str`` in
+ case of ``OSError`` / non-existing file."""
+ if not self.raw.co_filename:
+ return ""
+ try:
+ p = absolutepath(self.raw.co_filename)
+ # maybe don't try this checking
+ if not p.exists():
+ raise OSError("path check failed.")
+ return p
+ except OSError:
+ # XXX maybe try harder like the weird logic
+ # in the standard lib [linecache.updatecache] does?
+ return self.raw.co_filename
+
+ @property
+ def fullsource(self) -> Optional["Source"]:
+ """Return a _pytest._code.Source object for the full source file of the code."""
+ full, _ = findsource(self.raw)
+ return full
+
+ def source(self) -> "Source":
+ """Return a _pytest._code.Source object for the code object's source only."""
+ # return source only for that part of code
+ return Source(self.raw)
+
+ def getargs(self, var: bool = False) -> Tuple[str, ...]:
+ """Return a tuple with the argument names for the code object.
+
+ If 'var' is set True also return the names of the variable and
+ keyword arguments when present.
+ """
+ # Handy shortcut for getting args.
+ raw = self.raw
+ argcount = raw.co_argcount
+ if var:
+ argcount += raw.co_flags & CO_VARARGS
+ argcount += raw.co_flags & CO_VARKEYWORDS
+ return raw.co_varnames[:argcount]
+
+
+class Frame:
+ """Wrapper around a Python frame holding f_locals and f_globals
+ in which expressions can be evaluated."""
+
+ __slots__ = ("raw",)
+
+ def __init__(self, frame: FrameType) -> None:
+ self.raw = frame
+
+ @property
+ def lineno(self) -> int:
+ return self.raw.f_lineno - 1
+
+ @property
+ def f_globals(self) -> Dict[str, Any]:
+ return self.raw.f_globals
+
+ @property
+ def f_locals(self) -> Dict[str, Any]:
+ return self.raw.f_locals
+
+ @property
+ def code(self) -> Code:
+ return Code(self.raw.f_code)
+
+ @property
+ def statement(self) -> "Source":
+ """Statement this frame is at."""
+ if self.code.fullsource is None:
+ return Source("")
+ return self.code.fullsource.getstatement(self.lineno)
+
+ def eval(self, code, **vars):
+ """Evaluate 'code' in the frame.
+
+ 'vars' are optional additional local variables.
+
+ Returns the result of the evaluation.
+ """
+ f_locals = self.f_locals.copy()
+ f_locals.update(vars)
+ return eval(code, self.f_globals, f_locals)
+
+ def repr(self, object: object) -> str:
+ """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
+ return saferepr(object)
+
+ def getargs(self, var: bool = False):
+ """Return a list of tuples (name, value) for all arguments.
+
+ If 'var' is set True, also include the variable and keyword arguments
+ when present.
+ """
+ retval = []
+ for arg in self.code.getargs(var):
+ try:
+ retval.append((arg, self.f_locals[arg]))
+ except KeyError:
+ pass # this can occur when using Psyco
+ return retval
+
+
+class TracebackEntry:
+ """A single entry in a Traceback."""
+
+ __slots__ = ("_rawentry", "_repr_style")
+
+ def __init__(
+ self,
+ rawentry: TracebackType,
+ repr_style: Optional['Literal["short", "long"]'] = None,
+ ) -> None:
+ self._rawentry: "Final" = rawentry
+ self._repr_style: "Final" = repr_style
+
+ def with_repr_style(
+ self, repr_style: Optional['Literal["short", "long"]']
+ ) -> "TracebackEntry":
+ return TracebackEntry(self._rawentry, repr_style)
+
+ @property
+ def lineno(self) -> int:
+ return self._rawentry.tb_lineno - 1
+
+ @property
+ def frame(self) -> Frame:
+ return Frame(self._rawentry.tb_frame)
+
+ @property
+ def relline(self) -> int:
+ return self.lineno - self.frame.code.firstlineno
+
+ def __repr__(self) -> str:
+ return "" % (self.frame.code.path, self.lineno + 1)
+
+ @property
+ def statement(self) -> "Source":
+ """_pytest._code.Source object for the current statement."""
+ source = self.frame.code.fullsource
+ assert source is not None
+ return source.getstatement(self.lineno)
+
+ @property
+ def path(self) -> Union[Path, str]:
+ """Path to the source code."""
+ return self.frame.code.path
+
+ @property
+ def locals(self) -> Dict[str, Any]:
+ """Locals of underlying frame."""
+ return self.frame.f_locals
+
+ def getfirstlinesource(self) -> int:
+ return self.frame.code.firstlineno
+
+ def getsource(
+ self, astcache: Optional[Dict[Union[str, Path], ast.AST]] = None
+ ) -> Optional["Source"]:
+ """Return failing source code."""
+ # we use the passed in astcache to not reparse asttrees
+ # within exception info printing
+ source = self.frame.code.fullsource
+ if source is None:
+ return None
+ key = astnode = None
+ if astcache is not None:
+ key = self.frame.code.path
+ if key is not None:
+ astnode = astcache.get(key, None)
+ start = self.getfirstlinesource()
+ try:
+ astnode, _, end = getstatementrange_ast(
+ self.lineno, source, astnode=astnode
+ )
+ except SyntaxError:
+ end = self.lineno + 1
+ else:
+ if key is not None and astcache is not None:
+ astcache[key] = astnode
+ return source[start:end]
+
+ source = property(getsource)
+
+ def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
+ """Return True if the current frame has a var __tracebackhide__
+ resolving to True.
+
+ If __tracebackhide__ is a callable, it gets called with the
+ ExceptionInfo instance and can decide whether to hide the traceback.
+
+ Mostly for internal use.
+ """
+ tbh: Union[
+ bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]
+ ] = False
+ for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
+ # in normal cases, f_locals and f_globals are dictionaries
+ # however via `exec(...)` / `eval(...)` they can be other types
+ # (even incorrect types!).
+ # as such, we suppress all exceptions while accessing __tracebackhide__
+ try:
+ tbh = maybe_ns_dct["__tracebackhide__"]
+ except Exception:
+ pass
+ else:
+ break
+ if tbh and callable(tbh):
+ return tbh(excinfo)
+ return tbh
+
+ def __str__(self) -> str:
+ name = self.frame.code.name
+ try:
+ line = str(self.statement).lstrip()
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ line = "???"
+ # This output does not quite match Python's repr for traceback entries,
+ # but changing it to do so would break certain plugins. See
+ # https://github.com/pytest-dev/pytest/pull/7535/ for details.
+ return " File %r:%d in %s\n %s\n" % (
+ str(self.path),
+ self.lineno + 1,
+ name,
+ line,
+ )
+
+ @property
+ def name(self) -> str:
+ """co_name of underlying code."""
+ return self.frame.code.raw.co_name
+
+
+class Traceback(List[TracebackEntry]):
+ """Traceback objects encapsulate and offer higher level access to Traceback entries."""
+
+ def __init__(
+ self,
+ tb: Union[TracebackType, Iterable[TracebackEntry]],
+ ) -> None:
+ """Initialize from given python traceback object and ExceptionInfo."""
+ if isinstance(tb, TracebackType):
+
+ def f(cur: TracebackType) -> Iterable[TracebackEntry]:
+ cur_: Optional[TracebackType] = cur
+ while cur_ is not None:
+ yield TracebackEntry(cur_)
+ cur_ = cur_.tb_next
+
+ super().__init__(f(tb))
+ else:
+ super().__init__(tb)
+
+ def cut(
+ self,
+ path: Optional[Union["os.PathLike[str]", str]] = None,
+ lineno: Optional[int] = None,
+ firstlineno: Optional[int] = None,
+ excludepath: Optional["os.PathLike[str]"] = None,
+ ) -> "Traceback":
+ """Return a Traceback instance wrapping part of this Traceback.
+
+ By providing any combination of path, lineno and firstlineno, the
+ first frame to start the to-be-returned traceback is determined.
+
+ This allows cutting the first part of a Traceback instance e.g.
+ for formatting reasons (removing some uninteresting bits that deal
+ with handling of the exception/traceback).
+ """
+ path_ = None if path is None else os.fspath(path)
+ excludepath_ = None if excludepath is None else os.fspath(excludepath)
+ for x in self:
+ code = x.frame.code
+ codepath = code.path
+ if path is not None and str(codepath) != path_:
+ continue
+ if (
+ excludepath is not None
+ and isinstance(codepath, Path)
+ and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
+ ):
+ continue
+ if lineno is not None and x.lineno != lineno:
+ continue
+ if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
+ continue
+ return Traceback(x._rawentry)
+ return self
+
+ @overload
+ def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:
+ ...
+
+ @overload
+ def __getitem__(self, key: slice) -> "Traceback":
+ ...
+
+ def __getitem__(
+ self, key: Union["SupportsIndex", slice]
+ ) -> Union[TracebackEntry, "Traceback"]:
+ if isinstance(key, slice):
+ return self.__class__(super().__getitem__(key))
+ else:
+ return super().__getitem__(key)
+
+ def filter(
+ self,
+ # TODO(py38): change to positional only.
+ _excinfo_or_fn: Union[
+ "ExceptionInfo[BaseException]",
+ Callable[[TracebackEntry], bool],
+ ],
+ ) -> "Traceback":
+ """Return a Traceback instance with certain items removed.
+
+ If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
+ which are hidden (see ishidden() above).
+
+ Otherwise, the filter is a function that gets a single argument, a
+ ``TracebackEntry`` instance, and should return True when the item should
+ be added to the ``Traceback``, False when not.
+ """
+ if isinstance(_excinfo_or_fn, ExceptionInfo):
+ fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
+ else:
+ fn = _excinfo_or_fn
+ return Traceback(filter(fn, self))
+
+ def recursionindex(self) -> Optional[int]:
+ """Return the index of the frame/TracebackEntry where recursion originates if
+ appropriate, None if no recursion occurred."""
+ cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {}
+ for i, entry in enumerate(self):
+ # id for the code.raw is needed to work around
+ # the strange metaprogramming in the decorator lib from pypi
+ # which generates code objects that have hash/value equality
+ # XXX needs a test
+ key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
+ # print "checking for recursion at", key
+ values = cache.setdefault(key, [])
+ if values:
+ f = entry.frame
+ loc = f.f_locals
+ for otherloc in values:
+ if otherloc == loc:
+ return i
+ values.append(entry.frame.f_locals)
+ return None
+
+
+E = TypeVar("E", bound=BaseException, covariant=True)
+
+
+@final
+@dataclasses.dataclass
+class ExceptionInfo(Generic[E]):
+ """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
+
+ _assert_start_repr: ClassVar = "AssertionError('assert "
+
+ _excinfo: Optional[Tuple[Type["E"], "E", TracebackType]]
+ _striptext: str
+ _traceback: Optional[Traceback]
+
+ def __init__(
+ self,
+ excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],
+ striptext: str = "",
+ traceback: Optional[Traceback] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._excinfo = excinfo
+ self._striptext = striptext
+ self._traceback = traceback
+
+ @classmethod
+ def from_exception(
+ cls,
+ # Ignoring error: "Cannot use a covariant type variable as a parameter".
+ # This is OK to ignore because this class is (conceptually) readonly.
+ # See https://github.com/python/mypy/issues/7049.
+ exception: E, # type: ignore[misc]
+ exprinfo: Optional[str] = None,
+ ) -> "ExceptionInfo[E]":
+ """Return an ExceptionInfo for an existing exception.
+
+ The exception must have a non-``None`` ``__traceback__`` attribute,
+ otherwise this function fails with an assertion error. This means that
+ the exception must have been raised, or added a traceback with the
+ :py:meth:`~BaseException.with_traceback()` method.
+
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
+
+ .. versionadded:: 7.4
+ """
+ assert (
+ exception.__traceback__
+ ), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
+ exc_info = (type(exception), exception, exception.__traceback__)
+ return cls.from_exc_info(exc_info, exprinfo)
+
+ @classmethod
+ def from_exc_info(
+ cls,
+ exc_info: Tuple[Type[E], E, TracebackType],
+ exprinfo: Optional[str] = None,
+ ) -> "ExceptionInfo[E]":
+ """Like :func:`from_exception`, but using old-style exc_info tuple."""
+ _striptext = ""
+ if exprinfo is None and isinstance(exc_info[1], AssertionError):
+ exprinfo = getattr(exc_info[1], "msg", None)
+ if exprinfo is None:
+ exprinfo = saferepr(exc_info[1])
+ if exprinfo and exprinfo.startswith(cls._assert_start_repr):
+ _striptext = "AssertionError: "
+
+ return cls(exc_info, _striptext, _ispytest=True)
+
+ @classmethod
+ def from_current(
+ cls, exprinfo: Optional[str] = None
+ ) -> "ExceptionInfo[BaseException]":
+ """Return an ExceptionInfo matching the current traceback.
+
+ .. warning::
+
+ Experimental API
+
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
+ """
+ tup = sys.exc_info()
+ assert tup[0] is not None, "no current exception"
+ assert tup[1] is not None, "no current exception"
+ assert tup[2] is not None, "no current exception"
+ exc_info = (tup[0], tup[1], tup[2])
+ return ExceptionInfo.from_exc_info(exc_info, exprinfo)
+
+ @classmethod
+ def for_later(cls) -> "ExceptionInfo[E]":
+ """Return an unfilled ExceptionInfo."""
+ return cls(None, _ispytest=True)
+
+ def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
+ """Fill an unfilled ExceptionInfo created with ``for_later()``."""
+ assert self._excinfo is None, "ExceptionInfo was already filled"
+ self._excinfo = exc_info
+
+ @property
+ def type(self) -> Type[E]:
+ """The exception class."""
+ assert (
+ self._excinfo is not None
+ ), ".type can only be used after the context manager exits"
+ return self._excinfo[0]
+
+ @property
+ def value(self) -> E:
+ """The exception value."""
+ assert (
+ self._excinfo is not None
+ ), ".value can only be used after the context manager exits"
+ return self._excinfo[1]
+
+ @property
+ def tb(self) -> TracebackType:
+ """The exception raw traceback."""
+ assert (
+ self._excinfo is not None
+ ), ".tb can only be used after the context manager exits"
+ return self._excinfo[2]
+
+ @property
+ def typename(self) -> str:
+ """The type name of the exception."""
+ assert (
+ self._excinfo is not None
+ ), ".typename can only be used after the context manager exits"
+ return self.type.__name__
+
+ @property
+ def traceback(self) -> Traceback:
+ """The traceback."""
+ if self._traceback is None:
+ self._traceback = Traceback(self.tb)
+ return self._traceback
+
+ @traceback.setter
+ def traceback(self, value: Traceback) -> None:
+ self._traceback = value
+
+ def __repr__(self) -> str:
+ if self._excinfo is None:
+ return ""
+ return "<{} {} tblen={}>".format(
+ self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
+ )
+
+ def exconly(self, tryshort: bool = False) -> str:
+ """Return the exception as a string.
+
+ When 'tryshort' resolves to True, and the exception is an
+ AssertionError, only the actual exception part of the exception
+ representation is returned (so 'AssertionError: ' is removed from
+ the beginning).
+ """
+ lines = format_exception_only(self.type, self.value)
+ text = "".join(lines)
+ text = text.rstrip()
+ if tryshort:
+ if text.startswith(self._striptext):
+ text = text[len(self._striptext) :]
+ return text
+
+ def errisinstance(
+ self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]]
+ ) -> bool:
+ """Return True if the exception is an instance of exc.
+
+ Consider using ``isinstance(excinfo.value, exc)`` instead.
+ """
+ return isinstance(self.value, exc)
+
+ def _getreprcrash(self) -> Optional["ReprFileLocation"]:
+ # Find last non-hidden traceback entry that led to the exception of the
+ # traceback, or None if all hidden.
+ for i in range(-1, -len(self.traceback) - 1, -1):
+ entry = self.traceback[i]
+ if not entry.ishidden(self):
+ path, lineno = entry.frame.code.raw.co_filename, entry.lineno
+ exconly = self.exconly(tryshort=True)
+ return ReprFileLocation(path, lineno + 1, exconly)
+ return None
+
+ def getrepr(
+ self,
+ showlocals: bool = False,
+ style: "_TracebackStyle" = "long",
+ abspath: bool = False,
+ tbfilter: Union[
+ bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
+ ] = True,
+ funcargs: bool = False,
+ truncate_locals: bool = True,
+ chain: bool = True,
+ ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
+ """Return str()able representation of this exception info.
+
+ :param bool showlocals:
+ Show locals per traceback entry.
+ Ignored if ``style=="native"``.
+
+ :param str style:
+ long|short|line|no|native|value traceback style.
+
+ :param bool abspath:
+ If paths should be changed to absolute or left unchanged.
+
+ :param tbfilter:
+ A filter for traceback entries.
+
+ * If false, don't hide any entries.
+ * If true, hide internal entries and entries that contain a local
+ variable ``__tracebackhide__ = True``.
+ * If a callable, delegates the filtering to the callable.
+
+ Ignored if ``style`` is ``"native"``.
+
+ :param bool funcargs:
+ Show fixtures ("funcargs" for legacy purposes) per traceback entry.
+
+ :param bool truncate_locals:
+ With ``showlocals==True``, make sure locals can be safely represented as strings.
+
+ :param bool chain:
+ If chained exceptions in Python 3 should be shown.
+
+ .. versionchanged:: 3.9
+
+ Added the ``chain`` parameter.
+ """
+ if style == "native":
+ return ReprExceptionInfo(
+ reprtraceback=ReprTracebackNative(
+ traceback.format_exception(
+ self.type,
+ self.value,
+ self.traceback[0]._rawentry if self.traceback else None,
+ )
+ ),
+ reprcrash=self._getreprcrash(),
+ )
+
+ fmt = FormattedExcinfo(
+ showlocals=showlocals,
+ style=style,
+ abspath=abspath,
+ tbfilter=tbfilter,
+ funcargs=funcargs,
+ truncate_locals=truncate_locals,
+ chain=chain,
+ )
+ return fmt.repr_excinfo(self)
+
+ def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
+ """Check whether the regular expression `regexp` matches the string
+ representation of the exception using :func:`python:re.search`.
+
+ If it matches `True` is returned, otherwise an `AssertionError` is raised.
+ """
+ __tracebackhide__ = True
+ value = str(self.value)
+ msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
+ if regexp == value:
+ msg += "\n Did you mean to `re.escape()` the regex?"
+ assert re.search(regexp, value), msg
+ # Return True to allow for "assert excinfo.match()".
+ return True
+
+
+@dataclasses.dataclass
+class FormattedExcinfo:
+ """Presenting information about failing Functions and Generators."""
+
+ # for traceback entries
+ flow_marker: ClassVar = ">"
+ fail_marker: ClassVar = "E"
+
+ showlocals: bool = False
+ style: "_TracebackStyle" = "long"
+ abspath: bool = True
+ tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
+ funcargs: bool = False
+ truncate_locals: bool = True
+ chain: bool = True
+ astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
+ default_factory=dict, init=False, repr=False
+ )
+
+ def _getindent(self, source: "Source") -> int:
+ # Figure out indent for the given source.
+ try:
+ s = str(source.getstatement(len(source) - 1))
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ try:
+ s = str(source[-1])
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ return 0
+ return 4 + (len(s) - len(s.lstrip()))
+
+ def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:
+ source = entry.getsource(self.astcache)
+ if source is not None:
+ source = source.deindent()
+ return source
+
+ def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:
+ if self.funcargs:
+ args = []
+ for argname, argvalue in entry.frame.getargs(var=True):
+ args.append((argname, saferepr(argvalue)))
+ return ReprFuncArgs(args)
+ return None
+
+ def get_source(
+ self,
+ source: Optional["Source"],
+ line_index: int = -1,
+ excinfo: Optional[ExceptionInfo[BaseException]] = None,
+ short: bool = False,
+ ) -> List[str]:
+ """Return formatted and marked up source lines."""
+ lines = []
+ if source is not None and line_index < 0:
+ line_index += len(source)
+ if source is None or line_index >= len(source.lines) or line_index < 0:
+ # `line_index` could still be outside `range(len(source.lines))` if
+ # we're processing AST with pathological position attributes.
+ source = Source("???")
+ line_index = 0
+ space_prefix = " "
+ if short:
+ lines.append(space_prefix + source.lines[line_index].strip())
+ else:
+ for line in source.lines[:line_index]:
+ lines.append(space_prefix + line)
+ lines.append(self.flow_marker + " " + source.lines[line_index])
+ for line in source.lines[line_index + 1 :]:
+ lines.append(space_prefix + line)
+ if excinfo is not None:
+ indent = 4 if short else self._getindent(source)
+ lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
+ return lines
+
+ def get_exconly(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ indent: int = 4,
+ markall: bool = False,
+ ) -> List[str]:
+ lines = []
+ indentstr = " " * indent
+ # Get the real exception information out.
+ exlines = excinfo.exconly(tryshort=True).split("\n")
+ failindent = self.fail_marker + indentstr[1:]
+ for line in exlines:
+ lines.append(failindent + line)
+ if not markall:
+ failindent = indentstr
+ return lines
+
+ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
+ if self.showlocals:
+ lines = []
+ keys = [loc for loc in locals if loc[0] != "@"]
+ keys.sort()
+ for name in keys:
+ value = locals[name]
+ if name == "__builtins__":
+ lines.append("__builtins__ = ")
+ else:
+ # This formatting could all be handled by the
+ # _repr() function, which is only reprlib.Repr in
+ # disguise, so is very configurable.
+ if self.truncate_locals:
+ str_repr = saferepr(value)
+ else:
+ str_repr = safeformat(value)
+ # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
+ lines.append(f"{name:<10} = {str_repr}")
+ # else:
+ # self._line("%-10s =\\" % (name,))
+ # # XXX
+ # pprint.pprint(value, stream=self.excinfowriter)
+ return ReprLocals(lines)
+ return None
+
+ def repr_traceback_entry(
+ self,
+ entry: Optional[TracebackEntry],
+ excinfo: Optional[ExceptionInfo[BaseException]] = None,
+ ) -> "ReprEntry":
+ lines: List[str] = []
+ style = (
+ entry._repr_style
+ if entry is not None and entry._repr_style is not None
+ else self.style
+ )
+ if style in ("short", "long") and entry is not None:
+ source = self._getentrysource(entry)
+ if source is None:
+ source = Source("???")
+ line_index = 0
+ else:
+ line_index = entry.lineno - entry.getfirstlinesource()
+ short = style == "short"
+ reprargs = self.repr_args(entry) if not short else None
+ s = self.get_source(source, line_index, excinfo, short=short)
+ lines.extend(s)
+ if short:
+ message = "in %s" % (entry.name)
+ else:
+ message = excinfo and excinfo.typename or ""
+ entry_path = entry.path
+ path = self._makepath(entry_path)
+ reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
+ localsrepr = self.repr_locals(entry.locals)
+ return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
+ elif style == "value":
+ if excinfo:
+ lines.extend(str(excinfo.value).split("\n"))
+ return ReprEntry(lines, None, None, None, style)
+ else:
+ if excinfo:
+ lines.extend(self.get_exconly(excinfo, indent=4))
+ return ReprEntry(lines, None, None, None, style)
+
+ def _makepath(self, path: Union[Path, str]) -> str:
+ if not self.abspath and isinstance(path, Path):
+ try:
+ np = bestrelpath(Path.cwd(), path)
+ except OSError:
+ return str(path)
+ if len(np) < len(str(path)):
+ return np
+ return str(path)
+
+ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
+ traceback = excinfo.traceback
+ if callable(self.tbfilter):
+ traceback = self.tbfilter(excinfo)
+ elif self.tbfilter:
+ traceback = traceback.filter(excinfo)
+
+ if isinstance(excinfo.value, RecursionError):
+ traceback, extraline = self._truncate_recursive_traceback(traceback)
+ else:
+ extraline = None
+
+ if not traceback:
+ if extraline is None:
+ extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
+ entries = [self.repr_traceback_entry(None, excinfo)]
+ return ReprTraceback(entries, extraline, style=self.style)
+
+ last = traceback[-1]
+ if self.style == "value":
+ entries = [self.repr_traceback_entry(last, excinfo)]
+ return ReprTraceback(entries, None, style=self.style)
+
+ entries = [
+ self.repr_traceback_entry(entry, excinfo if last == entry else None)
+ for entry in traceback
+ ]
+ return ReprTraceback(entries, extraline, style=self.style)
+
+ def _truncate_recursive_traceback(
+ self, traceback: Traceback
+ ) -> Tuple[Traceback, Optional[str]]:
+ """Truncate the given recursive traceback trying to find the starting
+ point of the recursion.
+
+ The detection is done by going through each traceback entry and
+ finding the point in which the locals of the frame are equal to the
+ locals of a previous frame (see ``recursionindex()``).
+
+ Handle the situation where the recursion process might raise an
+ exception (for example comparing numpy arrays using equality raises a
+ TypeError), in which case we do our best to warn the user of the
+ error and show a limited traceback.
+ """
+ try:
+ recursionindex = traceback.recursionindex()
+ except Exception as e:
+ max_frames = 10
+ extraline: Optional[str] = (
+ "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
+ " The following exception happened when comparing locals in the stack frame:\n"
+ " {exc_type}: {exc_msg}\n"
+ " Displaying first and last {max_frames} stack frames out of {total}."
+ ).format(
+ exc_type=type(e).__name__,
+ exc_msg=str(e),
+ max_frames=max_frames,
+ total=len(traceback),
+ )
+ # Type ignored because adding two instances of a List subtype
+ # currently incorrectly has type List instead of the subtype.
+ traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
+ else:
+ if recursionindex is not None:
+ extraline = "!!! Recursion detected (same locals & position)"
+ traceback = traceback[: recursionindex + 1]
+ else:
+ extraline = None
+
+ return traceback, extraline
+
+ def repr_excinfo(
+ self, excinfo: ExceptionInfo[BaseException]
+ ) -> "ExceptionChainRepr":
+ repr_chain: List[
+ Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]
+ ] = []
+ e: Optional[BaseException] = excinfo.value
+ excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo
+ descr = None
+ seen: Set[int] = set()
+ while e is not None and id(e) not in seen:
+ seen.add(id(e))
+
+ if excinfo_:
+ # Fall back to native traceback as a temporary workaround until
+ # full support for exception groups added to ExceptionInfo.
+ # See https://github.com/pytest-dev/pytest/issues/9159
+ if isinstance(e, BaseExceptionGroup):
+ reprtraceback: Union[
+ ReprTracebackNative, ReprTraceback
+ ] = ReprTracebackNative(
+ traceback.format_exception(
+ type(excinfo_.value),
+ excinfo_.value,
+ excinfo_.traceback[0]._rawentry,
+ )
+ )
+ else:
+ reprtraceback = self.repr_traceback(excinfo_)
+ reprcrash = excinfo_._getreprcrash()
+ else:
+ # Fallback to native repr if the exception doesn't have a traceback:
+ # ExceptionInfo objects require a full traceback to work.
+ reprtraceback = ReprTracebackNative(
+ traceback.format_exception(type(e), e, None)
+ )
+ reprcrash = None
+ repr_chain += [(reprtraceback, reprcrash, descr)]
+
+ if e.__cause__ is not None and self.chain:
+ e = e.__cause__
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
+ descr = "The above exception was the direct cause of the following exception:"
+ elif (
+ e.__context__ is not None and not e.__suppress_context__ and self.chain
+ ):
+ e = e.__context__
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
+ descr = "During handling of the above exception, another exception occurred:"
+ else:
+ e = None
+ repr_chain.reverse()
+ return ExceptionChainRepr(repr_chain)
+
+
+@dataclasses.dataclass(eq=False)
+class TerminalRepr:
+ def __str__(self) -> str:
+ # FYI this is called from pytest-xdist's serialization of exception
+ # information.
+ io = StringIO()
+ tw = TerminalWriter(file=io)
+ self.toterminal(tw)
+ return io.getvalue().strip()
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__} instance at {id(self):0x}>"
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ raise NotImplementedError()
+
+
+# This class is abstract -- only subclasses are instantiated.
+@dataclasses.dataclass(eq=False)
+class ExceptionRepr(TerminalRepr):
+ # Provided by subclasses.
+ reprtraceback: "ReprTraceback"
+ reprcrash: Optional["ReprFileLocation"]
+ sections: List[Tuple[str, str, str]] = dataclasses.field(
+ init=False, default_factory=list
+ )
+
+ def addsection(self, name: str, content: str, sep: str = "-") -> None:
+ self.sections.append((name, content, sep))
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for name, content, sep in self.sections:
+ tw.sep(sep, name)
+ tw.line(content)
+
+
+@dataclasses.dataclass(eq=False)
+class ExceptionChainRepr(ExceptionRepr):
+ chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
+
+ def __init__(
+ self,
+ chain: Sequence[
+ Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
+ ],
+ ) -> None:
+ # reprcrash and reprtraceback of the outermost (the newest) exception
+ # in the chain.
+ super().__init__(
+ reprtraceback=chain[-1][0],
+ reprcrash=chain[-1][1],
+ )
+ self.chain = chain
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for element in self.chain:
+ element[0].toterminal(tw)
+ if element[2] is not None:
+ tw.line("")
+ tw.line(element[2], yellow=True)
+ super().toterminal(tw)
+
+
+@dataclasses.dataclass(eq=False)
+class ReprExceptionInfo(ExceptionRepr):
+ reprtraceback: "ReprTraceback"
+ reprcrash: Optional["ReprFileLocation"]
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ self.reprtraceback.toterminal(tw)
+ super().toterminal(tw)
+
+
+@dataclasses.dataclass(eq=False)
+class ReprTraceback(TerminalRepr):
+ reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
+ extraline: Optional[str]
+ style: "_TracebackStyle"
+
+ entrysep: ClassVar = "_ "
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # The entries might have different styles.
+ for i, entry in enumerate(self.reprentries):
+ if entry.style == "long":
+ tw.line("")
+ entry.toterminal(tw)
+ if i < len(self.reprentries) - 1:
+ next_entry = self.reprentries[i + 1]
+ if (
+ entry.style == "long"
+ or entry.style == "short"
+ and next_entry.style == "long"
+ ):
+ tw.sep(self.entrysep)
+
+ if self.extraline:
+ tw.line(self.extraline)
+
+
+class ReprTracebackNative(ReprTraceback):
+ def __init__(self, tblines: Sequence[str]) -> None:
+ self.reprentries = [ReprEntryNative(tblines)]
+ self.extraline = None
+ self.style = "native"
+
+
+@dataclasses.dataclass(eq=False)
+class ReprEntryNative(TerminalRepr):
+ lines: Sequence[str]
+
+ style: ClassVar["_TracebackStyle"] = "native"
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ tw.write("".join(self.lines))
+
+
+@dataclasses.dataclass(eq=False)
+class ReprEntry(TerminalRepr):
+ lines: Sequence[str]
+ reprfuncargs: Optional["ReprFuncArgs"]
+ reprlocals: Optional["ReprLocals"]
+ reprfileloc: Optional["ReprFileLocation"]
+ style: "_TracebackStyle"
+
+ def _write_entry_lines(self, tw: TerminalWriter) -> None:
+ """Write the source code portions of a list of traceback entries with syntax highlighting.
+
+ Usually entries are lines like these:
+
+ " x = 1"
+ "> assert x == 2"
+ "E assert 1 == 2"
+
+ This function takes care of rendering the "source" portions of it (the lines without
+ the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
+ character, as doing so might break line continuations.
+ """
+
+ if not self.lines:
+ return
+
+ # separate indents and source lines that are not failures: we want to
+ # highlight the code but not the indentation, which may contain markers
+ # such as "> assert 0"
+ fail_marker = f"{FormattedExcinfo.fail_marker} "
+ indent_size = len(fail_marker)
+ indents: List[str] = []
+ source_lines: List[str] = []
+ failure_lines: List[str] = []
+ for index, line in enumerate(self.lines):
+ is_failure_line = line.startswith(fail_marker)
+ if is_failure_line:
+ # from this point on all lines are considered part of the failure
+ failure_lines.extend(self.lines[index:])
+ break
+ else:
+ if self.style == "value":
+ source_lines.append(line)
+ else:
+ indents.append(line[:indent_size])
+ source_lines.append(line[indent_size:])
+
+ tw._write_source(source_lines, indents)
+
+ # failure lines are always completely red and bold
+ for line in failure_lines:
+ tw.line(line, bold=True, red=True)
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ if self.style == "short":
+ if self.reprfileloc:
+ self.reprfileloc.toterminal(tw)
+ self._write_entry_lines(tw)
+ if self.reprlocals:
+ self.reprlocals.toterminal(tw, indent=" " * 8)
+ return
+
+ if self.reprfuncargs:
+ self.reprfuncargs.toterminal(tw)
+
+ self._write_entry_lines(tw)
+
+ if self.reprlocals:
+ tw.line("")
+ self.reprlocals.toterminal(tw)
+ if self.reprfileloc:
+ if self.lines:
+ tw.line("")
+ self.reprfileloc.toterminal(tw)
+
+ def __str__(self) -> str:
+ return "{}\n{}\n{}".format(
+ "\n".join(self.lines), self.reprlocals, self.reprfileloc
+ )
+
+
+@dataclasses.dataclass(eq=False)
+class ReprFileLocation(TerminalRepr):
+ path: str
+ lineno: int
+ message: str
+
+ def __post_init__(self) -> None:
+ self.path = str(self.path)
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # Filename and lineno output for each entry, using an output format
+ # that most editors understand.
+ msg = self.message
+ i = msg.find("\n")
+ if i != -1:
+ msg = msg[:i]
+ tw.write(self.path, bold=True, red=True)
+ tw.line(f":{self.lineno}: {msg}")
+
+
+@dataclasses.dataclass(eq=False)
+class ReprLocals(TerminalRepr):
+ lines: Sequence[str]
+
+ def toterminal(self, tw: TerminalWriter, indent="") -> None:
+ for line in self.lines:
+ tw.line(indent + line)
+
+
+@dataclasses.dataclass(eq=False)
+class ReprFuncArgs(TerminalRepr):
+ args: Sequence[Tuple[str, object]]
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ if self.args:
+ linesofar = ""
+ for name, value in self.args:
+ ns = f"{name} = {value}"
+ if len(ns) + len(linesofar) + 2 > tw.fullwidth:
+ if linesofar:
+ tw.line(linesofar)
+ linesofar = ns
+ else:
+ if linesofar:
+ linesofar += ", " + ns
+ else:
+ linesofar = ns
+ if linesofar:
+ tw.line(linesofar)
+ tw.line("")
+
+
+def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
+ """Return source location (path, lineno) for the given object.
+
+ If the source cannot be determined return ("", -1).
+
+ The line number is 0-based.
+ """
+ # xxx let decorators etc specify a sane ordering
+ # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
+ # in 6ec13a2b9. It ("place_as") appears to be something very custom.
+ obj = get_real_func(obj)
+ if hasattr(obj, "place_as"):
+ obj = obj.place_as # type: ignore[attr-defined]
+
+ try:
+ code = Code.from_function(obj)
+ except TypeError:
+ try:
+ fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
+ except TypeError:
+ return "", -1
+
+ fspath = fn and absolutepath(fn) or ""
+ lineno = -1
+ if fspath:
+ try:
+ _, lineno = findsource(obj)
+ except OSError:
+ pass
+ return fspath, lineno
+
+ return code.path, code.firstlineno
+
+
+# Relative paths that we use to filter traceback entries from appearing to the user;
+# see filter_traceback.
+# note: if we need to add more paths than what we have now we should probably use a list
+# for better maintenance.
+
+_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
+# pluggy is either a package or a single module depending on the version
+if _PLUGGY_DIR.name == "__init__.py":
+ _PLUGGY_DIR = _PLUGGY_DIR.parent
+_PYTEST_DIR = Path(_pytest.__file__).parent
+
+
+def filter_traceback(entry: TracebackEntry) -> bool:
+ """Return True if a TracebackEntry instance should be included in tracebacks.
+
+ We hide traceback entries of:
+
+ * dynamically generated code (no code to show up for it);
+ * internal traceback from pytest or its internal libraries, py and pluggy.
+ """
+ # entry.path might sometimes return a str object when the entry
+ # points to dynamically generated code.
+ # See https://bitbucket.org/pytest-dev/py/issues/71.
+ raw_filename = entry.frame.code.raw.co_filename
+ is_generated = "<" in raw_filename and ">" in raw_filename
+ if is_generated:
+ return False
+
+ # entry.path might point to a non-existing file, in which case it will
+ # also return a str object. See #1133.
+ p = Path(entry.path)
+
+ parents = p.parents
+ if _PLUGGY_DIR in parents:
+ return False
+ if _PYTEST_DIR in parents:
+ return False
+
+ return True
diff --git a/venv/lib/python3.12/site-packages/_pytest/_code/source.py b/venv/lib/python3.12/site-packages/_pytest/_code/source.py
new file mode 100644
index 0000000..208cfb8
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_code/source.py
@@ -0,0 +1,217 @@
+import ast
+import inspect
+import textwrap
+import tokenize
+import types
+import warnings
+from bisect import bisect_right
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import overload
+from typing import Tuple
+from typing import Union
+
+
+class Source:
+ """An immutable object holding a source code fragment.
+
+ When using Source(...), the source lines are deindented.
+ """
+
+ def __init__(self, obj: object = None) -> None:
+ if not obj:
+ self.lines: List[str] = []
+ elif isinstance(obj, Source):
+ self.lines = obj.lines
+ elif isinstance(obj, (tuple, list)):
+ self.lines = deindent(x.rstrip("\n") for x in obj)
+ elif isinstance(obj, str):
+ self.lines = deindent(obj.split("\n"))
+ else:
+ try:
+ rawcode = getrawcode(obj)
+ src = inspect.getsource(rawcode)
+ except TypeError:
+ src = inspect.getsource(obj) # type: ignore[arg-type]
+ self.lines = deindent(src.split("\n"))
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Source):
+ return NotImplemented
+ return self.lines == other.lines
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @overload
+ def __getitem__(self, key: int) -> str:
+ ...
+
+ @overload
+ def __getitem__(self, key: slice) -> "Source":
+ ...
+
+ def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
+ if isinstance(key, int):
+ return self.lines[key]
+ else:
+ if key.step not in (None, 1):
+ raise IndexError("cannot slice a Source with a step")
+ newsource = Source()
+ newsource.lines = self.lines[key.start : key.stop]
+ return newsource
+
+ def __iter__(self) -> Iterator[str]:
+ return iter(self.lines)
+
+ def __len__(self) -> int:
+ return len(self.lines)
+
+ def strip(self) -> "Source":
+ """Return new Source object with trailing and leading blank lines removed."""
+ start, end = 0, len(self)
+ while start < end and not self.lines[start].strip():
+ start += 1
+ while end > start and not self.lines[end - 1].strip():
+ end -= 1
+ source = Source()
+ source.lines[:] = self.lines[start:end]
+ return source
+
+ def indent(self, indent: str = " " * 4) -> "Source":
+ """Return a copy of the source object with all lines indented by the
+ given indent-string."""
+ newsource = Source()
+ newsource.lines = [(indent + line) for line in self.lines]
+ return newsource
+
+ def getstatement(self, lineno: int) -> "Source":
+ """Return Source statement which contains the given linenumber
+ (counted from 0)."""
+ start, end = self.getstatementrange(lineno)
+ return self[start:end]
+
+ def getstatementrange(self, lineno: int) -> Tuple[int, int]:
+ """Return (start, end) tuple which spans the minimal statement region
+ which containing the given lineno."""
+ if not (0 <= lineno < len(self)):
+ raise IndexError("lineno out of range")
+ ast, start, end = getstatementrange_ast(lineno, self)
+ return start, end
+
+ def deindent(self) -> "Source":
+ """Return a new Source object deindented."""
+ newsource = Source()
+ newsource.lines[:] = deindent(self.lines)
+ return newsource
+
+ def __str__(self) -> str:
+ return "\n".join(self.lines)
+
+
+#
+# helper functions
+#
+
+
+def findsource(obj) -> Tuple[Optional[Source], int]:
+ try:
+ sourcelines, lineno = inspect.findsource(obj)
+ except Exception:
+ return None, -1
+ source = Source()
+ source.lines = [line.rstrip() for line in sourcelines]
+ return source, lineno
+
+
+def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
+ """Return code object for given function."""
+ try:
+ return obj.__code__ # type: ignore[attr-defined,no-any-return]
+ except AttributeError:
+ pass
+ if trycall:
+ call = getattr(obj, "__call__", None)
+ if call and not isinstance(obj, type):
+ return getrawcode(call, trycall=False)
+ raise TypeError(f"could not get code object for {obj!r}")
+
+
+def deindent(lines: Iterable[str]) -> List[str]:
+ return textwrap.dedent("\n".join(lines)).splitlines()
+
+
+def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
+ # Flatten all statements and except handlers into one lineno-list.
+ # AST's line numbers start indexing at 1.
+ values: List[int] = []
+ for x in ast.walk(node):
+ if isinstance(x, (ast.stmt, ast.ExceptHandler)):
+ # Before Python 3.8, the lineno of a decorated class or function pointed at the decorator.
+ # Since Python 3.8, the lineno points to the class/def, so need to include the decorators.
+ if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
+ for d in x.decorator_list:
+ values.append(d.lineno - 1)
+ values.append(x.lineno - 1)
+ for name in ("finalbody", "orelse"):
+ val: Optional[List[ast.stmt]] = getattr(x, name, None)
+ if val:
+ # Treat the finally/orelse part as its own statement.
+ values.append(val[0].lineno - 1 - 1)
+ values.sort()
+ insert_index = bisect_right(values, lineno)
+ start = values[insert_index - 1]
+ if insert_index >= len(values):
+ end = None
+ else:
+ end = values[insert_index]
+ return start, end
+
+
+def getstatementrange_ast(
+ lineno: int,
+ source: Source,
+ assertion: bool = False,
+ astnode: Optional[ast.AST] = None,
+) -> Tuple[ast.AST, int, int]:
+ if astnode is None:
+ content = str(source)
+ # See #4260:
+ # Don't produce duplicate warnings when compiling source to find AST.
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ astnode = ast.parse(content, "source", "exec")
+
+ start, end = get_statement_startend2(lineno, astnode)
+ # We need to correct the end:
+ # - ast-parsing strips comments
+ # - there might be empty lines
+ # - we might have lesser indented code blocks at the end
+ if end is None:
+ end = len(source.lines)
+
+ if end > start + 1:
+ # Make sure we don't span differently indented code blocks
+ # by using the BlockFinder helper used which inspect.getsource() uses itself.
+ block_finder = inspect.BlockFinder()
+ # If we start with an indented line, put blockfinder to "started" mode.
+ block_finder.started = source.lines[start][0].isspace()
+ it = ((x + "\n") for x in source.lines[start:end])
+ try:
+ for tok in tokenize.generate_tokens(lambda: next(it)):
+ block_finder.tokeneater(*tok)
+ except (inspect.EndOfBlock, IndentationError):
+ end = block_finder.last + start
+ except Exception:
+ pass
+
+ # The end might still point to a comment or empty line, correct it.
+ while end:
+ line = source.lines[end - 1].lstrip()
+ if line.startswith("#") or not line:
+ end -= 1
+ else:
+ break
+ return astnode, start, end
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/__init__.py b/venv/lib/python3.12/site-packages/_pytest/_io/__init__.py
new file mode 100644
index 0000000..db001e9
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_io/__init__.py
@@ -0,0 +1,8 @@
+from .terminalwriter import get_terminal_width
+from .terminalwriter import TerminalWriter
+
+
+__all__ = [
+ "TerminalWriter",
+ "get_terminal_width",
+]
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..70d469e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/saferepr.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/saferepr.cpython-312.pyc
new file mode 100644
index 0000000..4e75018
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/saferepr.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-312.pyc
new file mode 100644
index 0000000..8082d89
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/terminalwriter.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-312.pyc
new file mode 100644
index 0000000..16601ce
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_io/__pycache__/wcwidth.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/saferepr.py b/venv/lib/python3.12/site-packages/_pytest/_io/saferepr.py
new file mode 100644
index 0000000..c701872
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_io/saferepr.py
@@ -0,0 +1,180 @@
+import pprint
+import reprlib
+from typing import Any
+from typing import Dict
+from typing import IO
+from typing import Optional
+
+
+def _try_repr_or_str(obj: object) -> str:
+ try:
+ return repr(obj)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException:
+ return f'{type(obj).__name__}("{obj}")'
+
+
+def _format_repr_exception(exc: BaseException, obj: object) -> str:
+ try:
+ exc_info = _try_repr_or_str(exc)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as exc:
+ exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
+ return "<[{} raised in repr()] {} object at 0x{:x}>".format(
+ exc_info, type(obj).__name__, id(obj)
+ )
+
+
+def _ellipsize(s: str, maxsize: int) -> str:
+ if len(s) > maxsize:
+ i = max(0, (maxsize - 3) // 2)
+ j = max(0, maxsize - 3 - i)
+ return s[:i] + "..." + s[len(s) - j :]
+ return s
+
+
+class SafeRepr(reprlib.Repr):
+ """
+ repr.Repr that limits the resulting size of repr() and includes
+ information on exceptions raised during the call.
+ """
+
+ def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None:
+ """
+ :param maxsize:
+ If not None, will truncate the resulting repr to that specific size, using ellipsis
+ somewhere in the middle to hide the extra text.
+ If None, will not impose any size limits on the returning repr.
+ """
+ super().__init__()
+ # ``maxstring`` is used by the superclass, and needs to be an int; using a
+ # very large number in case maxsize is None, meaning we want to disable
+ # truncation.
+ self.maxstring = maxsize if maxsize is not None else 1_000_000_000
+ self.maxsize = maxsize
+ self.use_ascii = use_ascii
+
+ def repr(self, x: object) -> str:
+ try:
+ if self.use_ascii:
+ s = ascii(x)
+ else:
+ s = super().repr(x)
+
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as exc:
+ s = _format_repr_exception(exc, x)
+ if self.maxsize is not None:
+ s = _ellipsize(s, self.maxsize)
+ return s
+
+ def repr_instance(self, x: object, level: int) -> str:
+ try:
+ s = repr(x)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as exc:
+ s = _format_repr_exception(exc, x)
+ if self.maxsize is not None:
+ s = _ellipsize(s, self.maxsize)
+ return s
+
+
+def safeformat(obj: object) -> str:
+ """Return a pretty printed string for the given object.
+
+ Failing __repr__ functions of user instances will be represented
+ with a short exception info.
+ """
+ try:
+ return pprint.pformat(obj)
+ except Exception as exc:
+ return _format_repr_exception(exc, obj)
+
+
+# Maximum size of overall repr of objects to display during assertion errors.
+DEFAULT_REPR_MAX_SIZE = 240
+
+
+def saferepr(
+ obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
+) -> str:
+ """Return a size-limited safe repr-string for the given object.
+
+ Failing __repr__ functions of user instances will be represented
+ with a short exception info and 'saferepr' generally takes
+ care to never raise exceptions itself.
+
+ This function is a wrapper around the Repr/reprlib functionality of the
+ stdlib.
+ """
+
+ return SafeRepr(maxsize, use_ascii).repr(obj)
+
+
+def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
+ """Return an unlimited-size safe repr-string for the given object.
+
+ As with saferepr, failing __repr__ functions of user instances
+ will be represented with a short exception info.
+
+ This function is a wrapper around simple repr.
+
+ Note: a cleaner solution would be to alter ``saferepr``this way
+ when maxsize=None, but that might affect some other code.
+ """
+ try:
+ if use_ascii:
+ return ascii(obj)
+ return repr(obj)
+ except Exception as exc:
+ return _format_repr_exception(exc, obj)
+
+
+class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
+ """PrettyPrinter that always dispatches (regardless of width)."""
+
+ def _format(
+ self,
+ object: object,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: Dict[int, Any],
+ level: int,
+ ) -> None:
+ # Type ignored because _dispatch is private.
+ p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
+
+ objid = id(object)
+ if objid in context or p is None:
+ # Type ignored because _format is private.
+ super()._format( # type: ignore[misc]
+ object,
+ stream,
+ indent,
+ allowance,
+ context,
+ level,
+ )
+ return
+
+ context[objid] = 1
+ p(self, object, stream, indent, allowance, context, level + 1)
+ del context[objid]
+
+
+def _pformat_dispatch(
+ object: object,
+ indent: int = 1,
+ width: int = 80,
+ depth: Optional[int] = None,
+ *,
+ compact: bool = False,
+) -> str:
+ return AlwaysDispatchingPrettyPrinter(
+ indent=indent, width=width, depth=depth, compact=compact
+ ).pformat(object)
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py b/venv/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py
new file mode 100644
index 0000000..379035d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_io/terminalwriter.py
@@ -0,0 +1,233 @@
+"""Helper functions for writing to terminals and files."""
+import os
+import shutil
+import sys
+from typing import Optional
+from typing import Sequence
+from typing import TextIO
+
+from .wcwidth import wcswidth
+from _pytest.compat import final
+
+
+# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
+
+
+def get_terminal_width() -> int:
+ width, _ = shutil.get_terminal_size(fallback=(80, 24))
+
+ # The Windows get_terminal_size may be bogus, let's sanify a bit.
+ if width < 40:
+ width = 80
+
+ return width
+
+
+def should_do_markup(file: TextIO) -> bool:
+ if os.environ.get("PY_COLORS") == "1":
+ return True
+ if os.environ.get("PY_COLORS") == "0":
+ return False
+ if "NO_COLOR" in os.environ:
+ return False
+ if "FORCE_COLOR" in os.environ:
+ return True
+ return (
+ hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb"
+ )
+
+
+@final
+class TerminalWriter:
+ _esctable = dict(
+ black=30,
+ red=31,
+ green=32,
+ yellow=33,
+ blue=34,
+ purple=35,
+ cyan=36,
+ white=37,
+ Black=40,
+ Red=41,
+ Green=42,
+ Yellow=43,
+ Blue=44,
+ Purple=45,
+ Cyan=46,
+ White=47,
+ bold=1,
+ light=2,
+ blink=5,
+ invert=7,
+ )
+
+ def __init__(self, file: Optional[TextIO] = None) -> None:
+ if file is None:
+ file = sys.stdout
+ if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32":
+ try:
+ import colorama
+ except ImportError:
+ pass
+ else:
+ file = colorama.AnsiToWin32(file).stream
+ assert file is not None
+ self._file = file
+ self.hasmarkup = should_do_markup(file)
+ self._current_line = ""
+ self._terminal_width: Optional[int] = None
+ self.code_highlight = True
+
+ @property
+ def fullwidth(self) -> int:
+ if self._terminal_width is not None:
+ return self._terminal_width
+ return get_terminal_width()
+
+ @fullwidth.setter
+ def fullwidth(self, value: int) -> None:
+ self._terminal_width = value
+
+ @property
+ def width_of_current_line(self) -> int:
+ """Return an estimate of the width so far in the current line."""
+ return wcswidth(self._current_line)
+
+ def markup(self, text: str, **markup: bool) -> str:
+ for name in markup:
+ if name not in self._esctable:
+ raise ValueError(f"unknown markup: {name!r}")
+ if self.hasmarkup:
+ esc = [self._esctable[name] for name, on in markup.items() if on]
+ if esc:
+ text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m"
+ return text
+
+ def sep(
+ self,
+ sepchar: str,
+ title: Optional[str] = None,
+ fullwidth: Optional[int] = None,
+ **markup: bool,
+ ) -> None:
+ if fullwidth is None:
+ fullwidth = self.fullwidth
+ # The goal is to have the line be as long as possible
+ # under the condition that len(line) <= fullwidth.
+ if sys.platform == "win32":
+ # If we print in the last column on windows we are on a
+ # new line but there is no way to verify/neutralize this
+ # (we may not know the exact line width).
+ # So let's be defensive to avoid empty lines in the output.
+ fullwidth -= 1
+ if title is not None:
+ # we want 2 + 2*len(fill) + len(title) <= fullwidth
+ # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth
+ # 2*len(sepchar)*N <= fullwidth - len(title) - 2
+ # N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
+ N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
+ fill = sepchar * N
+ line = f"{fill} {title} {fill}"
+ else:
+ # we want len(sepchar)*N <= fullwidth
+ # i.e. N <= fullwidth // len(sepchar)
+ line = sepchar * (fullwidth // len(sepchar))
+ # In some situations there is room for an extra sepchar at the right,
+ # in particular if we consider that with a sepchar like "_ " the
+ # trailing space is not important at the end of the line.
+ if len(line) + len(sepchar.rstrip()) <= fullwidth:
+ line += sepchar.rstrip()
+
+ self.line(line, **markup)
+
+ def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None:
+ if msg:
+ current_line = msg.rsplit("\n", 1)[-1]
+ if "\n" in msg:
+ self._current_line = current_line
+ else:
+ self._current_line += current_line
+
+ msg = self.markup(msg, **markup)
+
+ try:
+ self._file.write(msg)
+ except UnicodeEncodeError:
+ # Some environments don't support printing general Unicode
+ # strings, due to misconfiguration or otherwise; in that case,
+ # print the string escaped to ASCII.
+ # When the Unicode situation improves we should consider
+ # letting the error propagate instead of masking it (see #7475
+ # for one brief attempt).
+ msg = msg.encode("unicode-escape").decode("ascii")
+ self._file.write(msg)
+
+ if flush:
+ self.flush()
+
+ def line(self, s: str = "", **markup: bool) -> None:
+ self.write(s, **markup)
+ self.write("\n")
+
+ def flush(self) -> None:
+ self._file.flush()
+
+ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None:
+ """Write lines of source code possibly highlighted.
+
+ Keeping this private for now because the API is clunky. We should discuss how
+ to evolve the terminal writer so we can have more precise color support, for example
+ being able to write part of a line in one color and the rest in another, and so on.
+ """
+ if indents and len(indents) != len(lines):
+ raise ValueError(
+ "indents size ({}) should have same size as lines ({})".format(
+ len(indents), len(lines)
+ )
+ )
+ if not indents:
+ indents = [""] * len(lines)
+ source = "\n".join(lines)
+ new_lines = self._highlight(source).splitlines()
+ for indent, new_line in zip(indents, new_lines):
+ self.line(indent + new_line)
+
+ def _highlight(self, source: str) -> str:
+ """Highlight the given source code if we have markup support."""
+ from _pytest.config.exceptions import UsageError
+
+ if not self.hasmarkup or not self.code_highlight:
+ return source
+ try:
+ from pygments.formatters.terminal import TerminalFormatter
+ from pygments.lexers.python import PythonLexer
+ from pygments import highlight
+ import pygments.util
+ except ImportError:
+ return source
+ else:
+ try:
+ highlighted: str = highlight(
+ source,
+ PythonLexer(),
+ TerminalFormatter(
+ bg=os.getenv("PYTEST_THEME_MODE", "dark"),
+ style=os.getenv("PYTEST_THEME"),
+ ),
+ )
+ return highlighted
+ except pygments.util.ClassNotFound:
+ raise UsageError(
+ "PYTEST_THEME environment variable had an invalid value: '{}'. "
+ "Only valid pygment styles are allowed.".format(
+ os.getenv("PYTEST_THEME")
+ )
+ )
+ except pygments.util.OptionError:
+ raise UsageError(
+ "PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
+ "The only allowed values are 'dark' and 'light'.".format(
+ os.getenv("PYTEST_THEME_MODE")
+ )
+ )
diff --git a/venv/lib/python3.12/site-packages/_pytest/_io/wcwidth.py b/venv/lib/python3.12/site-packages/_pytest/_io/wcwidth.py
new file mode 100644
index 0000000..e5c7bf4
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_io/wcwidth.py
@@ -0,0 +1,55 @@
+import unicodedata
+from functools import lru_cache
+
+
+@lru_cache(100)
+def wcwidth(c: str) -> int:
+ """Determine how many columns are needed to display a character in a terminal.
+
+ Returns -1 if the character is not printable.
+ Returns 0, 1 or 2 for other characters.
+ """
+ o = ord(c)
+
+ # ASCII fast path.
+ if 0x20 <= o < 0x07F:
+ return 1
+
+ # Some Cf/Zp/Zl characters which should be zero-width.
+ if (
+ o == 0x0000
+ or 0x200B <= o <= 0x200F
+ or 0x2028 <= o <= 0x202E
+ or 0x2060 <= o <= 0x2063
+ ):
+ return 0
+
+ category = unicodedata.category(c)
+
+ # Control characters.
+ if category == "Cc":
+ return -1
+
+ # Combining characters with zero width.
+ if category in ("Me", "Mn"):
+ return 0
+
+ # Full/Wide east asian characters.
+ if unicodedata.east_asian_width(c) in ("F", "W"):
+ return 2
+
+ return 1
+
+
+def wcswidth(s: str) -> int:
+ """Determine how many columns are needed to display a string in a terminal.
+
+ Returns -1 if the string contains non-printable characters.
+ """
+ width = 0
+ for c in unicodedata.normalize("NFC", s):
+ wc = wcwidth(c)
+ if wc < 0:
+ return -1
+ width += wc
+ return width
diff --git a/venv/lib/python3.12/site-packages/_pytest/_py/__init__.py b/venv/lib/python3.12/site-packages/_pytest/_py/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..1105589
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/error.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/error.cpython-312.pyc
new file mode 100644
index 0000000..d4a12e8
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/error.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/path.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/path.cpython-312.pyc
new file mode 100644
index 0000000..5c11072
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/_py/__pycache__/path.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/_py/error.py b/venv/lib/python3.12/site-packages/_pytest/_py/error.py
new file mode 100644
index 0000000..0b8f2d5
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_py/error.py
@@ -0,0 +1,109 @@
+"""create errno-specific classes for IO or os calls."""
+from __future__ import annotations
+
+import errno
+import os
+import sys
+from typing import Callable
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+if TYPE_CHECKING:
+ from typing_extensions import ParamSpec
+
+ P = ParamSpec("P")
+
+R = TypeVar("R")
+
+
+class Error(EnvironmentError):
+ def __repr__(self) -> str:
+ return "{}.{} {!r}: {} ".format(
+ self.__class__.__module__,
+ self.__class__.__name__,
+ self.__class__.__doc__,
+ " ".join(map(str, self.args)),
+ # repr(self.args)
+ )
+
+ def __str__(self) -> str:
+ s = "[{}]: {}".format(
+ self.__class__.__doc__,
+ " ".join(map(str, self.args)),
+ )
+ return s
+
+
+_winerrnomap = {
+ 2: errno.ENOENT,
+ 3: errno.ENOENT,
+ 17: errno.EEXIST,
+ 18: errno.EXDEV,
+ 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable
+ 22: errno.ENOTDIR,
+ 20: errno.ENOTDIR,
+ 267: errno.ENOTDIR,
+ 5: errno.EACCES, # anything better?
+}
+
+
+class ErrorMaker:
+ """lazily provides Exception classes for each possible POSIX errno
+ (as defined per the 'errno' module). All such instances
+ subclass EnvironmentError.
+ """
+
+ _errno2class: dict[int, type[Error]] = {}
+
+ def __getattr__(self, name: str) -> type[Error]:
+ if name[0] == "_":
+ raise AttributeError(name)
+ eno = getattr(errno, name)
+ cls = self._geterrnoclass(eno)
+ setattr(self, name, cls)
+ return cls
+
+ def _geterrnoclass(self, eno: int) -> type[Error]:
+ try:
+ return self._errno2class[eno]
+ except KeyError:
+ clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,))
+ errorcls = type(
+ clsname,
+ (Error,),
+ {"__module__": "py.error", "__doc__": os.strerror(eno)},
+ )
+ self._errno2class[eno] = errorcls
+ return errorcls
+
+ def checked_call(
+ self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
+ ) -> R:
+ """Call a function and raise an errno-exception if applicable."""
+ __tracebackhide__ = True
+ try:
+ return func(*args, **kwargs)
+ except Error:
+ raise
+ except OSError as value:
+ if not hasattr(value, "errno"):
+ raise
+ errno = value.errno
+ if sys.platform == "win32":
+ try:
+ cls = self._geterrnoclass(_winerrnomap[errno])
+ except KeyError:
+ raise value
+ else:
+ # we are not on Windows, or we got a proper OSError
+ cls = self._geterrnoclass(errno)
+
+ raise cls(f"{func.__name__}{args!r}")
+
+
+_error_maker = ErrorMaker()
+checked_call = _error_maker.checked_call
+
+
+def __getattr__(attr: str) -> type[Error]:
+ return getattr(_error_maker, attr) # type: ignore[no-any-return]
diff --git a/venv/lib/python3.12/site-packages/_pytest/_py/path.py b/venv/lib/python3.12/site-packages/_pytest/_py/path.py
new file mode 100644
index 0000000..73a070d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_py/path.py
@@ -0,0 +1,1475 @@
+"""local path implementation."""
+from __future__ import annotations
+
+import atexit
+import fnmatch
+import importlib.util
+import io
+import os
+import posixpath
+import sys
+import uuid
+import warnings
+from contextlib import contextmanager
+from os.path import abspath
+from os.path import dirname
+from os.path import exists
+from os.path import isabs
+from os.path import isdir
+from os.path import isfile
+from os.path import islink
+from os.path import normpath
+from stat import S_ISDIR
+from stat import S_ISLNK
+from stat import S_ISREG
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import overload
+from typing import TYPE_CHECKING
+
+from . import error
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+# Moved from local.py.
+iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
+
+
+class Checkers:
+ _depend_on_existence = "exists", "link", "dir", "file"
+
+ def __init__(self, path):
+ self.path = path
+
+ def dotfile(self):
+ return self.path.basename.startswith(".")
+
+ def ext(self, arg):
+ if not arg.startswith("."):
+ arg = "." + arg
+ return self.path.ext == arg
+
+ def basename(self, arg):
+ return self.path.basename == arg
+
+ def basestarts(self, arg):
+ return self.path.basename.startswith(arg)
+
+ def relto(self, arg):
+ return self.path.relto(arg)
+
+ def fnmatch(self, arg):
+ return self.path.fnmatch(arg)
+
+ def endswith(self, arg):
+ return str(self.path).endswith(arg)
+
+ def _evaluate(self, kw):
+ from .._code.source import getrawcode
+
+ for name, value in kw.items():
+ invert = False
+ meth = None
+ try:
+ meth = getattr(self, name)
+ except AttributeError:
+ if name[:3] == "not":
+ invert = True
+ try:
+ meth = getattr(self, name[3:])
+ except AttributeError:
+ pass
+ if meth is None:
+ raise TypeError(f"no {name!r} checker available for {self.path!r}")
+ try:
+ if getrawcode(meth).co_argcount > 1:
+ if (not meth(value)) ^ invert:
+ return False
+ else:
+ if bool(value) ^ bool(meth()) ^ invert:
+ return False
+ except (error.ENOENT, error.ENOTDIR, error.EBUSY):
+ # EBUSY feels not entirely correct,
+ # but its kind of necessary since ENOMEDIUM
+ # is not accessible in python
+ for name in self._depend_on_existence:
+ if name in kw:
+ if kw.get(name):
+ return False
+ name = "not" + name
+ if name in kw:
+ if not kw.get(name):
+ return False
+ return True
+
+ _statcache: Stat
+
+ def _stat(self) -> Stat:
+ try:
+ return self._statcache
+ except AttributeError:
+ try:
+ self._statcache = self.path.stat()
+ except error.ELOOP:
+ self._statcache = self.path.lstat()
+ return self._statcache
+
+ def dir(self):
+ return S_ISDIR(self._stat().mode)
+
+ def file(self):
+ return S_ISREG(self._stat().mode)
+
+ def exists(self):
+ return self._stat()
+
+ def link(self):
+ st = self.path.lstat()
+ return S_ISLNK(st.mode)
+
+
+class NeverRaised(Exception):
+ pass
+
+
+class Visitor:
+ def __init__(self, fil, rec, ignore, bf, sort):
+ if isinstance(fil, str):
+ fil = FNMatcher(fil)
+ if isinstance(rec, str):
+ self.rec: Callable[[LocalPath], bool] = FNMatcher(rec)
+ elif not hasattr(rec, "__call__") and rec:
+ self.rec = lambda path: True
+ else:
+ self.rec = rec
+ self.fil = fil
+ self.ignore = ignore
+ self.breadthfirst = bf
+ self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
+
+ def gen(self, path):
+ try:
+ entries = path.listdir()
+ except self.ignore:
+ return
+ rec = self.rec
+ dirs = self.optsort(
+ [p for p in entries if p.check(dir=1) and (rec is None or rec(p))]
+ )
+ if not self.breadthfirst:
+ for subdir in dirs:
+ for p in self.gen(subdir):
+ yield p
+ for p in self.optsort(entries):
+ if self.fil is None or self.fil(p):
+ yield p
+ if self.breadthfirst:
+ for subdir in dirs:
+ for p in self.gen(subdir):
+ yield p
+
+
+class FNMatcher:
+ def __init__(self, pattern):
+ self.pattern = pattern
+
+ def __call__(self, path):
+ pattern = self.pattern
+
+ if (
+ pattern.find(path.sep) == -1
+ and iswin32
+ and pattern.find(posixpath.sep) != -1
+ ):
+ # Running on Windows, the pattern has no Windows path separators,
+ # and the pattern has one or more Posix path separators. Replace
+ # the Posix path separators with the Windows path separator.
+ pattern = pattern.replace(posixpath.sep, path.sep)
+
+ if pattern.find(path.sep) == -1:
+ name = path.basename
+ else:
+ name = str(path) # path.strpath # XXX svn?
+ if not os.path.isabs(pattern):
+ pattern = "*" + path.sep + pattern
+ return fnmatch.fnmatch(name, pattern)
+
+
+def map_as_list(func, iter):
+ return list(map(func, iter))
+
+
+class Stat:
+ if TYPE_CHECKING:
+
+ @property
+ def size(self) -> int:
+ ...
+
+ @property
+ def mtime(self) -> float:
+ ...
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._osstatresult, "st_" + name)
+
+ def __init__(self, path, osstatresult):
+ self.path = path
+ self._osstatresult = osstatresult
+
+ @property
+ def owner(self):
+ if iswin32:
+ raise NotImplementedError("XXX win32")
+ import pwd
+
+ entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
+ return entry[0]
+
+ @property
+ def group(self):
+ """Return group name of file."""
+ if iswin32:
+ raise NotImplementedError("XXX win32")
+ import grp
+
+ entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
+ return entry[0]
+
+ def isdir(self):
+ return S_ISDIR(self._osstatresult.st_mode)
+
+ def isfile(self):
+ return S_ISREG(self._osstatresult.st_mode)
+
+ def islink(self):
+ self.path.lstat()
+ return S_ISLNK(self._osstatresult.st_mode)
+
+
+def getuserid(user):
+ import pwd
+
+ if not isinstance(user, int):
+ user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
+ return user
+
+
+def getgroupid(group):
+ import grp
+
+ if not isinstance(group, int):
+ group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
+ return group
+
+
+class LocalPath:
+ """Object oriented interface to os.path and other local filesystem
+ related information.
+ """
+
+ class ImportMismatchError(ImportError):
+ """raised on pyimport() if there is a mismatch of __file__'s"""
+
+ sep = os.sep
+
+ def __init__(self, path=None, expanduser=False):
+ """Initialize and return a local Path instance.
+
+ Path can be relative to the current directory.
+ If path is None it defaults to the current working directory.
+ If expanduser is True, tilde-expansion is performed.
+ Note that Path instances always carry an absolute path.
+ Note also that passing in a local path object will simply return
+ the exact same path object. Use new() to get a new copy.
+ """
+ if path is None:
+ self.strpath = error.checked_call(os.getcwd)
+ else:
+ try:
+ path = os.fspath(path)
+ except TypeError:
+ raise ValueError(
+ "can only pass None, Path instances "
+ "or non-empty strings to LocalPath"
+ )
+ if expanduser:
+ path = os.path.expanduser(path)
+ self.strpath = abspath(path)
+
+ if sys.platform != "win32":
+
+ def chown(self, user, group, rec=0):
+ """Change ownership to the given user and group.
+ user and group may be specified by a number or
+ by a name. if rec is True change ownership
+ recursively.
+ """
+ uid = getuserid(user)
+ gid = getgroupid(group)
+ if rec:
+ for x in self.visit(rec=lambda x: x.check(link=0)):
+ if x.check(link=0):
+ error.checked_call(os.chown, str(x), uid, gid)
+ error.checked_call(os.chown, str(self), uid, gid)
+
+ def readlink(self) -> str:
+ """Return value of a symbolic link."""
+ # https://github.com/python/mypy/issues/12278
+ return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value]
+
+ def mklinkto(self, oldname):
+ """Posix style hard link to another name."""
+ error.checked_call(os.link, str(oldname), str(self))
+
+ def mksymlinkto(self, value, absolute=1):
+ """Create a symbolic link with the given value (pointing to another name)."""
+ if absolute:
+ error.checked_call(os.symlink, str(value), self.strpath)
+ else:
+ base = self.common(value)
+ # with posix local paths '/' is always a common base
+ relsource = self.__class__(value).relto(base)
+ reldest = self.relto(base)
+ n = reldest.count(self.sep)
+ target = self.sep.join(("..",) * n + (relsource,))
+ error.checked_call(os.symlink, target, self.strpath)
+
+ def __div__(self, other):
+ return self.join(os.fspath(other))
+
+ __truediv__ = __div__ # py3k
+
+ @property
+ def basename(self):
+ """Basename part of path."""
+ return self._getbyspec("basename")[0]
+
+ @property
+ def dirname(self):
+ """Dirname part of path."""
+ return self._getbyspec("dirname")[0]
+
+ @property
+ def purebasename(self):
+ """Pure base name of the path."""
+ return self._getbyspec("purebasename")[0]
+
+ @property
+ def ext(self):
+ """Extension of the path (including the '.')."""
+ return self._getbyspec("ext")[0]
+
+ def read_binary(self):
+ """Read and return a bytestring from reading the path."""
+ with self.open("rb") as f:
+ return f.read()
+
+ def read_text(self, encoding):
+ """Read and return a Unicode string from reading the path."""
+ with self.open("r", encoding=encoding) as f:
+ return f.read()
+
+ def read(self, mode="r"):
+ """Read and return a bytestring from reading the path."""
+ with self.open(mode) as f:
+ return f.read()
+
+ def readlines(self, cr=1):
+ """Read and return a list of lines from the path. if cr is False, the
+ newline will be removed from the end of each line."""
+ mode = "r"
+
+ if not cr:
+ content = self.read(mode)
+ return content.split("\n")
+ else:
+ f = self.open(mode)
+ try:
+ return f.readlines()
+ finally:
+ f.close()
+
+ def load(self):
+ """(deprecated) return object unpickled from self.read()"""
+ f = self.open("rb")
+ try:
+ import pickle
+
+ return error.checked_call(pickle.load, f)
+ finally:
+ f.close()
+
+ def move(self, target):
+ """Move this path to target."""
+ if target.relto(self):
+ raise error.EINVAL(target, "cannot move path into a subdirectory of itself")
+ try:
+ self.rename(target)
+ except error.EXDEV: # invalid cross-device link
+ self.copy(target)
+ self.remove()
+
+ def fnmatch(self, pattern):
+ """Return true if the basename/fullname matches the glob-'pattern'.
+
+ valid pattern characters::
+
+ * matches everything
+ ? matches any single character
+ [seq] matches any character in seq
+ [!seq] matches any char not in seq
+
+ If the pattern contains a path-separator then the full path
+ is used for pattern matching and a '*' is prepended to the
+ pattern.
+
+ if the pattern doesn't contain a path-separator the pattern
+ is only matched against the basename.
+ """
+ return FNMatcher(pattern)(self)
+
+ def relto(self, relpath):
+ """Return a string which is the relative part of the path
+ to the given 'relpath'.
+ """
+ if not isinstance(relpath, (str, LocalPath)):
+ raise TypeError(f"{relpath!r}: not a string or path object")
+ strrelpath = str(relpath)
+ if strrelpath and strrelpath[-1] != self.sep:
+ strrelpath += self.sep
+ # assert strrelpath[-1] == self.sep
+ # assert strrelpath[-2] != self.sep
+ strself = self.strpath
+ if sys.platform == "win32" or getattr(os, "_name", None) == "nt":
+ if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)):
+ return strself[len(strrelpath) :]
+ elif strself.startswith(strrelpath):
+ return strself[len(strrelpath) :]
+ return ""
+
+ def ensure_dir(self, *args):
+ """Ensure the path joined with args is a directory."""
+ return self.ensure(*args, **{"dir": True})
+
+ def bestrelpath(self, dest):
+ """Return a string which is a relative path from self
+ (assumed to be a directory) to dest such that
+ self.join(bestrelpath) == dest and if not such
+ path can be determined return dest.
+ """
+ try:
+ if self == dest:
+ return os.curdir
+ base = self.common(dest)
+ if not base: # can be the case on windows
+ return str(dest)
+ self2base = self.relto(base)
+ reldest = dest.relto(base)
+ if self2base:
+ n = self2base.count(self.sep) + 1
+ else:
+ n = 0
+ lst = [os.pardir] * n
+ if reldest:
+ lst.append(reldest)
+ target = dest.sep.join(lst)
+ return target
+ except AttributeError:
+ return str(dest)
+
+ def exists(self):
+ return self.check()
+
+ def isdir(self):
+ return self.check(dir=1)
+
+ def isfile(self):
+ return self.check(file=1)
+
+ def parts(self, reverse=False):
+ """Return a root-first list of all ancestor directories
+ plus the path itself.
+ """
+ current = self
+ lst = [self]
+ while 1:
+ last = current
+ current = current.dirpath()
+ if last == current:
+ break
+ lst.append(current)
+ if not reverse:
+ lst.reverse()
+ return lst
+
+ def common(self, other):
+ """Return the common part shared with the other path
+ or None if there is no common part.
+ """
+ last = None
+ for x, y in zip(self.parts(), other.parts()):
+ if x != y:
+ return last
+ last = x
+ return last
+
+ def __add__(self, other):
+ """Return new path object with 'other' added to the basename"""
+ return self.new(basename=self.basename + str(other))
+
+ def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False):
+ """Yields all paths below the current one
+
+ fil is a filter (glob pattern or callable), if not matching the
+ path will not be yielded, defaulting to None (everything is
+ returned)
+
+ rec is a filter (glob pattern or callable) that controls whether
+ a node is descended, defaulting to None
+
+ ignore is an Exception class that is ignoredwhen calling dirlist()
+ on any of the paths (by default, all exceptions are reported)
+
+ bf if True will cause a breadthfirst search instead of the
+ default depthfirst. Default: False
+
+ sort if True will sort entries within each directory level.
+ """
+ yield from Visitor(fil, rec, ignore, bf, sort).gen(self)
+
+ def _sortlist(self, res, sort):
+ if sort:
+ if hasattr(sort, "__call__"):
+ warnings.warn(
+ DeprecationWarning(
+ "listdir(sort=callable) is deprecated and breaks on python3"
+ ),
+ stacklevel=3,
+ )
+ res.sort(sort)
+ else:
+ res.sort()
+
+ def __fspath__(self):
+ return self.strpath
+
+ def __hash__(self):
+ s = self.strpath
+ if iswin32:
+ s = s.lower()
+ return hash(s)
+
+ def __eq__(self, other):
+ s1 = os.fspath(self)
+ try:
+ s2 = os.fspath(other)
+ except TypeError:
+ return False
+ if iswin32:
+ s1 = s1.lower()
+ try:
+ s2 = s2.lower()
+ except AttributeError:
+ return False
+ return s1 == s2
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __lt__(self, other):
+ return os.fspath(self) < os.fspath(other)
+
+ def __gt__(self, other):
+ return os.fspath(self) > os.fspath(other)
+
+ def samefile(self, other):
+ """Return True if 'other' references the same file as 'self'."""
+ other = os.fspath(other)
+ if not isabs(other):
+ other = abspath(other)
+ if self == other:
+ return True
+ if not hasattr(os.path, "samefile"):
+ return False
+ return error.checked_call(os.path.samefile, self.strpath, other)
+
+ def remove(self, rec=1, ignore_errors=False):
+ """Remove a file or directory (or a directory tree if rec=1).
+ if ignore_errors is True, errors while removing directories will
+ be ignored.
+ """
+ if self.check(dir=1, link=0):
+ if rec:
+ # force remove of readonly files on windows
+ if iswin32:
+ self.chmod(0o700, rec=1)
+ import shutil
+
+ error.checked_call(
+ shutil.rmtree, self.strpath, ignore_errors=ignore_errors
+ )
+ else:
+ error.checked_call(os.rmdir, self.strpath)
+ else:
+ if iswin32:
+ self.chmod(0o700)
+ error.checked_call(os.remove, self.strpath)
+
+ def computehash(self, hashtype="md5", chunksize=524288):
+ """Return hexdigest of hashvalue for this file."""
+ try:
+ try:
+ import hashlib as mod
+ except ImportError:
+ if hashtype == "sha1":
+ hashtype = "sha"
+ mod = __import__(hashtype)
+ hash = getattr(mod, hashtype)()
+ except (AttributeError, ImportError):
+ raise ValueError(f"Don't know how to compute {hashtype!r} hash")
+ f = self.open("rb")
+ try:
+ while 1:
+ buf = f.read(chunksize)
+ if not buf:
+ return hash.hexdigest()
+ hash.update(buf)
+ finally:
+ f.close()
+
+ def new(self, **kw):
+ """Create a modified version of this path.
+ the following keyword arguments modify various path parts::
+
+ a:/some/path/to/a/file.ext
+ xx drive
+ xxxxxxxxxxxxxxxxx dirname
+ xxxxxxxx basename
+ xxxx purebasename
+ xxx ext
+ """
+ obj = object.__new__(self.__class__)
+ if not kw:
+ obj.strpath = self.strpath
+ return obj
+ drive, dirname, basename, purebasename, ext = self._getbyspec(
+ "drive,dirname,basename,purebasename,ext"
+ )
+ if "basename" in kw:
+ if "purebasename" in kw or "ext" in kw:
+ raise ValueError("invalid specification %r" % kw)
+ else:
+ pb = kw.setdefault("purebasename", purebasename)
+ try:
+ ext = kw["ext"]
+ except KeyError:
+ pass
+ else:
+ if ext and not ext.startswith("."):
+ ext = "." + ext
+ kw["basename"] = pb + ext
+
+ if "dirname" in kw and not kw["dirname"]:
+ kw["dirname"] = drive
+ else:
+ kw.setdefault("dirname", dirname)
+ kw.setdefault("sep", self.sep)
+ obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw)
+ return obj
+
+ def _getbyspec(self, spec: str) -> list[str]:
+ """See new for what 'spec' can be."""
+ res = []
+ parts = self.strpath.split(self.sep)
+
+ args = filter(None, spec.split(","))
+ for name in args:
+ if name == "drive":
+ res.append(parts[0])
+ elif name == "dirname":
+ res.append(self.sep.join(parts[:-1]))
+ else:
+ basename = parts[-1]
+ if name == "basename":
+ res.append(basename)
+ else:
+ i = basename.rfind(".")
+ if i == -1:
+ purebasename, ext = basename, ""
+ else:
+ purebasename, ext = basename[:i], basename[i:]
+ if name == "purebasename":
+ res.append(purebasename)
+ elif name == "ext":
+ res.append(ext)
+ else:
+ raise ValueError("invalid part specification %r" % name)
+ return res
+
+ def dirpath(self, *args, **kwargs):
+ """Return the directory path joined with any given path arguments."""
+ if not kwargs:
+ path = object.__new__(self.__class__)
+ path.strpath = dirname(self.strpath)
+ if args:
+ path = path.join(*args)
+ return path
+ return self.new(basename="").join(*args, **kwargs)
+
+ def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath:
+ """Return a new path by appending all 'args' as path
+ components. if abs=1 is used restart from root if any
+ of the args is an absolute path.
+ """
+ sep = self.sep
+ strargs = [os.fspath(arg) for arg in args]
+ strpath = self.strpath
+ if abs:
+ newargs: list[str] = []
+ for arg in reversed(strargs):
+ if isabs(arg):
+ strpath = arg
+ strargs = newargs
+ break
+ newargs.insert(0, arg)
+ # special case for when we have e.g. strpath == "/"
+ actual_sep = "" if strpath.endswith(sep) else sep
+ for arg in strargs:
+ arg = arg.strip(sep)
+ if iswin32:
+ # allow unix style paths even on windows.
+ arg = arg.strip("/")
+ arg = arg.replace("/", sep)
+ strpath = strpath + actual_sep + arg
+ actual_sep = sep
+ obj = object.__new__(self.__class__)
+ obj.strpath = normpath(strpath)
+ return obj
+
+ def open(self, mode="r", ensure=False, encoding=None):
+ """Return an opened file with the given mode.
+
+ If ensure is True, create parent directories if needed.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ if encoding:
+ return error.checked_call(io.open, self.strpath, mode, encoding=encoding)
+ return error.checked_call(open, self.strpath, mode)
+
+ def _fastjoin(self, name):
+ child = object.__new__(self.__class__)
+ child.strpath = self.strpath + self.sep + name
+ return child
+
+ def islink(self):
+ return islink(self.strpath)
+
+ def check(self, **kw):
+ """Check a path for existence and properties.
+
+ Without arguments, return True if the path exists, otherwise False.
+
+ valid checkers::
+
+ file=1 # is a file
+ file=0 # is not a file (may not even exist)
+ dir=1 # is a dir
+ link=1 # is a link
+ exists=1 # exists
+
+ You can specify multiple checker definitions, for example::
+
+ path.check(file=1, link=1) # a link pointing to a file
+ """
+ if not kw:
+ return exists(self.strpath)
+ if len(kw) == 1:
+ if "dir" in kw:
+ return not kw["dir"] ^ isdir(self.strpath)
+ if "file" in kw:
+ return not kw["file"] ^ isfile(self.strpath)
+ if not kw:
+ kw = {"exists": 1}
+ return Checkers(self)._evaluate(kw)
+
+ _patternchars = set("*?[" + os.sep)
+
+ def listdir(self, fil=None, sort=None):
+ """List directory contents, possibly filter by the given fil func
+ and possibly sorted.
+ """
+ if fil is None and sort is None:
+ names = error.checked_call(os.listdir, self.strpath)
+ return map_as_list(self._fastjoin, names)
+ if isinstance(fil, str):
+ if not self._patternchars.intersection(fil):
+ child = self._fastjoin(fil)
+ if exists(child.strpath):
+ return [child]
+ return []
+ fil = FNMatcher(fil)
+ names = error.checked_call(os.listdir, self.strpath)
+ res = []
+ for name in names:
+ child = self._fastjoin(name)
+ if fil is None or fil(child):
+ res.append(child)
+ self._sortlist(res, sort)
+ return res
+
+ def size(self) -> int:
+ """Return size of the underlying file object"""
+ return self.stat().size
+
+ def mtime(self) -> float:
+ """Return last modification time of the path."""
+ return self.stat().mtime
+
+ def copy(self, target, mode=False, stat=False):
+ """Copy path to target.
+
+ If mode is True, will copy copy permission from path to target.
+ If stat is True, copy permission, last modification
+ time, last access time, and flags from path to target.
+ """
+ if self.check(file=1):
+ if target.check(dir=1):
+ target = target.join(self.basename)
+ assert self != target
+ copychunked(self, target)
+ if mode:
+ copymode(self.strpath, target.strpath)
+ if stat:
+ copystat(self, target)
+ else:
+
+ def rec(p):
+ return p.check(link=0)
+
+ for x in self.visit(rec=rec):
+ relpath = x.relto(self)
+ newx = target.join(relpath)
+ newx.dirpath().ensure(dir=1)
+ if x.check(link=1):
+ newx.mksymlinkto(x.readlink())
+ continue
+ elif x.check(file=1):
+ copychunked(x, newx)
+ elif x.check(dir=1):
+ newx.ensure(dir=1)
+ if mode:
+ copymode(x.strpath, newx.strpath)
+ if stat:
+ copystat(x, newx)
+
+ def rename(self, target):
+ """Rename this path to target."""
+ target = os.fspath(target)
+ return error.checked_call(os.rename, self.strpath, target)
+
+ def dump(self, obj, bin=1):
+ """Pickle object into path location"""
+ f = self.open("wb")
+ import pickle
+
+ try:
+ error.checked_call(pickle.dump, obj, f, bin)
+ finally:
+ f.close()
+
+ def mkdir(self, *args):
+ """Create & return the directory joined with args."""
+ p = self.join(*args)
+ error.checked_call(os.mkdir, os.fspath(p))
+ return p
+
+ def write_binary(self, data, ensure=False):
+ """Write binary data into path. If ensure is True create
+ missing parent directories.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ with self.open("wb") as f:
+ f.write(data)
+
+ def write_text(self, data, encoding, ensure=False):
+ """Write text data into path using the specified encoding.
+ If ensure is True create missing parent directories.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ with self.open("w", encoding=encoding) as f:
+ f.write(data)
+
+ def write(self, data, mode="w", ensure=False):
+ """Write data into path. If ensure is True create
+ missing parent directories.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ if "b" in mode:
+ if not isinstance(data, bytes):
+ raise ValueError("can only process bytes")
+ else:
+ if not isinstance(data, str):
+ if not isinstance(data, bytes):
+ data = str(data)
+ else:
+ data = data.decode(sys.getdefaultencoding())
+ f = self.open(mode)
+ try:
+ f.write(data)
+ finally:
+ f.close()
+
+ def _ensuredirs(self):
+ parent = self.dirpath()
+ if parent == self:
+ return self
+ if parent.check(dir=0):
+ parent._ensuredirs()
+ if self.check(dir=0):
+ try:
+ self.mkdir()
+ except error.EEXIST:
+ # race condition: file/dir created by another thread/process.
+ # complain if it is not a dir
+ if self.check(dir=0):
+ raise
+ return self
+
+ def ensure(self, *args, **kwargs):
+ """Ensure that an args-joined path exists (by default as
+ a file). if you specify a keyword argument 'dir=True'
+ then the path is forced to be a directory path.
+ """
+ p = self.join(*args)
+ if kwargs.get("dir", 0):
+ return p._ensuredirs()
+ else:
+ p.dirpath()._ensuredirs()
+ if not p.check(file=1):
+ p.open("wb").close()
+ return p
+
+ @overload
+ def stat(self, raising: Literal[True] = ...) -> Stat:
+ ...
+
+ @overload
+ def stat(self, raising: Literal[False]) -> Stat | None:
+ ...
+
+ def stat(self, raising: bool = True) -> Stat | None:
+ """Return an os.stat() tuple."""
+ if raising:
+ return Stat(self, error.checked_call(os.stat, self.strpath))
+ try:
+ return Stat(self, os.stat(self.strpath))
+ except KeyboardInterrupt:
+ raise
+ except Exception:
+ return None
+
+ def lstat(self) -> Stat:
+ """Return an os.lstat() tuple."""
+ return Stat(self, error.checked_call(os.lstat, self.strpath))
+
+ def setmtime(self, mtime=None):
+ """Set modification time for the given path. if 'mtime' is None
+ (the default) then the file's mtime is set to current time.
+
+ Note that the resolution for 'mtime' is platform dependent.
+ """
+ if mtime is None:
+ return error.checked_call(os.utime, self.strpath, mtime)
+ try:
+ return error.checked_call(os.utime, self.strpath, (-1, mtime))
+ except error.EINVAL:
+ return error.checked_call(os.utime, self.strpath, (self.atime(), mtime))
+
+ def chdir(self):
+ """Change directory to self and return old current directory"""
+ try:
+ old = self.__class__()
+ except error.ENOENT:
+ old = None
+ error.checked_call(os.chdir, self.strpath)
+ return old
+
+ @contextmanager
+ def as_cwd(self):
+ """
+ Return a context manager, which changes to the path's dir during the
+ managed "with" context.
+ On __enter__ it returns the old dir, which might be ``None``.
+ """
+ old = self.chdir()
+ try:
+ yield old
+ finally:
+ if old is not None:
+ old.chdir()
+
+ def realpath(self):
+ """Return a new path which contains no symbolic links."""
+ return self.__class__(os.path.realpath(self.strpath))
+
+ def atime(self):
+ """Return last access time of the path."""
+ return self.stat().atime
+
+ def __repr__(self):
+ return "local(%r)" % self.strpath
+
+ def __str__(self):
+ """Return string representation of the Path."""
+ return self.strpath
+
+ def chmod(self, mode, rec=0):
+ """Change permissions to the given mode. If mode is an
+ integer it directly encodes the os-specific modes.
+ if rec is True perform recursively.
+ """
+ if not isinstance(mode, int):
+ raise TypeError(f"mode {mode!r} must be an integer")
+ if rec:
+ for x in self.visit(rec=rec):
+ error.checked_call(os.chmod, str(x), mode)
+ error.checked_call(os.chmod, self.strpath, mode)
+
+ def pypkgpath(self):
+ """Return the Python package path by looking for the last
+ directory upwards which still contains an __init__.py.
+ Return None if a pkgpath can not be determined.
+ """
+ pkgpath = None
+ for parent in self.parts(reverse=True):
+ if parent.isdir():
+ if not parent.join("__init__.py").exists():
+ break
+ if not isimportable(parent.basename):
+ break
+ pkgpath = parent
+ return pkgpath
+
+ def _ensuresyspath(self, ensuremode, path):
+ if ensuremode:
+ s = str(path)
+ if ensuremode == "append":
+ if s not in sys.path:
+ sys.path.append(s)
+ else:
+ if s != sys.path[0]:
+ sys.path.insert(0, s)
+
+ def pyimport(self, modname=None, ensuresyspath=True):
+ """Return path as an imported python module.
+
+ If modname is None, look for the containing package
+ and construct an according module name.
+ The module will be put/looked up in sys.modules.
+ if ensuresyspath is True then the root dir for importing
+ the file (taking __init__.py files into account) will
+ be prepended to sys.path if it isn't there already.
+ If ensuresyspath=="append" the root dir will be appended
+ if it isn't already contained in sys.path.
+ if ensuresyspath is False no modification of syspath happens.
+
+ Special value of ensuresyspath=="importlib" is intended
+ purely for using in pytest, it is capable only of importing
+ separate .py files outside packages, e.g. for test suite
+ without any __init__.py file. It effectively allows having
+ same-named test modules in different places and offers
+ mild opt-in via this option. Note that it works only in
+ recent versions of python.
+ """
+ if not self.check():
+ raise error.ENOENT(self)
+
+ if ensuresyspath == "importlib":
+ if modname is None:
+ modname = self.purebasename
+ spec = importlib.util.spec_from_file_location(modname, str(self))
+ if spec is None or spec.loader is None:
+ raise ImportError(
+ f"Can't find module {modname} at location {str(self)}"
+ )
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+ pkgpath = None
+ if modname is None:
+ pkgpath = self.pypkgpath()
+ if pkgpath is not None:
+ pkgroot = pkgpath.dirpath()
+ names = self.new(ext="").relto(pkgroot).split(self.sep)
+ if names[-1] == "__init__":
+ names.pop()
+ modname = ".".join(names)
+ else:
+ pkgroot = self.dirpath()
+ modname = self.purebasename
+
+ self._ensuresyspath(ensuresyspath, pkgroot)
+ __import__(modname)
+ mod = sys.modules[modname]
+ if self.basename == "__init__.py":
+ return mod # we don't check anything as we might
+ # be in a namespace package ... too icky to check
+ modfile = mod.__file__
+ assert modfile is not None
+ if modfile[-4:] in (".pyc", ".pyo"):
+ modfile = modfile[:-1]
+ elif modfile.endswith("$py.class"):
+ modfile = modfile[:-9] + ".py"
+ if modfile.endswith(os.sep + "__init__.py"):
+ if self.basename != "__init__.py":
+ modfile = modfile[:-12]
+ try:
+ issame = self.samefile(modfile)
+ except error.ENOENT:
+ issame = False
+ if not issame:
+ ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH")
+ if ignore != "1":
+ raise self.ImportMismatchError(modname, modfile, self)
+ return mod
+ else:
+ try:
+ return sys.modules[modname]
+ except KeyError:
+ # we have a custom modname, do a pseudo-import
+ import types
+
+ mod = types.ModuleType(modname)
+ mod.__file__ = str(self)
+ sys.modules[modname] = mod
+ try:
+ with open(str(self), "rb") as f:
+ exec(f.read(), mod.__dict__)
+ except BaseException:
+ del sys.modules[modname]
+ raise
+ return mod
+
+ def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str:
+ """Return stdout text from executing a system child process,
+ where the 'self' path points to executable.
+ The process is directly invoked and not through a system shell.
+ """
+ from subprocess import Popen, PIPE
+
+ popen_opts.pop("stdout", None)
+ popen_opts.pop("stderr", None)
+ proc = Popen(
+ [str(self)] + [str(arg) for arg in argv],
+ **popen_opts,
+ stdout=PIPE,
+ stderr=PIPE,
+ )
+ stdout: str | bytes
+ stdout, stderr = proc.communicate()
+ ret = proc.wait()
+ if isinstance(stdout, bytes):
+ stdout = stdout.decode(sys.getdefaultencoding())
+ if ret != 0:
+ if isinstance(stderr, bytes):
+ stderr = stderr.decode(sys.getdefaultencoding())
+ raise RuntimeError(
+ ret,
+ ret,
+ str(self),
+ stdout,
+ stderr,
+ )
+ return stdout
+
+ @classmethod
+ def sysfind(cls, name, checker=None, paths=None):
+ """Return a path object found by looking at the systems
+ underlying PATH specification. If the checker is not None
+ it will be invoked to filter matching paths. If a binary
+ cannot be found, None is returned
+ Note: This is probably not working on plain win32 systems
+ but may work on cygwin.
+ """
+ if isabs(name):
+ p = local(name)
+ if p.check(file=1):
+ return p
+ else:
+ if paths is None:
+ if iswin32:
+ paths = os.environ["Path"].split(";")
+ if "" not in paths and "." not in paths:
+ paths.append(".")
+ try:
+ systemroot = os.environ["SYSTEMROOT"]
+ except KeyError:
+ pass
+ else:
+ paths = [
+ path.replace("%SystemRoot%", systemroot) for path in paths
+ ]
+ else:
+ paths = os.environ["PATH"].split(":")
+ tryadd = []
+ if iswin32:
+ tryadd += os.environ["PATHEXT"].split(os.pathsep)
+ tryadd.append("")
+
+ for x in paths:
+ for addext in tryadd:
+ p = local(x).join(name, abs=True) + addext
+ try:
+ if p.check(file=1):
+ if checker:
+ if not checker(p):
+ continue
+ return p
+ except error.EACCES:
+ pass
+ return None
+
+ @classmethod
+ def _gethomedir(cls):
+ try:
+ x = os.environ["HOME"]
+ except KeyError:
+ try:
+ x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"]
+ except KeyError:
+ return None
+ return cls(x)
+
+ # """
+ # special class constructors for local filesystem paths
+ # """
+ @classmethod
+ def get_temproot(cls):
+ """Return the system's temporary directory
+ (where tempfiles are usually created in)
+ """
+ import tempfile
+
+ return local(tempfile.gettempdir())
+
+ @classmethod
+ def mkdtemp(cls, rootdir=None):
+ """Return a Path object pointing to a fresh new temporary directory
+ (which we created ourself).
+ """
+ import tempfile
+
+ if rootdir is None:
+ rootdir = cls.get_temproot()
+ return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir)))
+
+ @classmethod
+ def make_numbered_dir(
+ cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800
+ ): # two days
+ """Return unique directory with a number greater than the current
+ maximum one. The number is assumed to start directly after prefix.
+ if keep is true directories with a number less than (maxnum-keep)
+ will be removed. If .lock files are used (lock_timeout non-zero),
+ algorithm is multi-process safe.
+ """
+ if rootdir is None:
+ rootdir = cls.get_temproot()
+
+ nprefix = prefix.lower()
+
+ def parse_num(path):
+ """Parse the number out of a path (if it matches the prefix)"""
+ nbasename = path.basename.lower()
+ if nbasename.startswith(nprefix):
+ try:
+ return int(nbasename[len(nprefix) :])
+ except ValueError:
+ pass
+
+ def create_lockfile(path):
+ """Exclusively create lockfile. Throws when failed"""
+ mypid = os.getpid()
+ lockfile = path.join(".lock")
+ if hasattr(lockfile, "mksymlinkto"):
+ lockfile.mksymlinkto(str(mypid))
+ else:
+ fd = error.checked_call(
+ os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644
+ )
+ with os.fdopen(fd, "w") as f:
+ f.write(str(mypid))
+ return lockfile
+
+ def atexit_remove_lockfile(lockfile):
+ """Ensure lockfile is removed at process exit"""
+ mypid = os.getpid()
+
+ def try_remove_lockfile():
+ # in a fork() situation, only the last process should
+ # remove the .lock, otherwise the other processes run the
+ # risk of seeing their temporary dir disappear. For now
+ # we remove the .lock in the parent only (i.e. we assume
+ # that the children finish before the parent).
+ if os.getpid() != mypid:
+ return
+ try:
+ lockfile.remove()
+ except error.Error:
+ pass
+
+ atexit.register(try_remove_lockfile)
+
+ # compute the maximum number currently in use with the prefix
+ lastmax = None
+ while True:
+ maxnum = -1
+ for path in rootdir.listdir():
+ num = parse_num(path)
+ if num is not None:
+ maxnum = max(maxnum, num)
+
+ # make the new directory
+ try:
+ udir = rootdir.mkdir(prefix + str(maxnum + 1))
+ if lock_timeout:
+ lockfile = create_lockfile(udir)
+ atexit_remove_lockfile(lockfile)
+ except (error.EEXIST, error.ENOENT, error.EBUSY):
+ # race condition (1): another thread/process created the dir
+ # in the meantime - try again
+ # race condition (2): another thread/process spuriously acquired
+ # lock treating empty directory as candidate
+ # for removal - try again
+ # race condition (3): another thread/process tried to create the lock at
+ # the same time (happened in Python 3.3 on Windows)
+ # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa
+ if lastmax == maxnum:
+ raise
+ lastmax = maxnum
+ continue
+ break
+
+ def get_mtime(path):
+ """Read file modification time"""
+ try:
+ return path.lstat().mtime
+ except error.Error:
+ pass
+
+ garbage_prefix = prefix + "garbage-"
+
+ def is_garbage(path):
+ """Check if path denotes directory scheduled for removal"""
+ bn = path.basename
+ return bn.startswith(garbage_prefix)
+
+ # prune old directories
+ udir_time = get_mtime(udir)
+ if keep and udir_time:
+ for path in rootdir.listdir():
+ num = parse_num(path)
+ if num is not None and num <= (maxnum - keep):
+ try:
+ # try acquiring lock to remove directory as exclusive user
+ if lock_timeout:
+ create_lockfile(path)
+ except (error.EEXIST, error.ENOENT, error.EBUSY):
+ path_time = get_mtime(path)
+ if not path_time:
+ # assume directory doesn't exist now
+ continue
+ if abs(udir_time - path_time) < lock_timeout:
+ # assume directory with lockfile exists
+ # and lock timeout hasn't expired yet
+ continue
+
+ # path dir locked for exclusive use
+ # and scheduled for removal to avoid another thread/process
+ # treating it as a new directory or removal candidate
+ garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4()))
+ try:
+ path.rename(garbage_path)
+ garbage_path.remove(rec=1)
+ except KeyboardInterrupt:
+ raise
+ except Exception: # this might be error.Error, WindowsError ...
+ pass
+ if is_garbage(path):
+ try:
+ path.remove(rec=1)
+ except KeyboardInterrupt:
+ raise
+ except Exception: # this might be error.Error, WindowsError ...
+ pass
+
+ # make link...
+ try:
+ username = os.environ["USER"] # linux, et al
+ except KeyError:
+ try:
+ username = os.environ["USERNAME"] # windows
+ except KeyError:
+ username = "current"
+
+ src = str(udir)
+ dest = src[: src.rfind("-")] + "-" + username
+ try:
+ os.unlink(dest)
+ except OSError:
+ pass
+ try:
+ os.symlink(src, dest)
+ except (OSError, AttributeError, NotImplementedError):
+ pass
+
+ return udir
+
+
+def copymode(src, dest):
+ """Copy permission from src to dst."""
+ import shutil
+
+ shutil.copymode(src, dest)
+
+
+def copystat(src, dest):
+ """Copy permission, last modification time,
+ last access time, and flags from src to dst."""
+ import shutil
+
+ shutil.copystat(str(src), str(dest))
+
+
+def copychunked(src, dest):
+ chunksize = 524288 # half a meg of bytes
+ fsrc = src.open("rb")
+ try:
+ fdest = dest.open("wb")
+ try:
+ while 1:
+ buf = fsrc.read(chunksize)
+ if not buf:
+ break
+ fdest.write(buf)
+ finally:
+ fdest.close()
+ finally:
+ fsrc.close()
+
+
+def isimportable(name):
+ if name and (name[0].isalpha() or name[0] == "_"):
+ name = name.replace("_", "")
+ return not name or name.isalnum()
+
+
+local = LocalPath
diff --git a/venv/lib/python3.12/site-packages/_pytest/_version.py b/venv/lib/python3.12/site-packages/_pytest/_version.py
new file mode 100644
index 0000000..458d065
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/_version.py
@@ -0,0 +1,16 @@
+# file generated by setuptools_scm
+# don't change, don't track in version control
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Tuple, Union
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
+else:
+ VERSION_TUPLE = object
+
+version: str
+__version__: str
+__version_tuple__: VERSION_TUPLE
+version_tuple: VERSION_TUPLE
+
+__version__ = version = '7.4.4'
+__version_tuple__ = version_tuple = (7, 4, 4)
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/__init__.py b/venv/lib/python3.12/site-packages/_pytest/assertion/__init__.py
new file mode 100644
index 0000000..a46e581
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/assertion/__init__.py
@@ -0,0 +1,181 @@
+"""Support for presenting detailed information in failing assertions."""
+import sys
+from typing import Any
+from typing import Generator
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+
+from _pytest.assertion import rewrite
+from _pytest.assertion import truncate
+from _pytest.assertion import util
+from _pytest.assertion.rewrite import assertstate_key
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+
+if TYPE_CHECKING:
+ from _pytest.main import Session
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--assert",
+ action="store",
+ dest="assertmode",
+ choices=("rewrite", "plain"),
+ default="rewrite",
+ metavar="MODE",
+ help=(
+ "Control assertion debugging tools.\n"
+ "'plain' performs no assertion debugging.\n"
+ "'rewrite' (the default) rewrites assert statements in test modules"
+ " on import to provide assert expression information."
+ ),
+ )
+ parser.addini(
+ "enable_assertion_pass_hook",
+ type="bool",
+ default=False,
+ help="Enables the pytest_assertion_pass hook. "
+ "Make sure to delete any previously generated pyc cache files.",
+ )
+
+
+def register_assert_rewrite(*names: str) -> None:
+ """Register one or more module names to be rewritten on import.
+
+ This function will make sure that this module or all modules inside
+ the package will get their assert statements rewritten.
+ Thus you should make sure to call this before the module is
+ actually imported, usually in your __init__.py if you are a plugin
+ using a package.
+
+ :param names: The module names to register.
+ """
+ for name in names:
+ if not isinstance(name, str):
+ msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
+ raise TypeError(msg.format(repr(names)))
+ for hook in sys.meta_path:
+ if isinstance(hook, rewrite.AssertionRewritingHook):
+ importhook = hook
+ break
+ else:
+ # TODO(typing): Add a protocol for mark_rewrite() and use it
+ # for importhook and for PytestPluginManager.rewrite_hook.
+ importhook = DummyRewriteHook() # type: ignore
+ importhook.mark_rewrite(*names)
+
+
+class DummyRewriteHook:
+ """A no-op import hook for when rewriting is disabled."""
+
+ def mark_rewrite(self, *names: str) -> None:
+ pass
+
+
+class AssertionState:
+ """State for the assertion plugin."""
+
+ def __init__(self, config: Config, mode) -> None:
+ self.mode = mode
+ self.trace = config.trace.root.get("assertion")
+ self.hook: Optional[rewrite.AssertionRewritingHook] = None
+
+
+def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
+ """Try to install the rewrite hook, raise SystemError if it fails."""
+ config.stash[assertstate_key] = AssertionState(config, "rewrite")
+ config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
+ sys.meta_path.insert(0, hook)
+ config.stash[assertstate_key].trace("installed rewrite import hook")
+
+ def undo() -> None:
+ hook = config.stash[assertstate_key].hook
+ if hook is not None and hook in sys.meta_path:
+ sys.meta_path.remove(hook)
+
+ config.add_cleanup(undo)
+ return hook
+
+
+def pytest_collection(session: "Session") -> None:
+ # This hook is only called when test modules are collected
+ # so for example not in the managing process of pytest-xdist
+ # (which does not collect test modules).
+ assertstate = session.config.stash.get(assertstate_key, None)
+ if assertstate:
+ if assertstate.hook is not None:
+ assertstate.hook.set_session(session)
+
+
+@hookimpl(tryfirst=True, hookwrapper=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
+ """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
+
+ The rewrite module will use util._reprcompare if it exists to use custom
+ reporting via the pytest_assertrepr_compare hook. This sets up this custom
+ comparison for the test.
+ """
+
+ ihook = item.ihook
+
+ def callbinrepr(op, left: object, right: object) -> Optional[str]:
+ """Call the pytest_assertrepr_compare hook and prepare the result.
+
+ This uses the first result from the hook and then ensures the
+ following:
+ * Overly verbose explanations are truncated unless configured otherwise
+ (eg. if running in verbose mode).
+ * Embedded newlines are escaped to help util.format_explanation()
+ later.
+ * If the rewrite mode is used embedded %-characters are replaced
+ to protect later % formatting.
+
+ The result can be formatted by util.format_explanation() for
+ pretty printing.
+ """
+ hook_result = ihook.pytest_assertrepr_compare(
+ config=item.config, op=op, left=left, right=right
+ )
+ for new_expl in hook_result:
+ if new_expl:
+ new_expl = truncate.truncate_if_required(new_expl, item)
+ new_expl = [line.replace("\n", "\\n") for line in new_expl]
+ res = "\n~".join(new_expl)
+ if item.config.getvalue("assertmode") == "rewrite":
+ res = res.replace("%", "%%")
+ return res
+ return None
+
+ saved_assert_hooks = util._reprcompare, util._assertion_pass
+ util._reprcompare = callbinrepr
+ util._config = item.config
+
+ if ihook.pytest_assertion_pass.get_hookimpls():
+
+ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
+ ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
+
+ util._assertion_pass = call_assertion_pass_hook
+
+ yield
+
+ util._reprcompare, util._assertion_pass = saved_assert_hooks
+ util._config = None
+
+
+def pytest_sessionfinish(session: "Session") -> None:
+ assertstate = session.config.stash.get(assertstate_key, None)
+ if assertstate:
+ if assertstate.hook is not None:
+ assertstate.hook.set_session(None)
+
+
+def pytest_assertrepr_compare(
+ config: Config, op: str, left: Any, right: Any
+) -> Optional[List[str]]:
+ return util.assertrepr_compare(config=config, op=op, left=left, right=right)
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..3cd742e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/rewrite.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/rewrite.cpython-312.pyc
new file mode 100644
index 0000000..d3f4a35
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/rewrite.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/truncate.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/truncate.cpython-312.pyc
new file mode 100644
index 0000000..bf45896
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/truncate.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/util.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/util.cpython-312.pyc
new file mode 100644
index 0000000..7f472a0
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/assertion/__pycache__/util.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/rewrite.py b/venv/lib/python3.12/site-packages/_pytest/assertion/rewrite.py
new file mode 100644
index 0000000..d1974bb
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/assertion/rewrite.py
@@ -0,0 +1,1217 @@
+"""Rewrite assertion AST to produce nice error messages."""
+import ast
+import errno
+import functools
+import importlib.abc
+import importlib.machinery
+import importlib.util
+import io
+import itertools
+import marshal
+import os
+import struct
+import sys
+import tokenize
+import types
+from collections import defaultdict
+from pathlib import Path
+from pathlib import PurePath
+from typing import Callable
+from typing import Dict
+from typing import IO
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
+from _pytest._io.saferepr import saferepr
+from _pytest._version import version
+from _pytest.assertion import util
+from _pytest.assertion.util import ( # noqa: F401
+ format_explanation as _format_explanation,
+)
+from _pytest.config import Config
+from _pytest.main import Session
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import fnmatch_ex
+from _pytest.stash import StashKey
+
+if TYPE_CHECKING:
+ from _pytest.assertion import AssertionState
+
+if sys.version_info >= (3, 8):
+ namedExpr = ast.NamedExpr
+ astNameConstant = ast.Constant
+ astStr = ast.Constant
+ astNum = ast.Constant
+else:
+ namedExpr = ast.Expr
+ astNameConstant = ast.NameConstant
+ astStr = ast.Str
+ astNum = ast.Num
+
+
+class Sentinel:
+ pass
+
+
+assertstate_key = StashKey["AssertionState"]()
+
+# pytest caches rewritten pycs in pycache dirs
+PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
+PYC_EXT = ".py" + (__debug__ and "c" or "o")
+PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
+
+# Special marker that denotes we have just left a scope definition
+_SCOPE_END_MARKER = Sentinel()
+
+
+class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
+ """PEP302/PEP451 import hook which rewrites asserts."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ try:
+ self.fnpats = config.getini("python_files")
+ except ValueError:
+ self.fnpats = ["test_*.py", "*_test.py"]
+ self.session: Optional[Session] = None
+ self._rewritten_names: Dict[str, Path] = {}
+ self._must_rewrite: Set[str] = set()
+ # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
+ # which might result in infinite recursion (#3506)
+ self._writing_pyc = False
+ self._basenames_to_check_rewrite = {"conftest"}
+ self._marked_for_rewrite_cache: Dict[str, bool] = {}
+ self._session_paths_checked = False
+
+ def set_session(self, session: Optional[Session]) -> None:
+ self.session = session
+ self._session_paths_checked = False
+
+ # Indirection so we can mock calls to find_spec originated from the hook during testing
+ _find_spec = importlib.machinery.PathFinder.find_spec
+
+ def find_spec(
+ self,
+ name: str,
+ path: Optional[Sequence[Union[str, bytes]]] = None,
+ target: Optional[types.ModuleType] = None,
+ ) -> Optional[importlib.machinery.ModuleSpec]:
+ if self._writing_pyc:
+ return None
+ state = self.config.stash[assertstate_key]
+ if self._early_rewrite_bailout(name, state):
+ return None
+ state.trace("find_module called for: %s" % name)
+
+ # Type ignored because mypy is confused about the `self` binding here.
+ spec = self._find_spec(name, path) # type: ignore
+ if (
+ # the import machinery could not find a file to import
+ spec is None
+ # this is a namespace package (without `__init__.py`)
+ # there's nothing to rewrite there
+ or spec.origin is None
+ # we can only rewrite source files
+ or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
+ # if the file doesn't exist, we can't rewrite it
+ or not os.path.exists(spec.origin)
+ ):
+ return None
+ else:
+ fn = spec.origin
+
+ if not self._should_rewrite(name, fn, state):
+ return None
+
+ return importlib.util.spec_from_file_location(
+ name,
+ fn,
+ loader=self,
+ submodule_search_locations=spec.submodule_search_locations,
+ )
+
+ def create_module(
+ self, spec: importlib.machinery.ModuleSpec
+ ) -> Optional[types.ModuleType]:
+ return None # default behaviour is fine
+
+ def exec_module(self, module: types.ModuleType) -> None:
+ assert module.__spec__ is not None
+ assert module.__spec__.origin is not None
+ fn = Path(module.__spec__.origin)
+ state = self.config.stash[assertstate_key]
+
+ self._rewritten_names[module.__name__] = fn
+
+ # The requested module looks like a test file, so rewrite it. This is
+ # the most magical part of the process: load the source, rewrite the
+ # asserts, and load the rewritten source. We also cache the rewritten
+ # module code in a special pyc. We must be aware of the possibility of
+ # concurrent pytest processes rewriting and loading pycs. To avoid
+ # tricky race conditions, we maintain the following invariant: The
+ # cached pyc is always a complete, valid pyc. Operations on it must be
+ # atomic. POSIX's atomic rename comes in handy.
+ write = not sys.dont_write_bytecode
+ cache_dir = get_cache_dir(fn)
+ if write:
+ ok = try_makedirs(cache_dir)
+ if not ok:
+ write = False
+ state.trace(f"read only directory: {cache_dir}")
+
+ cache_name = fn.name[:-3] + PYC_TAIL
+ pyc = cache_dir / cache_name
+ # Notice that even if we're in a read-only directory, I'm going
+ # to check for a cached pyc. This may not be optimal...
+ co = _read_pyc(fn, pyc, state.trace)
+ if co is None:
+ state.trace(f"rewriting {fn!r}")
+ source_stat, co = _rewrite_test(fn, self.config)
+ if write:
+ self._writing_pyc = True
+ try:
+ _write_pyc(state, co, source_stat, pyc)
+ finally:
+ self._writing_pyc = False
+ else:
+ state.trace(f"found cached rewritten pyc for {fn}")
+ exec(co, module.__dict__)
+
+ def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
+ """A fast way to get out of rewriting modules.
+
+ Profiling has shown that the call to PathFinder.find_spec (inside of
+ the find_spec from this class) is a major slowdown, so, this method
+ tries to filter what we're sure won't be rewritten before getting to
+ it.
+ """
+ if self.session is not None and not self._session_paths_checked:
+ self._session_paths_checked = True
+ for initial_path in self.session._initialpaths:
+ # Make something as c:/projects/my_project/path.py ->
+ # ['c:', 'projects', 'my_project', 'path.py']
+ parts = str(initial_path).split(os.sep)
+ # add 'path' to basenames to be checked.
+ self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
+
+ # Note: conftest already by default in _basenames_to_check_rewrite.
+ parts = name.split(".")
+ if parts[-1] in self._basenames_to_check_rewrite:
+ return False
+
+ # For matching the name it must be as if it was a filename.
+ path = PurePath(*parts).with_suffix(".py")
+
+ for pat in self.fnpats:
+ # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
+ # on the name alone because we need to match against the full path
+ if os.path.dirname(pat):
+ return False
+ if fnmatch_ex(pat, path):
+ return False
+
+ if self._is_marked_for_rewrite(name, state):
+ return False
+
+ state.trace(f"early skip of rewriting module: {name}")
+ return True
+
+ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool:
+ # always rewrite conftest files
+ if os.path.basename(fn) == "conftest.py":
+ state.trace(f"rewriting conftest file: {fn!r}")
+ return True
+
+ if self.session is not None:
+ if self.session.isinitpath(absolutepath(fn)):
+ state.trace(f"matched test file (was specified on cmdline): {fn!r}")
+ return True
+
+ # modules not passed explicitly on the command line are only
+ # rewritten if they match the naming convention for test files
+ fn_path = PurePath(fn)
+ for pat in self.fnpats:
+ if fnmatch_ex(pat, fn_path):
+ state.trace(f"matched test file {fn!r}")
+ return True
+
+ return self._is_marked_for_rewrite(name, state)
+
+ def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool:
+ try:
+ return self._marked_for_rewrite_cache[name]
+ except KeyError:
+ for marked in self._must_rewrite:
+ if name == marked or name.startswith(marked + "."):
+ state.trace(f"matched marked file {name!r} (from {marked!r})")
+ self._marked_for_rewrite_cache[name] = True
+ return True
+
+ self._marked_for_rewrite_cache[name] = False
+ return False
+
+ def mark_rewrite(self, *names: str) -> None:
+ """Mark import names as needing to be rewritten.
+
+ The named module or package as well as any nested modules will
+ be rewritten on import.
+ """
+ already_imported = (
+ set(names).intersection(sys.modules).difference(self._rewritten_names)
+ )
+ for name in already_imported:
+ mod = sys.modules[name]
+ if not AssertionRewriter.is_rewrite_disabled(
+ mod.__doc__ or ""
+ ) and not isinstance(mod.__loader__, type(self)):
+ self._warn_already_imported(name)
+ self._must_rewrite.update(names)
+ self._marked_for_rewrite_cache.clear()
+
+ def _warn_already_imported(self, name: str) -> None:
+ from _pytest.warning_types import PytestAssertRewriteWarning
+
+ self.config.issue_config_time_warning(
+ PytestAssertRewriteWarning(
+ "Module already imported so cannot be rewritten: %s" % name
+ ),
+ stacklevel=5,
+ )
+
+ def get_data(self, pathname: Union[str, bytes]) -> bytes:
+ """Optional PEP302 get_data API."""
+ with open(pathname, "rb") as f:
+ return f.read()
+
+ if sys.version_info >= (3, 10):
+ if sys.version_info >= (3, 12):
+ from importlib.resources.abc import TraversableResources
+ else:
+ from importlib.abc import TraversableResources
+
+ def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
+ if sys.version_info < (3, 11):
+ from importlib.readers import FileReader
+ else:
+ from importlib.resources.readers import FileReader
+
+ return FileReader( # type:ignore[no-any-return]
+ types.SimpleNamespace(path=self._rewritten_names[name])
+ )
+
+
+def _write_pyc_fp(
+ fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
+) -> None:
+ # Technically, we don't have to have the same pyc format as
+ # (C)Python, since these "pycs" should never be seen by builtin
+ # import. However, there's little reason to deviate.
+ fp.write(importlib.util.MAGIC_NUMBER)
+ # https://www.python.org/dev/peps/pep-0552/
+ flags = b"\x00\x00\x00\x00"
+ fp.write(flags)
+ # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
+ mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
+ size = source_stat.st_size & 0xFFFFFFFF
+ # " bool:
+ proc_pyc = f"{pyc}.{os.getpid()}"
+ try:
+ with open(proc_pyc, "wb") as fp:
+ _write_pyc_fp(fp, source_stat, co)
+ except OSError as e:
+ state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
+ return False
+
+ try:
+ os.replace(proc_pyc, pyc)
+ except OSError as e:
+ state.trace(f"error writing pyc file at {pyc}: {e}")
+ # we ignore any failure to write the cache file
+ # there are many reasons, permission-denied, pycache dir being a
+ # file etc.
+ return False
+ return True
+
+
+def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
+ """Read and rewrite *fn* and return the code object."""
+ stat = os.stat(fn)
+ source = fn.read_bytes()
+ strfn = str(fn)
+ tree = ast.parse(source, filename=strfn)
+ rewrite_asserts(tree, source, strfn, config)
+ co = compile(tree, strfn, "exec", dont_inherit=True)
+ return stat, co
+
+
+def _read_pyc(
+ source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
+) -> Optional[types.CodeType]:
+ """Possibly read a pytest pyc containing rewritten code.
+
+ Return rewritten code if successful or None if not.
+ """
+ try:
+ fp = open(pyc, "rb")
+ except OSError:
+ return None
+ with fp:
+ try:
+ stat_result = os.stat(source)
+ mtime = int(stat_result.st_mtime)
+ size = stat_result.st_size
+ data = fp.read(16)
+ except OSError as e:
+ trace(f"_read_pyc({source}): OSError {e}")
+ return None
+ # Check for invalid or out of date pyc file.
+ if len(data) != (16):
+ trace("_read_pyc(%s): invalid pyc (too short)" % source)
+ return None
+ if data[:4] != importlib.util.MAGIC_NUMBER:
+ trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
+ return None
+ if data[4:8] != b"\x00\x00\x00\x00":
+ trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
+ return None
+ mtime_data = data[8:12]
+ if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
+ trace("_read_pyc(%s): out of date" % source)
+ return None
+ size_data = data[12:16]
+ if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
+ trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
+ return None
+ try:
+ co = marshal.load(fp)
+ except Exception as e:
+ trace(f"_read_pyc({source}): marshal.load error {e}")
+ return None
+ if not isinstance(co, types.CodeType):
+ trace("_read_pyc(%s): not a code object" % source)
+ return None
+ return co
+
+
+def rewrite_asserts(
+ mod: ast.Module,
+ source: bytes,
+ module_path: Optional[str] = None,
+ config: Optional[Config] = None,
+) -> None:
+ """Rewrite the assert statements in mod."""
+ AssertionRewriter(module_path, config, source).run(mod)
+
+
+def _saferepr(obj: object) -> str:
+ r"""Get a safe repr of an object for assertion error messages.
+
+ The assertion formatting (util.format_explanation()) requires
+ newlines to be escaped since they are a special character for it.
+ Normally assertion.util.format_explanation() does this but for a
+ custom repr it is possible to contain one of the special escape
+ sequences, especially '\n{' and '\n}' are likely to be present in
+ JSON reprs.
+ """
+ maxsize = _get_maxsize_for_saferepr(util._config)
+ return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
+
+
+def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
+ """Get `maxsize` configuration for saferepr based on the given config object."""
+ verbosity = config.getoption("verbose") if config is not None else 0
+ if verbosity >= 2:
+ return None
+ if verbosity >= 1:
+ return DEFAULT_REPR_MAX_SIZE * 10
+ return DEFAULT_REPR_MAX_SIZE
+
+
+def _format_assertmsg(obj: object) -> str:
+ r"""Format the custom assertion message given.
+
+ For strings this simply replaces newlines with '\n~' so that
+ util.format_explanation() will preserve them instead of escaping
+ newlines. For other objects saferepr() is used first.
+ """
+ # reprlib appears to have a bug which means that if a string
+ # contains a newline it gets escaped, however if an object has a
+ # .__repr__() which contains newlines it does not get escaped.
+ # However in either case we want to preserve the newline.
+ replaces = [("\n", "\n~"), ("%", "%%")]
+ if not isinstance(obj, str):
+ obj = saferepr(obj)
+ replaces.append(("\\n", "\n~"))
+
+ for r1, r2 in replaces:
+ obj = obj.replace(r1, r2)
+
+ return obj
+
+
+def _should_repr_global_name(obj: object) -> bool:
+ if callable(obj):
+ return False
+
+ try:
+ return not hasattr(obj, "__name__")
+ except Exception:
+ return True
+
+
+def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
+ explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
+ return explanation.replace("%", "%%")
+
+
+def _call_reprcompare(
+ ops: Sequence[str],
+ results: Sequence[bool],
+ expls: Sequence[str],
+ each_obj: Sequence[object],
+) -> str:
+ for i, res, expl in zip(range(len(ops)), results, expls):
+ try:
+ done = not res
+ except Exception:
+ done = True
+ if done:
+ break
+ if util._reprcompare is not None:
+ custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
+ if custom is not None:
+ return custom
+ return expl
+
+
+def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
+ if util._assertion_pass is not None:
+ util._assertion_pass(lineno, orig, expl)
+
+
+def _check_if_assertion_pass_impl() -> bool:
+ """Check if any plugins implement the pytest_assertion_pass hook
+ in order not to generate explanation unnecessarily (might be expensive)."""
+ return True if util._assertion_pass else False
+
+
+UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
+
+BINOP_MAP = {
+ ast.BitOr: "|",
+ ast.BitXor: "^",
+ ast.BitAnd: "&",
+ ast.LShift: "<<",
+ ast.RShift: ">>",
+ ast.Add: "+",
+ ast.Sub: "-",
+ ast.Mult: "*",
+ ast.Div: "/",
+ ast.FloorDiv: "//",
+ ast.Mod: "%%", # escaped for string formatting
+ ast.Eq: "==",
+ ast.NotEq: "!=",
+ ast.Lt: "<",
+ ast.LtE: "<=",
+ ast.Gt: ">",
+ ast.GtE: ">=",
+ ast.Pow: "**",
+ ast.Is: "is",
+ ast.IsNot: "is not",
+ ast.In: "in",
+ ast.NotIn: "not in",
+ ast.MatMult: "@",
+}
+
+
+def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
+ """Recursively yield node and all its children in depth-first order."""
+ yield node
+ for child in ast.iter_child_nodes(node):
+ yield from traverse_node(child)
+
+
+@functools.lru_cache(maxsize=1)
+def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
+ """Return a mapping from {lineno: "assertion test expression"}."""
+ ret: Dict[int, str] = {}
+
+ depth = 0
+ lines: List[str] = []
+ assert_lineno: Optional[int] = None
+ seen_lines: Set[int] = set()
+
+ def _write_and_reset() -> None:
+ nonlocal depth, lines, assert_lineno, seen_lines
+ assert assert_lineno is not None
+ ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
+ depth = 0
+ lines = []
+ assert_lineno = None
+ seen_lines = set()
+
+ tokens = tokenize.tokenize(io.BytesIO(src).readline)
+ for tp, source, (lineno, offset), _, line in tokens:
+ if tp == tokenize.NAME and source == "assert":
+ assert_lineno = lineno
+ elif assert_lineno is not None:
+ # keep track of depth for the assert-message `,` lookup
+ if tp == tokenize.OP and source in "([{":
+ depth += 1
+ elif tp == tokenize.OP and source in ")]}":
+ depth -= 1
+
+ if not lines:
+ lines.append(line[offset:])
+ seen_lines.add(lineno)
+ # a non-nested comma separates the expression from the message
+ elif depth == 0 and tp == tokenize.OP and source == ",":
+ # one line assert with message
+ if lineno in seen_lines and len(lines) == 1:
+ offset_in_trimmed = offset + len(lines[-1]) - len(line)
+ lines[-1] = lines[-1][:offset_in_trimmed]
+ # multi-line assert with message
+ elif lineno in seen_lines:
+ lines[-1] = lines[-1][:offset]
+ # multi line assert with escapd newline before message
+ else:
+ lines.append(line[:offset])
+ _write_and_reset()
+ elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}:
+ _write_and_reset()
+ elif lines and lineno not in seen_lines:
+ lines.append(line)
+ seen_lines.add(lineno)
+
+ return ret
+
+
+def _get_ast_constant_value(value: astStr) -> object:
+ if sys.version_info >= (3, 8):
+ return value.value
+ else:
+ return value.s
+
+
+class AssertionRewriter(ast.NodeVisitor):
+ """Assertion rewriting implementation.
+
+ The main entrypoint is to call .run() with an ast.Module instance,
+ this will then find all the assert statements and rewrite them to
+ provide intermediate values and a detailed assertion error. See
+ http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
+ for an overview of how this works.
+
+ The entry point here is .run() which will iterate over all the
+ statements in an ast.Module and for each ast.Assert statement it
+ finds call .visit() with it. Then .visit_Assert() takes over and
+ is responsible for creating new ast statements to replace the
+ original assert statement: it rewrites the test of an assertion
+ to provide intermediate values and replace it with an if statement
+ which raises an assertion error with a detailed explanation in
+ case the expression is false and calls pytest_assertion_pass hook
+ if expression is true.
+
+ For this .visit_Assert() uses the visitor pattern to visit all the
+ AST nodes of the ast.Assert.test field, each visit call returning
+ an AST node and the corresponding explanation string. During this
+ state is kept in several instance attributes:
+
+ :statements: All the AST statements which will replace the assert
+ statement.
+
+ :variables: This is populated by .variable() with each variable
+ used by the statements so that they can all be set to None at
+ the end of the statements.
+
+ :variable_counter: Counter to create new unique variables needed
+ by statements. Variables are created using .variable() and
+ have the form of "@py_assert0".
+
+ :expl_stmts: The AST statements which will be executed to get
+ data from the assertion. This is the code which will construct
+ the detailed assertion message that is used in the AssertionError
+ or for the pytest_assertion_pass hook.
+
+ :explanation_specifiers: A dict filled by .explanation_param()
+ with %-formatting placeholders and their corresponding
+ expressions to use in the building of an assertion message.
+ This is used by .pop_format_context() to build a message.
+
+ :stack: A stack of the explanation_specifiers dicts maintained by
+ .push_format_context() and .pop_format_context() which allows
+ to build another %-formatted string while already building one.
+
+ :scope: A tuple containing the current scope used for variables_overwrite.
+
+ :variables_overwrite: A dict filled with references to variables
+ that change value within an assert. This happens when a variable is
+ reassigned with the walrus operator
+
+ This state, except the variables_overwrite, is reset on every new assert
+ statement visited and used by the other visitors.
+ """
+
+ def __init__(
+ self, module_path: Optional[str], config: Optional[Config], source: bytes
+ ) -> None:
+ super().__init__()
+ self.module_path = module_path
+ self.config = config
+ if config is not None:
+ self.enable_assertion_pass_hook = config.getini(
+ "enable_assertion_pass_hook"
+ )
+ else:
+ self.enable_assertion_pass_hook = False
+ self.source = source
+ self.scope: tuple[ast.AST, ...] = ()
+ self.variables_overwrite: defaultdict[
+ tuple[ast.AST, ...], Dict[str, str]
+ ] = defaultdict(dict)
+
+ def run(self, mod: ast.Module) -> None:
+ """Find all assert statements in *mod* and rewrite them."""
+ if not mod.body:
+ # Nothing to do.
+ return
+
+ # We'll insert some special imports at the top of the module, but after any
+ # docstrings and __future__ imports, so first figure out where that is.
+ doc = getattr(mod, "docstring", None)
+ expect_docstring = doc is None
+ if doc is not None and self.is_rewrite_disabled(doc):
+ return
+ pos = 0
+ item = None
+ for item in mod.body:
+ if (
+ expect_docstring
+ and isinstance(item, ast.Expr)
+ and isinstance(item.value, astStr)
+ and isinstance(_get_ast_constant_value(item.value), str)
+ ):
+ doc = _get_ast_constant_value(item.value)
+ assert isinstance(doc, str)
+ if self.is_rewrite_disabled(doc):
+ return
+ expect_docstring = False
+ elif (
+ isinstance(item, ast.ImportFrom)
+ and item.level == 0
+ and item.module == "__future__"
+ ):
+ pass
+ else:
+ break
+ pos += 1
+ # Special case: for a decorated function, set the lineno to that of the
+ # first decorator, not the `def`. Issue #4984.
+ if isinstance(item, ast.FunctionDef) and item.decorator_list:
+ lineno = item.decorator_list[0].lineno
+ else:
+ lineno = item.lineno
+ # Now actually insert the special imports.
+ if sys.version_info >= (3, 10):
+ aliases = [
+ ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0),
+ ast.alias(
+ "_pytest.assertion.rewrite",
+ "@pytest_ar",
+ lineno=lineno,
+ col_offset=0,
+ ),
+ ]
+ else:
+ aliases = [
+ ast.alias("builtins", "@py_builtins"),
+ ast.alias("_pytest.assertion.rewrite", "@pytest_ar"),
+ ]
+ imports = [
+ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
+ ]
+ mod.body[pos:pos] = imports
+
+ # Collect asserts.
+ self.scope = (mod,)
+ nodes: List[Union[ast.AST, Sentinel]] = [mod]
+ while nodes:
+ node = nodes.pop()
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
+ self.scope = tuple((*self.scope, node))
+ nodes.append(_SCOPE_END_MARKER)
+ if node == _SCOPE_END_MARKER:
+ self.scope = self.scope[:-1]
+ continue
+ assert isinstance(node, ast.AST)
+ for name, field in ast.iter_fields(node):
+ if isinstance(field, list):
+ new: List[ast.AST] = []
+ for i, child in enumerate(field):
+ if isinstance(child, ast.Assert):
+ # Transform assert.
+ new.extend(self.visit(child))
+ else:
+ new.append(child)
+ if isinstance(child, ast.AST):
+ nodes.append(child)
+ setattr(node, name, new)
+ elif (
+ isinstance(field, ast.AST)
+ # Don't recurse into expressions as they can't contain
+ # asserts.
+ and not isinstance(field, ast.expr)
+ ):
+ nodes.append(field)
+
+ @staticmethod
+ def is_rewrite_disabled(docstring: str) -> bool:
+ return "PYTEST_DONT_REWRITE" in docstring
+
+ def variable(self) -> str:
+ """Get a new variable."""
+ # Use a character invalid in python identifiers to avoid clashing.
+ name = "@py_assert" + str(next(self.variable_counter))
+ self.variables.append(name)
+ return name
+
+ def assign(self, expr: ast.expr) -> ast.Name:
+ """Give *expr* a name."""
+ name = self.variable()
+ self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
+ return ast.Name(name, ast.Load())
+
+ def display(self, expr: ast.expr) -> ast.expr:
+ """Call saferepr on the expression."""
+ return self.helper("_saferepr", expr)
+
+ def helper(self, name: str, *args: ast.expr) -> ast.expr:
+ """Call a helper in this module."""
+ py_name = ast.Name("@pytest_ar", ast.Load())
+ attr = ast.Attribute(py_name, name, ast.Load())
+ return ast.Call(attr, list(args), [])
+
+ def builtin(self, name: str) -> ast.Attribute:
+ """Return the builtin called *name*."""
+ builtin_name = ast.Name("@py_builtins", ast.Load())
+ return ast.Attribute(builtin_name, name, ast.Load())
+
+ def explanation_param(self, expr: ast.expr) -> str:
+ """Return a new named %-formatting placeholder for expr.
+
+ This creates a %-formatting placeholder for expr in the
+ current formatting context, e.g. ``%(py0)s``. The placeholder
+ and expr are placed in the current format context so that it
+ can be used on the next call to .pop_format_context().
+ """
+ specifier = "py" + str(next(self.variable_counter))
+ self.explanation_specifiers[specifier] = expr
+ return "%(" + specifier + ")s"
+
+ def push_format_context(self) -> None:
+ """Create a new formatting context.
+
+ The format context is used for when an explanation wants to
+ have a variable value formatted in the assertion message. In
+ this case the value required can be added using
+ .explanation_param(). Finally .pop_format_context() is used
+ to format a string of %-formatted values as added by
+ .explanation_param().
+ """
+ self.explanation_specifiers: Dict[str, ast.expr] = {}
+ self.stack.append(self.explanation_specifiers)
+
+ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
+ """Format the %-formatted string with current format context.
+
+ The expl_expr should be an str ast.expr instance constructed from
+ the %-placeholders created by .explanation_param(). This will
+ add the required code to format said string to .expl_stmts and
+ return the ast.Name instance of the formatted string.
+ """
+ current = self.stack.pop()
+ if self.stack:
+ self.explanation_specifiers = self.stack[-1]
+ keys = [astStr(key) for key in current.keys()]
+ format_dict = ast.Dict(keys, list(current.values()))
+ form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
+ name = "@py_format" + str(next(self.variable_counter))
+ if self.enable_assertion_pass_hook:
+ self.format_variables.append(name)
+ self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
+ return ast.Name(name, ast.Load())
+
+ def generic_visit(self, node: ast.AST) -> Tuple[ast.Name, str]:
+ """Handle expressions we don't have custom code for."""
+ assert isinstance(node, ast.expr)
+ res = self.assign(node)
+ return res, self.explanation_param(self.display(res))
+
+ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
+ """Return the AST statements to replace the ast.Assert instance.
+
+ This rewrites the test of an assertion to provide
+ intermediate values and replace it with an if statement which
+ raises an assertion error with a detailed explanation in case
+ the expression is false.
+ """
+ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
+ from _pytest.warning_types import PytestAssertRewriteWarning
+ import warnings
+
+ # TODO: This assert should not be needed.
+ assert self.module_path is not None
+ warnings.warn_explicit(
+ PytestAssertRewriteWarning(
+ "assertion is always true, perhaps remove parentheses?"
+ ),
+ category=None,
+ filename=self.module_path,
+ lineno=assert_.lineno,
+ )
+
+ self.statements: List[ast.stmt] = []
+ self.variables: List[str] = []
+ self.variable_counter = itertools.count()
+
+ if self.enable_assertion_pass_hook:
+ self.format_variables: List[str] = []
+
+ self.stack: List[Dict[str, ast.expr]] = []
+ self.expl_stmts: List[ast.stmt] = []
+ self.push_format_context()
+ # Rewrite assert into a bunch of statements.
+ top_condition, explanation = self.visit(assert_.test)
+
+ negation = ast.UnaryOp(ast.Not(), top_condition)
+
+ if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
+ msg = self.pop_format_context(astStr(explanation))
+
+ # Failed
+ if assert_.msg:
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
+ gluestr = "\n>assert "
+ else:
+ assertmsg = astStr("")
+ gluestr = "assert "
+ err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
+ err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
+ err_name = ast.Name("AssertionError", ast.Load())
+ fmt = self.helper("_format_explanation", err_msg)
+ exc = ast.Call(err_name, [fmt], [])
+ raise_ = ast.Raise(exc, None)
+ statements_fail = []
+ statements_fail.extend(self.expl_stmts)
+ statements_fail.append(raise_)
+
+ # Passed
+ fmt_pass = self.helper("_format_explanation", msg)
+ orig = _get_assertion_exprs(self.source)[assert_.lineno]
+ hook_call_pass = ast.Expr(
+ self.helper(
+ "_call_assertion_pass",
+ astNum(assert_.lineno),
+ astStr(orig),
+ fmt_pass,
+ )
+ )
+ # If any hooks implement assert_pass hook
+ hook_impl_test = ast.If(
+ self.helper("_check_if_assertion_pass_impl"),
+ self.expl_stmts + [hook_call_pass],
+ [],
+ )
+ statements_pass = [hook_impl_test]
+
+ # Test for assertion condition
+ main_test = ast.If(negation, statements_fail, statements_pass)
+ self.statements.append(main_test)
+ if self.format_variables:
+ variables = [
+ ast.Name(name, ast.Store()) for name in self.format_variables
+ ]
+ clear_format = ast.Assign(variables, astNameConstant(None))
+ self.statements.append(clear_format)
+
+ else: # Original assertion rewriting
+ # Create failure message.
+ body = self.expl_stmts
+ self.statements.append(ast.If(negation, body, []))
+ if assert_.msg:
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
+ explanation = "\n>assert " + explanation
+ else:
+ assertmsg = astStr("")
+ explanation = "assert " + explanation
+ template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
+ msg = self.pop_format_context(template)
+ fmt = self.helper("_format_explanation", msg)
+ err_name = ast.Name("AssertionError", ast.Load())
+ exc = ast.Call(err_name, [fmt], [])
+ raise_ = ast.Raise(exc, None)
+
+ body.append(raise_)
+
+ # Clear temporary variables by setting them to None.
+ if self.variables:
+ variables = [ast.Name(name, ast.Store()) for name in self.variables]
+ clear = ast.Assign(variables, astNameConstant(None))
+ self.statements.append(clear)
+ # Fix locations (line numbers/column offsets).
+ for stmt in self.statements:
+ for node in traverse_node(stmt):
+ ast.copy_location(node, assert_)
+ return self.statements
+
+ def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
+ # This method handles the 'walrus operator' repr of the target
+ # name if it's a local variable or _should_repr_global_name()
+ # thinks it's acceptable.
+ locs = ast.Call(self.builtin("locals"), [], [])
+ target_id = name.target.id # type: ignore[attr-defined]
+ inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
+ dorepr = self.helper("_should_repr_global_name", name)
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
+ expr = ast.IfExp(test, self.display(name), astStr(target_id))
+ return name, self.explanation_param(expr)
+
+ def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
+ # Display the repr of the name if it's a local variable or
+ # _should_repr_global_name() thinks it's acceptable.
+ locs = ast.Call(self.builtin("locals"), [], [])
+ inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
+ dorepr = self.helper("_should_repr_global_name", name)
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
+ expr = ast.IfExp(test, self.display(name), astStr(name.id))
+ return name, self.explanation_param(expr)
+
+ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
+ res_var = self.variable()
+ expl_list = self.assign(ast.List([], ast.Load()))
+ app = ast.Attribute(expl_list, "append", ast.Load())
+ is_or = int(isinstance(boolop.op, ast.Or))
+ body = save = self.statements
+ fail_save = self.expl_stmts
+ levels = len(boolop.values) - 1
+ self.push_format_context()
+ # Process each operand, short-circuiting if needed.
+ for i, v in enumerate(boolop.values):
+ if i:
+ fail_inner: List[ast.stmt] = []
+ # cond is set in a prior loop iteration below
+ self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
+ self.expl_stmts = fail_inner
+ # Check if the left operand is a namedExpr and the value has already been visited
+ if (
+ isinstance(v, ast.Compare)
+ and isinstance(v.left, namedExpr)
+ and v.left.target.id
+ in [
+ ast_expr.id
+ for ast_expr in boolop.values[:i]
+ if hasattr(ast_expr, "id")
+ ]
+ ):
+ pytest_temp = self.variable()
+ self.variables_overwrite[self.scope][
+ v.left.target.id
+ ] = v.left # type:ignore[assignment]
+ v.left.target.id = pytest_temp
+ self.push_format_context()
+ res, expl = self.visit(v)
+ body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
+ expl_format = self.pop_format_context(astStr(expl))
+ call = ast.Call(app, [expl_format], [])
+ self.expl_stmts.append(ast.Expr(call))
+ if i < levels:
+ cond: ast.expr = res
+ if is_or:
+ cond = ast.UnaryOp(ast.Not(), cond)
+ inner: List[ast.stmt] = []
+ self.statements.append(ast.If(cond, inner, []))
+ self.statements = body = inner
+ self.statements = save
+ self.expl_stmts = fail_save
+ expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
+ expl = self.pop_format_context(expl_template)
+ return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
+
+ def visit_UnaryOp(self, unary: ast.UnaryOp) -> Tuple[ast.Name, str]:
+ pattern = UNARY_MAP[unary.op.__class__]
+ operand_res, operand_expl = self.visit(unary.operand)
+ res = self.assign(ast.UnaryOp(unary.op, operand_res))
+ return res, pattern % (operand_expl,)
+
+ def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]:
+ symbol = BINOP_MAP[binop.op.__class__]
+ left_expr, left_expl = self.visit(binop.left)
+ right_expr, right_expl = self.visit(binop.right)
+ explanation = f"({left_expl} {symbol} {right_expl})"
+ res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
+ return res, explanation
+
+ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
+ new_func, func_expl = self.visit(call.func)
+ arg_expls = []
+ new_args = []
+ new_kwargs = []
+ for arg in call.args:
+ if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
+ self.scope, {}
+ ):
+ arg = self.variables_overwrite[self.scope][
+ arg.id
+ ] # type:ignore[assignment]
+ res, expl = self.visit(arg)
+ arg_expls.append(expl)
+ new_args.append(res)
+ for keyword in call.keywords:
+ if isinstance(
+ keyword.value, ast.Name
+ ) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
+ keyword.value = self.variables_overwrite[self.scope][
+ keyword.value.id
+ ] # type:ignore[assignment]
+ res, expl = self.visit(keyword.value)
+ new_kwargs.append(ast.keyword(keyword.arg, res))
+ if keyword.arg:
+ arg_expls.append(keyword.arg + "=" + expl)
+ else: # **args have `arg` keywords with an .arg of None
+ arg_expls.append("**" + expl)
+
+ expl = "{}({})".format(func_expl, ", ".join(arg_expls))
+ new_call = ast.Call(new_func, new_args, new_kwargs)
+ res = self.assign(new_call)
+ res_expl = self.explanation_param(self.display(res))
+ outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
+ return res, outer_expl
+
+ def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
+ # A Starred node can appear in a function call.
+ res, expl = self.visit(starred.value)
+ new_starred = ast.Starred(res, starred.ctx)
+ return new_starred, "*" + expl
+
+ def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]:
+ if not isinstance(attr.ctx, ast.Load):
+ return self.generic_visit(attr)
+ value, value_expl = self.visit(attr.value)
+ res = self.assign(ast.Attribute(value, attr.attr, ast.Load()))
+ res_expl = self.explanation_param(self.display(res))
+ pat = "%s\n{%s = %s.%s\n}"
+ expl = pat % (res_expl, res_expl, value_expl, attr.attr)
+ return res, expl
+
+ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
+ self.push_format_context()
+ # We first check if we have overwritten a variable in the previous assert
+ if isinstance(
+ comp.left, ast.Name
+ ) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
+ comp.left = self.variables_overwrite[self.scope][
+ comp.left.id
+ ] # type:ignore[assignment]
+ if isinstance(comp.left, namedExpr):
+ self.variables_overwrite[self.scope][
+ comp.left.target.id
+ ] = comp.left # type:ignore[assignment]
+ left_res, left_expl = self.visit(comp.left)
+ if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
+ left_expl = f"({left_expl})"
+ res_variables = [self.variable() for i in range(len(comp.ops))]
+ load_names = [ast.Name(v, ast.Load()) for v in res_variables]
+ store_names = [ast.Name(v, ast.Store()) for v in res_variables]
+ it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
+ expls = []
+ syms = []
+ results = [left_res]
+ for i, op, next_operand in it:
+ if (
+ isinstance(next_operand, namedExpr)
+ and isinstance(left_res, ast.Name)
+ and next_operand.target.id == left_res.id
+ ):
+ next_operand.target.id = self.variable()
+ self.variables_overwrite[self.scope][
+ left_res.id
+ ] = next_operand # type:ignore[assignment]
+ next_res, next_expl = self.visit(next_operand)
+ if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
+ next_expl = f"({next_expl})"
+ results.append(next_res)
+ sym = BINOP_MAP[op.__class__]
+ syms.append(astStr(sym))
+ expl = f"{left_expl} {sym} {next_expl}"
+ expls.append(astStr(expl))
+ res_expr = ast.Compare(left_res, [op], [next_res])
+ self.statements.append(ast.Assign([store_names[i]], res_expr))
+ left_res, left_expl = next_res, next_expl
+ # Use pytest.assertion.util._reprcompare if that's available.
+ expl_call = self.helper(
+ "_call_reprcompare",
+ ast.Tuple(syms, ast.Load()),
+ ast.Tuple(load_names, ast.Load()),
+ ast.Tuple(expls, ast.Load()),
+ ast.Tuple(results, ast.Load()),
+ )
+ if len(comp.ops) > 1:
+ res: ast.expr = ast.BoolOp(ast.And(), load_names)
+ else:
+ res = load_names[0]
+
+ return res, self.explanation_param(self.pop_format_context(expl_call))
+
+
+def try_makedirs(cache_dir: Path) -> bool:
+ """Attempt to create the given directory and sub-directories exist.
+
+ Returns True if successful or if it already exists.
+ """
+ try:
+ os.makedirs(cache_dir, exist_ok=True)
+ except (FileNotFoundError, NotADirectoryError, FileExistsError):
+ # One of the path components was not a directory:
+ # - we're in a zip file
+ # - it is a file
+ return False
+ except PermissionError:
+ return False
+ except OSError as e:
+ # as of now, EROFS doesn't have an equivalent OSError-subclass
+ if e.errno == errno.EROFS:
+ return False
+ raise
+ return True
+
+
+def get_cache_dir(file_path: Path) -> Path:
+ """Return the cache directory to write .pyc files for the given .py file path."""
+ if sys.version_info >= (3, 8) and sys.pycache_prefix:
+ # given:
+ # prefix = '/tmp/pycs'
+ # path = '/home/user/proj/test_app.py'
+ # we want:
+ # '/tmp/pycs/home/user/proj'
+ return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
+ else:
+ # classic pycache directory
+ return file_path.parent / "__pycache__"
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/truncate.py b/venv/lib/python3.12/site-packages/_pytest/assertion/truncate.py
new file mode 100644
index 0000000..dfd6f65
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/assertion/truncate.py
@@ -0,0 +1,115 @@
+"""Utilities for truncating assertion output.
+
+Current default behaviour is to truncate assertion explanations at
+~8 terminal lines, unless running in "-vv" mode or running on CI.
+"""
+from typing import List
+from typing import Optional
+
+from _pytest.assertion import util
+from _pytest.nodes import Item
+
+
+DEFAULT_MAX_LINES = 8
+DEFAULT_MAX_CHARS = 8 * 80
+USAGE_MSG = "use '-vv' to show"
+
+
+def truncate_if_required(
+ explanation: List[str], item: Item, max_length: Optional[int] = None
+) -> List[str]:
+ """Truncate this assertion explanation if the given test item is eligible."""
+ if _should_truncate_item(item):
+ return _truncate_explanation(explanation)
+ return explanation
+
+
+def _should_truncate_item(item: Item) -> bool:
+ """Whether or not this test item is eligible for truncation."""
+ verbose = item.config.option.verbose
+ return verbose < 2 and not util.running_on_ci()
+
+
+def _truncate_explanation(
+ input_lines: List[str],
+ max_lines: Optional[int] = None,
+ max_chars: Optional[int] = None,
+) -> List[str]:
+ """Truncate given list of strings that makes up the assertion explanation.
+
+ Truncates to either 8 lines, or 640 characters - whichever the input reaches
+ first, taking the truncation explanation into account. The remaining lines
+ will be replaced by a usage message.
+ """
+ if max_lines is None:
+ max_lines = DEFAULT_MAX_LINES
+ if max_chars is None:
+ max_chars = DEFAULT_MAX_CHARS
+
+ # Check if truncation required
+ input_char_count = len("".join(input_lines))
+ # The length of the truncation explanation depends on the number of lines
+ # removed but is at least 68 characters:
+ # The real value is
+ # 64 (for the base message:
+ # '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
+ # )
+ # + 1 (for plural)
+ # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
+ # + 3 for the '...' added to the truncated line
+ # But if there's more than 100 lines it's very likely that we're going to
+ # truncate, so we don't need the exact value using log10.
+ tolerable_max_chars = (
+ max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
+ )
+ # The truncation explanation add two lines to the output
+ tolerable_max_lines = max_lines + 2
+ if (
+ len(input_lines) <= tolerable_max_lines
+ and input_char_count <= tolerable_max_chars
+ ):
+ return input_lines
+ # Truncate first to max_lines, and then truncate to max_chars if necessary
+ truncated_explanation = input_lines[:max_lines]
+ truncated_char = True
+ # We reevaluate the need to truncate chars following removal of some lines
+ if len("".join(truncated_explanation)) > tolerable_max_chars:
+ truncated_explanation = _truncate_by_char_count(
+ truncated_explanation, max_chars
+ )
+ else:
+ truncated_char = False
+
+ truncated_line_count = len(input_lines) - len(truncated_explanation)
+ if truncated_explanation[-1]:
+ # Add ellipsis and take into account part-truncated final line
+ truncated_explanation[-1] = truncated_explanation[-1] + "..."
+ if truncated_char:
+ # It's possible that we did not remove any char from this line
+ truncated_line_count += 1
+ else:
+ # Add proper ellipsis when we were able to fit a full line exactly
+ truncated_explanation[-1] = "..."
+ return truncated_explanation + [
+ "",
+ f"...Full output truncated ({truncated_line_count} line"
+ f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
+ ]
+
+
+def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
+ # Find point at which input length exceeds total allowed length
+ iterated_char_count = 0
+ for iterated_index, input_line in enumerate(input_lines):
+ if iterated_char_count + len(input_line) > max_chars:
+ break
+ iterated_char_count += len(input_line)
+
+ # Create truncated explanation with modified final line
+ truncated_result = input_lines[:iterated_index]
+ final_line = input_lines[iterated_index]
+ if final_line:
+ final_line_truncate_point = max_chars - iterated_char_count
+ final_line = final_line[:final_line_truncate_point]
+ truncated_result.append(final_line)
+ return truncated_result
diff --git a/venv/lib/python3.12/site-packages/_pytest/assertion/util.py b/venv/lib/python3.12/site-packages/_pytest/assertion/util.py
new file mode 100644
index 0000000..39ca540
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/assertion/util.py
@@ -0,0 +1,522 @@
+"""Utilities for assertion debugging."""
+import collections.abc
+import os
+import pprint
+from typing import AbstractSet
+from typing import Any
+from typing import Callable
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from unicodedata import normalize
+
+import _pytest._code
+from _pytest import outcomes
+from _pytest._io.saferepr import _pformat_dispatch
+from _pytest._io.saferepr import saferepr
+from _pytest._io.saferepr import saferepr_unlimited
+from _pytest.config import Config
+
+# The _reprcompare attribute on the util module is used by the new assertion
+# interpretation code and assertion rewriter to detect this plugin was
+# loaded and in turn call the hooks defined here as part of the
+# DebugInterpreter.
+_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None
+
+# Works similarly as _reprcompare attribute. Is populated with the hook call
+# when pytest_runtest_setup is called.
+_assertion_pass: Optional[Callable[[int, str, str], None]] = None
+
+# Config object which is assigned during pytest_runtest_protocol.
+_config: Optional[Config] = None
+
+
+def format_explanation(explanation: str) -> str:
+ r"""Format an explanation.
+
+ Normally all embedded newlines are escaped, however there are
+ three exceptions: \n{, \n} and \n~. The first two are intended
+ cover nested explanations, see function and attribute explanations
+ for examples (.visit_Call(), visit_Attribute()). The last one is
+ for when one explanation needs to span multiple lines, e.g. when
+ displaying diffs.
+ """
+ lines = _split_explanation(explanation)
+ result = _format_lines(lines)
+ return "\n".join(result)
+
+
+def _split_explanation(explanation: str) -> List[str]:
+ r"""Return a list of individual lines in the explanation.
+
+ This will return a list of lines split on '\n{', '\n}' and '\n~'.
+ Any other newlines will be escaped and appear in the line as the
+ literal '\n' characters.
+ """
+ raw_lines = (explanation or "").split("\n")
+ lines = [raw_lines[0]]
+ for values in raw_lines[1:]:
+ if values and values[0] in ["{", "}", "~", ">"]:
+ lines.append(values)
+ else:
+ lines[-1] += "\\n" + values
+ return lines
+
+
+def _format_lines(lines: Sequence[str]) -> List[str]:
+ """Format the individual lines.
+
+ This will replace the '{', '}' and '~' characters of our mini formatting
+ language with the proper 'where ...', 'and ...' and ' + ...' text, taking
+ care of indentation along the way.
+
+ Return a list of formatted lines.
+ """
+ result = list(lines[:1])
+ stack = [0]
+ stackcnt = [0]
+ for line in lines[1:]:
+ if line.startswith("{"):
+ if stackcnt[-1]:
+ s = "and "
+ else:
+ s = "where "
+ stack.append(len(result))
+ stackcnt[-1] += 1
+ stackcnt.append(0)
+ result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
+ elif line.startswith("}"):
+ stack.pop()
+ stackcnt.pop()
+ result[stack[-1]] += line[1:]
+ else:
+ assert line[0] in ["~", ">"]
+ stack[-1] += 1
+ indent = len(stack) if line.startswith("~") else len(stack) - 1
+ result.append(" " * indent + line[1:])
+ assert len(stack) == 1
+ return result
+
+
+def issequence(x: Any) -> bool:
+ return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
+
+
+def istext(x: Any) -> bool:
+ return isinstance(x, str)
+
+
+def isdict(x: Any) -> bool:
+ return isinstance(x, dict)
+
+
+def isset(x: Any) -> bool:
+ return isinstance(x, (set, frozenset))
+
+
+def isnamedtuple(obj: Any) -> bool:
+ return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
+
+
+def isdatacls(obj: Any) -> bool:
+ return getattr(obj, "__dataclass_fields__", None) is not None
+
+
+def isattrs(obj: Any) -> bool:
+ return getattr(obj, "__attrs_attrs__", None) is not None
+
+
+def isiterable(obj: Any) -> bool:
+ try:
+ iter(obj)
+ return not istext(obj)
+ except Exception:
+ return False
+
+
+def has_default_eq(
+ obj: object,
+) -> bool:
+ """Check if an instance of an object contains the default eq
+
+ First, we check if the object's __eq__ attribute has __code__,
+ if so, we check the equally of the method code filename (__code__.co_filename)
+ to the default one generated by the dataclass and attr module
+ for dataclasses the default co_filename is , for attrs class, the __eq__ should contain "attrs eq generated"
+ """
+ # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
+ if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
+ code_filename = obj.__eq__.__code__.co_filename
+
+ if isattrs(obj):
+ return "attrs generated eq" in code_filename
+
+ return code_filename == "" # data class
+ return True
+
+
+def assertrepr_compare(
+ config, op: str, left: Any, right: Any, use_ascii: bool = False
+) -> Optional[List[str]]:
+ """Return specialised explanations for some operators/operands."""
+ verbose = config.getoption("verbose")
+
+ # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
+ # See issue #3246.
+ use_ascii = (
+ isinstance(left, str)
+ and isinstance(right, str)
+ and normalize("NFD", left) == normalize("NFD", right)
+ )
+
+ if verbose > 1:
+ left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
+ right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
+ else:
+ # XXX: "15 chars indentation" is wrong
+ # ("E AssertionError: assert "); should use term width.
+ maxsize = (
+ 80 - 15 - len(op) - 2
+ ) // 2 # 15 chars indentation, 1 space around op
+
+ left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
+ right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
+
+ summary = f"{left_repr} {op} {right_repr}"
+
+ explanation = None
+ try:
+ if op == "==":
+ explanation = _compare_eq_any(left, right, verbose)
+ elif op == "not in":
+ if istext(left) and istext(right):
+ explanation = _notin_text(left, right, verbose)
+ except outcomes.Exit:
+ raise
+ except Exception:
+ explanation = [
+ "(pytest_assertion plugin: representation of details failed: {}.".format(
+ _pytest._code.ExceptionInfo.from_current()._getreprcrash()
+ ),
+ " Probably an object has a faulty __repr__.)",
+ ]
+
+ if not explanation:
+ return None
+
+ return [summary] + explanation
+
+
+def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
+ explanation = []
+ if istext(left) and istext(right):
+ explanation = _diff_text(left, right, verbose)
+ else:
+ from _pytest.python_api import ApproxBase
+
+ if isinstance(left, ApproxBase) or isinstance(right, ApproxBase):
+ # Although the common order should be obtained == expected, this ensures both ways
+ approx_side = left if isinstance(left, ApproxBase) else right
+ other_side = right if isinstance(left, ApproxBase) else left
+
+ explanation = approx_side._repr_compare(other_side)
+ elif type(left) == type(right) and (
+ isdatacls(left) or isattrs(left) or isnamedtuple(left)
+ ):
+ # Note: unlike dataclasses/attrs, namedtuples compare only the
+ # field values, not the type or field names. But this branch
+ # intentionally only handles the same-type case, which was often
+ # used in older code bases before dataclasses/attrs were available.
+ explanation = _compare_eq_cls(left, right, verbose)
+ elif issequence(left) and issequence(right):
+ explanation = _compare_eq_sequence(left, right, verbose)
+ elif isset(left) and isset(right):
+ explanation = _compare_eq_set(left, right, verbose)
+ elif isdict(left) and isdict(right):
+ explanation = _compare_eq_dict(left, right, verbose)
+
+ if isiterable(left) and isiterable(right):
+ expl = _compare_eq_iterable(left, right, verbose)
+ explanation.extend(expl)
+
+ return explanation
+
+
+def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
+ """Return the explanation for the diff between text.
+
+ Unless --verbose is used this will skip leading and trailing
+ characters which are identical to keep the diff minimal.
+ """
+ from difflib import ndiff
+
+ explanation: List[str] = []
+
+ if verbose < 1:
+ i = 0 # just in case left or right has zero length
+ for i in range(min(len(left), len(right))):
+ if left[i] != right[i]:
+ break
+ if i > 42:
+ i -= 10 # Provide some context
+ explanation = [
+ "Skipping %s identical leading characters in diff, use -v to show" % i
+ ]
+ left = left[i:]
+ right = right[i:]
+ if len(left) == len(right):
+ for i in range(len(left)):
+ if left[-i] != right[-i]:
+ break
+ if i > 42:
+ i -= 10 # Provide some context
+ explanation += [
+ "Skipping {} identical trailing "
+ "characters in diff, use -v to show".format(i)
+ ]
+ left = left[:-i]
+ right = right[:-i]
+ keepends = True
+ if left.isspace() or right.isspace():
+ left = repr(str(left))
+ right = repr(str(right))
+ explanation += ["Strings contain only whitespace, escaping them using repr()"]
+ # "right" is the expected base against which we compare "left",
+ # see https://github.com/pytest-dev/pytest/issues/3333
+ explanation += [
+ line.strip("\n")
+ for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
+ ]
+ return explanation
+
+
+def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
+ """Move opening/closing parenthesis/bracket to own lines."""
+ opening = lines[0][:1]
+ if opening in ["(", "[", "{"]:
+ lines[0] = " " + lines[0][1:]
+ lines[:] = [opening] + lines
+ closing = lines[-1][-1:]
+ if closing in [")", "]", "}"]:
+ lines[-1] = lines[-1][:-1] + ","
+ lines[:] = lines + [closing]
+
+
+def _compare_eq_iterable(
+ left: Iterable[Any], right: Iterable[Any], verbose: int = 0
+) -> List[str]:
+ if verbose <= 0 and not running_on_ci():
+ return ["Use -v to get more diff"]
+ # dynamic import to speedup pytest
+ import difflib
+
+ left_formatting = pprint.pformat(left).splitlines()
+ right_formatting = pprint.pformat(right).splitlines()
+
+ # Re-format for different output lengths.
+ lines_left = len(left_formatting)
+ lines_right = len(right_formatting)
+ if lines_left != lines_right:
+ left_formatting = _pformat_dispatch(left).splitlines()
+ right_formatting = _pformat_dispatch(right).splitlines()
+
+ if lines_left > 1 or lines_right > 1:
+ _surrounding_parens_on_own_lines(left_formatting)
+ _surrounding_parens_on_own_lines(right_formatting)
+
+ explanation = ["Full diff:"]
+ # "right" is the expected base against which we compare "left",
+ # see https://github.com/pytest-dev/pytest/issues/3333
+ explanation.extend(
+ line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
+ )
+ return explanation
+
+
+def _compare_eq_sequence(
+ left: Sequence[Any], right: Sequence[Any], verbose: int = 0
+) -> List[str]:
+ comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
+ explanation: List[str] = []
+ len_left = len(left)
+ len_right = len(right)
+ for i in range(min(len_left, len_right)):
+ if left[i] != right[i]:
+ if comparing_bytes:
+ # when comparing bytes, we want to see their ascii representation
+ # instead of their numeric values (#5260)
+ # using a slice gives us the ascii representation:
+ # >>> s = b'foo'
+ # >>> s[0]
+ # 102
+ # >>> s[0:1]
+ # b'f'
+ left_value = left[i : i + 1]
+ right_value = right[i : i + 1]
+ else:
+ left_value = left[i]
+ right_value = right[i]
+
+ explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
+ break
+
+ if comparing_bytes:
+ # when comparing bytes, it doesn't help to show the "sides contain one or more
+ # items" longer explanation, so skip it
+
+ return explanation
+
+ len_diff = len_left - len_right
+ if len_diff:
+ if len_diff > 0:
+ dir_with_more = "Left"
+ extra = saferepr(left[len_right])
+ else:
+ len_diff = 0 - len_diff
+ dir_with_more = "Right"
+ extra = saferepr(right[len_left])
+
+ if len_diff == 1:
+ explanation += [f"{dir_with_more} contains one more item: {extra}"]
+ else:
+ explanation += [
+ "%s contains %d more items, first extra item: %s"
+ % (dir_with_more, len_diff, extra)
+ ]
+ return explanation
+
+
+def _compare_eq_set(
+ left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
+) -> List[str]:
+ explanation = []
+ diff_left = left - right
+ diff_right = right - left
+ if diff_left:
+ explanation.append("Extra items in the left set:")
+ for item in diff_left:
+ explanation.append(saferepr(item))
+ if diff_right:
+ explanation.append("Extra items in the right set:")
+ for item in diff_right:
+ explanation.append(saferepr(item))
+ return explanation
+
+
+def _compare_eq_dict(
+ left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
+) -> List[str]:
+ explanation: List[str] = []
+ set_left = set(left)
+ set_right = set(right)
+ common = set_left.intersection(set_right)
+ same = {k: left[k] for k in common if left[k] == right[k]}
+ if same and verbose < 2:
+ explanation += ["Omitting %s identical items, use -vv to show" % len(same)]
+ elif same:
+ explanation += ["Common items:"]
+ explanation += pprint.pformat(same).splitlines()
+ diff = {k for k in common if left[k] != right[k]}
+ if diff:
+ explanation += ["Differing items:"]
+ for k in diff:
+ explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})]
+ extra_left = set_left - set_right
+ len_extra_left = len(extra_left)
+ if len_extra_left:
+ explanation.append(
+ "Left contains %d more item%s:"
+ % (len_extra_left, "" if len_extra_left == 1 else "s")
+ )
+ explanation.extend(
+ pprint.pformat({k: left[k] for k in extra_left}).splitlines()
+ )
+ extra_right = set_right - set_left
+ len_extra_right = len(extra_right)
+ if len_extra_right:
+ explanation.append(
+ "Right contains %d more item%s:"
+ % (len_extra_right, "" if len_extra_right == 1 else "s")
+ )
+ explanation.extend(
+ pprint.pformat({k: right[k] for k in extra_right}).splitlines()
+ )
+ return explanation
+
+
+def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
+ if not has_default_eq(left):
+ return []
+ if isdatacls(left):
+ import dataclasses
+
+ all_fields = dataclasses.fields(left)
+ fields_to_check = [info.name for info in all_fields if info.compare]
+ elif isattrs(left):
+ all_fields = left.__attrs_attrs__
+ fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
+ elif isnamedtuple(left):
+ fields_to_check = left._fields
+ else:
+ assert False
+
+ indent = " "
+ same = []
+ diff = []
+ for field in fields_to_check:
+ if getattr(left, field) == getattr(right, field):
+ same.append(field)
+ else:
+ diff.append(field)
+
+ explanation = []
+ if same or diff:
+ explanation += [""]
+ if same and verbose < 2:
+ explanation.append("Omitting %s identical items, use -vv to show" % len(same))
+ elif same:
+ explanation += ["Matching attributes:"]
+ explanation += pprint.pformat(same).splitlines()
+ if diff:
+ explanation += ["Differing attributes:"]
+ explanation += pprint.pformat(diff).splitlines()
+ for field in diff:
+ field_left = getattr(left, field)
+ field_right = getattr(right, field)
+ explanation += [
+ "",
+ "Drill down into differing attribute %s:" % field,
+ ("%s%s: %r != %r") % (indent, field, field_left, field_right),
+ ]
+ explanation += [
+ indent + line
+ for line in _compare_eq_any(field_left, field_right, verbose)
+ ]
+ return explanation
+
+
+def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
+ index = text.find(term)
+ head = text[:index]
+ tail = text[index + len(term) :]
+ correct_text = head + tail
+ diff = _diff_text(text, correct_text, verbose)
+ newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)]
+ for line in diff:
+ if line.startswith("Skipping"):
+ continue
+ if line.startswith("- "):
+ continue
+ if line.startswith("+ "):
+ newdiff.append(" " + line[2:])
+ else:
+ newdiff.append(line)
+ return newdiff
+
+
+def running_on_ci() -> bool:
+ """Check if we're currently running on a CI system."""
+ env_vars = ["CI", "BUILD_NUMBER"]
+ return any(var in os.environ for var in env_vars)
diff --git a/venv/lib/python3.12/site-packages/_pytest/cacheprovider.py b/venv/lib/python3.12/site-packages/_pytest/cacheprovider.py
new file mode 100644
index 0000000..1ecb865
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/cacheprovider.py
@@ -0,0 +1,602 @@
+"""Implementation of the cache provider."""
+# This plugin was not named "cache" to avoid conflicts with the external
+# pytest-cache version.
+import dataclasses
+import json
+import os
+from pathlib import Path
+from typing import Dict
+from typing import Generator
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Union
+
+from .pathlib import resolve_from_str
+from .pathlib import rm_rf
+from .reports import CollectReport
+from _pytest import nodes
+from _pytest._io import TerminalWriter
+from _pytest.compat import final
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.nodes import File
+from _pytest.python import Package
+from _pytest.reports import TestReport
+
+README_CONTENT = """\
+# pytest cache directory #
+
+This directory contains data from the pytest's cache plugin,
+which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
+
+**Do not** commit this to version control.
+
+See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
+"""
+
+CACHEDIR_TAG_CONTENT = b"""\
+Signature: 8a477f597d28d172789f06886806bc55
+# This file is a cache directory tag created by pytest.
+# For information about cache directory tags, see:
+# https://bford.info/cachedir/spec.html
+"""
+
+
+@final
+@dataclasses.dataclass
+class Cache:
+ """Instance of the `cache` fixture."""
+
+ _cachedir: Path = dataclasses.field(repr=False)
+ _config: Config = dataclasses.field(repr=False)
+
+ # Sub-directory under cache-dir for directories created by `mkdir()`.
+ _CACHE_PREFIX_DIRS = "d"
+
+ # Sub-directory under cache-dir for values created by `set()`.
+ _CACHE_PREFIX_VALUES = "v"
+
+ def __init__(
+ self, cachedir: Path, config: Config, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._cachedir = cachedir
+ self._config = config
+
+ @classmethod
+ def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache":
+ """Create the Cache instance for a Config.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ cachedir = cls.cache_dir_from_config(config, _ispytest=True)
+ if config.getoption("cacheclear") and cachedir.is_dir():
+ cls.clear_cache(cachedir, _ispytest=True)
+ return cls(cachedir, config, _ispytest=True)
+
+ @classmethod
+ def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
+ """Clear the sub-directories used to hold cached directories and values.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
+ d = cachedir / prefix
+ if d.is_dir():
+ rm_rf(d)
+
+ @staticmethod
+ def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
+ """Get the path to the cache directory for a Config.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ return resolve_from_str(config.getini("cache_dir"), config.rootpath)
+
+ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
+ """Issue a cache warning.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ import warnings
+ from _pytest.warning_types import PytestCacheWarning
+
+ warnings.warn(
+ PytestCacheWarning(fmt.format(**args) if args else fmt),
+ self._config.hook,
+ stacklevel=3,
+ )
+
+ def mkdir(self, name: str) -> Path:
+ """Return a directory path object with the given name.
+
+ If the directory does not yet exist, it will be created. You can use
+ it to manage files to e.g. store/retrieve database dumps across test
+ sessions.
+
+ .. versionadded:: 7.0
+
+ :param name:
+ Must be a string not containing a ``/`` separator.
+ Make sure the name contains your plugin or application
+ identifiers to prevent clashes with other cache users.
+ """
+ path = Path(name)
+ if len(path.parts) > 1:
+ raise ValueError("name is not allowed to contain path separators")
+ res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
+ res.mkdir(exist_ok=True, parents=True)
+ return res
+
+ def _getvaluepath(self, key: str) -> Path:
+ return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
+
+ def get(self, key: str, default):
+ """Return the cached value for the given key.
+
+ If no value was yet cached or the value cannot be read, the specified
+ default is returned.
+
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param default:
+ The value to return in case of a cache-miss or invalid cache value.
+ """
+ path = self._getvaluepath(key)
+ try:
+ with path.open("r", encoding="UTF-8") as f:
+ return json.load(f)
+ except (ValueError, OSError):
+ return default
+
+ def set(self, key: str, value: object) -> None:
+ """Save value for the given key.
+
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param value:
+ Must be of any combination of basic python types,
+ including nested types like lists of dictionaries.
+ """
+ path = self._getvaluepath(key)
+ try:
+ if path.parent.is_dir():
+ cache_dir_exists_already = True
+ else:
+ cache_dir_exists_already = self._cachedir.exists()
+ path.parent.mkdir(exist_ok=True, parents=True)
+ except OSError as exc:
+ self.warn(
+ f"could not create cache path {path}: {exc}",
+ _ispytest=True,
+ )
+ return
+ if not cache_dir_exists_already:
+ self._ensure_supporting_files()
+ data = json.dumps(value, ensure_ascii=False, indent=2)
+ try:
+ f = path.open("w", encoding="UTF-8")
+ except OSError as exc:
+ self.warn(
+ f"cache could not write path {path}: {exc}",
+ _ispytest=True,
+ )
+ else:
+ with f:
+ f.write(data)
+
+ def _ensure_supporting_files(self) -> None:
+ """Create supporting files in the cache dir that are not really part of the cache."""
+ readme_path = self._cachedir / "README.md"
+ readme_path.write_text(README_CONTENT, encoding="UTF-8")
+
+ gitignore_path = self._cachedir.joinpath(".gitignore")
+ msg = "# Created by pytest automatically.\n*\n"
+ gitignore_path.write_text(msg, encoding="UTF-8")
+
+ cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
+ cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
+
+
+class LFPluginCollWrapper:
+ def __init__(self, lfplugin: "LFPlugin") -> None:
+ self.lfplugin = lfplugin
+ self._collected_at_least_one_failure = False
+
+ @hookimpl(hookwrapper=True)
+ def pytest_make_collect_report(self, collector: nodes.Collector):
+ if isinstance(collector, (Session, Package)):
+ out = yield
+ res: CollectReport = out.get_result()
+
+ # Sort any lf-paths to the beginning.
+ lf_paths = self.lfplugin._last_failed_paths
+
+ # Use stable sort to priorize last failed.
+ def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
+ # Package.path is the __init__.py file, we need the directory.
+ if isinstance(node, Package):
+ path = node.path.parent
+ else:
+ path = node.path
+ return path in lf_paths
+
+ res.result = sorted(
+ res.result,
+ key=sort_key,
+ reverse=True,
+ )
+ return
+
+ elif isinstance(collector, File):
+ if collector.path in self.lfplugin._last_failed_paths:
+ out = yield
+ res = out.get_result()
+ result = res.result
+ lastfailed = self.lfplugin.lastfailed
+
+ # Only filter with known failures.
+ if not self._collected_at_least_one_failure:
+ if not any(x.nodeid in lastfailed for x in result):
+ return
+ self.lfplugin.config.pluginmanager.register(
+ LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
+ )
+ self._collected_at_least_one_failure = True
+
+ session = collector.session
+ result[:] = [
+ x
+ for x in result
+ if x.nodeid in lastfailed
+ # Include any passed arguments (not trivial to filter).
+ or session.isinitpath(x.path)
+ # Keep all sub-collectors.
+ or isinstance(x, nodes.Collector)
+ ]
+ return
+ yield
+
+
+class LFPluginCollSkipfiles:
+ def __init__(self, lfplugin: "LFPlugin") -> None:
+ self.lfplugin = lfplugin
+
+ @hookimpl
+ def pytest_make_collect_report(
+ self, collector: nodes.Collector
+ ) -> Optional[CollectReport]:
+ # Packages are Files, but we only want to skip test-bearing Files,
+ # so don't filter Packages.
+ if isinstance(collector, File) and not isinstance(collector, Package):
+ if collector.path not in self.lfplugin._last_failed_paths:
+ self.lfplugin._skipped_files += 1
+
+ return CollectReport(
+ collector.nodeid, "passed", longrepr=None, result=[]
+ )
+ return None
+
+
+class LFPlugin:
+ """Plugin which implements the --lf (run last-failing) option."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ active_keys = "lf", "failedfirst"
+ self.active = any(config.getoption(key) for key in active_keys)
+ assert config.cache
+ self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {})
+ self._previously_failed_count: Optional[int] = None
+ self._report_status: Optional[str] = None
+ self._skipped_files = 0 # count skipped files during collection due to --lf
+
+ if config.getoption("lf"):
+ self._last_failed_paths = self.get_last_failed_paths()
+ config.pluginmanager.register(
+ LFPluginCollWrapper(self), "lfplugin-collwrapper"
+ )
+
+ def get_last_failed_paths(self) -> Set[Path]:
+ """Return a set with all Paths of the previously failed nodeids and
+ their parents."""
+ rootpath = self.config.rootpath
+ result = set()
+ for nodeid in self.lastfailed:
+ path = rootpath / nodeid.split("::")[0]
+ result.add(path)
+ result.update(path.parents)
+ return {x for x in result if x.exists()}
+
+ def pytest_report_collectionfinish(self) -> Optional[str]:
+ if self.active and self.config.getoption("verbose") >= 0:
+ return "run-last-failure: %s" % self._report_status
+ return None
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ if (report.when == "call" and report.passed) or report.skipped:
+ self.lastfailed.pop(report.nodeid, None)
+ elif report.failed:
+ self.lastfailed[report.nodeid] = True
+
+ def pytest_collectreport(self, report: CollectReport) -> None:
+ passed = report.outcome in ("passed", "skipped")
+ if passed:
+ if report.nodeid in self.lastfailed:
+ self.lastfailed.pop(report.nodeid)
+ self.lastfailed.update((item.nodeid, True) for item in report.result)
+ else:
+ self.lastfailed[report.nodeid] = True
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_collection_modifyitems(
+ self, config: Config, items: List[nodes.Item]
+ ) -> Generator[None, None, None]:
+ yield
+
+ if not self.active:
+ return
+
+ if self.lastfailed:
+ previously_failed = []
+ previously_passed = []
+ for item in items:
+ if item.nodeid in self.lastfailed:
+ previously_failed.append(item)
+ else:
+ previously_passed.append(item)
+ self._previously_failed_count = len(previously_failed)
+
+ if not previously_failed:
+ # Running a subset of all tests with recorded failures
+ # only outside of it.
+ self._report_status = "%d known failures not in selected tests" % (
+ len(self.lastfailed),
+ )
+ else:
+ if self.config.getoption("lf"):
+ items[:] = previously_failed
+ config.hook.pytest_deselected(items=previously_passed)
+ else: # --failedfirst
+ items[:] = previously_failed + previously_passed
+
+ noun = "failure" if self._previously_failed_count == 1 else "failures"
+ suffix = " first" if self.config.getoption("failedfirst") else ""
+ self._report_status = "rerun previous {count} {noun}{suffix}".format(
+ count=self._previously_failed_count, suffix=suffix, noun=noun
+ )
+
+ if self._skipped_files > 0:
+ files_noun = "file" if self._skipped_files == 1 else "files"
+ self._report_status += " (skipped {files} {files_noun})".format(
+ files=self._skipped_files, files_noun=files_noun
+ )
+ else:
+ self._report_status = "no previously failed tests, "
+ if self.config.getoption("last_failed_no_failures") == "none":
+ self._report_status += "deselecting all items."
+ config.hook.pytest_deselected(items=items[:])
+ items[:] = []
+ else:
+ self._report_status += "not deselecting items."
+
+ def pytest_sessionfinish(self, session: Session) -> None:
+ config = self.config
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
+ return
+
+ assert config.cache is not None
+ saved_lastfailed = config.cache.get("cache/lastfailed", {})
+ if saved_lastfailed != self.lastfailed:
+ config.cache.set("cache/lastfailed", self.lastfailed)
+
+
+class NFPlugin:
+ """Plugin which implements the --nf (run new-first) option."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ self.active = config.option.newfirst
+ assert config.cache is not None
+ self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_collection_modifyitems(
+ self, items: List[nodes.Item]
+ ) -> Generator[None, None, None]:
+ yield
+
+ if self.active:
+ new_items: Dict[str, nodes.Item] = {}
+ other_items: Dict[str, nodes.Item] = {}
+ for item in items:
+ if item.nodeid not in self.cached_nodeids:
+ new_items[item.nodeid] = item
+ else:
+ other_items[item.nodeid] = item
+
+ items[:] = self._get_increasing_order(
+ new_items.values()
+ ) + self._get_increasing_order(other_items.values())
+ self.cached_nodeids.update(new_items)
+ else:
+ self.cached_nodeids.update(item.nodeid for item in items)
+
+ def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
+ return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
+
+ def pytest_sessionfinish(self) -> None:
+ config = self.config
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
+ return
+
+ if config.getoption("collectonly"):
+ return
+
+ assert config.cache is not None
+ config.cache.set("cache/nodeids", sorted(self.cached_nodeids))
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--lf",
+ "--last-failed",
+ action="store_true",
+ dest="lf",
+ help="Rerun only the tests that failed "
+ "at the last run (or all if none failed)",
+ )
+ group.addoption(
+ "--ff",
+ "--failed-first",
+ action="store_true",
+ dest="failedfirst",
+ help="Run all tests, but run the last failures first. "
+ "This may re-order tests and thus lead to "
+ "repeated fixture setup/teardown.",
+ )
+ group.addoption(
+ "--nf",
+ "--new-first",
+ action="store_true",
+ dest="newfirst",
+ help="Run tests from new files first, then the rest of the tests "
+ "sorted by file mtime",
+ )
+ group.addoption(
+ "--cache-show",
+ action="append",
+ nargs="?",
+ dest="cacheshow",
+ help=(
+ "Show cache contents, don't perform collection or tests. "
+ "Optional argument: glob (default: '*')."
+ ),
+ )
+ group.addoption(
+ "--cache-clear",
+ action="store_true",
+ dest="cacheclear",
+ help="Remove all cache contents at start of test run",
+ )
+ cache_dir_default = ".pytest_cache"
+ if "TOX_ENV_DIR" in os.environ:
+ cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default)
+ parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path")
+ group.addoption(
+ "--lfnf",
+ "--last-failed-no-failures",
+ action="store",
+ dest="last_failed_no_failures",
+ choices=("all", "none"),
+ default="all",
+ help="With ``--lf``, determines whether to execute tests when there "
+ "are no previously (known) failures or when no "
+ "cached ``lastfailed`` data was found. "
+ "``all`` (the default) runs the full test suite again. "
+ "``none`` just emits a message about no known failures and exits successfully.",
+ )
+
+
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.cacheshow and not config.option.help:
+ from _pytest.main import wrap_session
+
+ return wrap_session(config, cacheshow)
+ return None
+
+
+@hookimpl(tryfirst=True)
+def pytest_configure(config: Config) -> None:
+ config.cache = Cache.for_config(config, _ispytest=True)
+ config.pluginmanager.register(LFPlugin(config), "lfplugin")
+ config.pluginmanager.register(NFPlugin(config), "nfplugin")
+
+
+@fixture
+def cache(request: FixtureRequest) -> Cache:
+ """Return a cache object that can persist state between testing sessions.
+
+ cache.get(key, default)
+ cache.set(key, value)
+
+ Keys must be ``/`` separated strings, where the first part is usually the
+ name of your plugin or application to avoid clashes with other cache users.
+
+ Values can be any object handled by the json stdlib module.
+ """
+ assert request.config.cache is not None
+ return request.config.cache
+
+
+def pytest_report_header(config: Config) -> Optional[str]:
+ """Display cachedir with --cache-show and if non-default."""
+ if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache":
+ assert config.cache is not None
+ cachedir = config.cache._cachedir
+ # TODO: evaluate generating upward relative paths
+ # starting with .., ../.. if sensible
+
+ try:
+ displaypath = cachedir.relative_to(config.rootpath)
+ except ValueError:
+ displaypath = cachedir
+ return f"cachedir: {displaypath}"
+ return None
+
+
+def cacheshow(config: Config, session: Session) -> int:
+ from pprint import pformat
+
+ assert config.cache is not None
+
+ tw = TerminalWriter()
+ tw.line("cachedir: " + str(config.cache._cachedir))
+ if not config.cache._cachedir.is_dir():
+ tw.line("cache is empty")
+ return 0
+
+ glob = config.option.cacheshow[0]
+ if glob is None:
+ glob = "*"
+
+ dummy = object()
+ basedir = config.cache._cachedir
+ vdir = basedir / Cache._CACHE_PREFIX_VALUES
+ tw.sep("-", "cache values for %r" % glob)
+ for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
+ key = str(valpath.relative_to(vdir))
+ val = config.cache.get(key, dummy)
+ if val is dummy:
+ tw.line("%s contains unreadable content, will be ignored" % key)
+ else:
+ tw.line("%s contains:" % key)
+ for line in pformat(val).splitlines():
+ tw.line(" " + line)
+
+ ddir = basedir / Cache._CACHE_PREFIX_DIRS
+ if ddir.is_dir():
+ contents = sorted(ddir.rglob(glob))
+ tw.sep("-", "cache directories for %r" % glob)
+ for p in contents:
+ # if p.is_dir():
+ # print("%s/" % p.relative_to(basedir))
+ if p.is_file():
+ key = str(p.relative_to(basedir))
+ tw.line(f"{key} is a file of length {p.stat().st_size:d}")
+ return 0
diff --git a/venv/lib/python3.12/site-packages/_pytest/capture.py b/venv/lib/python3.12/site-packages/_pytest/capture.py
new file mode 100644
index 0000000..a8ca086
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/capture.py
@@ -0,0 +1,1082 @@
+"""Per-test stdout/stderr capturing mechanism."""
+import abc
+import collections
+import contextlib
+import io
+import os
+import sys
+from io import UnsupportedOperation
+from tempfile import TemporaryFile
+from types import TracebackType
+from typing import Any
+from typing import AnyStr
+from typing import BinaryIO
+from typing import Generator
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import NamedTuple
+from typing import Optional
+from typing import TextIO
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from _pytest.compat import final
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import SubRequest
+from _pytest.nodes import Collector
+from _pytest.nodes import File
+from _pytest.nodes import Item
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+ from typing_extensions import Literal
+
+ _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption(
+ "--capture",
+ action="store",
+ default="fd",
+ metavar="method",
+ choices=["fd", "sys", "no", "tee-sys"],
+ help="Per-test capturing method: one of fd|sys|no|tee-sys",
+ )
+ group._addoption(
+ "-s",
+ action="store_const",
+ const="no",
+ dest="capture",
+ help="Shortcut for --capture=no",
+ )
+
+
+def _colorama_workaround() -> None:
+ """Ensure colorama is imported so that it attaches to the correct stdio
+ handles on Windows.
+
+ colorama uses the terminal on import time. So if something does the
+ first import of colorama while I/O capture is active, colorama will
+ fail in various ways.
+ """
+ if sys.platform.startswith("win32"):
+ try:
+ import colorama # noqa: F401
+ except ImportError:
+ pass
+
+
+def _windowsconsoleio_workaround(stream: TextIO) -> None:
+ """Workaround for Windows Unicode console handling.
+
+ Python 3.6 implemented Unicode console handling for Windows. This works
+ by reading/writing to the raw console handle using
+ ``{Read,Write}ConsoleW``.
+
+ The problem is that we are going to ``dup2`` over the stdio file
+ descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
+ handles used by Python to write to the console. Though there is still some
+ weirdness and the console handle seems to only be closed randomly and not
+ on the first call to ``CloseHandle``, or maybe it gets reopened with the
+ same handle value when we suspend capturing.
+
+ The workaround in this case will reopen stdio with a different fd which
+ also means a different handle by replicating the logic in
+ "Py_lifecycle.c:initstdio/create_stdio".
+
+ :param stream:
+ In practice ``sys.stdout`` or ``sys.stderr``, but given
+ here as parameter for unittesting purposes.
+
+ See https://github.com/pytest-dev/py/issues/103.
+ """
+ if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
+ return
+
+ # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
+ if not hasattr(stream, "buffer"): # type: ignore[unreachable]
+ return
+
+ buffered = hasattr(stream.buffer, "raw")
+ raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
+
+ if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
+ return
+
+ def _reopen_stdio(f, mode):
+ if not buffered and mode[0] == "w":
+ buffering = 0
+ else:
+ buffering = -1
+
+ return io.TextIOWrapper(
+ open(os.dup(f.fileno()), mode, buffering),
+ f.encoding,
+ f.errors,
+ f.newlines,
+ f.line_buffering,
+ )
+
+ sys.stdin = _reopen_stdio(sys.stdin, "rb")
+ sys.stdout = _reopen_stdio(sys.stdout, "wb")
+ sys.stderr = _reopen_stdio(sys.stderr, "wb")
+
+
+@hookimpl(hookwrapper=True)
+def pytest_load_initial_conftests(early_config: Config):
+ ns = early_config.known_args_namespace
+ if ns.capture == "fd":
+ _windowsconsoleio_workaround(sys.stdout)
+ _colorama_workaround()
+ pluginmanager = early_config.pluginmanager
+ capman = CaptureManager(ns.capture)
+ pluginmanager.register(capman, "capturemanager")
+
+ # Make sure that capturemanager is properly reset at final shutdown.
+ early_config.add_cleanup(capman.stop_global_capturing)
+
+ # Finally trigger conftest loading but while capturing (issue #93).
+ capman.start_global_capturing()
+ outcome = yield
+ capman.suspend_global_capture()
+ if outcome.excinfo is not None:
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+
+# IO Helpers.
+
+
+class EncodedFile(io.TextIOWrapper):
+ __slots__ = ()
+
+ @property
+ def name(self) -> str:
+ # Ensure that file.name is a string. Workaround for a Python bug
+ # fixed in >=3.7.4: https://bugs.python.org/issue36015
+ return repr(self.buffer)
+
+ @property
+ def mode(self) -> str:
+ # TextIOWrapper doesn't expose a mode, but at least some of our
+ # tests check it.
+ return self.buffer.mode.replace("b", "")
+
+
+class CaptureIO(io.TextIOWrapper):
+ def __init__(self) -> None:
+ super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
+
+ def getvalue(self) -> str:
+ assert isinstance(self.buffer, io.BytesIO)
+ return self.buffer.getvalue().decode("UTF-8")
+
+
+class TeeCaptureIO(CaptureIO):
+ def __init__(self, other: TextIO) -> None:
+ self._other = other
+ super().__init__()
+
+ def write(self, s: str) -> int:
+ super().write(s)
+ return self._other.write(s)
+
+
+class DontReadFromInput(TextIO):
+ @property
+ def encoding(self) -> str:
+ return sys.__stdin__.encoding
+
+ def read(self, size: int = -1) -> str:
+ raise OSError(
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
+ )
+
+ readline = read
+
+ def __next__(self) -> str:
+ return self.readline()
+
+ def readlines(self, hint: Optional[int] = -1) -> List[str]:
+ raise OSError(
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
+ )
+
+ def __iter__(self) -> Iterator[str]:
+ return self
+
+ def fileno(self) -> int:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
+
+ def flush(self) -> None:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
+
+ def isatty(self) -> bool:
+ return False
+
+ def close(self) -> None:
+ pass
+
+ def readable(self) -> bool:
+ return False
+
+ def seek(self, offset: int, whence: int = 0) -> int:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
+
+ def seekable(self) -> bool:
+ return False
+
+ def tell(self) -> int:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
+
+ def truncate(self, size: Optional[int] = None) -> int:
+ raise UnsupportedOperation("cannot truncate stdin")
+
+ def write(self, data: str) -> int:
+ raise UnsupportedOperation("cannot write to stdin")
+
+ def writelines(self, lines: Iterable[str]) -> None:
+ raise UnsupportedOperation("Cannot write to stdin")
+
+ def writable(self) -> bool:
+ return False
+
+ def __enter__(self) -> "DontReadFromInput":
+ return self
+
+ def __exit__(
+ self,
+ type: Optional[Type[BaseException]],
+ value: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ pass
+
+ @property
+ def buffer(self) -> BinaryIO:
+ # The str/bytes doesn't actually matter in this type, so OK to fake.
+ return self # type: ignore[return-value]
+
+
+# Capture classes.
+
+
+class CaptureBase(abc.ABC, Generic[AnyStr]):
+ EMPTY_BUFFER: AnyStr
+
+ @abc.abstractmethod
+ def __init__(self, fd: int) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def start(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def done(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def suspend(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def resume(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def writeorg(self, data: AnyStr) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def snap(self) -> AnyStr:
+ raise NotImplementedError()
+
+
+patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
+
+
+class NoCapture(CaptureBase[str]):
+ EMPTY_BUFFER = ""
+
+ def __init__(self, fd: int) -> None:
+ pass
+
+ def start(self) -> None:
+ pass
+
+ def done(self) -> None:
+ pass
+
+ def suspend(self) -> None:
+ pass
+
+ def resume(self) -> None:
+ pass
+
+ def snap(self) -> str:
+ return ""
+
+ def writeorg(self, data: str) -> None:
+ pass
+
+
+class SysCaptureBase(CaptureBase[AnyStr]):
+ def __init__(
+ self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
+ ) -> None:
+ name = patchsysdict[fd]
+ self._old: TextIO = getattr(sys, name)
+ self.name = name
+ if tmpfile is None:
+ if name == "stdin":
+ tmpfile = DontReadFromInput()
+ else:
+ tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)
+ self.tmpfile = tmpfile
+ self._state = "initialized"
+
+ def repr(self, class_name: str) -> str:
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
+ class_name,
+ self.name,
+ hasattr(self, "_old") and repr(self._old) or "",
+ self._state,
+ self.tmpfile,
+ )
+
+ def __repr__(self) -> str:
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
+ self.__class__.__name__,
+ self.name,
+ hasattr(self, "_old") and repr(self._old) or "",
+ self._state,
+ self.tmpfile,
+ )
+
+ def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
+ assert (
+ self._state in states
+ ), "cannot {} in state {!r}: expected one of {}".format(
+ op, self._state, ", ".join(states)
+ )
+
+ def start(self) -> None:
+ self._assert_state("start", ("initialized",))
+ setattr(sys, self.name, self.tmpfile)
+ self._state = "started"
+
+ def done(self) -> None:
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
+ if self._state == "done":
+ return
+ setattr(sys, self.name, self._old)
+ del self._old
+ self.tmpfile.close()
+ self._state = "done"
+
+ def suspend(self) -> None:
+ self._assert_state("suspend", ("started", "suspended"))
+ setattr(sys, self.name, self._old)
+ self._state = "suspended"
+
+ def resume(self) -> None:
+ self._assert_state("resume", ("started", "suspended"))
+ if self._state == "started":
+ return
+ setattr(sys, self.name, self.tmpfile)
+ self._state = "started"
+
+
+class SysCaptureBinary(SysCaptureBase[bytes]):
+ EMPTY_BUFFER = b""
+
+ def snap(self) -> bytes:
+ self._assert_state("snap", ("started", "suspended"))
+ self.tmpfile.seek(0)
+ res = self.tmpfile.buffer.read()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data: bytes) -> None:
+ self._assert_state("writeorg", ("started", "suspended"))
+ self._old.flush()
+ self._old.buffer.write(data)
+ self._old.buffer.flush()
+
+
+class SysCapture(SysCaptureBase[str]):
+ EMPTY_BUFFER = ""
+
+ def snap(self) -> str:
+ self._assert_state("snap", ("started", "suspended"))
+ assert isinstance(self.tmpfile, CaptureIO)
+ res = self.tmpfile.getvalue()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data: str) -> None:
+ self._assert_state("writeorg", ("started", "suspended"))
+ self._old.write(data)
+ self._old.flush()
+
+
+class FDCaptureBase(CaptureBase[AnyStr]):
+ def __init__(self, targetfd: int) -> None:
+ self.targetfd = targetfd
+
+ try:
+ os.fstat(targetfd)
+ except OSError:
+ # FD capturing is conceptually simple -- create a temporary file,
+ # redirect the FD to it, redirect back when done. But when the
+ # target FD is invalid it throws a wrench into this lovely scheme.
+ #
+ # Tests themselves shouldn't care if the FD is valid, FD capturing
+ # should work regardless of external circumstances. So falling back
+ # to just sys capturing is not a good option.
+ #
+ # Further complications are the need to support suspend() and the
+ # possibility of FD reuse (e.g. the tmpfile getting the very same
+ # target FD). The following approach is robust, I believe.
+ self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
+ os.dup2(self.targetfd_invalid, targetfd)
+ else:
+ self.targetfd_invalid = None
+ self.targetfd_save = os.dup(targetfd)
+
+ if targetfd == 0:
+ self.tmpfile = open(os.devnull, encoding="utf-8")
+ self.syscapture: CaptureBase[str] = SysCapture(targetfd)
+ else:
+ self.tmpfile = EncodedFile(
+ TemporaryFile(buffering=0),
+ encoding="utf-8",
+ errors="replace",
+ newline="",
+ write_through=True,
+ )
+ if targetfd in patchsysdict:
+ self.syscapture = SysCapture(targetfd, self.tmpfile)
+ else:
+ self.syscapture = NoCapture(targetfd)
+
+ self._state = "initialized"
+
+ def __repr__(self) -> str:
+ return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
+ self.__class__.__name__,
+ self.targetfd,
+ self.targetfd_save,
+ self._state,
+ self.tmpfile,
+ )
+
+ def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
+ assert (
+ self._state in states
+ ), "cannot {} in state {!r}: expected one of {}".format(
+ op, self._state, ", ".join(states)
+ )
+
+ def start(self) -> None:
+ """Start capturing on targetfd using memorized tmpfile."""
+ self._assert_state("start", ("initialized",))
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
+ self.syscapture.start()
+ self._state = "started"
+
+ def done(self) -> None:
+ """Stop capturing, restore streams, return original capture file,
+ seeked to position zero."""
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
+ if self._state == "done":
+ return
+ os.dup2(self.targetfd_save, self.targetfd)
+ os.close(self.targetfd_save)
+ if self.targetfd_invalid is not None:
+ if self.targetfd_invalid != self.targetfd:
+ os.close(self.targetfd)
+ os.close(self.targetfd_invalid)
+ self.syscapture.done()
+ self.tmpfile.close()
+ self._state = "done"
+
+ def suspend(self) -> None:
+ self._assert_state("suspend", ("started", "suspended"))
+ if self._state == "suspended":
+ return
+ self.syscapture.suspend()
+ os.dup2(self.targetfd_save, self.targetfd)
+ self._state = "suspended"
+
+ def resume(self) -> None:
+ self._assert_state("resume", ("started", "suspended"))
+ if self._state == "started":
+ return
+ self.syscapture.resume()
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
+ self._state = "started"
+
+
+class FDCaptureBinary(FDCaptureBase[bytes]):
+ """Capture IO to/from a given OS-level file descriptor.
+
+ snap() produces `bytes`.
+ """
+
+ EMPTY_BUFFER = b""
+
+ def snap(self) -> bytes:
+ self._assert_state("snap", ("started", "suspended"))
+ self.tmpfile.seek(0)
+ res = self.tmpfile.buffer.read()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data: bytes) -> None:
+ """Write to original file descriptor."""
+ self._assert_state("writeorg", ("started", "suspended"))
+ os.write(self.targetfd_save, data)
+
+
+class FDCapture(FDCaptureBase[str]):
+ """Capture IO to/from a given OS-level file descriptor.
+
+ snap() produces text.
+ """
+
+ EMPTY_BUFFER = ""
+
+ def snap(self) -> str:
+ self._assert_state("snap", ("started", "suspended"))
+ self.tmpfile.seek(0)
+ res = self.tmpfile.read()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data: str) -> None:
+ """Write to original file descriptor."""
+ self._assert_state("writeorg", ("started", "suspended"))
+ # XXX use encoding of original stream
+ os.write(self.targetfd_save, data.encode("utf-8"))
+
+
+# MultiCapture
+
+
+# Generic NamedTuple only supported since Python 3.11.
+if sys.version_info >= (3, 11) or TYPE_CHECKING:
+
+ @final
+ class CaptureResult(NamedTuple, Generic[AnyStr]):
+ """The result of :method:`CaptureFixture.readouterr`."""
+
+ out: AnyStr
+ err: AnyStr
+
+else:
+
+ class CaptureResult(
+ collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
+ ):
+ """The result of :method:`CaptureFixture.readouterr`."""
+
+ __slots__ = ()
+
+
+class MultiCapture(Generic[AnyStr]):
+ _state = None
+ _in_suspended = False
+
+ def __init__(
+ self,
+ in_: Optional[CaptureBase[AnyStr]],
+ out: Optional[CaptureBase[AnyStr]],
+ err: Optional[CaptureBase[AnyStr]],
+ ) -> None:
+ self.in_: Optional[CaptureBase[AnyStr]] = in_
+ self.out: Optional[CaptureBase[AnyStr]] = out
+ self.err: Optional[CaptureBase[AnyStr]] = err
+
+ def __repr__(self) -> str:
+ return "".format(
+ self.out,
+ self.err,
+ self.in_,
+ self._state,
+ self._in_suspended,
+ )
+
+ def start_capturing(self) -> None:
+ self._state = "started"
+ if self.in_:
+ self.in_.start()
+ if self.out:
+ self.out.start()
+ if self.err:
+ self.err.start()
+
+ def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
+ """Pop current snapshot out/err capture and flush to orig streams."""
+ out, err = self.readouterr()
+ if out:
+ assert self.out is not None
+ self.out.writeorg(out)
+ if err:
+ assert self.err is not None
+ self.err.writeorg(err)
+ return out, err
+
+ def suspend_capturing(self, in_: bool = False) -> None:
+ self._state = "suspended"
+ if self.out:
+ self.out.suspend()
+ if self.err:
+ self.err.suspend()
+ if in_ and self.in_:
+ self.in_.suspend()
+ self._in_suspended = True
+
+ def resume_capturing(self) -> None:
+ self._state = "started"
+ if self.out:
+ self.out.resume()
+ if self.err:
+ self.err.resume()
+ if self._in_suspended:
+ assert self.in_ is not None
+ self.in_.resume()
+ self._in_suspended = False
+
+ def stop_capturing(self) -> None:
+ """Stop capturing and reset capturing streams."""
+ if self._state == "stopped":
+ raise ValueError("was already stopped")
+ self._state = "stopped"
+ if self.out:
+ self.out.done()
+ if self.err:
+ self.err.done()
+ if self.in_:
+ self.in_.done()
+
+ def is_started(self) -> bool:
+ """Whether actively capturing -- not suspended or stopped."""
+ return self._state == "started"
+
+ def readouterr(self) -> CaptureResult[AnyStr]:
+ out = self.out.snap() if self.out else ""
+ err = self.err.snap() if self.err else ""
+ # TODO: This type error is real, need to fix.
+ return CaptureResult(out, err) # type: ignore[arg-type]
+
+
+def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
+ if method == "fd":
+ return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
+ elif method == "sys":
+ return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))
+ elif method == "no":
+ return MultiCapture(in_=None, out=None, err=None)
+ elif method == "tee-sys":
+ return MultiCapture(
+ in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
+ )
+ raise ValueError(f"unknown capturing method: {method!r}")
+
+
+# CaptureManager and CaptureFixture
+
+
+class CaptureManager:
+ """The capture plugin.
+
+ Manages that the appropriate capture method is enabled/disabled during
+ collection and each test phase (setup, call, teardown). After each of
+ those points, the captured output is obtained and attached to the
+ collection/runtest report.
+
+ There are two levels of capture:
+
+ * global: enabled by default and can be suppressed by the ``-s``
+ option. This is always enabled/disabled during collection and each test
+ phase.
+
+ * fixture: when a test function or one of its fixture depend on the
+ ``capsys`` or ``capfd`` fixtures. In this case special handling is
+ needed to ensure the fixtures take precedence over the global capture.
+ """
+
+ def __init__(self, method: "_CaptureMethod") -> None:
+ self._method: Final = method
+ self._global_capturing: Optional[MultiCapture[str]] = None
+ self._capture_fixture: Optional[CaptureFixture[Any]] = None
+
+ def __repr__(self) -> str:
+ return "".format(
+ self._method, self._global_capturing, self._capture_fixture
+ )
+
+ def is_capturing(self) -> Union[str, bool]:
+ if self.is_globally_capturing():
+ return "global"
+ if self._capture_fixture:
+ return "fixture %s" % self._capture_fixture.request.fixturename
+ return False
+
+ # Global capturing control
+
+ def is_globally_capturing(self) -> bool:
+ return self._method != "no"
+
+ def start_global_capturing(self) -> None:
+ assert self._global_capturing is None
+ self._global_capturing = _get_multicapture(self._method)
+ self._global_capturing.start_capturing()
+
+ def stop_global_capturing(self) -> None:
+ if self._global_capturing is not None:
+ self._global_capturing.pop_outerr_to_orig()
+ self._global_capturing.stop_capturing()
+ self._global_capturing = None
+
+ def resume_global_capture(self) -> None:
+ # During teardown of the python process, and on rare occasions, capture
+ # attributes can be `None` while trying to resume global capture.
+ if self._global_capturing is not None:
+ self._global_capturing.resume_capturing()
+
+ def suspend_global_capture(self, in_: bool = False) -> None:
+ if self._global_capturing is not None:
+ self._global_capturing.suspend_capturing(in_=in_)
+
+ def suspend(self, in_: bool = False) -> None:
+ # Need to undo local capsys-et-al if it exists before disabling global capture.
+ self.suspend_fixture()
+ self.suspend_global_capture(in_)
+
+ def resume(self) -> None:
+ self.resume_global_capture()
+ self.resume_fixture()
+
+ def read_global_capture(self) -> CaptureResult[str]:
+ assert self._global_capturing is not None
+ return self._global_capturing.readouterr()
+
+ # Fixture Control
+
+ def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:
+ if self._capture_fixture:
+ current_fixture = self._capture_fixture.request.fixturename
+ requested_fixture = capture_fixture.request.fixturename
+ capture_fixture.request.raiseerror(
+ "cannot use {} and {} at the same time".format(
+ requested_fixture, current_fixture
+ )
+ )
+ self._capture_fixture = capture_fixture
+
+ def unset_fixture(self) -> None:
+ self._capture_fixture = None
+
+ def activate_fixture(self) -> None:
+ """If the current item is using ``capsys`` or ``capfd``, activate
+ them so they take precedence over the global capture."""
+ if self._capture_fixture:
+ self._capture_fixture._start()
+
+ def deactivate_fixture(self) -> None:
+ """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
+ if self._capture_fixture:
+ self._capture_fixture.close()
+
+ def suspend_fixture(self) -> None:
+ if self._capture_fixture:
+ self._capture_fixture._suspend()
+
+ def resume_fixture(self) -> None:
+ if self._capture_fixture:
+ self._capture_fixture._resume()
+
+ # Helper context managers
+
+ @contextlib.contextmanager
+ def global_and_fixture_disabled(self) -> Generator[None, None, None]:
+ """Context manager to temporarily disable global and current fixture capturing."""
+ do_fixture = self._capture_fixture and self._capture_fixture._is_started()
+ if do_fixture:
+ self.suspend_fixture()
+ do_global = self._global_capturing and self._global_capturing.is_started()
+ if do_global:
+ self.suspend_global_capture()
+ try:
+ yield
+ finally:
+ if do_global:
+ self.resume_global_capture()
+ if do_fixture:
+ self.resume_fixture()
+
+ @contextlib.contextmanager
+ def item_capture(self, when: str, item: Item) -> Generator[None, None, None]:
+ self.resume_global_capture()
+ self.activate_fixture()
+ try:
+ yield
+ finally:
+ self.deactivate_fixture()
+ self.suspend_global_capture(in_=False)
+
+ out, err = self.read_global_capture()
+ item.add_report_section(when, "stdout", out)
+ item.add_report_section(when, "stderr", err)
+
+ # Hooks
+
+ @hookimpl(hookwrapper=True)
+ def pytest_make_collect_report(self, collector: Collector):
+ if isinstance(collector, File):
+ self.resume_global_capture()
+ outcome = yield
+ self.suspend_global_capture()
+ out, err = self.read_global_capture()
+ rep = outcome.get_result()
+ if out:
+ rep.sections.append(("Captured stdout", out))
+ if err:
+ rep.sections.append(("Captured stderr", err))
+ else:
+ yield
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
+ with self.item_capture("setup", item):
+ yield
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
+ with self.item_capture("call", item):
+ yield
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
+ with self.item_capture("teardown", item):
+ yield
+
+ @hookimpl(tryfirst=True)
+ def pytest_keyboard_interrupt(self) -> None:
+ self.stop_global_capturing()
+
+ @hookimpl(tryfirst=True)
+ def pytest_internalerror(self) -> None:
+ self.stop_global_capturing()
+
+
+class CaptureFixture(Generic[AnyStr]):
+ """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
+ :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
+
+ def __init__(
+ self,
+ captureclass: Type[CaptureBase[AnyStr]],
+ request: SubRequest,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
+ self.request = request
+ self._capture: Optional[MultiCapture[AnyStr]] = None
+ self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
+ self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
+
+ def _start(self) -> None:
+ if self._capture is None:
+ self._capture = MultiCapture(
+ in_=None,
+ out=self.captureclass(1),
+ err=self.captureclass(2),
+ )
+ self._capture.start_capturing()
+
+ def close(self) -> None:
+ if self._capture is not None:
+ out, err = self._capture.pop_outerr_to_orig()
+ self._captured_out += out
+ self._captured_err += err
+ self._capture.stop_capturing()
+ self._capture = None
+
+ def readouterr(self) -> CaptureResult[AnyStr]:
+ """Read and return the captured output so far, resetting the internal
+ buffer.
+
+ :returns:
+ The captured content as a namedtuple with ``out`` and ``err``
+ string attributes.
+ """
+ captured_out, captured_err = self._captured_out, self._captured_err
+ if self._capture is not None:
+ out, err = self._capture.readouterr()
+ captured_out += out
+ captured_err += err
+ self._captured_out = self.captureclass.EMPTY_BUFFER
+ self._captured_err = self.captureclass.EMPTY_BUFFER
+ return CaptureResult(captured_out, captured_err)
+
+ def _suspend(self) -> None:
+ """Suspend this fixture's own capturing temporarily."""
+ if self._capture is not None:
+ self._capture.suspend_capturing()
+
+ def _resume(self) -> None:
+ """Resume this fixture's own capturing temporarily."""
+ if self._capture is not None:
+ self._capture.resume_capturing()
+
+ def _is_started(self) -> bool:
+ """Whether actively capturing -- not disabled or closed."""
+ if self._capture is not None:
+ return self._capture.is_started()
+ return False
+
+ @contextlib.contextmanager
+ def disabled(self) -> Generator[None, None, None]:
+ """Temporarily disable capturing while inside the ``with`` block."""
+ capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
+ "capturemanager"
+ )
+ with capmanager.global_and_fixture_disabled():
+ yield
+
+
+# The fixtures.
+
+
+@fixture
+def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
+ r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
+
+ The captured output is made available via ``capsys.readouterr()`` method
+ calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``text`` objects.
+
+ Returns an instance of :class:`CaptureFixture[str] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_output(capsys):
+ print("hello")
+ captured = capsys.readouterr()
+ assert captured.out == "hello\n"
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
+ r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
+
+ The captured output is made available via ``capsysbinary.readouterr()``
+ method calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``bytes`` objects.
+
+ Returns an instance of :class:`CaptureFixture[bytes] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_output(capsysbinary):
+ print("hello")
+ captured = capsysbinary.readouterr()
+ assert captured.out == b"hello\n"
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
+ r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
+
+ The captured output is made available via ``capfd.readouterr()`` method
+ calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``text`` objects.
+
+ Returns an instance of :class:`CaptureFixture[str] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_system_echo(capfd):
+ os.system('echo "hello"')
+ captured = capfd.readouterr()
+ assert captured.out == "hello\n"
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
+ r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
+
+ The captured output is made available via ``capfd.readouterr()`` method
+ calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``byte`` objects.
+
+ Returns an instance of :class:`CaptureFixture[bytes] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_system_echo(capfdbinary):
+ os.system('echo "hello"')
+ captured = capfdbinary.readouterr()
+ assert captured.out == b"hello\n"
+
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
diff --git a/venv/lib/python3.12/site-packages/_pytest/compat.py b/venv/lib/python3.12/site-packages/_pytest/compat.py
new file mode 100644
index 0000000..1d0add7
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/compat.py
@@ -0,0 +1,435 @@
+"""Python version compatibility code."""
+from __future__ import annotations
+
+import dataclasses
+import enum
+import functools
+import inspect
+import os
+import sys
+from inspect import Parameter
+from inspect import signature
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import Generic
+from typing import NoReturn
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+import py
+
+# fmt: off
+# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
+# If `overload` is imported from `compat` instead of from `typing`,
+# Sphinx doesn't recognize it as `overload` and the API docs for
+# overloaded functions look good again. But type checkers handle
+# it fine.
+# fmt: on
+if True:
+ from typing import overload as overload
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+
+
+_T = TypeVar("_T")
+_S = TypeVar("_S")
+
+#: constant to prepare valuing pylib path replacements/lazy proxies later on
+# intended for removal in pytest 8.0 or 9.0
+
+# fmt: off
+# intentional space to create a fake difference for the verification
+LEGACY_PATH = py.path. local
+# fmt: on
+
+
+def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
+ """Internal wrapper to prepare lazy proxies for legacy_path instances"""
+ return LEGACY_PATH(path)
+
+
+# fmt: off
+# Singleton type for NOTSET, as described in:
+# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
+class NotSetType(enum.Enum):
+ token = 0
+NOTSET: Final = NotSetType.token # noqa: E305
+# fmt: on
+
+if sys.version_info >= (3, 8):
+ import importlib.metadata
+
+ importlib_metadata = importlib.metadata
+else:
+ import importlib_metadata as importlib_metadata # noqa: F401
+
+
+def _format_args(func: Callable[..., Any]) -> str:
+ return str(signature(func))
+
+
+def is_generator(func: object) -> bool:
+ genfunc = inspect.isgeneratorfunction(func)
+ return genfunc and not iscoroutinefunction(func)
+
+
+def iscoroutinefunction(func: object) -> bool:
+ """Return True if func is a coroutine function (a function defined with async
+ def syntax, and doesn't contain yield), or a function decorated with
+ @asyncio.coroutine.
+
+ Note: copied and modified from Python 3.5's builtin couroutines.py to avoid
+ importing asyncio directly, which in turns also initializes the "logging"
+ module as a side-effect (see issue #8).
+ """
+ return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
+
+
+def is_async_function(func: object) -> bool:
+ """Return True if the given function seems to be an async function or
+ an async generator."""
+ return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
+
+
+def getlocation(function, curdir: str | None = None) -> str:
+ function = get_real_func(function)
+ fn = Path(inspect.getfile(function))
+ lineno = function.__code__.co_firstlineno
+ if curdir is not None:
+ try:
+ relfn = fn.relative_to(curdir)
+ except ValueError:
+ pass
+ else:
+ return "%s:%d" % (relfn, lineno + 1)
+ return "%s:%d" % (fn, lineno + 1)
+
+
+def num_mock_patch_args(function) -> int:
+ """Return number of arguments used up by mock arguments (if any)."""
+ patchings = getattr(function, "patchings", None)
+ if not patchings:
+ return 0
+
+ mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
+ ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
+
+ return len(
+ [
+ p
+ for p in patchings
+ if not p.attribute_name
+ and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
+ ]
+ )
+
+
+def getfuncargnames(
+ function: Callable[..., Any],
+ *,
+ name: str = "",
+ is_method: bool = False,
+ cls: type | None = None,
+) -> tuple[str, ...]:
+ """Return the names of a function's mandatory arguments.
+
+ Should return the names of all function arguments that:
+ * Aren't bound to an instance or type as in instance or class methods.
+ * Don't have default values.
+ * Aren't bound with functools.partial.
+ * Aren't replaced with mocks.
+
+ The is_method and cls arguments indicate that the function should
+ be treated as a bound method even though it's not unless, only in
+ the case of cls, the function is a static method.
+
+ The name parameter should be the original name in which the function was collected.
+ """
+ # TODO(RonnyPfannschmidt): This function should be refactored when we
+ # revisit fixtures. The fixture mechanism should ask the node for
+ # the fixture names, and not try to obtain directly from the
+ # function object well after collection has occurred.
+
+ # The parameters attribute of a Signature object contains an
+ # ordered mapping of parameter names to Parameter instances. This
+ # creates a tuple of the names of the parameters that don't have
+ # defaults.
+ try:
+ parameters = signature(function).parameters
+ except (ValueError, TypeError) as e:
+ from _pytest.outcomes import fail
+
+ fail(
+ f"Could not determine arguments of {function!r}: {e}",
+ pytrace=False,
+ )
+
+ arg_names = tuple(
+ p.name
+ for p in parameters.values()
+ if (
+ p.kind is Parameter.POSITIONAL_OR_KEYWORD
+ or p.kind is Parameter.KEYWORD_ONLY
+ )
+ and p.default is Parameter.empty
+ )
+ if not name:
+ name = function.__name__
+
+ # If this function should be treated as a bound method even though
+ # it's passed as an unbound method or function, remove the first
+ # parameter name.
+ if is_method or (
+ # Not using `getattr` because we don't want to resolve the staticmethod.
+ # Not using `cls.__dict__` because we want to check the entire MRO.
+ cls
+ and not isinstance(
+ inspect.getattr_static(cls, name, default=None), staticmethod
+ )
+ ):
+ arg_names = arg_names[1:]
+ # Remove any names that will be replaced with mocks.
+ if hasattr(function, "__wrapped__"):
+ arg_names = arg_names[num_mock_patch_args(function) :]
+ return arg_names
+
+
+def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
+ # Note: this code intentionally mirrors the code at the beginning of
+ # getfuncargnames, to get the arguments which were excluded from its result
+ # because they had default values.
+ return tuple(
+ p.name
+ for p in signature(function).parameters.values()
+ if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
+ and p.default is not Parameter.empty
+ )
+
+
+_non_printable_ascii_translate_table = {
+ i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
+}
+_non_printable_ascii_translate_table.update(
+ {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
+)
+
+
+def _translate_non_printable(s: str) -> str:
+ return s.translate(_non_printable_ascii_translate_table)
+
+
+STRING_TYPES = bytes, str
+
+
+def _bytes_to_ascii(val: bytes) -> str:
+ return val.decode("ascii", "backslashreplace")
+
+
+def ascii_escaped(val: bytes | str) -> str:
+ r"""If val is pure ASCII, return it as an str, otherwise, escape
+ bytes objects into a sequence of escaped bytes:
+
+ b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
+
+ and escapes unicode objects into a sequence of escaped unicode
+ ids, e.g.:
+
+ r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
+
+ Note:
+ The obvious "v.decode('unicode-escape')" will return
+ valid UTF-8 unicode if it finds them in bytes, but we
+ want to return escaped bytes for any byte, even if they match
+ a UTF-8 string.
+ """
+ if isinstance(val, bytes):
+ ret = _bytes_to_ascii(val)
+ else:
+ ret = val.encode("unicode_escape").decode("ascii")
+ return _translate_non_printable(ret)
+
+
+@dataclasses.dataclass
+class _PytestWrapper:
+ """Dummy wrapper around a function object for internal use only.
+
+ Used to correctly unwrap the underlying function object when we are
+ creating fixtures, because we wrap the function object ourselves with a
+ decorator to issue warnings when the fixture function is called directly.
+ """
+
+ obj: Any
+
+
+def get_real_func(obj):
+ """Get the real function object of the (possibly) wrapped object by
+ functools.wraps or functools.partial."""
+ start_obj = obj
+ for i in range(100):
+ # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
+ # to trigger a warning if it gets called directly instead of by pytest: we don't
+ # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
+ new_obj = getattr(obj, "__pytest_wrapped__", None)
+ if isinstance(new_obj, _PytestWrapper):
+ obj = new_obj.obj
+ break
+ new_obj = getattr(obj, "__wrapped__", None)
+ if new_obj is None:
+ break
+ obj = new_obj
+ else:
+ from _pytest._io.saferepr import saferepr
+
+ raise ValueError(
+ ("could not find real function of {start}\nstopped at {current}").format(
+ start=saferepr(start_obj), current=saferepr(obj)
+ )
+ )
+ if isinstance(obj, functools.partial):
+ obj = obj.func
+ return obj
+
+
+def get_real_method(obj, holder):
+ """Attempt to obtain the real function object that might be wrapping
+ ``obj``, while at the same time returning a bound method to ``holder`` if
+ the original object was a bound method."""
+ try:
+ is_method = hasattr(obj, "__func__")
+ obj = get_real_func(obj)
+ except Exception: # pragma: no cover
+ return obj
+ if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
+ obj = obj.__get__(holder)
+ return obj
+
+
+def getimfunc(func):
+ try:
+ return func.__func__
+ except AttributeError:
+ return func
+
+
+def safe_getattr(object: Any, name: str, default: Any) -> Any:
+ """Like getattr but return default upon any Exception or any OutcomeException.
+
+ Attribute access can potentially fail for 'evil' Python objects.
+ See issue #214.
+ It catches OutcomeException because of #2490 (issue #580), new outcomes
+ are derived from BaseException instead of Exception (for more details
+ check #2707).
+ """
+ from _pytest.outcomes import TEST_OUTCOME
+
+ try:
+ return getattr(object, name, default)
+ except TEST_OUTCOME:
+ return default
+
+
+def safe_isclass(obj: object) -> bool:
+ """Ignore any exception via isinstance on Python 3."""
+ try:
+ return inspect.isclass(obj)
+ except Exception:
+ return False
+
+
+if TYPE_CHECKING:
+ if sys.version_info >= (3, 8):
+ from typing import final as final
+ else:
+ from typing_extensions import final as final
+elif sys.version_info >= (3, 8):
+ from typing import final as final
+else:
+
+ def final(f):
+ return f
+
+
+if sys.version_info >= (3, 8):
+ from functools import cached_property as cached_property
+else:
+
+ class cached_property(Generic[_S, _T]):
+ __slots__ = ("func", "__doc__")
+
+ def __init__(self, func: Callable[[_S], _T]) -> None:
+ self.func = func
+ self.__doc__ = func.__doc__
+
+ @overload
+ def __get__(
+ self, instance: None, owner: type[_S] | None = ...
+ ) -> cached_property[_S, _T]:
+ ...
+
+ @overload
+ def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
+ ...
+
+ def __get__(self, instance, owner=None):
+ if instance is None:
+ return self
+ value = instance.__dict__[self.func.__name__] = self.func(instance)
+ return value
+
+
+def get_user_id() -> int | None:
+ """Return the current process's real user id or None if it could not be
+ determined.
+
+ :return: The user id or None if it could not be determined.
+ """
+ # mypy follows the version and platform checking expectation of PEP 484:
+ # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
+ # Containment checks are too complex for mypy v1.5.0 and cause failure.
+ if sys.platform == "win32" or sys.platform == "emscripten":
+ # win32 does not have a getuid() function.
+ # Emscripten has a return 0 stub.
+ return None
+ else:
+ # On other platforms, a return value of -1 is assumed to indicate that
+ # the current process's real user id could not be determined.
+ ERROR = -1
+ uid = os.getuid()
+ return uid if uid != ERROR else None
+
+
+# Perform exhaustiveness checking.
+#
+# Consider this example:
+#
+# MyUnion = Union[int, str]
+#
+# def handle(x: MyUnion) -> int {
+# if isinstance(x, int):
+# return 1
+# elif isinstance(x, str):
+# return 2
+# else:
+# raise Exception('unreachable')
+#
+# Now suppose we add a new variant:
+#
+# MyUnion = Union[int, str, bytes]
+#
+# After doing this, we must remember ourselves to go and update the handle
+# function to handle the new variant.
+#
+# With `assert_never` we can do better:
+#
+# // raise Exception('unreachable')
+# return assert_never(x)
+#
+# Now, if we forget to handle the new variant, the type-checker will emit a
+# compile-time error, instead of the runtime error we would have gotten
+# previously.
+#
+# This also work for Enums (if you use `is` to compare) and Literals.
+def assert_never(value: NoReturn) -> NoReturn:
+ assert False, f"Unhandled value: {value} ({type(value).__name__})"
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/__init__.py b/venv/lib/python3.12/site-packages/_pytest/config/__init__.py
new file mode 100644
index 0000000..e3990d1
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/config/__init__.py
@@ -0,0 +1,1816 @@
+"""Command line options, ini-file and conftest.py processing."""
+import argparse
+import collections.abc
+import copy
+import dataclasses
+import enum
+import glob
+import inspect
+import os
+import re
+import shlex
+import sys
+import types
+import warnings
+from functools import lru_cache
+from pathlib import Path
+from textwrap import dedent
+from types import FunctionType
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generator
+from typing import IO
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import TextIO
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from pluggy import HookimplMarker
+from pluggy import HookspecMarker
+from pluggy import PluginManager
+
+import _pytest._code
+import _pytest.deprecated
+import _pytest.hookspec
+from .exceptions import PrintHelp as PrintHelp
+from .exceptions import UsageError as UsageError
+from .findpaths import determine_setup
+from _pytest._code import ExceptionInfo
+from _pytest._code import filter_traceback
+from _pytest._io import TerminalWriter
+from _pytest.compat import final
+from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
+from _pytest.outcomes import fail
+from _pytest.outcomes import Skipped
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import import_path
+from _pytest.pathlib import ImportMode
+from _pytest.pathlib import resolve_package_path
+from _pytest.pathlib import safe_exists
+from _pytest.stash import Stash
+from _pytest.warning_types import PytestConfigWarning
+from _pytest.warning_types import warn_explicit_for
+
+if TYPE_CHECKING:
+ from _pytest._code.code import _TracebackStyle
+ from _pytest.terminal import TerminalReporter
+ from .argparsing import Argument
+
+
+_PluggyPlugin = object
+"""A type to represent plugin objects.
+
+Plugins can be any namespace, so we can't narrow it down much, but we use an
+alias to make the intent clear.
+
+Ideally this type would be provided by pluggy itself.
+"""
+
+
+hookimpl = HookimplMarker("pytest")
+hookspec = HookspecMarker("pytest")
+
+
+@final
+class ExitCode(enum.IntEnum):
+ """Encodes the valid exit codes by pytest.
+
+ Currently users and plugins may supply other exit codes as well.
+
+ .. versionadded:: 5.0
+ """
+
+ #: Tests passed.
+ OK = 0
+ #: Tests failed.
+ TESTS_FAILED = 1
+ #: pytest was interrupted.
+ INTERRUPTED = 2
+ #: An internal error got in the way.
+ INTERNAL_ERROR = 3
+ #: pytest was misused.
+ USAGE_ERROR = 4
+ #: pytest couldn't find tests.
+ NO_TESTS_COLLECTED = 5
+
+
+class ConftestImportFailure(Exception):
+ def __init__(
+ self,
+ path: Path,
+ excinfo: Tuple[Type[Exception], Exception, TracebackType],
+ ) -> None:
+ super().__init__(path, excinfo)
+ self.path = path
+ self.excinfo = excinfo
+
+ def __str__(self) -> str:
+ return "{}: {} (from {})".format(
+ self.excinfo[0].__name__, self.excinfo[1], self.path
+ )
+
+
+def filter_traceback_for_conftest_import_failure(
+ entry: _pytest._code.TracebackEntry,
+) -> bool:
+ """Filter tracebacks entries which point to pytest internals or importlib.
+
+ Make a special case for importlib because we use it to import test modules and conftest files
+ in _pytest.pathlib.import_path.
+ """
+ return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
+
+
+def main(
+ args: Optional[Union[List[str], "os.PathLike[str]"]] = None,
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
+) -> Union[int, ExitCode]:
+ """Perform an in-process test run.
+
+ :param args:
+ List of command line arguments. If `None` or not given, defaults to reading
+ arguments directly from the process command line (:data:`sys.argv`).
+ :param plugins: List of plugin objects to be auto-registered during initialization.
+
+ :returns: An exit code.
+ """
+ try:
+ try:
+ config = _prepareconfig(args, plugins)
+ except ConftestImportFailure as e:
+ exc_info = ExceptionInfo.from_exc_info(e.excinfo)
+ tw = TerminalWriter(sys.stderr)
+ tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
+ exc_info.traceback = exc_info.traceback.filter(
+ filter_traceback_for_conftest_import_failure
+ )
+ exc_repr = (
+ exc_info.getrepr(style="short", chain=False)
+ if exc_info.traceback
+ else exc_info.exconly()
+ )
+ formatted_tb = str(exc_repr)
+ for line in formatted_tb.splitlines():
+ tw.line(line.rstrip(), red=True)
+ return ExitCode.USAGE_ERROR
+ else:
+ try:
+ ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
+ config=config
+ )
+ try:
+ return ExitCode(ret)
+ except ValueError:
+ return ret
+ finally:
+ config._ensure_unconfigure()
+ except UsageError as e:
+ tw = TerminalWriter(sys.stderr)
+ for msg in e.args:
+ tw.line(f"ERROR: {msg}\n", red=True)
+ return ExitCode.USAGE_ERROR
+
+
+def console_main() -> int:
+ """The CLI entry point of pytest.
+
+ This function is not meant for programmable use; use `main()` instead.
+ """
+ # https://docs.python.org/3/library/signal.html#note-on-sigpipe
+ try:
+ code = main()
+ sys.stdout.flush()
+ return code
+ except BrokenPipeError:
+ # Python flushes standard streams on exit; redirect remaining output
+ # to devnull to avoid another BrokenPipeError at shutdown
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ os.dup2(devnull, sys.stdout.fileno())
+ return 1 # Python exits with error code 1 on EPIPE
+
+
+class cmdline: # compatibility namespace
+ main = staticmethod(main)
+
+
+def filename_arg(path: str, optname: str) -> str:
+ """Argparse type validator for filename arguments.
+
+ :path: Path of filename.
+ :optname: Name of the option.
+ """
+ if os.path.isdir(path):
+ raise UsageError(f"{optname} must be a filename, given: {path}")
+ return path
+
+
+def directory_arg(path: str, optname: str) -> str:
+ """Argparse type validator for directory arguments.
+
+ :path: Path of directory.
+ :optname: Name of the option.
+ """
+ if not os.path.isdir(path):
+ raise UsageError(f"{optname} must be a directory, given: {path}")
+ return path
+
+
+# Plugins that cannot be disabled via "-p no:X" currently.
+essential_plugins = (
+ "mark",
+ "main",
+ "runner",
+ "fixtures",
+ "helpconfig", # Provides -p.
+)
+
+default_plugins = essential_plugins + (
+ "python",
+ "terminal",
+ "debugging",
+ "unittest",
+ "capture",
+ "skipping",
+ "legacypath",
+ "tmpdir",
+ "monkeypatch",
+ "recwarn",
+ "pastebin",
+ "nose",
+ "assertion",
+ "junitxml",
+ "doctest",
+ "cacheprovider",
+ "freeze_support",
+ "setuponly",
+ "setupplan",
+ "stepwise",
+ "warnings",
+ "logging",
+ "reports",
+ "python_path",
+ *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
+ "faulthandler",
+)
+
+builtin_plugins = set(default_plugins)
+builtin_plugins.add("pytester")
+builtin_plugins.add("pytester_assertions")
+
+
+def get_config(
+ args: Optional[List[str]] = None,
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
+) -> "Config":
+ # subsequent calls to main will create a fresh instance
+ pluginmanager = PytestPluginManager()
+ config = Config(
+ pluginmanager,
+ invocation_params=Config.InvocationParams(
+ args=args or (),
+ plugins=plugins,
+ dir=Path.cwd(),
+ ),
+ )
+
+ if args is not None:
+ # Handle any "-p no:plugin" args.
+ pluginmanager.consider_preparse(args, exclude_only=True)
+
+ for spec in default_plugins:
+ pluginmanager.import_plugin(spec)
+
+ return config
+
+
+def get_plugin_manager() -> "PytestPluginManager":
+ """Obtain a new instance of the
+ :py:class:`pytest.PytestPluginManager`, with default plugins
+ already loaded.
+
+ This function can be used by integration with other tools, like hooking
+ into pytest to run tests into an IDE.
+ """
+ return get_config().pluginmanager
+
+
+def _prepareconfig(
+ args: Optional[Union[List[str], "os.PathLike[str]"]] = None,
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
+) -> "Config":
+ if args is None:
+ args = sys.argv[1:]
+ elif isinstance(args, os.PathLike):
+ args = [os.fspath(args)]
+ elif not isinstance(args, list):
+ msg = ( # type:ignore[unreachable]
+ "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
+ )
+ raise TypeError(msg.format(args, type(args)))
+
+ config = get_config(args, plugins)
+ pluginmanager = config.pluginmanager
+ try:
+ if plugins:
+ for plugin in plugins:
+ if isinstance(plugin, str):
+ pluginmanager.consider_pluginarg(plugin)
+ else:
+ pluginmanager.register(plugin)
+ config = pluginmanager.hook.pytest_cmdline_parse(
+ pluginmanager=pluginmanager, args=args
+ )
+ return config
+ except BaseException:
+ config._ensure_unconfigure()
+ raise
+
+
+def _get_directory(path: Path) -> Path:
+ """Get the directory of a path - itself if already a directory."""
+ if path.is_file():
+ return path.parent
+ else:
+ return path
+
+
+def _get_legacy_hook_marks(
+ method: Any,
+ hook_type: str,
+ opt_names: Tuple[str, ...],
+) -> Dict[str, bool]:
+ if TYPE_CHECKING:
+ # abuse typeguard from importlib to avoid massive method type union thats lacking a alias
+ assert inspect.isroutine(method)
+ known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
+ must_warn: list[str] = []
+ opts: dict[str, bool] = {}
+ for opt_name in opt_names:
+ opt_attr = getattr(method, opt_name, AttributeError)
+ if opt_attr is not AttributeError:
+ must_warn.append(f"{opt_name}={opt_attr}")
+ opts[opt_name] = True
+ elif opt_name in known_marks:
+ must_warn.append(f"{opt_name}=True")
+ opts[opt_name] = True
+ else:
+ opts[opt_name] = False
+ if must_warn:
+ hook_opts = ", ".join(must_warn)
+ message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
+ type=hook_type,
+ fullname=method.__qualname__,
+ hook_opts=hook_opts,
+ )
+ warn_explicit_for(cast(FunctionType, method), message)
+ return opts
+
+
+@final
+class PytestPluginManager(PluginManager):
+ """A :py:class:`pluggy.PluginManager ` with
+ additional pytest-specific functionality:
+
+ * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
+ ``pytest_plugins`` global variables found in plugins being loaded.
+ * ``conftest.py`` loading during start-up.
+ """
+
+ def __init__(self) -> None:
+ import _pytest.assertion
+
+ super().__init__("pytest")
+
+ # -- State related to local conftest plugins.
+ # All loaded conftest modules.
+ self._conftest_plugins: Set[types.ModuleType] = set()
+ # All conftest modules applicable for a directory.
+ # This includes the directory's own conftest modules as well
+ # as those of its parent directories.
+ self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
+ # Cutoff directory above which conftests are no longer discovered.
+ self._confcutdir: Optional[Path] = None
+ # If set, conftest loading is skipped.
+ self._noconftest = False
+
+ # _getconftestmodules()'s call to _get_directory() causes a stat
+ # storm when it's called potentially thousands of times in a test
+ # session (#9478), often with the same path, so cache it.
+ self._get_directory = lru_cache(256)(_get_directory)
+
+ self._duplicatepaths: Set[Path] = set()
+
+ # plugins that were explicitly skipped with pytest.skip
+ # list of (module name, skip reason)
+ # previously we would issue a warning when a plugin was skipped, but
+ # since we refactored warnings as first citizens of Config, they are
+ # just stored here to be used later.
+ self.skipped_plugins: List[Tuple[str, str]] = []
+
+ self.add_hookspecs(_pytest.hookspec)
+ self.register(self)
+ if os.environ.get("PYTEST_DEBUG"):
+ err: IO[str] = sys.stderr
+ encoding: str = getattr(err, "encoding", "utf8")
+ try:
+ err = open(
+ os.dup(err.fileno()),
+ mode=err.mode,
+ buffering=1,
+ encoding=encoding,
+ )
+ except Exception:
+ pass
+ self.trace.root.setwriter(err.write)
+ self.enable_tracing()
+
+ # Config._consider_importhook will set a real object if required.
+ self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
+ # Used to know when we are importing conftests after the pytest_configure stage.
+ self._configured = False
+
+ def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
+ # pytest hooks are always prefixed with "pytest_",
+ # so we avoid accessing possibly non-readable attributes
+ # (see issue #1073).
+ if not name.startswith("pytest_"):
+ return None
+ # Ignore names which can not be hooks.
+ if name == "pytest_plugins":
+ return None
+
+ opts = super().parse_hookimpl_opts(plugin, name)
+ if opts is not None:
+ return opts
+
+ method = getattr(plugin, name)
+ # Consider only actual functions for hooks (#3775).
+ if not inspect.isroutine(method):
+ return None
+ # Collect unmarked hooks as long as they have the `pytest_' prefix.
+ return _get_legacy_hook_marks( # type: ignore[return-value]
+ method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
+ )
+
+ def parse_hookspec_opts(self, module_or_class, name: str):
+ opts = super().parse_hookspec_opts(module_or_class, name)
+ if opts is None:
+ method = getattr(module_or_class, name)
+ if name.startswith("pytest_"):
+ opts = _get_legacy_hook_marks( # type: ignore[assignment]
+ method,
+ "spec",
+ ("firstresult", "historic"),
+ )
+ return opts
+
+ def register(
+ self, plugin: _PluggyPlugin, name: Optional[str] = None
+ ) -> Optional[str]:
+ if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
+ warnings.warn(
+ PytestConfigWarning(
+ "{} plugin has been merged into the core, "
+ "please remove it from your requirements.".format(
+ name.replace("_", "-")
+ )
+ )
+ )
+ return None
+ ret: Optional[str] = super().register(plugin, name)
+ if ret:
+ self.hook.pytest_plugin_registered.call_historic(
+ kwargs=dict(plugin=plugin, manager=self)
+ )
+
+ if isinstance(plugin, types.ModuleType):
+ self.consider_module(plugin)
+ return ret
+
+ def getplugin(self, name: str):
+ # Support deprecated naming because plugins (xdist e.g.) use it.
+ plugin: Optional[_PluggyPlugin] = self.get_plugin(name)
+ return plugin
+
+ def hasplugin(self, name: str) -> bool:
+ """Return whether a plugin with the given name is registered."""
+ return bool(self.get_plugin(name))
+
+ def pytest_configure(self, config: "Config") -> None:
+ """:meta private:"""
+ # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
+ # we should remove tryfirst/trylast as markers.
+ config.addinivalue_line(
+ "markers",
+ "tryfirst: mark a hook implementation function such that the "
+ "plugin machinery will try to call it first/as early as possible. "
+ "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "trylast: mark a hook implementation function such that the "
+ "plugin machinery will try to call it last/as late as possible. "
+ "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",
+ )
+ self._configured = True
+
+ #
+ # Internal API for local conftest plugin handling.
+ #
+ def _set_initial_conftests(
+ self,
+ args: Sequence[Union[str, Path]],
+ pyargs: bool,
+ noconftest: bool,
+ rootpath: Path,
+ confcutdir: Optional[Path],
+ importmode: Union[ImportMode, str],
+ ) -> None:
+ """Load initial conftest files given a preparsed "namespace".
+
+ As conftest files may add their own command line options which have
+ arguments ('--my-opt somepath') we might get some false positives.
+ All builtin and 3rd party plugins will have been loaded, however, so
+ common options will not confuse our logic here.
+ """
+ current = Path.cwd()
+ self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
+ self._noconftest = noconftest
+ self._using_pyargs = pyargs
+ foundanchor = False
+ for intitial_path in args:
+ path = str(intitial_path)
+ # remove node-id syntax
+ i = path.find("::")
+ if i != -1:
+ path = path[:i]
+ anchor = absolutepath(current / path)
+
+ # Ensure we do not break if what appears to be an anchor
+ # is in fact a very long option (#10169, #11394).
+ if safe_exists(anchor):
+ self._try_load_conftest(anchor, importmode, rootpath)
+ foundanchor = True
+ if not foundanchor:
+ self._try_load_conftest(current, importmode, rootpath)
+
+ def _is_in_confcutdir(self, path: Path) -> bool:
+ """Whether a path is within the confcutdir.
+
+ When false, should not load conftest.
+ """
+ if self._confcutdir is None:
+ return True
+ return path not in self._confcutdir.parents
+
+ def _try_load_conftest(
+ self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
+ ) -> None:
+ self._getconftestmodules(anchor, importmode, rootpath)
+ # let's also consider test* subdirs
+ if anchor.is_dir():
+ for x in anchor.glob("test*"):
+ if x.is_dir():
+ self._getconftestmodules(x, importmode, rootpath)
+
+ def _getconftestmodules(
+ self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
+ ) -> Sequence[types.ModuleType]:
+ if self._noconftest:
+ return []
+
+ directory = self._get_directory(path)
+
+ # Optimization: avoid repeated searches in the same directory.
+ # Assumes always called with same importmode and rootpath.
+ existing_clist = self._dirpath2confmods.get(directory)
+ if existing_clist is not None:
+ return existing_clist
+
+ # XXX these days we may rather want to use config.rootpath
+ # and allow users to opt into looking into the rootdir parent
+ # directories instead of requiring to specify confcutdir.
+ clist = []
+ for parent in reversed((directory, *directory.parents)):
+ if self._is_in_confcutdir(parent):
+ conftestpath = parent / "conftest.py"
+ if conftestpath.is_file():
+ mod = self._importconftest(conftestpath, importmode, rootpath)
+ clist.append(mod)
+ self._dirpath2confmods[directory] = clist
+ return clist
+
+ def _rget_with_confmod(
+ self,
+ name: str,
+ path: Path,
+ importmode: Union[str, ImportMode],
+ rootpath: Path,
+ ) -> Tuple[types.ModuleType, Any]:
+ modules = self._getconftestmodules(path, importmode, rootpath=rootpath)
+ for mod in reversed(modules):
+ try:
+ return mod, getattr(mod, name)
+ except AttributeError:
+ continue
+ raise KeyError(name)
+
+ def _importconftest(
+ self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
+ ) -> types.ModuleType:
+ existing = self.get_plugin(str(conftestpath))
+ if existing is not None:
+ return cast(types.ModuleType, existing)
+
+ pkgpath = resolve_package_path(conftestpath)
+ if pkgpath is None:
+ _ensure_removed_sysmodule(conftestpath.stem)
+
+ try:
+ mod = import_path(conftestpath, mode=importmode, root=rootpath)
+ except Exception as e:
+ assert e.__traceback__ is not None
+ exc_info = (type(e), e, e.__traceback__)
+ raise ConftestImportFailure(conftestpath, exc_info) from e
+
+ self._check_non_top_pytest_plugins(mod, conftestpath)
+
+ self._conftest_plugins.add(mod)
+ dirpath = conftestpath.parent
+ if dirpath in self._dirpath2confmods:
+ for path, mods in self._dirpath2confmods.items():
+ if dirpath in path.parents or path == dirpath:
+ assert mod not in mods
+ mods.append(mod)
+ self.trace(f"loading conftestmodule {mod!r}")
+ self.consider_conftest(mod)
+ return mod
+
+ def _check_non_top_pytest_plugins(
+ self,
+ mod: types.ModuleType,
+ conftestpath: Path,
+ ) -> None:
+ if (
+ hasattr(mod, "pytest_plugins")
+ and self._configured
+ and not self._using_pyargs
+ ):
+ msg = (
+ "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
+ "It affects the entire test suite instead of just below the conftest as expected.\n"
+ " {}\n"
+ "Please move it to a top level conftest file at the rootdir:\n"
+ " {}\n"
+ "For more information, visit:\n"
+ " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
+ )
+ fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
+
+ #
+ # API for bootstrapping plugin loading
+ #
+ #
+
+ def consider_preparse(
+ self, args: Sequence[str], *, exclude_only: bool = False
+ ) -> None:
+ """:meta private:"""
+ i = 0
+ n = len(args)
+ while i < n:
+ opt = args[i]
+ i += 1
+ if isinstance(opt, str):
+ if opt == "-p":
+ try:
+ parg = args[i]
+ except IndexError:
+ return
+ i += 1
+ elif opt.startswith("-p"):
+ parg = opt[2:]
+ else:
+ continue
+ parg = parg.strip()
+ if exclude_only and not parg.startswith("no:"):
+ continue
+ self.consider_pluginarg(parg)
+
+ def consider_pluginarg(self, arg: str) -> None:
+ """:meta private:"""
+ if arg.startswith("no:"):
+ name = arg[3:]
+ if name in essential_plugins:
+ raise UsageError("plugin %s cannot be disabled" % name)
+
+ # PR #4304: remove stepwise if cacheprovider is blocked.
+ if name == "cacheprovider":
+ self.set_blocked("stepwise")
+ self.set_blocked("pytest_stepwise")
+
+ self.set_blocked(name)
+ if not name.startswith("pytest_"):
+ self.set_blocked("pytest_" + name)
+ else:
+ name = arg
+ # Unblock the plugin. None indicates that it has been blocked.
+ # There is no interface with pluggy for this.
+ if self._name2plugin.get(name, -1) is None:
+ del self._name2plugin[name]
+ if not name.startswith("pytest_"):
+ if self._name2plugin.get("pytest_" + name, -1) is None:
+ del self._name2plugin["pytest_" + name]
+ self.import_plugin(arg, consider_entry_points=True)
+
+ def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
+ """:meta private:"""
+ self.register(conftestmodule, name=conftestmodule.__file__)
+
+ def consider_env(self) -> None:
+ """:meta private:"""
+ self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
+
+ def consider_module(self, mod: types.ModuleType) -> None:
+ """:meta private:"""
+ self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
+
+ def _import_plugin_specs(
+ self, spec: Union[None, types.ModuleType, str, Sequence[str]]
+ ) -> None:
+ plugins = _get_plugin_specs_as_list(spec)
+ for import_spec in plugins:
+ self.import_plugin(import_spec)
+
+ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
+ """Import a plugin with ``modname``.
+
+ If ``consider_entry_points`` is True, entry point names are also
+ considered to find a plugin.
+ """
+ # Most often modname refers to builtin modules, e.g. "pytester",
+ # "terminal" or "capture". Those plugins are registered under their
+ # basename for historic purposes but must be imported with the
+ # _pytest prefix.
+ assert isinstance(modname, str), (
+ "module name as text required, got %r" % modname
+ )
+ if self.is_blocked(modname) or self.get_plugin(modname) is not None:
+ return
+
+ importspec = "_pytest." + modname if modname in builtin_plugins else modname
+ self.rewrite_hook.mark_rewrite(importspec)
+
+ if consider_entry_points:
+ loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
+ if loaded:
+ return
+
+ try:
+ __import__(importspec)
+ except ImportError as e:
+ raise ImportError(
+ f'Error importing plugin "{modname}": {e.args[0]}'
+ ).with_traceback(e.__traceback__) from e
+
+ except Skipped as e:
+ self.skipped_plugins.append((modname, e.msg or ""))
+ else:
+ mod = sys.modules[importspec]
+ self.register(mod, modname)
+
+
+def _get_plugin_specs_as_list(
+ specs: Union[None, types.ModuleType, str, Sequence[str]]
+) -> List[str]:
+ """Parse a plugins specification into a list of plugin names."""
+ # None means empty.
+ if specs is None:
+ return []
+ # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
+ if isinstance(specs, types.ModuleType):
+ return []
+ # Comma-separated list.
+ if isinstance(specs, str):
+ return specs.split(",") if specs else []
+ # Direct specification.
+ if isinstance(specs, collections.abc.Sequence):
+ return list(specs)
+ raise UsageError(
+ "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r"
+ % specs
+ )
+
+
+def _ensure_removed_sysmodule(modname: str) -> None:
+ try:
+ del sys.modules[modname]
+ except KeyError:
+ pass
+
+
+class Notset:
+ def __repr__(self):
+ return ""
+
+
+notset = Notset()
+
+
+def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
+ """Given an iterable of file names in a source distribution, return the "names" that should
+ be marked for assertion rewrite.
+
+ For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
+ the assertion rewrite mechanism.
+
+ This function has to deal with dist-info based distributions and egg based distributions
+ (which are still very much in use for "editable" installs).
+
+ Here are the file names as seen in a dist-info based distribution:
+
+ pytest_mock/__init__.py
+ pytest_mock/_version.py
+ pytest_mock/plugin.py
+ pytest_mock.egg-info/PKG-INFO
+
+ Here are the file names as seen in an egg based distribution:
+
+ src/pytest_mock/__init__.py
+ src/pytest_mock/_version.py
+ src/pytest_mock/plugin.py
+ src/pytest_mock.egg-info/PKG-INFO
+ LICENSE
+ setup.py
+
+ We have to take in account those two distribution flavors in order to determine which
+ names should be considered for assertion rewriting.
+
+ More information:
+ https://github.com/pytest-dev/pytest-mock/issues/167
+ """
+ package_files = list(package_files)
+ seen_some = False
+ for fn in package_files:
+ is_simple_module = "/" not in fn and fn.endswith(".py")
+ is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
+ if is_simple_module:
+ module_name, _ = os.path.splitext(fn)
+ # we ignore "setup.py" at the root of the distribution
+ # as well as editable installation finder modules made by setuptools
+ if module_name != "setup" and not module_name.startswith("__editable__"):
+ seen_some = True
+ yield module_name
+ elif is_package:
+ package_name = os.path.dirname(fn)
+ seen_some = True
+ yield package_name
+
+ if not seen_some:
+ # At this point we did not find any packages or modules suitable for assertion
+ # rewriting, so we try again by stripping the first path component (to account for
+ # "src" based source trees for example).
+ # This approach lets us have the common case continue to be fast, as egg-distributions
+ # are rarer.
+ new_package_files = []
+ for fn in package_files:
+ parts = fn.split("/")
+ new_fn = "/".join(parts[1:])
+ if new_fn:
+ new_package_files.append(new_fn)
+ if new_package_files:
+ yield from _iter_rewritable_modules(new_package_files)
+
+
+@final
+class Config:
+ """Access to configuration values, pluginmanager and plugin hooks.
+
+ :param PytestPluginManager pluginmanager:
+ A pytest PluginManager.
+
+ :param InvocationParams invocation_params:
+ Object containing parameters regarding the :func:`pytest.main`
+ invocation.
+ """
+
+ @final
+ @dataclasses.dataclass(frozen=True)
+ class InvocationParams:
+ """Holds parameters passed during :func:`pytest.main`.
+
+ The object attributes are read-only.
+
+ .. versionadded:: 5.1
+
+ .. note::
+
+ Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
+ ini option are handled by pytest, not being included in the ``args`` attribute.
+
+ Plugins accessing ``InvocationParams`` must be aware of that.
+ """
+
+ args: Tuple[str, ...]
+ """The command-line arguments as passed to :func:`pytest.main`."""
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
+ """Extra plugins, might be `None`."""
+ dir: Path
+ """The directory from which :func:`pytest.main` was invoked."""
+
+ def __init__(
+ self,
+ *,
+ args: Iterable[str],
+ plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
+ dir: Path,
+ ) -> None:
+ object.__setattr__(self, "args", tuple(args))
+ object.__setattr__(self, "plugins", plugins)
+ object.__setattr__(self, "dir", dir)
+
+ class ArgsSource(enum.Enum):
+ """Indicates the source of the test arguments.
+
+ .. versionadded:: 7.2
+ """
+
+ #: Command line arguments.
+ ARGS = enum.auto()
+ #: Invocation directory.
+ INCOVATION_DIR = enum.auto()
+ #: 'testpaths' configuration value.
+ TESTPATHS = enum.auto()
+
+ def __init__(
+ self,
+ pluginmanager: PytestPluginManager,
+ *,
+ invocation_params: Optional[InvocationParams] = None,
+ ) -> None:
+ from .argparsing import Parser, FILE_OR_DIR
+
+ if invocation_params is None:
+ invocation_params = self.InvocationParams(
+ args=(), plugins=None, dir=Path.cwd()
+ )
+
+ self.option = argparse.Namespace()
+ """Access to command line option as attributes.
+
+ :type: argparse.Namespace
+ """
+
+ self.invocation_params = invocation_params
+ """The parameters with which pytest was invoked.
+
+ :type: InvocationParams
+ """
+
+ _a = FILE_OR_DIR
+ self._parser = Parser(
+ usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",
+ processopt=self._processopt,
+ _ispytest=True,
+ )
+ self.pluginmanager = pluginmanager
+ """The plugin manager handles plugin registration and hook invocation.
+
+ :type: PytestPluginManager
+ """
+
+ self.stash = Stash()
+ """A place where plugins can store information on the config for their
+ own use.
+
+ :type: Stash
+ """
+ # Deprecated alias. Was never public. Can be removed in a few releases.
+ self._store = self.stash
+
+ from .compat import PathAwareHookProxy
+
+ self.trace = self.pluginmanager.trace.root.get("config")
+ self.hook = PathAwareHookProxy(self.pluginmanager.hook)
+ self._inicache: Dict[str, Any] = {}
+ self._override_ini: Sequence[str] = ()
+ self._opt2dest: Dict[str, str] = {}
+ self._cleanup: List[Callable[[], None]] = []
+ self.pluginmanager.register(self, "pytestconfig")
+ self._configured = False
+ self.hook.pytest_addoption.call_historic(
+ kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
+ )
+ self.args_source = Config.ArgsSource.ARGS
+ self.args: List[str] = []
+
+ if TYPE_CHECKING:
+ from _pytest.cacheprovider import Cache
+
+ self.cache: Optional[Cache] = None
+
+ @property
+ def rootpath(self) -> Path:
+ """The path to the :ref:`rootdir `.
+
+ :type: pathlib.Path
+
+ .. versionadded:: 6.1
+ """
+ return self._rootpath
+
+ @property
+ def inipath(self) -> Optional[Path]:
+ """The path to the :ref:`configfile `.
+
+ :type: Optional[pathlib.Path]
+
+ .. versionadded:: 6.1
+ """
+ return self._inipath
+
+ def add_cleanup(self, func: Callable[[], None]) -> None:
+ """Add a function to be called when the config object gets out of
+ use (usually coinciding with pytest_unconfigure)."""
+ self._cleanup.append(func)
+
+ def _do_configure(self) -> None:
+ assert not self._configured
+ self._configured = True
+ with warnings.catch_warnings():
+ warnings.simplefilter("default")
+ self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
+
+ def _ensure_unconfigure(self) -> None:
+ if self._configured:
+ self._configured = False
+ self.hook.pytest_unconfigure(config=self)
+ self.hook.pytest_configure._call_history = []
+ while self._cleanup:
+ fin = self._cleanup.pop()
+ fin()
+
+ def get_terminal_writer(self) -> TerminalWriter:
+ terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
+ "terminalreporter"
+ )
+ assert terminalreporter is not None
+ return terminalreporter._tw
+
+ def pytest_cmdline_parse(
+ self, pluginmanager: PytestPluginManager, args: List[str]
+ ) -> "Config":
+ try:
+ self.parse(args)
+ except UsageError:
+ # Handle --version and --help here in a minimal fashion.
+ # This gets done via helpconfig normally, but its
+ # pytest_cmdline_main is not called in case of errors.
+ if getattr(self.option, "version", False) or "--version" in args:
+ from _pytest.helpconfig import showversion
+
+ showversion(self)
+ elif (
+ getattr(self.option, "help", False) or "--help" in args or "-h" in args
+ ):
+ self._parser._getparser().print_help()
+ sys.stdout.write(
+ "\nNOTE: displaying only minimal help due to UsageError.\n\n"
+ )
+
+ raise
+
+ return self
+
+ def notify_exception(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ option: Optional[argparse.Namespace] = None,
+ ) -> None:
+ if option and getattr(option, "fulltrace", False):
+ style: _TracebackStyle = "long"
+ else:
+ style = "native"
+ excrepr = excinfo.getrepr(
+ funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
+ )
+ res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
+ if not any(res):
+ for line in str(excrepr).split("\n"):
+ sys.stderr.write("INTERNALERROR> %s\n" % line)
+ sys.stderr.flush()
+
+ def cwd_relative_nodeid(self, nodeid: str) -> str:
+ # nodeid's are relative to the rootpath, compute relative to cwd.
+ if self.invocation_params.dir != self.rootpath:
+ fullpath = self.rootpath / nodeid
+ nodeid = bestrelpath(self.invocation_params.dir, fullpath)
+ return nodeid
+
+ @classmethod
+ def fromdictargs(cls, option_dict, args) -> "Config":
+ """Constructor usable for subprocesses."""
+ config = get_config(args)
+ config.option.__dict__.update(option_dict)
+ config.parse(args, addopts=False)
+ for x in config.option.plugins:
+ config.pluginmanager.consider_pluginarg(x)
+ return config
+
+ def _processopt(self, opt: "Argument") -> None:
+ for name in opt._short_opts + opt._long_opts:
+ self._opt2dest[name] = opt.dest
+
+ if hasattr(opt, "default"):
+ if not hasattr(self.option, opt.dest):
+ setattr(self.option, opt.dest, opt.default)
+
+ @hookimpl(trylast=True)
+ def pytest_load_initial_conftests(self, early_config: "Config") -> None:
+ # We haven't fully parsed the command line arguments yet, so
+ # early_config.args it not set yet. But we need it for
+ # discovering the initial conftests. So "pre-run" the logic here.
+ # It will be done for real in `parse()`.
+ args, args_source = early_config._decide_args(
+ args=early_config.known_args_namespace.file_or_dir,
+ pyargs=early_config.known_args_namespace.pyargs,
+ testpaths=early_config.getini("testpaths"),
+ invocation_dir=early_config.invocation_params.dir,
+ rootpath=early_config.rootpath,
+ warn=False,
+ )
+ self.pluginmanager._set_initial_conftests(
+ args=args,
+ pyargs=early_config.known_args_namespace.pyargs,
+ noconftest=early_config.known_args_namespace.noconftest,
+ rootpath=early_config.rootpath,
+ confcutdir=early_config.known_args_namespace.confcutdir,
+ importmode=early_config.known_args_namespace.importmode,
+ )
+
+ def _initini(self, args: Sequence[str]) -> None:
+ ns, unknown_args = self._parser.parse_known_and_unknown_args(
+ args, namespace=copy.copy(self.option)
+ )
+ rootpath, inipath, inicfg = determine_setup(
+ ns.inifilename,
+ ns.file_or_dir + unknown_args,
+ rootdir_cmd_arg=ns.rootdir or None,
+ config=self,
+ )
+ self._rootpath = rootpath
+ self._inipath = inipath
+ self.inicfg = inicfg
+ self._parser.extra_info["rootdir"] = str(self.rootpath)
+ self._parser.extra_info["inifile"] = str(self.inipath)
+ self._parser.addini("addopts", "Extra command line options", "args")
+ self._parser.addini("minversion", "Minimally required pytest version")
+ self._parser.addini(
+ "required_plugins",
+ "Plugins that must be present for pytest to run",
+ type="args",
+ default=[],
+ )
+ self._override_ini = ns.override_ini or ()
+
+ def _consider_importhook(self, args: Sequence[str]) -> None:
+ """Install the PEP 302 import hook if using assertion rewriting.
+
+ Needs to parse the --assert= option from the commandline
+ and find all the installed plugins to mark them for rewriting
+ by the importhook.
+ """
+ ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
+ mode = getattr(ns, "assertmode", "plain")
+ if mode == "rewrite":
+ import _pytest.assertion
+
+ try:
+ hook = _pytest.assertion.install_importhook(self)
+ except SystemError:
+ mode = "plain"
+ else:
+ self._mark_plugins_for_rewrite(hook)
+ self._warn_about_missing_assertion(mode)
+
+ def _mark_plugins_for_rewrite(self, hook) -> None:
+ """Given an importhook, mark for rewrite any top-level
+ modules or packages in the distribution package for
+ all pytest plugins."""
+ self.pluginmanager.rewrite_hook = hook
+
+ if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
+ # We don't autoload from setuptools entry points, no need to continue.
+ return
+
+ package_files = (
+ str(file)
+ for dist in importlib_metadata.distributions()
+ if any(ep.group == "pytest11" for ep in dist.entry_points)
+ for file in dist.files or []
+ )
+
+ for name in _iter_rewritable_modules(package_files):
+ hook.mark_rewrite(name)
+
+ def _validate_args(self, args: List[str], via: str) -> List[str]:
+ """Validate known args."""
+ self._parser._config_source_hint = via # type: ignore
+ try:
+ self._parser.parse_known_and_unknown_args(
+ args, namespace=copy.copy(self.option)
+ )
+ finally:
+ del self._parser._config_source_hint # type: ignore
+
+ return args
+
+ def _decide_args(
+ self,
+ *,
+ args: List[str],
+ pyargs: List[str],
+ testpaths: List[str],
+ invocation_dir: Path,
+ rootpath: Path,
+ warn: bool,
+ ) -> Tuple[List[str], ArgsSource]:
+ """Decide the args (initial paths/nodeids) to use given the relevant inputs.
+
+ :param warn: Whether can issue warnings.
+ """
+ if args:
+ source = Config.ArgsSource.ARGS
+ result = args
+ else:
+ if invocation_dir == rootpath:
+ source = Config.ArgsSource.TESTPATHS
+ if pyargs:
+ result = testpaths
+ else:
+ result = []
+ for path in testpaths:
+ result.extend(sorted(glob.iglob(path, recursive=True)))
+ if testpaths and not result:
+ if warn:
+ warning_text = (
+ "No files were found in testpaths; "
+ "consider removing or adjusting your testpaths configuration. "
+ "Searching recursively from the current directory instead."
+ )
+ self.issue_config_time_warning(
+ PytestConfigWarning(warning_text), stacklevel=3
+ )
+ else:
+ result = []
+ if not result:
+ source = Config.ArgsSource.INCOVATION_DIR
+ result = [str(invocation_dir)]
+ return result, source
+
+ def _preparse(self, args: List[str], addopts: bool = True) -> None:
+ if addopts:
+ env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
+ if len(env_addopts):
+ args[:] = (
+ self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ + args
+ )
+ self._initini(args)
+ if addopts:
+ args[:] = (
+ self._validate_args(self.getini("addopts"), "via addopts config") + args
+ )
+
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.option)
+ )
+ self._checkversion()
+ self._consider_importhook(args)
+ self.pluginmanager.consider_preparse(args, exclude_only=False)
+ if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
+ # Don't autoload from setuptools entry point. Only explicitly specified
+ # plugins are going to be loaded.
+ self.pluginmanager.load_setuptools_entrypoints("pytest11")
+ self.pluginmanager.consider_env()
+
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.known_args_namespace)
+ )
+
+ self._validate_plugins()
+ self._warn_about_skipped_plugins()
+
+ if self.known_args_namespace.strict:
+ self.issue_config_time_warning(
+ _pytest.deprecated.STRICT_OPTION, stacklevel=2
+ )
+
+ if self.known_args_namespace.confcutdir is None:
+ if self.inipath is not None:
+ confcutdir = str(self.inipath.parent)
+ else:
+ confcutdir = str(self.rootpath)
+ self.known_args_namespace.confcutdir = confcutdir
+ try:
+ self.hook.pytest_load_initial_conftests(
+ early_config=self, args=args, parser=self._parser
+ )
+ except ConftestImportFailure as e:
+ if self.known_args_namespace.help or self.known_args_namespace.version:
+ # we don't want to prevent --help/--version to work
+ # so just let is pass and print a warning at the end
+ self.issue_config_time_warning(
+ PytestConfigWarning(f"could not load initial conftests: {e.path}"),
+ stacklevel=2,
+ )
+ else:
+ raise
+
+ @hookimpl(hookwrapper=True)
+ def pytest_collection(self) -> Generator[None, None, None]:
+ # Validate invalid ini keys after collection is done so we take in account
+ # options added by late-loading conftest files.
+ yield
+ self._validate_config_options()
+
+ def _checkversion(self) -> None:
+ import pytest
+
+ minver = self.inicfg.get("minversion", None)
+ if minver:
+ # Imported lazily to improve start-up time.
+ from packaging.version import Version
+
+ if not isinstance(minver, str):
+ raise pytest.UsageError(
+ "%s: 'minversion' must be a single value" % self.inipath
+ )
+
+ if Version(minver) > Version(pytest.__version__):
+ raise pytest.UsageError(
+ "%s: 'minversion' requires pytest-%s, actual pytest-%s'"
+ % (
+ self.inipath,
+ minver,
+ pytest.__version__,
+ )
+ )
+
+ def _validate_config_options(self) -> None:
+ for key in sorted(self._get_unknown_ini_keys()):
+ self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
+
+ def _validate_plugins(self) -> None:
+ required_plugins = sorted(self.getini("required_plugins"))
+ if not required_plugins:
+ return
+
+ # Imported lazily to improve start-up time.
+ from packaging.version import Version
+ from packaging.requirements import InvalidRequirement, Requirement
+
+ plugin_info = self.pluginmanager.list_plugin_distinfo()
+ plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
+
+ missing_plugins = []
+ for required_plugin in required_plugins:
+ try:
+ req = Requirement(required_plugin)
+ except InvalidRequirement:
+ missing_plugins.append(required_plugin)
+ continue
+
+ if req.name not in plugin_dist_info:
+ missing_plugins.append(required_plugin)
+ elif not req.specifier.contains(
+ Version(plugin_dist_info[req.name]), prereleases=True
+ ):
+ missing_plugins.append(required_plugin)
+
+ if missing_plugins:
+ raise UsageError(
+ "Missing required plugins: {}".format(", ".join(missing_plugins)),
+ )
+
+ def _warn_or_fail_if_strict(self, message: str) -> None:
+ if self.known_args_namespace.strict_config:
+ raise UsageError(message)
+
+ self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
+
+ def _get_unknown_ini_keys(self) -> List[str]:
+ parser_inicfg = self._parser._inidict
+ return [name for name in self.inicfg if name not in parser_inicfg]
+
+ def parse(self, args: List[str], addopts: bool = True) -> None:
+ # Parse given cmdline arguments into this config object.
+ assert (
+ self.args == []
+ ), "can only parse cmdline args at most once per Config object"
+ self.hook.pytest_addhooks.call_historic(
+ kwargs=dict(pluginmanager=self.pluginmanager)
+ )
+ self._preparse(args, addopts=addopts)
+ # XXX deprecated hook:
+ self.hook.pytest_cmdline_preparse(config=self, args=args)
+ self._parser.after_preparse = True # type: ignore
+ try:
+ args = self._parser.parse_setoption(
+ args, self.option, namespace=self.option
+ )
+ self.args, self.args_source = self._decide_args(
+ args=args,
+ pyargs=self.known_args_namespace.pyargs,
+ testpaths=self.getini("testpaths"),
+ invocation_dir=self.invocation_params.dir,
+ rootpath=self.rootpath,
+ warn=True,
+ )
+ except PrintHelp:
+ pass
+
+ def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
+ """Issue and handle a warning during the "configure" stage.
+
+ During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
+ function because it is not possible to have hookwrappers around ``pytest_configure``.
+
+ This function is mainly intended for plugins that need to issue warnings during
+ ``pytest_configure`` (or similar stages).
+
+ :param warning: The warning instance.
+ :param stacklevel: stacklevel forwarded to warnings.warn.
+ """
+ if self.pluginmanager.is_blocked("warnings"):
+ return
+
+ cmdline_filters = self.known_args_namespace.pythonwarnings or []
+ config_filters = self.getini("filterwarnings")
+
+ with warnings.catch_warnings(record=True) as records:
+ warnings.simplefilter("always", type(warning))
+ apply_warning_filters(config_filters, cmdline_filters)
+ warnings.warn(warning, stacklevel=stacklevel)
+
+ if records:
+ frame = sys._getframe(stacklevel - 1)
+ location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
+ self.hook.pytest_warning_recorded.call_historic(
+ kwargs=dict(
+ warning_message=records[0],
+ when="config",
+ nodeid="",
+ location=location,
+ )
+ )
+
+ def addinivalue_line(self, name: str, line: str) -> None:
+ """Add a line to an ini-file option. The option must have been
+ declared but might not yet be set in which case the line becomes
+ the first line in its value."""
+ x = self.getini(name)
+ assert isinstance(x, list)
+ x.append(line) # modifies the cached list inline
+
+ def getini(self, name: str):
+ """Return configuration value from an :ref:`ini file `.
+
+ If the specified name hasn't been registered through a prior
+ :func:`parser.addini ` call (usually from a
+ plugin), a ValueError is raised.
+ """
+ try:
+ return self._inicache[name]
+ except KeyError:
+ self._inicache[name] = val = self._getini(name)
+ return val
+
+ # Meant for easy monkeypatching by legacypath plugin.
+ # Can be inlined back (with no cover removed) once legacypath is gone.
+ def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]):
+ msg = f"unknown configuration type: {type}"
+ raise ValueError(msg, value) # pragma: no cover
+
+ def _getini(self, name: str):
+ try:
+ description, type, default = self._parser._inidict[name]
+ except KeyError as e:
+ raise ValueError(f"unknown configuration value: {name!r}") from e
+ override_value = self._get_override_ini_value(name)
+ if override_value is None:
+ try:
+ value = self.inicfg[name]
+ except KeyError:
+ if default is not None:
+ return default
+ if type is None:
+ return ""
+ return []
+ else:
+ value = override_value
+ # Coerce the values based on types.
+ #
+ # Note: some coercions are only required if we are reading from .ini files, because
+ # the file format doesn't contain type information, but when reading from toml we will
+ # get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
+ # For example:
+ #
+ # ini:
+ # a_line_list = "tests acceptance"
+ # in this case, we need to split the string to obtain a list of strings.
+ #
+ # toml:
+ # a_line_list = ["tests", "acceptance"]
+ # in this case, we already have a list ready to use.
+ #
+ if type == "paths":
+ # TODO: This assert is probably not valid in all cases.
+ assert self.inipath is not None
+ dp = self.inipath.parent
+ input_values = shlex.split(value) if isinstance(value, str) else value
+ return [dp / x for x in input_values]
+ elif type == "args":
+ return shlex.split(value) if isinstance(value, str) else value
+ elif type == "linelist":
+ if isinstance(value, str):
+ return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
+ else:
+ return value
+ elif type == "bool":
+ return _strtobool(str(value).strip())
+ elif type == "string":
+ return value
+ elif type is None:
+ return value
+ else:
+ return self._getini_unknown_type(name, type, value)
+
+ def _getconftest_pathlist(
+ self, name: str, path: Path, rootpath: Path
+ ) -> Optional[List[Path]]:
+ try:
+ mod, relroots = self.pluginmanager._rget_with_confmod(
+ name, path, self.getoption("importmode"), rootpath
+ )
+ except KeyError:
+ return None
+ assert mod.__file__ is not None
+ modpath = Path(mod.__file__).parent
+ values: List[Path] = []
+ for relroot in relroots:
+ if isinstance(relroot, os.PathLike):
+ relroot = Path(relroot)
+ else:
+ relroot = relroot.replace("/", os.sep)
+ relroot = absolutepath(modpath / relroot)
+ values.append(relroot)
+ return values
+
+ def _get_override_ini_value(self, name: str) -> Optional[str]:
+ value = None
+ # override_ini is a list of "ini=value" options.
+ # Always use the last item if multiple values are set for same ini-name,
+ # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
+ for ini_config in self._override_ini:
+ try:
+ key, user_ini_value = ini_config.split("=", 1)
+ except ValueError as e:
+ raise UsageError(
+ "-o/--override-ini expects option=value style (got: {!r}).".format(
+ ini_config
+ )
+ ) from e
+ else:
+ if key == name:
+ value = user_ini_value
+ return value
+
+ def getoption(self, name: str, default=notset, skip: bool = False):
+ """Return command line option value.
+
+ :param name: Name of the option. You may also specify
+ the literal ``--OPT`` option instead of the "dest" option name.
+ :param default: Default value if no option of that name exists.
+ :param skip: If True, raise pytest.skip if option does not exists
+ or has a None value.
+ """
+ name = self._opt2dest.get(name, name)
+ try:
+ val = getattr(self.option, name)
+ if val is None and skip:
+ raise AttributeError(name)
+ return val
+ except AttributeError as e:
+ if default is not notset:
+ return default
+ if skip:
+ import pytest
+
+ pytest.skip(f"no {name!r} option found")
+ raise ValueError(f"no option named {name!r}") from e
+
+ def getvalue(self, name: str, path=None):
+ """Deprecated, use getoption() instead."""
+ return self.getoption(name)
+
+ def getvalueorskip(self, name: str, path=None):
+ """Deprecated, use getoption(skip=True) instead."""
+ return self.getoption(name, skip=True)
+
+ def _warn_about_missing_assertion(self, mode: str) -> None:
+ if not _assertion_supported():
+ if mode == "plain":
+ warning_text = (
+ "ASSERTIONS ARE NOT EXECUTED"
+ " and FAILING TESTS WILL PASS. Are you"
+ " using python -O?"
+ )
+ else:
+ warning_text = (
+ "assertions not in test modules or"
+ " plugins will be ignored"
+ " because assert statements are not executed "
+ "by the underlying Python interpreter "
+ "(are you using python -O?)\n"
+ )
+ self.issue_config_time_warning(
+ PytestConfigWarning(warning_text),
+ stacklevel=3,
+ )
+
+ def _warn_about_skipped_plugins(self) -> None:
+ for module_name, msg in self.pluginmanager.skipped_plugins:
+ self.issue_config_time_warning(
+ PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
+ stacklevel=2,
+ )
+
+
+def _assertion_supported() -> bool:
+ try:
+ assert False
+ except AssertionError:
+ return True
+ else:
+ return False # type: ignore[unreachable]
+
+
+def create_terminal_writer(
+ config: Config, file: Optional[TextIO] = None
+) -> TerminalWriter:
+ """Create a TerminalWriter instance configured according to the options
+ in the config object.
+
+ Every code which requires a TerminalWriter object and has access to a
+ config object should use this function.
+ """
+ tw = TerminalWriter(file=file)
+
+ if config.option.color == "yes":
+ tw.hasmarkup = True
+ elif config.option.color == "no":
+ tw.hasmarkup = False
+
+ if config.option.code_highlight == "yes":
+ tw.code_highlight = True
+ elif config.option.code_highlight == "no":
+ tw.code_highlight = False
+
+ return tw
+
+
+def _strtobool(val: str) -> bool:
+ """Convert a string representation of truth to True or False.
+
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
+ 'val' is anything else.
+
+ .. note:: Copied from distutils.util.
+ """
+ val = val.lower()
+ if val in ("y", "yes", "t", "true", "on", "1"):
+ return True
+ elif val in ("n", "no", "f", "false", "off", "0"):
+ return False
+ else:
+ raise ValueError(f"invalid truth value {val!r}")
+
+
+@lru_cache(maxsize=50)
+def parse_warning_filter(
+ arg: str, *, escape: bool
+) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]:
+ """Parse a warnings filter string.
+
+ This is copied from warnings._setoption with the following changes:
+
+ * Does not apply the filter.
+ * Escaping is optional.
+ * Raises UsageError so we get nice error messages on failure.
+ """
+ __tracebackhide__ = True
+ error_template = dedent(
+ f"""\
+ while parsing the following warning configuration:
+
+ {arg}
+
+ This error occurred:
+
+ {{error}}
+ """
+ )
+
+ parts = arg.split(":")
+ if len(parts) > 5:
+ doc_url = (
+ "https://docs.python.org/3/library/warnings.html#describing-warning-filters"
+ )
+ error = dedent(
+ f"""\
+ Too many fields ({len(parts)}), expected at most 5 separated by colons:
+
+ action:message:category:module:line
+
+ For more information please consult: {doc_url}
+ """
+ )
+ raise UsageError(error_template.format(error=error))
+
+ while len(parts) < 5:
+ parts.append("")
+ action_, message, category_, module, lineno_ = (s.strip() for s in parts)
+ try:
+ action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
+ except warnings._OptionError as e:
+ raise UsageError(error_template.format(error=str(e)))
+ try:
+ category: Type[Warning] = _resolve_warning_category(category_)
+ except Exception:
+ exc_info = ExceptionInfo.from_current()
+ exception_text = exc_info.getrepr(style="native")
+ raise UsageError(error_template.format(error=exception_text))
+ if message and escape:
+ message = re.escape(message)
+ if module and escape:
+ module = re.escape(module) + r"\Z"
+ if lineno_:
+ try:
+ lineno = int(lineno_)
+ if lineno < 0:
+ raise ValueError("number is negative")
+ except ValueError as e:
+ raise UsageError(
+ error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
+ )
+ else:
+ lineno = 0
+ return action, message, category, module, lineno
+
+
+def _resolve_warning_category(category: str) -> Type[Warning]:
+ """
+ Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
+ propagate so we can get access to their tracebacks (#9218).
+ """
+ __tracebackhide__ = True
+ if not category:
+ return Warning
+
+ if "." not in category:
+ import builtins as m
+
+ klass = category
+ else:
+ module, _, klass = category.rpartition(".")
+ m = __import__(module, None, None, [klass])
+ cat = getattr(m, klass)
+ if not issubclass(cat, Warning):
+ raise UsageError(f"{cat} is not a Warning subclass")
+ return cast(Type[Warning], cat)
+
+
+def apply_warning_filters(
+ config_filters: Iterable[str], cmdline_filters: Iterable[str]
+) -> None:
+ """Applies pytest-configured filters to the warnings module"""
+ # Filters should have this precedence: cmdline options, config.
+ # Filters should be applied in the inverse order of precedence.
+ for arg in config_filters:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
+
+ for arg in cmdline_filters:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..0db6a6c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/argparsing.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/argparsing.cpython-312.pyc
new file mode 100644
index 0000000..65f6124
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/argparsing.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/compat.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/compat.cpython-312.pyc
new file mode 100644
index 0000000..23147c7
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/compat.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/exceptions.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/exceptions.cpython-312.pyc
new file mode 100644
index 0000000..582ebf6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/exceptions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/findpaths.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/findpaths.cpython-312.pyc
new file mode 100644
index 0000000..0a12899
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/config/__pycache__/findpaths.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/argparsing.py b/venv/lib/python3.12/site-packages/_pytest/config/argparsing.py
new file mode 100644
index 0000000..d3f0191
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/config/argparsing.py
@@ -0,0 +1,551 @@
+import argparse
+import os
+import sys
+import warnings
+from gettext import gettext
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import List
+from typing import Mapping
+from typing import NoReturn
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+import _pytest._io
+from _pytest.compat import final
+from _pytest.config.exceptions import UsageError
+from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
+from _pytest.deprecated import ARGUMENT_TYPE_STR
+from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
+from _pytest.deprecated import check_ispytest
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+FILE_OR_DIR = "file_or_dir"
+
+
+@final
+class Parser:
+ """Parser for command line arguments and ini-file values.
+
+ :ivar extra_info: Dict of generic param -> value to display in case
+ there's an error processing the command line arguments.
+ """
+
+ prog: Optional[str] = None
+
+ def __init__(
+ self,
+ usage: Optional[str] = None,
+ processopt: Optional[Callable[["Argument"], None]] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True)
+ self._groups: List[OptionGroup] = []
+ self._processopt = processopt
+ self._usage = usage
+ self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {}
+ self._ininames: List[str] = []
+ self.extra_info: Dict[str, Any] = {}
+
+ def processoption(self, option: "Argument") -> None:
+ if self._processopt:
+ if option.dest:
+ self._processopt(option)
+
+ def getgroup(
+ self, name: str, description: str = "", after: Optional[str] = None
+ ) -> "OptionGroup":
+ """Get (or create) a named option Group.
+
+ :param name: Name of the option group.
+ :param description: Long description for --help output.
+ :param after: Name of another group, used for ordering --help output.
+ :returns: The option group.
+
+ The returned group object has an ``addoption`` method with the same
+ signature as :func:`parser.addoption ` but
+ will be shown in the respective group in the output of
+ ``pytest --help``.
+ """
+ for group in self._groups:
+ if group.name == name:
+ return group
+ group = OptionGroup(name, description, parser=self, _ispytest=True)
+ i = 0
+ for i, grp in enumerate(self._groups):
+ if grp.name == after:
+ break
+ self._groups.insert(i + 1, group)
+ return group
+
+ def addoption(self, *opts: str, **attrs: Any) -> None:
+ """Register a command line option.
+
+ :param opts:
+ Option names, can be short or long options.
+ :param attrs:
+ Same attributes as the argparse library's :py:func:`add_argument()
+ ` function accepts.
+
+ After command line parsing, options are available on the pytest config
+ object via ``config.option.NAME`` where ``NAME`` is usually set
+ by passing a ``dest`` attribute, for example
+ ``addoption("--long", dest="NAME", ...)``.
+ """
+ self._anonymous.addoption(*opts, **attrs)
+
+ def parse(
+ self,
+ args: Sequence[Union[str, "os.PathLike[str]"]],
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> argparse.Namespace:
+ from _pytest._argcomplete import try_argcomplete
+
+ self.optparser = self._getparser()
+ try_argcomplete(self.optparser)
+ strargs = [os.fspath(x) for x in args]
+ return self.optparser.parse_args(strargs, namespace=namespace)
+
+ def _getparser(self) -> "MyOptionParser":
+ from _pytest._argcomplete import filescompleter
+
+ optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
+ groups = self._groups + [self._anonymous]
+ for group in groups:
+ if group.options:
+ desc = group.description or group.name
+ arggroup = optparser.add_argument_group(desc)
+ for option in group.options:
+ n = option.names()
+ a = option.attrs()
+ arggroup.add_argument(*n, **a)
+ file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*")
+ # bash like autocompletion for dirs (appending '/')
+ # Type ignored because typeshed doesn't know about argcomplete.
+ file_or_dir_arg.completer = filescompleter # type: ignore
+ return optparser
+
+ def parse_setoption(
+ self,
+ args: Sequence[Union[str, "os.PathLike[str]"]],
+ option: argparse.Namespace,
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> List[str]:
+ parsedoption = self.parse(args, namespace=namespace)
+ for name, value in parsedoption.__dict__.items():
+ setattr(option, name, value)
+ return cast(List[str], getattr(parsedoption, FILE_OR_DIR))
+
+ def parse_known_args(
+ self,
+ args: Sequence[Union[str, "os.PathLike[str]"]],
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> argparse.Namespace:
+ """Parse the known arguments at this point.
+
+ :returns: An argparse namespace object.
+ """
+ return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
+
+ def parse_known_and_unknown_args(
+ self,
+ args: Sequence[Union[str, "os.PathLike[str]"]],
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> Tuple[argparse.Namespace, List[str]]:
+ """Parse the known arguments at this point, and also return the
+ remaining unknown arguments.
+
+ :returns:
+ A tuple containing an argparse namespace object for the known
+ arguments, and a list of the unknown arguments.
+ """
+ optparser = self._getparser()
+ strargs = [os.fspath(x) for x in args]
+ return optparser.parse_known_args(strargs, namespace=namespace)
+
+ def addini(
+ self,
+ name: str,
+ help: str,
+ type: Optional[
+ "Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
+ ] = None,
+ default: Any = None,
+ ) -> None:
+ """Register an ini-file option.
+
+ :param name:
+ Name of the ini-variable.
+ :param type:
+ Type of the variable. Can be:
+
+ * ``string``: a string
+ * ``bool``: a boolean
+ * ``args``: a list of strings, separated as in a shell
+ * ``linelist``: a list of strings, separated by line breaks
+ * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
+ * ``pathlist``: a list of ``py.path``, separated as in a shell
+
+ .. versionadded:: 7.0
+ The ``paths`` variable type.
+
+ Defaults to ``string`` if ``None`` or not passed.
+ :param default:
+ Default value if no ini-file option exists but is queried.
+
+ The value of ini-variables can be retrieved via a call to
+ :py:func:`config.getini(name) `.
+ """
+ assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
+ self._inidict[name] = (help, type, default)
+ self._ininames.append(name)
+
+
+class ArgumentError(Exception):
+ """Raised if an Argument instance is created with invalid or
+ inconsistent arguments."""
+
+ def __init__(self, msg: str, option: Union["Argument", str]) -> None:
+ self.msg = msg
+ self.option_id = str(option)
+
+ def __str__(self) -> str:
+ if self.option_id:
+ return f"option {self.option_id}: {self.msg}"
+ else:
+ return self.msg
+
+
+class Argument:
+ """Class that mimics the necessary behaviour of optparse.Option.
+
+ It's currently a least effort implementation and ignoring choices
+ and integer prefixes.
+
+ https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
+ """
+
+ _typ_map = {"int": int, "string": str, "float": float, "complex": complex}
+
+ def __init__(self, *names: str, **attrs: Any) -> None:
+ """Store params in private vars for use in add_argument."""
+ self._attrs = attrs
+ self._short_opts: List[str] = []
+ self._long_opts: List[str] = []
+ if "%default" in (attrs.get("help") or ""):
+ warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3)
+ try:
+ typ = attrs["type"]
+ except KeyError:
+ pass
+ else:
+ # This might raise a keyerror as well, don't want to catch that.
+ if isinstance(typ, str):
+ if typ == "choice":
+ warnings.warn(
+ ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names),
+ stacklevel=4,
+ )
+ # argparse expects a type here take it from
+ # the type of the first element
+ attrs["type"] = type(attrs["choices"][0])
+ else:
+ warnings.warn(
+ ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4
+ )
+ attrs["type"] = Argument._typ_map[typ]
+ # Used in test_parseopt -> test_parse_defaultgetter.
+ self.type = attrs["type"]
+ else:
+ self.type = typ
+ try:
+ # Attribute existence is tested in Config._processopt.
+ self.default = attrs["default"]
+ except KeyError:
+ pass
+ self._set_opt_strings(names)
+ dest: Optional[str] = attrs.get("dest")
+ if dest:
+ self.dest = dest
+ elif self._long_opts:
+ self.dest = self._long_opts[0][2:].replace("-", "_")
+ else:
+ try:
+ self.dest = self._short_opts[0][1:]
+ except IndexError as e:
+ self.dest = "???" # Needed for the error repr.
+ raise ArgumentError("need a long or short option", self) from e
+
+ def names(self) -> List[str]:
+ return self._short_opts + self._long_opts
+
+ def attrs(self) -> Mapping[str, Any]:
+ # Update any attributes set by processopt.
+ attrs = "default dest help".split()
+ attrs.append(self.dest)
+ for attr in attrs:
+ try:
+ self._attrs[attr] = getattr(self, attr)
+ except AttributeError:
+ pass
+ if self._attrs.get("help"):
+ a = self._attrs["help"]
+ a = a.replace("%default", "%(default)s")
+ # a = a.replace('%prog', '%(prog)s')
+ self._attrs["help"] = a
+ return self._attrs
+
+ def _set_opt_strings(self, opts: Sequence[str]) -> None:
+ """Directly from optparse.
+
+ Might not be necessary as this is passed to argparse later on.
+ """
+ for opt in opts:
+ if len(opt) < 2:
+ raise ArgumentError(
+ "invalid option string %r: "
+ "must be at least two characters long" % opt,
+ self,
+ )
+ elif len(opt) == 2:
+ if not (opt[0] == "-" and opt[1] != "-"):
+ raise ArgumentError(
+ "invalid short option string %r: "
+ "must be of the form -x, (x any non-dash char)" % opt,
+ self,
+ )
+ self._short_opts.append(opt)
+ else:
+ if not (opt[0:2] == "--" and opt[2] != "-"):
+ raise ArgumentError(
+ "invalid long option string %r: "
+ "must start with --, followed by non-dash" % opt,
+ self,
+ )
+ self._long_opts.append(opt)
+
+ def __repr__(self) -> str:
+ args: List[str] = []
+ if self._short_opts:
+ args += ["_short_opts: " + repr(self._short_opts)]
+ if self._long_opts:
+ args += ["_long_opts: " + repr(self._long_opts)]
+ args += ["dest: " + repr(self.dest)]
+ if hasattr(self, "type"):
+ args += ["type: " + repr(self.type)]
+ if hasattr(self, "default"):
+ args += ["default: " + repr(self.default)]
+ return "Argument({})".format(", ".join(args))
+
+
+class OptionGroup:
+ """A group of options shown in its own section."""
+
+ def __init__(
+ self,
+ name: str,
+ description: str = "",
+ parser: Optional[Parser] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self.name = name
+ self.description = description
+ self.options: List[Argument] = []
+ self.parser = parser
+
+ def addoption(self, *opts: str, **attrs: Any) -> None:
+ """Add an option to this group.
+
+ If a shortened version of a long option is specified, it will
+ be suppressed in the help. ``addoption('--twowords', '--two-words')``
+ results in help showing ``--two-words`` only, but ``--twowords`` gets
+ accepted **and** the automatic destination is in ``args.twowords``.
+
+ :param opts:
+ Option names, can be short or long options.
+ :param attrs:
+ Same attributes as the argparse library's :py:func:`add_argument()
+ ` function accepts.
+ """
+ conflict = set(opts).intersection(
+ name for opt in self.options for name in opt.names()
+ )
+ if conflict:
+ raise ValueError("option names %s already added" % conflict)
+ option = Argument(*opts, **attrs)
+ self._addoption_instance(option, shortupper=False)
+
+ def _addoption(self, *opts: str, **attrs: Any) -> None:
+ option = Argument(*opts, **attrs)
+ self._addoption_instance(option, shortupper=True)
+
+ def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
+ if not shortupper:
+ for opt in option._short_opts:
+ if opt[0] == "-" and opt[1].islower():
+ raise ValueError("lowercase shortoptions reserved")
+ if self.parser:
+ self.parser.processoption(option)
+ self.options.append(option)
+
+
+class MyOptionParser(argparse.ArgumentParser):
+ def __init__(
+ self,
+ parser: Parser,
+ extra_info: Optional[Dict[str, Any]] = None,
+ prog: Optional[str] = None,
+ ) -> None:
+ self._parser = parser
+ super().__init__(
+ prog=prog,
+ usage=parser._usage,
+ add_help=False,
+ formatter_class=DropShorterLongHelpFormatter,
+ allow_abbrev=False,
+ )
+ # extra_info is a dict of (param -> value) to display if there's
+ # an usage error to provide more contextual information to the user.
+ self.extra_info = extra_info if extra_info else {}
+
+ def error(self, message: str) -> NoReturn:
+ """Transform argparse error message into UsageError."""
+ msg = f"{self.prog}: error: {message}"
+
+ if hasattr(self._parser, "_config_source_hint"):
+ # Type ignored because the attribute is set dynamically.
+ msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
+
+ raise UsageError(self.format_usage() + msg)
+
+ # Type ignored because typeshed has a very complex type in the superclass.
+ def parse_args( # type: ignore
+ self,
+ args: Optional[Sequence[str]] = None,
+ namespace: Optional[argparse.Namespace] = None,
+ ) -> argparse.Namespace:
+ """Allow splitting of positional arguments."""
+ parsed, unrecognized = self.parse_known_args(args, namespace)
+ if unrecognized:
+ for arg in unrecognized:
+ if arg and arg[0] == "-":
+ lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
+ for k, v in sorted(self.extra_info.items()):
+ lines.append(f" {k}: {v}")
+ self.error("\n".join(lines))
+ getattr(parsed, FILE_OR_DIR).extend(unrecognized)
+ return parsed
+
+ if sys.version_info[:2] < (3, 9): # pragma: no cover
+ # Backport of https://github.com/python/cpython/pull/14316 so we can
+ # disable long --argument abbreviations without breaking short flags.
+ def _parse_optional(
+ self, arg_string: str
+ ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
+ if not arg_string:
+ return None
+ if not arg_string[0] in self.prefix_chars:
+ return None
+ if arg_string in self._option_string_actions:
+ action = self._option_string_actions[arg_string]
+ return action, arg_string, None
+ if len(arg_string) == 1:
+ return None
+ if "=" in arg_string:
+ option_string, explicit_arg = arg_string.split("=", 1)
+ if option_string in self._option_string_actions:
+ action = self._option_string_actions[option_string]
+ return action, option_string, explicit_arg
+ if self.allow_abbrev or not arg_string.startswith("--"):
+ option_tuples = self._get_option_tuples(arg_string)
+ if len(option_tuples) > 1:
+ msg = gettext(
+ "ambiguous option: %(option)s could match %(matches)s"
+ )
+ options = ", ".join(option for _, option, _ in option_tuples)
+ self.error(msg % {"option": arg_string, "matches": options})
+ elif len(option_tuples) == 1:
+ (option_tuple,) = option_tuples
+ return option_tuple
+ if self._negative_number_matcher.match(arg_string):
+ if not self._has_negative_number_optionals:
+ return None
+ if " " in arg_string:
+ return None
+ return None, arg_string, None
+
+
+class DropShorterLongHelpFormatter(argparse.HelpFormatter):
+ """Shorten help for long options that differ only in extra hyphens.
+
+ - Collapse **long** options that are the same except for extra hyphens.
+ - Shortcut if there are only two options and one of them is a short one.
+ - Cache result on the action object as this is called at least 2 times.
+ """
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ # Use more accurate terminal width.
+ if "width" not in kwargs:
+ kwargs["width"] = _pytest._io.get_terminal_width()
+ super().__init__(*args, **kwargs)
+
+ def _format_action_invocation(self, action: argparse.Action) -> str:
+ orgstr = super()._format_action_invocation(action)
+ if orgstr and orgstr[0] != "-": # only optional arguments
+ return orgstr
+ res: Optional[str] = getattr(action, "_formatted_action_invocation", None)
+ if res:
+ return res
+ options = orgstr.split(", ")
+ if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
+ # a shortcut for '-h, --help' or '--abc', '-a'
+ action._formatted_action_invocation = orgstr # type: ignore
+ return orgstr
+ return_list = []
+ short_long: Dict[str, str] = {}
+ for option in options:
+ if len(option) == 2 or option[2] == " ":
+ continue
+ if not option.startswith("--"):
+ raise ArgumentError(
+ 'long optional argument without "--": [%s]' % (option), option
+ )
+ xxoption = option[2:]
+ shortened = xxoption.replace("-", "")
+ if shortened not in short_long or len(short_long[shortened]) < len(
+ xxoption
+ ):
+ short_long[shortened] = xxoption
+ # now short_long has been filled out to the longest with dashes
+ # **and** we keep the right option ordering from add_argument
+ for option in options:
+ if len(option) == 2 or option[2] == " ":
+ return_list.append(option)
+ if option[2:] == short_long.get(option.replace("-", "")):
+ return_list.append(option.replace(" ", "=", 1))
+ formatted_action_invocation = ", ".join(return_list)
+ action._formatted_action_invocation = formatted_action_invocation # type: ignore
+ return formatted_action_invocation
+
+ def _split_lines(self, text, width):
+ """Wrap lines after splitting on original newlines.
+
+ This allows to have explicit line breaks in the help text.
+ """
+ import textwrap
+
+ lines = []
+ for line in text.splitlines():
+ lines.extend(textwrap.wrap(line.strip(), width))
+ return lines
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/compat.py b/venv/lib/python3.12/site-packages/_pytest/config/compat.py
new file mode 100644
index 0000000..5bd922a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/config/compat.py
@@ -0,0 +1,70 @@
+import functools
+import warnings
+from pathlib import Path
+from typing import Optional
+
+from ..compat import LEGACY_PATH
+from ..compat import legacy_path
+from ..deprecated import HOOK_LEGACY_PATH_ARG
+from _pytest.nodes import _check_path
+
+# hookname: (Path, LEGACY_PATH)
+imply_paths_hooks = {
+ "pytest_ignore_collect": ("collection_path", "path"),
+ "pytest_collect_file": ("file_path", "path"),
+ "pytest_pycollect_makemodule": ("module_path", "path"),
+ "pytest_report_header": ("start_path", "startdir"),
+ "pytest_report_collectionfinish": ("start_path", "startdir"),
+}
+
+
+class PathAwareHookProxy:
+ """
+ this helper wraps around hook callers
+ until pluggy supports fixingcalls, this one will do
+
+ it currently doesn't return full hook caller proxies for fixed hooks,
+ this may have to be changed later depending on bugs
+ """
+
+ def __init__(self, hook_caller):
+ self.__hook_caller = hook_caller
+
+ def __dir__(self):
+ return dir(self.__hook_caller)
+
+ def __getattr__(self, key, _wraps=functools.wraps):
+ hook = getattr(self.__hook_caller, key)
+ if key not in imply_paths_hooks:
+ self.__dict__[key] = hook
+ return hook
+ else:
+ path_var, fspath_var = imply_paths_hooks[key]
+
+ @_wraps(hook)
+ def fixed_hook(**kw):
+ path_value: Optional[Path] = kw.pop(path_var, None)
+ fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
+ if fspath_value is not None:
+ warnings.warn(
+ HOOK_LEGACY_PATH_ARG.format(
+ pylib_path_arg=fspath_var, pathlib_path_arg=path_var
+ ),
+ stacklevel=2,
+ )
+ if path_value is not None:
+ if fspath_value is not None:
+ _check_path(path_value, fspath_value)
+ else:
+ fspath_value = legacy_path(path_value)
+ else:
+ assert fspath_value is not None
+ path_value = Path(fspath_value)
+
+ kw[path_var] = path_value
+ kw[fspath_var] = fspath_value
+ return hook(**kw)
+
+ fixed_hook.__name__ = key
+ self.__dict__[key] = fixed_hook
+ return fixed_hook
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/exceptions.py b/venv/lib/python3.12/site-packages/_pytest/config/exceptions.py
new file mode 100644
index 0000000..4f1320e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/config/exceptions.py
@@ -0,0 +1,11 @@
+from _pytest.compat import final
+
+
+@final
+class UsageError(Exception):
+ """Error in pytest usage or invocation."""
+
+
+class PrintHelp(Exception):
+ """Raised when pytest should print its help to skip the rest of the
+ argument parsing and validation."""
diff --git a/venv/lib/python3.12/site-packages/_pytest/config/findpaths.py b/venv/lib/python3.12/site-packages/_pytest/config/findpaths.py
new file mode 100644
index 0000000..02674ff
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/config/findpaths.py
@@ -0,0 +1,218 @@
+import os
+import sys
+from pathlib import Path
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+import iniconfig
+
+from .exceptions import UsageError
+from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import commonpath
+from _pytest.pathlib import safe_exists
+
+if TYPE_CHECKING:
+ from . import Config
+
+
+def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
+ """Parse the given generic '.ini' file using legacy IniConfig parser, returning
+ the parsed object.
+
+ Raise UsageError if the file cannot be parsed.
+ """
+ try:
+ return iniconfig.IniConfig(str(path))
+ except iniconfig.ParseError as exc:
+ raise UsageError(str(exc)) from exc
+
+
+def load_config_dict_from_file(
+ filepath: Path,
+) -> Optional[Dict[str, Union[str, List[str]]]]:
+ """Load pytest configuration from the given file path, if supported.
+
+ Return None if the file does not contain valid pytest configuration.
+ """
+
+ # Configuration from ini files are obtained from the [pytest] section, if present.
+ if filepath.suffix == ".ini":
+ iniconfig = _parse_ini_config(filepath)
+
+ if "pytest" in iniconfig:
+ return dict(iniconfig["pytest"].items())
+ else:
+ # "pytest.ini" files are always the source of configuration, even if empty.
+ if filepath.name == "pytest.ini":
+ return {}
+
+ # '.cfg' files are considered if they contain a "[tool:pytest]" section.
+ elif filepath.suffix == ".cfg":
+ iniconfig = _parse_ini_config(filepath)
+
+ if "tool:pytest" in iniconfig.sections:
+ return dict(iniconfig["tool:pytest"].items())
+ elif "pytest" in iniconfig.sections:
+ # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
+ # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
+ fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
+
+ # '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
+ elif filepath.suffix == ".toml":
+ if sys.version_info >= (3, 11):
+ import tomllib
+ else:
+ import tomli as tomllib
+
+ toml_text = filepath.read_text(encoding="utf-8")
+ try:
+ config = tomllib.loads(toml_text)
+ except tomllib.TOMLDecodeError as exc:
+ raise UsageError(f"{filepath}: {exc}") from exc
+
+ result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
+ if result is not None:
+ # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc),
+ # however we need to convert all scalar values to str for compatibility with the rest
+ # of the configuration system, which expects strings only.
+ def make_scalar(v: object) -> Union[str, List[str]]:
+ return v if isinstance(v, list) else str(v)
+
+ return {k: make_scalar(v) for k, v in result.items()}
+
+ return None
+
+
+def locate_config(
+ args: Iterable[Path],
+) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
+ """Search in the list of arguments for a valid ini-file for pytest,
+ and return a tuple of (rootdir, inifile, cfg-dict)."""
+ config_names = [
+ "pytest.ini",
+ ".pytest.ini",
+ "pyproject.toml",
+ "tox.ini",
+ "setup.cfg",
+ ]
+ args = [x for x in args if not str(x).startswith("-")]
+ if not args:
+ args = [Path.cwd()]
+ for arg in args:
+ argpath = absolutepath(arg)
+ for base in (argpath, *argpath.parents):
+ for config_name in config_names:
+ p = base / config_name
+ if p.is_file():
+ ini_config = load_config_dict_from_file(p)
+ if ini_config is not None:
+ return base, p, ini_config
+ return None, None, {}
+
+
+def get_common_ancestor(paths: Iterable[Path]) -> Path:
+ common_ancestor: Optional[Path] = None
+ for path in paths:
+ if not path.exists():
+ continue
+ if common_ancestor is None:
+ common_ancestor = path
+ else:
+ if common_ancestor in path.parents or path == common_ancestor:
+ continue
+ elif path in common_ancestor.parents:
+ common_ancestor = path
+ else:
+ shared = commonpath(path, common_ancestor)
+ if shared is not None:
+ common_ancestor = shared
+ if common_ancestor is None:
+ common_ancestor = Path.cwd()
+ elif common_ancestor.is_file():
+ common_ancestor = common_ancestor.parent
+ return common_ancestor
+
+
+def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
+ def is_option(x: str) -> bool:
+ return x.startswith("-")
+
+ def get_file_part_from_node_id(x: str) -> str:
+ return x.split("::")[0]
+
+ def get_dir_from_path(path: Path) -> Path:
+ if path.is_dir():
+ return path
+ return path.parent
+
+ # These look like paths but may not exist
+ possible_paths = (
+ absolutepath(get_file_part_from_node_id(arg))
+ for arg in args
+ if not is_option(arg)
+ )
+
+ return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
+
+
+CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
+
+
+def determine_setup(
+ inifile: Optional[str],
+ args: Sequence[str],
+ rootdir_cmd_arg: Optional[str] = None,
+ config: Optional["Config"] = None,
+) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
+ rootdir = None
+ dirs = get_dirs_from_args(args)
+ if inifile:
+ inipath_ = absolutepath(inifile)
+ inipath: Optional[Path] = inipath_
+ inicfg = load_config_dict_from_file(inipath_) or {}
+ if rootdir_cmd_arg is None:
+ rootdir = inipath_.parent
+ else:
+ ancestor = get_common_ancestor(dirs)
+ rootdir, inipath, inicfg = locate_config([ancestor])
+ if rootdir is None and rootdir_cmd_arg is None:
+ for possible_rootdir in (ancestor, *ancestor.parents):
+ if (possible_rootdir / "setup.py").is_file():
+ rootdir = possible_rootdir
+ break
+ else:
+ if dirs != [ancestor]:
+ rootdir, inipath, inicfg = locate_config(dirs)
+ if rootdir is None:
+ if config is not None:
+ cwd = config.invocation_params.dir
+ else:
+ cwd = Path.cwd()
+ rootdir = get_common_ancestor([cwd, ancestor])
+ if is_fs_root(rootdir):
+ rootdir = ancestor
+ if rootdir_cmd_arg:
+ rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
+ if not rootdir.is_dir():
+ raise UsageError(
+ "Directory '{}' not found. Check your '--rootdir' option.".format(
+ rootdir
+ )
+ )
+ assert rootdir is not None
+ return rootdir, inipath, inicfg or {}
+
+
+def is_fs_root(p: Path) -> bool:
+ r"""
+ Return True if the given path is pointing to the root of the
+ file system ("/" on Unix and "C:\\" on Windows for example).
+ """
+ return os.path.splitdrive(str(p))[1] == os.sep
diff --git a/venv/lib/python3.12/site-packages/_pytest/debugging.py b/venv/lib/python3.12/site-packages/_pytest/debugging.py
new file mode 100644
index 0000000..a3f8080
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/debugging.py
@@ -0,0 +1,391 @@
+"""Interactive debugging with PDB, the Python Debugger."""
+import argparse
+import functools
+import sys
+import types
+import unittest
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from _pytest import outcomes
+from _pytest._code import ExceptionInfo
+from _pytest.config import Config
+from _pytest.config import ConftestImportFailure
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.config.argparsing import Parser
+from _pytest.config.exceptions import UsageError
+from _pytest.nodes import Node
+from _pytest.reports import BaseReport
+
+if TYPE_CHECKING:
+ from _pytest.capture import CaptureManager
+ from _pytest.runner import CallInfo
+
+
+def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
+ """Validate syntax of --pdbcls option."""
+ try:
+ modname, classname = value.split(":")
+ except ValueError as e:
+ raise argparse.ArgumentTypeError(
+ f"{value!r} is not in the format 'modname:classname'"
+ ) from e
+ return (modname, classname)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption(
+ "--pdb",
+ dest="usepdb",
+ action="store_true",
+ help="Start the interactive Python debugger on errors or KeyboardInterrupt",
+ )
+ group._addoption(
+ "--pdbcls",
+ dest="usepdb_cls",
+ metavar="modulename:classname",
+ type=_validate_usepdb_cls,
+ help="Specify a custom interactive Python debugger for use with --pdb."
+ "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
+ )
+ group._addoption(
+ "--trace",
+ dest="trace",
+ action="store_true",
+ help="Immediately break when running each test",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ import pdb
+
+ if config.getvalue("trace"):
+ config.pluginmanager.register(PdbTrace(), "pdbtrace")
+ if config.getvalue("usepdb"):
+ config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
+
+ pytestPDB._saved.append(
+ (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
+ )
+ pdb.set_trace = pytestPDB.set_trace
+ pytestPDB._pluginmanager = config.pluginmanager
+ pytestPDB._config = config
+
+ # NOTE: not using pytest_unconfigure, since it might get called although
+ # pytest_configure was not (if another plugin raises UsageError).
+ def fin() -> None:
+ (
+ pdb.set_trace,
+ pytestPDB._pluginmanager,
+ pytestPDB._config,
+ ) = pytestPDB._saved.pop()
+
+ config.add_cleanup(fin)
+
+
+class pytestPDB:
+ """Pseudo PDB that defers to the real pdb."""
+
+ _pluginmanager: Optional[PytestPluginManager] = None
+ _config: Optional[Config] = None
+ _saved: List[
+ Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]]
+ ] = []
+ _recursive_debug = 0
+ _wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None
+
+ @classmethod
+ def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
+ if capman:
+ return capman.is_capturing()
+ return False
+
+ @classmethod
+ def _import_pdb_cls(cls, capman: Optional["CaptureManager"]):
+ if not cls._config:
+ import pdb
+
+ # Happens when using pytest.set_trace outside of a test.
+ return pdb.Pdb
+
+ usepdb_cls = cls._config.getvalue("usepdb_cls")
+
+ if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
+ return cls._wrapped_pdb_cls[1]
+
+ if usepdb_cls:
+ modname, classname = usepdb_cls
+
+ try:
+ __import__(modname)
+ mod = sys.modules[modname]
+
+ # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
+ parts = classname.split(".")
+ pdb_cls = getattr(mod, parts[0])
+ for part in parts[1:]:
+ pdb_cls = getattr(pdb_cls, part)
+ except Exception as exc:
+ value = ":".join((modname, classname))
+ raise UsageError(
+ f"--pdbcls: could not import {value!r}: {exc}"
+ ) from exc
+ else:
+ import pdb
+
+ pdb_cls = pdb.Pdb
+
+ wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
+ cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
+ return wrapped_cls
+
+ @classmethod
+ def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
+ import _pytest.config
+
+ # Type ignored because mypy doesn't support "dynamic"
+ # inheritance like this.
+ class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
+ _pytest_capman = capman
+ _continued = False
+
+ def do_debug(self, arg):
+ cls._recursive_debug += 1
+ ret = super().do_debug(arg)
+ cls._recursive_debug -= 1
+ return ret
+
+ def do_continue(self, arg):
+ ret = super().do_continue(arg)
+ if cls._recursive_debug == 0:
+ assert cls._config is not None
+ tw = _pytest.config.create_terminal_writer(cls._config)
+ tw.line()
+
+ capman = self._pytest_capman
+ capturing = pytestPDB._is_capturing(capman)
+ if capturing:
+ if capturing == "global":
+ tw.sep(">", "PDB continue (IO-capturing resumed)")
+ else:
+ tw.sep(
+ ">",
+ "PDB continue (IO-capturing resumed for %s)"
+ % capturing,
+ )
+ assert capman is not None
+ capman.resume()
+ else:
+ tw.sep(">", "PDB continue")
+ assert cls._pluginmanager is not None
+ cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
+ self._continued = True
+ return ret
+
+ do_c = do_cont = do_continue
+
+ def do_quit(self, arg):
+ """Raise Exit outcome when quit command is used in pdb.
+
+ This is a bit of a hack - it would be better if BdbQuit
+ could be handled, but this would require to wrap the
+ whole pytest run, and adjust the report etc.
+ """
+ ret = super().do_quit(arg)
+
+ if cls._recursive_debug == 0:
+ outcomes.exit("Quitting debugger")
+
+ return ret
+
+ do_q = do_quit
+ do_exit = do_quit
+
+ def setup(self, f, tb):
+ """Suspend on setup().
+
+ Needed after do_continue resumed, and entering another
+ breakpoint again.
+ """
+ ret = super().setup(f, tb)
+ if not ret and self._continued:
+ # pdb.setup() returns True if the command wants to exit
+ # from the interaction: do not suspend capturing then.
+ if self._pytest_capman:
+ self._pytest_capman.suspend_global_capture(in_=True)
+ return ret
+
+ def get_stack(self, f, t):
+ stack, i = super().get_stack(f, t)
+ if f is None:
+ # Find last non-hidden frame.
+ i = max(0, len(stack) - 1)
+ while i and stack[i][0].f_locals.get("__tracebackhide__", False):
+ i -= 1
+ return stack, i
+
+ return PytestPdbWrapper
+
+ @classmethod
+ def _init_pdb(cls, method, *args, **kwargs):
+ """Initialize PDB debugging, dropping any IO capturing."""
+ import _pytest.config
+
+ if cls._pluginmanager is None:
+ capman: Optional[CaptureManager] = None
+ else:
+ capman = cls._pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend(in_=True)
+
+ if cls._config:
+ tw = _pytest.config.create_terminal_writer(cls._config)
+ tw.line()
+
+ if cls._recursive_debug == 0:
+ # Handle header similar to pdb.set_trace in py37+.
+ header = kwargs.pop("header", None)
+ if header is not None:
+ tw.sep(">", header)
+ else:
+ capturing = cls._is_capturing(capman)
+ if capturing == "global":
+ tw.sep(">", f"PDB {method} (IO-capturing turned off)")
+ elif capturing:
+ tw.sep(
+ ">",
+ "PDB %s (IO-capturing turned off for %s)"
+ % (method, capturing),
+ )
+ else:
+ tw.sep(">", f"PDB {method}")
+
+ _pdb = cls._import_pdb_cls(capman)(**kwargs)
+
+ if cls._pluginmanager:
+ cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
+ return _pdb
+
+ @classmethod
+ def set_trace(cls, *args, **kwargs) -> None:
+ """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
+ frame = sys._getframe().f_back
+ _pdb = cls._init_pdb("set_trace", *args, **kwargs)
+ _pdb.set_trace(frame)
+
+
+class PdbInvoke:
+ def pytest_exception_interact(
+ self, node: Node, call: "CallInfo[Any]", report: BaseReport
+ ) -> None:
+ capman = node.config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture(in_=True)
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stdout.write(err)
+ assert call.excinfo is not None
+
+ if not isinstance(call.excinfo.value, unittest.SkipTest):
+ _enter_pdb(node, call.excinfo, report)
+
+ def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
+ tb = _postmortem_traceback(excinfo)
+ post_mortem(tb)
+
+
+class PdbTrace:
+ @hookimpl(hookwrapper=True)
+ def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
+ wrap_pytest_function_for_tracing(pyfuncitem)
+ yield
+
+
+def wrap_pytest_function_for_tracing(pyfuncitem):
+ """Change the Python function object of the given Function item by a
+ wrapper which actually enters pdb before calling the python function
+ itself, effectively leaving the user in the pdb prompt in the first
+ statement of the function."""
+ _pdb = pytestPDB._init_pdb("runcall")
+ testfunction = pyfuncitem.obj
+
+ # we can't just return `partial(pdb.runcall, testfunction)` because (on
+ # python < 3.7.4) runcall's first param is `func`, which means we'd get
+ # an exception if one of the kwargs to testfunction was called `func`.
+ @functools.wraps(testfunction)
+ def wrapper(*args, **kwargs):
+ func = functools.partial(testfunction, *args, **kwargs)
+ _pdb.runcall(func)
+
+ pyfuncitem.obj = wrapper
+
+
+def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
+ """Wrap the given pytestfunct item for tracing support if --trace was given in
+ the command line."""
+ if pyfuncitem.config.getvalue("trace"):
+ wrap_pytest_function_for_tracing(pyfuncitem)
+
+
+def _enter_pdb(
+ node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
+) -> BaseReport:
+ # XXX we re-use the TerminalReporter's terminalwriter
+ # because this seems to avoid some encoding related troubles
+ # for not completely clear reasons.
+ tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
+ tw.line()
+
+ showcapture = node.config.option.showcapture
+
+ for sectionname, content in (
+ ("stdout", rep.capstdout),
+ ("stderr", rep.capstderr),
+ ("log", rep.caplog),
+ ):
+ if showcapture in (sectionname, "all") and content:
+ tw.sep(">", "captured " + sectionname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ tw.line(content)
+
+ tw.sep(">", "traceback")
+ rep.toterminal(tw)
+ tw.sep(">", "entering PDB")
+ tb = _postmortem_traceback(excinfo)
+ rep._pdbshown = True # type: ignore[attr-defined]
+ post_mortem(tb)
+ return rep
+
+
+def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
+ from doctest import UnexpectedException
+
+ if isinstance(excinfo.value, UnexpectedException):
+ # A doctest.UnexpectedException is not useful for post_mortem.
+ # Use the underlying exception instead:
+ return excinfo.value.exc_info[2]
+ elif isinstance(excinfo.value, ConftestImportFailure):
+ # A config.ConftestImportFailure is not useful for post_mortem.
+ # Use the underlying exception instead:
+ return excinfo.value.excinfo[2]
+ else:
+ assert excinfo._excinfo is not None
+ return excinfo._excinfo[2]
+
+
+def post_mortem(t: types.TracebackType) -> None:
+ p = pytestPDB._init_pdb("post_mortem")
+ p.reset()
+ p.interaction(None, t)
+ if p.quitting:
+ outcomes.exit("Quitting debugger")
diff --git a/venv/lib/python3.12/site-packages/_pytest/deprecated.py b/venv/lib/python3.12/site-packages/_pytest/deprecated.py
new file mode 100644
index 0000000..b9c10df
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/deprecated.py
@@ -0,0 +1,146 @@
+"""Deprecation messages and bits of code used elsewhere in the codebase that
+is planned to be removed in the next pytest release.
+
+Keeping it in a central location makes it easy to track what is deprecated and should
+be removed when the time comes.
+
+All constants defined in this module should be either instances of
+:class:`PytestWarning`, or :class:`UnformattedWarning`
+in case of warnings which need to format their messages.
+"""
+from warnings import warn
+
+from _pytest.warning_types import PytestDeprecationWarning
+from _pytest.warning_types import PytestRemovedIn8Warning
+from _pytest.warning_types import UnformattedWarning
+
+# set of plugins which have been integrated into the core; we use this list to ignore
+# them during registration to avoid conflicts
+DEPRECATED_EXTERNAL_PLUGINS = {
+ "pytest_catchlog",
+ "pytest_capturelog",
+ "pytest_faulthandler",
+}
+
+NOSE_SUPPORT = UnformattedWarning(
+ PytestRemovedIn8Warning,
+ "Support for nose tests is deprecated and will be removed in a future release.\n"
+ "{nodeid} is using nose method: `{method}` ({stage})\n"
+ "See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
+)
+
+NOSE_SUPPORT_METHOD = UnformattedWarning(
+ PytestRemovedIn8Warning,
+ "Support for nose tests is deprecated and will be removed in a future release.\n"
+ "{nodeid} is using nose-specific method: `{method}(self)`\n"
+ "To remove this warning, rename it to `{method}_method(self)`\n"
+ "See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
+)
+
+
+# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
+# * If you're in the future: "could have been".
+YIELD_FIXTURE = PytestDeprecationWarning(
+ "@pytest.yield_fixture is deprecated.\n"
+ "Use @pytest.fixture instead; they are the same."
+)
+
+WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
+ "The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
+ "Please use pytest_load_initial_conftests hook instead."
+)
+
+FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestRemovedIn8Warning(
+ "The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
+ "use self.session.gethookproxy() and self.session.isinitpath() instead. "
+)
+
+STRICT_OPTION = PytestRemovedIn8Warning(
+ "The --strict option is deprecated, use --strict-markers instead."
+)
+
+# This deprecation is never really meant to be removed.
+PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
+
+ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
+ 'pytest now uses argparse. "%default" should be changed to "%(default)s"',
+)
+
+ARGUMENT_TYPE_STR_CHOICE = UnformattedWarning(
+ PytestRemovedIn8Warning,
+ "`type` argument to addoption() is the string {typ!r}."
+ " For choices this is optional and can be omitted, "
+ " but when supplied should be a type (for example `str` or `int`)."
+ " (options: {names})",
+)
+
+ARGUMENT_TYPE_STR = UnformattedWarning(
+ PytestRemovedIn8Warning,
+ "`type` argument to addoption() is the string {typ!r}, "
+ " but when supplied should be a type (for example `str` or `int`)."
+ " (options: {names})",
+)
+
+
+HOOK_LEGACY_PATH_ARG = UnformattedWarning(
+ PytestRemovedIn8Warning,
+ "The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
+ "see https://docs.pytest.org/en/latest/deprecations.html"
+ "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
+)
+
+NODE_CTOR_FSPATH_ARG = UnformattedWarning(
+ PytestRemovedIn8Warning,
+ "The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
+ "Please use the (path: pathlib.Path) argument instead.\n"
+ "See https://docs.pytest.org/en/latest/deprecations.html"
+ "#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
+)
+
+WARNS_NONE_ARG = PytestRemovedIn8Warning(
+ "Passing None has been deprecated.\n"
+ "See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
+ "#additional-use-cases-of-warnings-in-tests"
+ " for alternatives in common use cases."
+)
+
+KEYWORD_MSG_ARG = UnformattedWarning(
+ PytestRemovedIn8Warning,
+ "pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead",
+)
+
+INSTANCE_COLLECTOR = PytestRemovedIn8Warning(
+ "The pytest.Instance collector type is deprecated and is no longer used. "
+ "See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
+)
+HOOK_LEGACY_MARKING = UnformattedWarning(
+ PytestDeprecationWarning,
+ "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
+ "Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
+ " to configure the hooks.\n"
+ " See https://docs.pytest.org/en/latest/deprecations.html"
+ "#configuring-hook-specs-impls-using-markers",
+)
+
+# You want to make some `__init__` or function "private".
+#
+# def my_private_function(some, args):
+# ...
+#
+# Do this:
+#
+# def my_private_function(some, args, *, _ispytest: bool = False):
+# check_ispytest(_ispytest)
+# ...
+#
+# Change all internal/allowed calls to
+#
+# my_private_function(some, args, _ispytest=True)
+#
+# All other calls will get the default _ispytest=False and trigger
+# the warning (possibly error in the future).
+
+
+def check_ispytest(ispytest: bool) -> None:
+ if not ispytest:
+ warn(PRIVATE, stacklevel=3)
diff --git a/venv/lib/python3.12/site-packages/_pytest/doctest.py b/venv/lib/python3.12/site-packages/_pytest/doctest.py
new file mode 100644
index 0000000..ca41a98
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/doctest.py
@@ -0,0 +1,771 @@
+"""Discover and run doctests in modules and test files."""
+import bdb
+import functools
+import inspect
+import os
+import platform
+import sys
+import traceback
+import types
+import warnings
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generator
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Pattern
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from _pytest import outcomes
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ReprFileLocation
+from _pytest._code.code import TerminalRepr
+from _pytest._io import TerminalWriter
+from _pytest.compat import safe_getattr
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import OutcomeException
+from _pytest.outcomes import skip
+from _pytest.pathlib import fnmatch_ex
+from _pytest.pathlib import import_path
+from _pytest.python import Module
+from _pytest.python_api import approx
+from _pytest.warning_types import PytestWarning
+
+if TYPE_CHECKING:
+ import doctest
+
+DOCTEST_REPORT_CHOICE_NONE = "none"
+DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
+DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
+DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
+DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
+
+DOCTEST_REPORT_CHOICES = (
+ DOCTEST_REPORT_CHOICE_NONE,
+ DOCTEST_REPORT_CHOICE_CDIFF,
+ DOCTEST_REPORT_CHOICE_NDIFF,
+ DOCTEST_REPORT_CHOICE_UDIFF,
+ DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
+)
+
+# Lazy definition of runner class
+RUNNER_CLASS = None
+# Lazy definition of output checker class
+CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "doctest_optionflags",
+ "Option flags for doctests",
+ type="args",
+ default=["ELLIPSIS"],
+ )
+ parser.addini(
+ "doctest_encoding", "Encoding used for doctest files", default="utf-8"
+ )
+ group = parser.getgroup("collect")
+ group.addoption(
+ "--doctest-modules",
+ action="store_true",
+ default=False,
+ help="Run doctests in all .py modules",
+ dest="doctestmodules",
+ )
+ group.addoption(
+ "--doctest-report",
+ type=str.lower,
+ default="udiff",
+ help="Choose another output format for diffs on doctest failure",
+ choices=DOCTEST_REPORT_CHOICES,
+ dest="doctestreport",
+ )
+ group.addoption(
+ "--doctest-glob",
+ action="append",
+ default=[],
+ metavar="pat",
+ help="Doctests file matching pattern, default: test*.txt",
+ dest="doctestglob",
+ )
+ group.addoption(
+ "--doctest-ignore-import-errors",
+ action="store_true",
+ default=False,
+ help="Ignore doctest ImportErrors",
+ dest="doctest_ignore_import_errors",
+ )
+ group.addoption(
+ "--doctest-continue-on-failure",
+ action="store_true",
+ default=False,
+ help="For a given doctest, continue to run after the first failure",
+ dest="doctest_continue_on_failure",
+ )
+
+
+def pytest_unconfigure() -> None:
+ global RUNNER_CLASS
+
+ RUNNER_CLASS = None
+
+
+def pytest_collect_file(
+ file_path: Path,
+ parent: Collector,
+) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
+ config = parent.config
+ if file_path.suffix == ".py":
+ if config.option.doctestmodules and not any(
+ (_is_setup_py(file_path), _is_main_py(file_path))
+ ):
+ mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
+ return mod
+ elif _is_doctest(config, file_path, parent):
+ txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
+ return txt
+ return None
+
+
+def _is_setup_py(path: Path) -> bool:
+ if path.name != "setup.py":
+ return False
+ contents = path.read_bytes()
+ return b"setuptools" in contents or b"distutils" in contents
+
+
+def _is_doctest(config: Config, path: Path, parent: Collector) -> bool:
+ if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
+ return True
+ globs = config.getoption("doctestglob") or ["test*.txt"]
+ return any(fnmatch_ex(glob, path) for glob in globs)
+
+
+def _is_main_py(path: Path) -> bool:
+ return path.name == "__main__.py"
+
+
+class ReprFailDoctest(TerminalRepr):
+ def __init__(
+ self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
+ ) -> None:
+ self.reprlocation_lines = reprlocation_lines
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for reprlocation, lines in self.reprlocation_lines:
+ for line in lines:
+ tw.line(line)
+ reprlocation.toterminal(tw)
+
+
+class MultipleDoctestFailures(Exception):
+ def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
+ super().__init__()
+ self.failures = failures
+
+
+def _init_runner_class() -> Type["doctest.DocTestRunner"]:
+ import doctest
+
+ class PytestDoctestRunner(doctest.DebugRunner):
+ """Runner to collect failures.
+
+ Note that the out variable in this case is a list instead of a
+ stdout-like object.
+ """
+
+ def __init__(
+ self,
+ checker: Optional["doctest.OutputChecker"] = None,
+ verbose: Optional[bool] = None,
+ optionflags: int = 0,
+ continue_on_failure: bool = True,
+ ) -> None:
+ super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
+ self.continue_on_failure = continue_on_failure
+
+ def report_failure(
+ self,
+ out,
+ test: "doctest.DocTest",
+ example: "doctest.Example",
+ got: str,
+ ) -> None:
+ failure = doctest.DocTestFailure(test, example, got)
+ if self.continue_on_failure:
+ out.append(failure)
+ else:
+ raise failure
+
+ def report_unexpected_exception(
+ self,
+ out,
+ test: "doctest.DocTest",
+ example: "doctest.Example",
+ exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
+ ) -> None:
+ if isinstance(exc_info[1], OutcomeException):
+ raise exc_info[1]
+ if isinstance(exc_info[1], bdb.BdbQuit):
+ outcomes.exit("Quitting debugger")
+ failure = doctest.UnexpectedException(test, example, exc_info)
+ if self.continue_on_failure:
+ out.append(failure)
+ else:
+ raise failure
+
+ return PytestDoctestRunner
+
+
+def _get_runner(
+ checker: Optional["doctest.OutputChecker"] = None,
+ verbose: Optional[bool] = None,
+ optionflags: int = 0,
+ continue_on_failure: bool = True,
+) -> "doctest.DocTestRunner":
+ # We need this in order to do a lazy import on doctest
+ global RUNNER_CLASS
+ if RUNNER_CLASS is None:
+ RUNNER_CLASS = _init_runner_class()
+ # Type ignored because the continue_on_failure argument is only defined on
+ # PytestDoctestRunner, which is lazily defined so can't be used as a type.
+ return RUNNER_CLASS( # type: ignore
+ checker=checker,
+ verbose=verbose,
+ optionflags=optionflags,
+ continue_on_failure=continue_on_failure,
+ )
+
+
+class DoctestItem(Item):
+ def __init__(
+ self,
+ name: str,
+ parent: "Union[DoctestTextfile, DoctestModule]",
+ runner: Optional["doctest.DocTestRunner"] = None,
+ dtest: Optional["doctest.DocTest"] = None,
+ ) -> None:
+ super().__init__(name, parent)
+ self.runner = runner
+ self.dtest = dtest
+ self.obj = None
+ self.fixture_request: Optional[FixtureRequest] = None
+
+ @classmethod
+ def from_parent( # type: ignore
+ cls,
+ parent: "Union[DoctestTextfile, DoctestModule]",
+ *,
+ name: str,
+ runner: "doctest.DocTestRunner",
+ dtest: "doctest.DocTest",
+ ):
+ # incompatible signature due to imposed limits on subclass
+ """The public named constructor."""
+ return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
+
+ def setup(self) -> None:
+ if self.dtest is not None:
+ self.fixture_request = _setup_fixtures(self)
+ globs = dict(getfixture=self.fixture_request.getfixturevalue)
+ for name, value in self.fixture_request.getfixturevalue(
+ "doctest_namespace"
+ ).items():
+ globs[name] = value
+ self.dtest.globs.update(globs)
+
+ def runtest(self) -> None:
+ assert self.dtest is not None
+ assert self.runner is not None
+ _check_all_skipped(self.dtest)
+ self._disable_output_capturing_for_darwin()
+ failures: List["doctest.DocTestFailure"] = []
+ # Type ignored because we change the type of `out` from what
+ # doctest expects.
+ self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
+ if failures:
+ raise MultipleDoctestFailures(failures)
+
+ def _disable_output_capturing_for_darwin(self) -> None:
+ """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
+ if platform.system() != "Darwin":
+ return
+ capman = self.config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture(in_=True)
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+ # TODO: Type ignored -- breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ ) -> Union[str, TerminalRepr]:
+ import doctest
+
+ failures: Optional[
+ Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
+ ] = None
+ if isinstance(
+ excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
+ ):
+ failures = [excinfo.value]
+ elif isinstance(excinfo.value, MultipleDoctestFailures):
+ failures = excinfo.value.failures
+
+ if failures is None:
+ return super().repr_failure(excinfo)
+
+ reprlocation_lines = []
+ for failure in failures:
+ example = failure.example
+ test = failure.test
+ filename = test.filename
+ if test.lineno is None:
+ lineno = None
+ else:
+ lineno = test.lineno + example.lineno + 1
+ message = type(failure).__name__
+ # TODO: ReprFileLocation doesn't expect a None lineno.
+ reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
+ checker = _get_checker()
+ report_choice = _get_report_choice(self.config.getoption("doctestreport"))
+ if lineno is not None:
+ assert failure.test.docstring is not None
+ lines = failure.test.docstring.splitlines(False)
+ # add line numbers to the left of the error message
+ assert test.lineno is not None
+ lines = [
+ "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
+ ]
+ # trim docstring error lines to 10
+ lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
+ else:
+ lines = [
+ "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
+ ]
+ indent = ">>>"
+ for line in example.source.splitlines():
+ lines.append(f"??? {indent} {line}")
+ indent = "..."
+ if isinstance(failure, doctest.DocTestFailure):
+ lines += checker.output_difference(
+ example, failure.got, report_choice
+ ).split("\n")
+ else:
+ inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
+ lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
+ lines += [
+ x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
+ ]
+ reprlocation_lines.append((reprlocation, lines))
+ return ReprFailDoctest(reprlocation_lines)
+
+ def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
+ assert self.dtest is not None
+ return self.path, self.dtest.lineno, "[doctest] %s" % self.name
+
+
+def _get_flag_lookup() -> Dict[str, int]:
+ import doctest
+
+ return dict(
+ DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
+ DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
+ NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
+ ELLIPSIS=doctest.ELLIPSIS,
+ IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
+ COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
+ ALLOW_UNICODE=_get_allow_unicode_flag(),
+ ALLOW_BYTES=_get_allow_bytes_flag(),
+ NUMBER=_get_number_flag(),
+ )
+
+
+def get_optionflags(parent):
+ optionflags_str = parent.config.getini("doctest_optionflags")
+ flag_lookup_table = _get_flag_lookup()
+ flag_acc = 0
+ for flag in optionflags_str:
+ flag_acc |= flag_lookup_table[flag]
+ return flag_acc
+
+
+def _get_continue_on_failure(config):
+ continue_on_failure = config.getvalue("doctest_continue_on_failure")
+ if continue_on_failure:
+ # We need to turn off this if we use pdb since we should stop at
+ # the first failure.
+ if config.getvalue("usepdb"):
+ continue_on_failure = False
+ return continue_on_failure
+
+
+class DoctestTextfile(Module):
+ obj = None
+
+ def collect(self) -> Iterable[DoctestItem]:
+ import doctest
+
+ # Inspired by doctest.testfile; ideally we would use it directly,
+ # but it doesn't support passing a custom checker.
+ encoding = self.config.getini("doctest_encoding")
+ text = self.path.read_text(encoding)
+ filename = str(self.path)
+ name = self.path.name
+ globs = {"__name__": "__main__"}
+
+ optionflags = get_optionflags(self)
+
+ runner = _get_runner(
+ verbose=False,
+ optionflags=optionflags,
+ checker=_get_checker(),
+ continue_on_failure=_get_continue_on_failure(self.config),
+ )
+
+ parser = doctest.DocTestParser()
+ test = parser.get_doctest(text, globs, name, filename, 0)
+ if test.examples:
+ yield DoctestItem.from_parent(
+ self, name=test.name, runner=runner, dtest=test
+ )
+
+
+def _check_all_skipped(test: "doctest.DocTest") -> None:
+ """Raise pytest.skip() if all examples in the given DocTest have the SKIP
+ option set."""
+ import doctest
+
+ all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
+ if all_skipped:
+ skip("all tests skipped by +SKIP option")
+
+
+def _is_mocked(obj: object) -> bool:
+ """Return if an object is possibly a mock object by checking the
+ existence of a highly improbable attribute."""
+ return (
+ safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
+ is not None
+ )
+
+
+@contextmanager
+def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
+ """Context manager which replaces ``inspect.unwrap`` with a version
+ that's aware of mock objects and doesn't recurse into them."""
+ real_unwrap = inspect.unwrap
+
+ def _mock_aware_unwrap(
+ func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
+ ) -> Any:
+ try:
+ if stop is None or stop is _is_mocked:
+ return real_unwrap(func, stop=_is_mocked)
+ _stop = stop
+ return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
+ except Exception as e:
+ warnings.warn(
+ "Got %r when unwrapping %r. This is usually caused "
+ "by a violation of Python's object protocol; see e.g. "
+ "https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
+ PytestWarning,
+ )
+ raise
+
+ inspect.unwrap = _mock_aware_unwrap
+ try:
+ yield
+ finally:
+ inspect.unwrap = real_unwrap
+
+
+class DoctestModule(Module):
+ def collect(self) -> Iterable[DoctestItem]:
+ import doctest
+
+ class MockAwareDocTestFinder(doctest.DocTestFinder):
+ """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
+
+ https://github.com/pytest-dev/pytest/issues/3456
+ https://bugs.python.org/issue25532
+ """
+
+ def _find_lineno(self, obj, source_lines):
+ """Doctest code does not take into account `@property`, this
+ is a hackish way to fix it. https://bugs.python.org/issue17446
+
+ Wrapped Doctests will need to be unwrapped so the correct
+ line number is returned. This will be reported upstream. #8796
+ """
+ if isinstance(obj, property):
+ obj = getattr(obj, "fget", obj)
+
+ if hasattr(obj, "__wrapped__"):
+ # Get the main obj in case of it being wrapped
+ obj = inspect.unwrap(obj)
+
+ # Type ignored because this is a private function.
+ return super()._find_lineno( # type:ignore[misc]
+ obj,
+ source_lines,
+ )
+
+ def _find(
+ self, tests, obj, name, module, source_lines, globs, seen
+ ) -> None:
+ if _is_mocked(obj):
+ return
+ with _patch_unwrap_mock_aware():
+ # Type ignored because this is a private function.
+ super()._find( # type:ignore[misc]
+ tests, obj, name, module, source_lines, globs, seen
+ )
+
+ if sys.version_info < (3, 13):
+
+ def _from_module(self, module, object):
+ """`cached_property` objects are never considered a part
+ of the 'current module'. As such they are skipped by doctest.
+ Here we override `_from_module` to check the underlying
+ function instead. https://github.com/python/cpython/issues/107995
+ """
+ if hasattr(functools, "cached_property") and isinstance(
+ object, functools.cached_property
+ ):
+ object = object.func
+
+ # Type ignored because this is a private function.
+ return super()._from_module(module, object) # type: ignore[misc]
+
+ else: # pragma: no cover
+ pass
+
+ if self.path.name == "conftest.py":
+ module = self.config.pluginmanager._importconftest(
+ self.path,
+ self.config.getoption("importmode"),
+ rootpath=self.config.rootpath,
+ )
+ else:
+ try:
+ module = import_path(
+ self.path,
+ root=self.config.rootpath,
+ mode=self.config.getoption("importmode"),
+ )
+ except ImportError:
+ if self.config.getvalue("doctest_ignore_import_errors"):
+ skip("unable to import module %r" % self.path)
+ else:
+ raise
+ # Uses internal doctest module parsing mechanism.
+ finder = MockAwareDocTestFinder()
+ optionflags = get_optionflags(self)
+ runner = _get_runner(
+ verbose=False,
+ optionflags=optionflags,
+ checker=_get_checker(),
+ continue_on_failure=_get_continue_on_failure(self.config),
+ )
+
+ for test in finder.find(module, module.__name__):
+ if test.examples: # skip empty doctests
+ yield DoctestItem.from_parent(
+ self, name=test.name, runner=runner, dtest=test
+ )
+
+
+def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
+ """Used by DoctestTextfile and DoctestItem to setup fixture information."""
+
+ def func() -> None:
+ pass
+
+ doctest_item.funcargs = {} # type: ignore[attr-defined]
+ fm = doctest_item.session._fixturemanager
+ doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
+ node=doctest_item, func=func, cls=None, funcargs=False
+ )
+ fixture_request = FixtureRequest(doctest_item, _ispytest=True)
+ fixture_request._fillfixtures()
+ return fixture_request
+
+
+def _init_checker_class() -> Type["doctest.OutputChecker"]:
+ import doctest
+ import re
+
+ class LiteralsOutputChecker(doctest.OutputChecker):
+ # Based on doctest_nose_plugin.py from the nltk project
+ # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
+ # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
+
+ _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
+ _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
+ _number_re = re.compile(
+ r"""
+ (?P
+ (?P
+ (?P [+-]?\d*)\.(?P\d+)
+ |
+ (?P [+-]?\d+)\.
+ )
+ (?:
+ [Ee]
+ (?P [+-]?\d+)
+ )?
+ |
+ (?P [+-]?\d+)
+ (?:
+ [Ee]
+ (?P [+-]?\d+)
+ )
+ )
+ """,
+ re.VERBOSE,
+ )
+
+ def check_output(self, want: str, got: str, optionflags: int) -> bool:
+ if super().check_output(want, got, optionflags):
+ return True
+
+ allow_unicode = optionflags & _get_allow_unicode_flag()
+ allow_bytes = optionflags & _get_allow_bytes_flag()
+ allow_number = optionflags & _get_number_flag()
+
+ if not allow_unicode and not allow_bytes and not allow_number:
+ return False
+
+ def remove_prefixes(regex: Pattern[str], txt: str) -> str:
+ return re.sub(regex, r"\1\2", txt)
+
+ if allow_unicode:
+ want = remove_prefixes(self._unicode_literal_re, want)
+ got = remove_prefixes(self._unicode_literal_re, got)
+
+ if allow_bytes:
+ want = remove_prefixes(self._bytes_literal_re, want)
+ got = remove_prefixes(self._bytes_literal_re, got)
+
+ if allow_number:
+ got = self._remove_unwanted_precision(want, got)
+
+ return super().check_output(want, got, optionflags)
+
+ def _remove_unwanted_precision(self, want: str, got: str) -> str:
+ wants = list(self._number_re.finditer(want))
+ gots = list(self._number_re.finditer(got))
+ if len(wants) != len(gots):
+ return got
+ offset = 0
+ for w, g in zip(wants, gots):
+ fraction: Optional[str] = w.group("fraction")
+ exponent: Optional[str] = w.group("exponent1")
+ if exponent is None:
+ exponent = w.group("exponent2")
+ precision = 0 if fraction is None else len(fraction)
+ if exponent is not None:
+ precision -= int(exponent)
+ if float(w.group()) == approx(float(g.group()), abs=10**-precision):
+ # They're close enough. Replace the text we actually
+ # got with the text we want, so that it will match when we
+ # check the string literally.
+ got = (
+ got[: g.start() + offset] + w.group() + got[g.end() + offset :]
+ )
+ offset += w.end() - w.start() - (g.end() - g.start())
+ return got
+
+ return LiteralsOutputChecker
+
+
+def _get_checker() -> "doctest.OutputChecker":
+ """Return a doctest.OutputChecker subclass that supports some
+ additional options:
+
+ * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
+ prefixes (respectively) in string literals. Useful when the same
+ doctest should run in Python 2 and Python 3.
+
+ * NUMBER to ignore floating-point differences smaller than the
+ precision of the literal number in the doctest.
+
+ An inner class is used to avoid importing "doctest" at the module
+ level.
+ """
+ global CHECKER_CLASS
+ if CHECKER_CLASS is None:
+ CHECKER_CLASS = _init_checker_class()
+ return CHECKER_CLASS()
+
+
+def _get_allow_unicode_flag() -> int:
+ """Register and return the ALLOW_UNICODE flag."""
+ import doctest
+
+ return doctest.register_optionflag("ALLOW_UNICODE")
+
+
+def _get_allow_bytes_flag() -> int:
+ """Register and return the ALLOW_BYTES flag."""
+ import doctest
+
+ return doctest.register_optionflag("ALLOW_BYTES")
+
+
+def _get_number_flag() -> int:
+ """Register and return the NUMBER flag."""
+ import doctest
+
+ return doctest.register_optionflag("NUMBER")
+
+
+def _get_report_choice(key: str) -> int:
+ """Return the actual `doctest` module flag value.
+
+ We want to do it as late as possible to avoid importing `doctest` and all
+ its dependencies when parsing options, as it adds overhead and breaks tests.
+ """
+ import doctest
+
+ return {
+ DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
+ DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
+ DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
+ DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
+ DOCTEST_REPORT_CHOICE_NONE: 0,
+ }[key]
+
+
+@fixture(scope="session")
+def doctest_namespace() -> Dict[str, Any]:
+ """Fixture that returns a :py:class:`dict` that will be injected into the
+ namespace of doctests.
+
+ Usually this fixture is used in conjunction with another ``autouse`` fixture:
+
+ .. code-block:: python
+
+ @pytest.fixture(autouse=True)
+ def add_np(doctest_namespace):
+ doctest_namespace["np"] = numpy
+
+ For more details: :ref:`doctest_namespace`.
+ """
+ return dict()
diff --git a/venv/lib/python3.12/site-packages/_pytest/faulthandler.py b/venv/lib/python3.12/site-packages/_pytest/faulthandler.py
new file mode 100644
index 0000000..d8c7e9f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/faulthandler.py
@@ -0,0 +1,102 @@
+import os
+import sys
+from typing import Generator
+
+import pytest
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+from _pytest.stash import StashKey
+
+
+fault_handler_original_stderr_fd_key = StashKey[int]()
+fault_handler_stderr_fd_key = StashKey[int]()
+
+
+def pytest_addoption(parser: Parser) -> None:
+ help = (
+ "Dump the traceback of all threads if a test takes "
+ "more than TIMEOUT seconds to finish"
+ )
+ parser.addini("faulthandler_timeout", help, default=0.0)
+
+
+def pytest_configure(config: Config) -> None:
+ import faulthandler
+
+ # at teardown we want to restore the original faulthandler fileno
+ # but faulthandler has no api to return the original fileno
+ # so here we stash the stderr fileno to be used at teardown
+ # sys.stderr and sys.__stderr__ may be closed or patched during the session
+ # so we can't rely on their values being good at that point (#11572).
+ stderr_fileno = get_stderr_fileno()
+ if faulthandler.is_enabled():
+ config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
+ config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
+ faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
+
+
+def pytest_unconfigure(config: Config) -> None:
+ import faulthandler
+
+ faulthandler.disable()
+ # Close the dup file installed during pytest_configure.
+ if fault_handler_stderr_fd_key in config.stash:
+ os.close(config.stash[fault_handler_stderr_fd_key])
+ del config.stash[fault_handler_stderr_fd_key]
+ # Re-enable the faulthandler if it was originally enabled.
+ if fault_handler_original_stderr_fd_key in config.stash:
+ faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
+ del config.stash[fault_handler_original_stderr_fd_key]
+
+
+def get_stderr_fileno() -> int:
+ try:
+ fileno = sys.stderr.fileno()
+ # The Twisted Logger will return an invalid file descriptor since it is not backed
+ # by an FD. So, let's also forward this to the same code path as with pytest-xdist.
+ if fileno == -1:
+ raise AttributeError()
+ return fileno
+ except (AttributeError, ValueError):
+ # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
+ # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
+ # This is potentially dangerous, but the best we can do.
+ return sys.__stderr__.fileno()
+
+
+def get_timeout_config_value(config: Config) -> float:
+ return float(config.getini("faulthandler_timeout") or 0.0)
+
+
+@pytest.hookimpl(hookwrapper=True, trylast=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
+ timeout = get_timeout_config_value(item.config)
+ if timeout > 0:
+ import faulthandler
+
+ stderr = item.config.stash[fault_handler_stderr_fd_key]
+ faulthandler.dump_traceback_later(timeout, file=stderr)
+ try:
+ yield
+ finally:
+ faulthandler.cancel_dump_traceback_later()
+ else:
+ yield
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_enter_pdb() -> None:
+ """Cancel any traceback dumping due to timeout before entering pdb."""
+ import faulthandler
+
+ faulthandler.cancel_dump_traceback_later()
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_exception_interact() -> None:
+ """Cancel any traceback dumping due to an interactive exception being
+ raised."""
+ import faulthandler
+
+ faulthandler.cancel_dump_traceback_later()
diff --git a/venv/lib/python3.12/site-packages/_pytest/fixtures.py b/venv/lib/python3.12/site-packages/_pytest/fixtures.py
new file mode 100644
index 0000000..0462504
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/fixtures.py
@@ -0,0 +1,1713 @@
+import dataclasses
+import functools
+import inspect
+import os
+import sys
+import warnings
+from collections import defaultdict
+from collections import deque
+from contextlib import suppress
+from pathlib import Path
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generator
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import MutableMapping
+from typing import NoReturn
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+import _pytest
+from _pytest import nodes
+from _pytest._code import getfslineno
+from _pytest._code.code import FormattedExcinfo
+from _pytest._code.code import TerminalRepr
+from _pytest._io import TerminalWriter
+from _pytest.compat import _format_args
+from _pytest.compat import _PytestWrapper
+from _pytest.compat import assert_never
+from _pytest.compat import final
+from _pytest.compat import get_real_func
+from _pytest.compat import get_real_method
+from _pytest.compat import getfuncargnames
+from _pytest.compat import getimfunc
+from _pytest.compat import getlocation
+from _pytest.compat import is_generator
+from _pytest.compat import NOTSET
+from _pytest.compat import NotSetType
+from _pytest.compat import overload
+from _pytest.compat import safe_getattr
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.deprecated import YIELD_FIXTURE
+from _pytest.mark import Mark
+from _pytest.mark import ParameterSet
+from _pytest.mark.structures import MarkDecorator
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.outcomes import TEST_OUTCOME
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.scope import HIGH_SCOPES
+from _pytest.scope import Scope
+from _pytest.stash import StashKey
+
+
+if TYPE_CHECKING:
+ from typing import Deque
+
+ from _pytest.scope import _ScopeName
+ from _pytest.main import Session
+ from _pytest.python import CallSpec2
+ from _pytest.python import Metafunc
+
+
+# The value of the fixture -- return/yield of the fixture function (type variable).
+FixtureValue = TypeVar("FixtureValue")
+# The type of the fixture function (type variable).
+FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
+# The type of a fixture function (type alias generic in fixture value).
+_FixtureFunc = Union[
+ Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]]
+]
+# The type of FixtureDef.cached_result (type alias generic in fixture value).
+_FixtureCachedResult = Union[
+ Tuple[
+ # The result.
+ FixtureValue,
+ # Cache key.
+ object,
+ None,
+ ],
+ Tuple[
+ None,
+ # Cache key.
+ object,
+ # Exc info if raised.
+ Tuple[Type[BaseException], BaseException, TracebackType],
+ ],
+]
+
+
+@dataclasses.dataclass(frozen=True)
+class PseudoFixtureDef(Generic[FixtureValue]):
+ cached_result: "_FixtureCachedResult[FixtureValue]"
+ _scope: Scope
+
+
+def pytest_sessionstart(session: "Session") -> None:
+ session._fixturemanager = FixtureManager(session)
+
+
+def get_scope_package(
+ node: nodes.Item,
+ fixturedef: "FixtureDef[object]",
+) -> Optional[Union[nodes.Item, nodes.Collector]]:
+ from _pytest.python import Package
+
+ current: Optional[Union[nodes.Item, nodes.Collector]] = node
+ fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
+ while current and (
+ not isinstance(current, Package) or fixture_package_name != current.nodeid
+ ):
+ current = current.parent # type: ignore[assignment]
+ if current is None:
+ return node.session
+ return current
+
+
+def get_scope_node(
+ node: nodes.Node, scope: Scope
+) -> Optional[Union[nodes.Item, nodes.Collector]]:
+ import _pytest.python
+
+ if scope is Scope.Function:
+ return node.getparent(nodes.Item)
+ elif scope is Scope.Class:
+ return node.getparent(_pytest.python.Class)
+ elif scope is Scope.Module:
+ return node.getparent(_pytest.python.Module)
+ elif scope is Scope.Package:
+ return node.getparent(_pytest.python.Package)
+ elif scope is Scope.Session:
+ return node.getparent(_pytest.main.Session)
+ else:
+ assert_never(scope)
+
+
+# Used for storing artificial fixturedefs for direct parametrization.
+name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]()
+
+
+def add_funcarg_pseudo_fixture_def(
+ collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
+) -> None:
+ # This function will transform all collected calls to functions
+ # if they use direct funcargs (i.e. direct parametrization)
+ # because we want later test execution to be able to rely on
+ # an existing FixtureDef structure for all arguments.
+ # XXX we can probably avoid this algorithm if we modify CallSpec2
+ # to directly care for creating the fixturedefs within its methods.
+ if not metafunc._calls[0].funcargs:
+ # This function call does not have direct parametrization.
+ return
+ # Collect funcargs of all callspecs into a list of values.
+ arg2params: Dict[str, List[object]] = {}
+ arg2scope: Dict[str, Scope] = {}
+ for callspec in metafunc._calls:
+ for argname, argvalue in callspec.funcargs.items():
+ assert argname not in callspec.params
+ callspec.params[argname] = argvalue
+ arg2params_list = arg2params.setdefault(argname, [])
+ callspec.indices[argname] = len(arg2params_list)
+ arg2params_list.append(argvalue)
+ if argname not in arg2scope:
+ scope = callspec._arg2scope.get(argname, Scope.Function)
+ arg2scope[argname] = scope
+ callspec.funcargs.clear()
+
+ # Register artificial FixtureDef's so that later at test execution
+ # time we can rely on a proper FixtureDef to exist for fixture setup.
+ arg2fixturedefs = metafunc._arg2fixturedefs
+ for argname, valuelist in arg2params.items():
+ # If we have a scope that is higher than function, we need
+ # to make sure we only ever create an according fixturedef on
+ # a per-scope basis. We thus store and cache the fixturedef on the
+ # node related to the scope.
+ scope = arg2scope[argname]
+ node = None
+ if scope is not Scope.Function:
+ node = get_scope_node(collector, scope)
+ if node is None:
+ assert scope is Scope.Class and isinstance(
+ collector, _pytest.python.Module
+ )
+ # Use module-level collector for class-scope (for now).
+ node = collector
+ if node is None:
+ name2pseudofixturedef = None
+ else:
+ default: Dict[str, FixtureDef[Any]] = {}
+ name2pseudofixturedef = node.stash.setdefault(
+ name2pseudofixturedef_key, default
+ )
+ if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
+ arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
+ else:
+ fixturedef = FixtureDef(
+ fixturemanager=fixturemanager,
+ baseid="",
+ argname=argname,
+ func=get_direct_param_fixture_func,
+ scope=arg2scope[argname],
+ params=valuelist,
+ unittest=False,
+ ids=None,
+ )
+ arg2fixturedefs[argname] = [fixturedef]
+ if name2pseudofixturedef is not None:
+ name2pseudofixturedef[argname] = fixturedef
+
+
+def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
+ """Return fixturemarker or None if it doesn't exist or raised
+ exceptions."""
+ return cast(
+ Optional[FixtureFunctionMarker],
+ safe_getattr(obj, "_pytestfixturefunction", None),
+ )
+
+
+# Parametrized fixture key, helper alias for code below.
+_Key = Tuple[object, ...]
+
+
+def get_parametrized_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[_Key]:
+ """Return list of keys for all parametrized arguments which match
+ the specified scope."""
+ assert scope is not Scope.Function
+ try:
+ callspec = item.callspec # type: ignore[attr-defined]
+ except AttributeError:
+ pass
+ else:
+ cs: CallSpec2 = callspec
+ # cs.indices.items() is random order of argnames. Need to
+ # sort this so that different calls to
+ # get_parametrized_fixture_keys will be deterministic.
+ for argname, param_index in sorted(cs.indices.items()):
+ if cs._arg2scope[argname] != scope:
+ continue
+ if scope is Scope.Session:
+ key: _Key = (argname, param_index)
+ elif scope is Scope.Package:
+ key = (argname, param_index, item.path.parent)
+ elif scope is Scope.Module:
+ key = (argname, param_index, item.path)
+ elif scope is Scope.Class:
+ item_cls = item.cls # type: ignore[attr-defined]
+ key = (argname, param_index, item.path, item_cls)
+ else:
+ assert_never(scope)
+ yield key
+
+
+# Algorithm for sorting on a per-parametrized resource setup basis.
+# It is called for Session scope first and performs sorting
+# down to the lower scopes such as to minimize number of "high scope"
+# setups and teardowns.
+
+
+def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
+ argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]] = {}
+ items_by_argkey: Dict[Scope, Dict[_Key, Deque[nodes.Item]]] = {}
+ for scope in HIGH_SCOPES:
+ d: Dict[nodes.Item, Dict[_Key, None]] = {}
+ argkeys_cache[scope] = d
+ item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque)
+ items_by_argkey[scope] = item_d
+ for item in items:
+ keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
+ if keys:
+ d[item] = keys
+ for key in keys:
+ item_d[key].append(item)
+ items_dict = dict.fromkeys(items, None)
+ return list(
+ reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
+ )
+
+
+def fix_cache_order(
+ item: nodes.Item,
+ argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
+ items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
+) -> None:
+ for scope in HIGH_SCOPES:
+ for key in argkeys_cache[scope].get(item, []):
+ items_by_argkey[scope][key].appendleft(item)
+
+
+def reorder_items_atscope(
+ items: Dict[nodes.Item, None],
+ argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[_Key, None]]],
+ items_by_argkey: Dict[Scope, Dict[_Key, "Deque[nodes.Item]"]],
+ scope: Scope,
+) -> Dict[nodes.Item, None]:
+ if scope is Scope.Function or len(items) < 3:
+ return items
+ ignore: Set[Optional[_Key]] = set()
+ items_deque = deque(items)
+ items_done: Dict[nodes.Item, None] = {}
+ scoped_items_by_argkey = items_by_argkey[scope]
+ scoped_argkeys_cache = argkeys_cache[scope]
+ while items_deque:
+ no_argkey_group: Dict[nodes.Item, None] = {}
+ slicing_argkey = None
+ while items_deque:
+ item = items_deque.popleft()
+ if item in items_done or item in no_argkey_group:
+ continue
+ argkeys = dict.fromkeys(
+ (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
+ )
+ if not argkeys:
+ no_argkey_group[item] = None
+ else:
+ slicing_argkey, _ = argkeys.popitem()
+ # We don't have to remove relevant items from later in the
+ # deque because they'll just be ignored.
+ matching_items = [
+ i for i in scoped_items_by_argkey[slicing_argkey] if i in items
+ ]
+ for i in reversed(matching_items):
+ fix_cache_order(i, argkeys_cache, items_by_argkey)
+ items_deque.appendleft(i)
+ break
+ if no_argkey_group:
+ no_argkey_group = reorder_items_atscope(
+ no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower()
+ )
+ for item in no_argkey_group:
+ items_done[item] = None
+ ignore.add(slicing_argkey)
+ return items_done
+
+
+def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
+ return request.param
+
+
+@dataclasses.dataclass
+class FuncFixtureInfo:
+ __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
+
+ # Original function argument names.
+ argnames: Tuple[str, ...]
+ # Argnames that function immediately requires. These include argnames +
+ # fixture names specified via usefixtures and via autouse=True in fixture
+ # definitions.
+ initialnames: Tuple[str, ...]
+ names_closure: List[str]
+ name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
+
+ def prune_dependency_tree(self) -> None:
+ """Recompute names_closure from initialnames and name2fixturedefs.
+
+ Can only reduce names_closure, which means that the new closure will
+ always be a subset of the old one. The order is preserved.
+
+ This method is needed because direct parametrization may shadow some
+ of the fixtures that were included in the originally built dependency
+ tree. In this way the dependency tree can get pruned, and the closure
+ of argnames may get reduced.
+ """
+ closure: Set[str] = set()
+ working_set = set(self.initialnames)
+ while working_set:
+ argname = working_set.pop()
+ # Argname may be smth not included in the original names_closure,
+ # in which case we ignore it. This currently happens with pseudo
+ # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
+ # So they introduce the new dependency 'request' which might have
+ # been missing in the original tree (closure).
+ if argname not in closure and argname in self.names_closure:
+ closure.add(argname)
+ if argname in self.name2fixturedefs:
+ working_set.update(self.name2fixturedefs[argname][-1].argnames)
+
+ self.names_closure[:] = sorted(closure, key=self.names_closure.index)
+
+
+class FixtureRequest:
+ """A request for a fixture from a test or fixture function.
+
+ A request object gives access to the requesting test context and has
+ an optional ``param`` attribute in case the fixture is parametrized
+ indirectly.
+ """
+
+ def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._pyfuncitem = pyfuncitem
+ #: Fixture for which this request is being performed.
+ self.fixturename: Optional[str] = None
+ self._scope = Scope.Function
+ self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
+ fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo
+ self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
+ self._arg2index: Dict[str, int] = {}
+ self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager
+ # Notes on the type of `param`:
+ # -`request.param` is only defined in parametrized fixtures, and will raise
+ # AttributeError otherwise. Python typing has no notion of "undefined", so
+ # this cannot be reflected in the type.
+ # - Technically `param` is only (possibly) defined on SubRequest, not
+ # FixtureRequest, but the typing of that is still in flux so this cheats.
+ # - In the future we might consider using a generic for the param type, but
+ # for now just using Any.
+ self.param: Any
+
+ @property
+ def scope(self) -> "_ScopeName":
+ """Scope string, one of "function", "class", "module", "package", "session"."""
+ return self._scope.value
+
+ @property
+ def fixturenames(self) -> List[str]:
+ """Names of all active fixtures in this request."""
+ result = list(self._pyfuncitem._fixtureinfo.names_closure)
+ result.extend(set(self._fixture_defs).difference(result))
+ return result
+
+ @property
+ def node(self):
+ """Underlying collection node (depends on current request scope)."""
+ scope = self._scope
+ if scope is Scope.Function:
+ # This might also be a non-function Item despite its attribute name.
+ node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
+ elif scope is Scope.Package:
+ # FIXME: _fixturedef is not defined on FixtureRequest (this class),
+ # but on FixtureRequest (a subclass).
+ node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
+ else:
+ node = get_scope_node(self._pyfuncitem, scope)
+ if node is None and scope is Scope.Class:
+ # Fallback to function item itself.
+ node = self._pyfuncitem
+ assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
+ scope, self._pyfuncitem
+ )
+ return node
+
+ def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
+ fixturedefs = self._arg2fixturedefs.get(argname, None)
+ if fixturedefs is None:
+ # We arrive here because of a dynamic call to
+ # getfixturevalue(argname) usage which was naturally
+ # not known at parsing/collection time.
+ assert self._pyfuncitem.parent is not None
+ parentid = self._pyfuncitem.parent.nodeid
+ fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
+ # TODO: Fix this type ignore. Either add assert or adjust types.
+ # Can this be None here?
+ self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment]
+ # fixturedefs list is immutable so we maintain a decreasing index.
+ index = self._arg2index.get(argname, 0) - 1
+ if fixturedefs is None or (-index > len(fixturedefs)):
+ raise FixtureLookupError(argname, self)
+ self._arg2index[argname] = index
+ return fixturedefs[index]
+
+ @property
+ def config(self) -> Config:
+ """The pytest config object associated with this request."""
+ return self._pyfuncitem.config # type: ignore[no-any-return]
+
+ @property
+ def function(self):
+ """Test function object if the request has a per-function scope."""
+ if self.scope != "function":
+ raise AttributeError(
+ f"function not available in {self.scope}-scoped context"
+ )
+ return self._pyfuncitem.obj
+
+ @property
+ def cls(self):
+ """Class (can be None) where the test function was collected."""
+ if self.scope not in ("class", "function"):
+ raise AttributeError(f"cls not available in {self.scope}-scoped context")
+ clscol = self._pyfuncitem.getparent(_pytest.python.Class)
+ if clscol:
+ return clscol.obj
+
+ @property
+ def instance(self):
+ """Instance (can be None) on which test function was collected."""
+ # unittest support hack, see _pytest.unittest.TestCaseFunction.
+ try:
+ return self._pyfuncitem._testcase
+ except AttributeError:
+ function = getattr(self, "function", None)
+ return getattr(function, "__self__", None)
+
+ @property
+ def module(self):
+ """Python module object where the test function was collected."""
+ if self.scope not in ("function", "class", "module"):
+ raise AttributeError(f"module not available in {self.scope}-scoped context")
+ return self._pyfuncitem.getparent(_pytest.python.Module).obj
+
+ @property
+ def path(self) -> Path:
+ """Path where the test function was collected."""
+ if self.scope not in ("function", "class", "module", "package"):
+ raise AttributeError(f"path not available in {self.scope}-scoped context")
+ # TODO: Remove ignore once _pyfuncitem is properly typed.
+ return self._pyfuncitem.path # type: ignore
+
+ @property
+ def keywords(self) -> MutableMapping[str, Any]:
+ """Keywords/markers dictionary for the underlying node."""
+ node: nodes.Node = self.node
+ return node.keywords
+
+ @property
+ def session(self) -> "Session":
+ """Pytest session object."""
+ return self._pyfuncitem.session # type: ignore[no-any-return]
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ """Add finalizer/teardown function to be called without arguments after
+ the last test within the requesting test context finished execution."""
+ # XXX usually this method is shadowed by fixturedef specific ones.
+ self.node.addfinalizer(finalizer)
+
+ def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
+ """Apply a marker to a single test function invocation.
+
+ This method is useful if you don't want to have a keyword/marker
+ on all function invocations.
+
+ :param marker:
+ An object created by a call to ``pytest.mark.NAME(...)``.
+ """
+ self.node.add_marker(marker)
+
+ def raiseerror(self, msg: Optional[str]) -> NoReturn:
+ """Raise a FixtureLookupError exception.
+
+ :param msg:
+ An optional custom error message.
+ """
+ raise self._fixturemanager.FixtureLookupError(None, self, msg)
+
+ def _fillfixtures(self) -> None:
+ item = self._pyfuncitem
+ fixturenames = getattr(item, "fixturenames", self.fixturenames)
+ for argname in fixturenames:
+ if argname not in item.funcargs:
+ item.funcargs[argname] = self.getfixturevalue(argname)
+
+ def getfixturevalue(self, argname: str) -> Any:
+ """Dynamically run a named fixture function.
+
+ Declaring fixtures via function argument is recommended where possible.
+ But if you can only decide whether to use another fixture at test
+ setup time, you may use this function to retrieve it inside a fixture
+ or test function body.
+
+ This method can be used during the test setup phase or the test run
+ phase, but during the test teardown phase a fixture's value may not
+ be available.
+
+ :param argname:
+ The fixture name.
+ :raises pytest.FixtureLookupError:
+ If the given fixture could not be found.
+ """
+ fixturedef = self._get_active_fixturedef(argname)
+ assert fixturedef.cached_result is not None, (
+ f'The fixture value for "{argname}" is not available. '
+ "This can happen when the fixture has already been torn down."
+ )
+ return fixturedef.cached_result[0]
+
+ def _get_active_fixturedef(
+ self, argname: str
+ ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
+ try:
+ return self._fixture_defs[argname]
+ except KeyError:
+ try:
+ fixturedef = self._getnextfixturedef(argname)
+ except FixtureLookupError:
+ if argname == "request":
+ cached_result = (self, [0], None)
+ return PseudoFixtureDef(cached_result, Scope.Function)
+ raise
+ # Remove indent to prevent the python3 exception
+ # from leaking into the call.
+ self._compute_fixture_value(fixturedef)
+ self._fixture_defs[argname] = fixturedef
+ return fixturedef
+
+ def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
+ current = self
+ values: List[FixtureDef[Any]] = []
+ while isinstance(current, SubRequest):
+ values.append(current._fixturedef) # type: ignore[has-type]
+ current = current._parent_request
+ values.reverse()
+ return values
+
+ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
+ """Create a SubRequest based on "self" and call the execute method
+ of the given FixtureDef object.
+
+ This will force the FixtureDef object to throw away any previous
+ results and compute a new fixture value, which will be stored into
+ the FixtureDef object itself.
+ """
+ # prepare a subrequest object before calling fixture function
+ # (latter managed by fixturedef)
+ argname = fixturedef.argname
+ funcitem = self._pyfuncitem
+ scope = fixturedef._scope
+ try:
+ callspec = funcitem.callspec
+ except AttributeError:
+ callspec = None
+ if callspec is not None and argname in callspec.params:
+ param = callspec.params[argname]
+ param_index = callspec.indices[argname]
+ # If a parametrize invocation set a scope it will override
+ # the static scope defined with the fixture function.
+ with suppress(KeyError):
+ scope = callspec._arg2scope[argname]
+ else:
+ param = NOTSET
+ param_index = 0
+ has_params = fixturedef.params is not None
+ fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
+ if has_params and fixtures_not_supported:
+ msg = (
+ "{name} does not support fixtures, maybe unittest.TestCase subclass?\n"
+ "Node id: {nodeid}\n"
+ "Function type: {typename}"
+ ).format(
+ name=funcitem.name,
+ nodeid=funcitem.nodeid,
+ typename=type(funcitem).__name__,
+ )
+ fail(msg, pytrace=False)
+ if has_params:
+ frame = inspect.stack()[3]
+ frameinfo = inspect.getframeinfo(frame[0])
+ source_path = absolutepath(frameinfo.filename)
+ source_lineno = frameinfo.lineno
+ try:
+ source_path_str = str(
+ source_path.relative_to(funcitem.config.rootpath)
+ )
+ except ValueError:
+ source_path_str = str(source_path)
+ msg = (
+ "The requested fixture has no parameter defined for test:\n"
+ " {}\n\n"
+ "Requested fixture '{}' defined in:\n{}"
+ "\n\nRequested here:\n{}:{}".format(
+ funcitem.nodeid,
+ fixturedef.argname,
+ getlocation(fixturedef.func, funcitem.config.rootpath),
+ source_path_str,
+ source_lineno,
+ )
+ )
+ fail(msg, pytrace=False)
+
+ subrequest = SubRequest(
+ self, scope, param, param_index, fixturedef, _ispytest=True
+ )
+
+ # Check if a higher-level scoped fixture accesses a lower level one.
+ subrequest._check_scope(argname, self._scope, scope)
+ try:
+ # Call the fixture function.
+ fixturedef.execute(request=subrequest)
+ finally:
+ self._schedule_finalizers(fixturedef, subrequest)
+
+ def _schedule_finalizers(
+ self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
+ ) -> None:
+ # If fixture function failed it might have registered finalizers.
+ subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest))
+
+ def _check_scope(
+ self,
+ argname: str,
+ invoking_scope: Scope,
+ requested_scope: Scope,
+ ) -> None:
+ if argname == "request":
+ return
+ if invoking_scope > requested_scope:
+ # Try to report something helpful.
+ text = "\n".join(self._factorytraceback())
+ fail(
+ f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
+ f"fixture {argname} with a {invoking_scope.value} scoped request object, "
+ f"involved factories:\n{text}",
+ pytrace=False,
+ )
+
+ def _factorytraceback(self) -> List[str]:
+ lines = []
+ for fixturedef in self._get_fixturestack():
+ factory = fixturedef.func
+ fs, lineno = getfslineno(factory)
+ if isinstance(fs, Path):
+ session: Session = self._pyfuncitem.session
+ p = bestrelpath(session.path, fs)
+ else:
+ p = fs
+ args = _format_args(factory)
+ lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
+ return lines
+
+ def __repr__(self) -> str:
+ return "" % (self.node)
+
+
+@final
+class SubRequest(FixtureRequest):
+ """A sub request for handling getting a fixture from a test function/fixture."""
+
+ def __init__(
+ self,
+ request: "FixtureRequest",
+ scope: Scope,
+ param: Any,
+ param_index: int,
+ fixturedef: "FixtureDef[object]",
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._parent_request = request
+ self.fixturename = fixturedef.argname
+ if param is not NOTSET:
+ self.param = param
+ self.param_index = param_index
+ self._scope = scope
+ self._fixturedef = fixturedef
+ self._pyfuncitem = request._pyfuncitem
+ self._fixture_defs = request._fixture_defs
+ self._arg2fixturedefs = request._arg2fixturedefs
+ self._arg2index = request._arg2index
+ self._fixturemanager = request._fixturemanager
+
+ def __repr__(self) -> str:
+ return f""
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ """Add finalizer/teardown function to be called without arguments after
+ the last test within the requesting test context finished execution."""
+ self._fixturedef.addfinalizer(finalizer)
+
+ def _schedule_finalizers(
+ self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
+ ) -> None:
+ # If the executing fixturedef was not explicitly requested in the argument list (via
+ # getfixturevalue inside the fixture call) then ensure this fixture def will be finished
+ # first.
+ if fixturedef.argname not in self.fixturenames:
+ fixturedef.addfinalizer(
+ functools.partial(self._fixturedef.finish, request=self)
+ )
+ super()._schedule_finalizers(fixturedef, subrequest)
+
+
+@final
+class FixtureLookupError(LookupError):
+ """Could not return a requested fixture (missing or invalid)."""
+
+ def __init__(
+ self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None
+ ) -> None:
+ self.argname = argname
+ self.request = request
+ self.fixturestack = request._get_fixturestack()
+ self.msg = msg
+
+ def formatrepr(self) -> "FixtureLookupErrorRepr":
+ tblines: List[str] = []
+ addline = tblines.append
+ stack = [self.request._pyfuncitem.obj]
+ stack.extend(map(lambda x: x.func, self.fixturestack))
+ msg = self.msg
+ if msg is not None:
+ # The last fixture raise an error, let's present
+ # it at the requesting side.
+ stack = stack[:-1]
+ for function in stack:
+ fspath, lineno = getfslineno(function)
+ try:
+ lines, _ = inspect.getsourcelines(get_real_func(function))
+ except (OSError, IndexError, TypeError):
+ error_msg = "file %s, line %s: source code not available"
+ addline(error_msg % (fspath, lineno + 1))
+ else:
+ addline(f"file {fspath}, line {lineno + 1}")
+ for i, line in enumerate(lines):
+ line = line.rstrip()
+ addline(" " + line)
+ if line.lstrip().startswith("def"):
+ break
+
+ if msg is None:
+ fm = self.request._fixturemanager
+ available = set()
+ parentid = self.request._pyfuncitem.parent.nodeid
+ for name, fixturedefs in fm._arg2fixturedefs.items():
+ faclist = list(fm._matchfactories(fixturedefs, parentid))
+ if faclist:
+ available.add(name)
+ if self.argname in available:
+ msg = " recursive dependency involving fixture '{}' detected".format(
+ self.argname
+ )
+ else:
+ msg = f"fixture '{self.argname}' not found"
+ msg += "\n available fixtures: {}".format(", ".join(sorted(available)))
+ msg += "\n use 'pytest --fixtures [testpath]' for help on them."
+
+ return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
+
+
+class FixtureLookupErrorRepr(TerminalRepr):
+ def __init__(
+ self,
+ filename: Union[str, "os.PathLike[str]"],
+ firstlineno: int,
+ tblines: Sequence[str],
+ errorstring: str,
+ argname: Optional[str],
+ ) -> None:
+ self.tblines = tblines
+ self.errorstring = errorstring
+ self.filename = filename
+ self.firstlineno = firstlineno
+ self.argname = argname
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # tw.line("FixtureLookupError: %s" %(self.argname), red=True)
+ for tbline in self.tblines:
+ tw.line(tbline.rstrip())
+ lines = self.errorstring.split("\n")
+ if lines:
+ tw.line(
+ f"{FormattedExcinfo.fail_marker} {lines[0].strip()}",
+ red=True,
+ )
+ for line in lines[1:]:
+ tw.line(
+ f"{FormattedExcinfo.flow_marker} {line.strip()}",
+ red=True,
+ )
+ tw.line()
+ tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1))
+
+
+def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
+ fs, lineno = getfslineno(fixturefunc)
+ location = f"{fs}:{lineno + 1}"
+ source = _pytest._code.Source(fixturefunc)
+ fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)
+
+
+def call_fixture_func(
+ fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
+) -> FixtureValue:
+ if is_generator(fixturefunc):
+ fixturefunc = cast(
+ Callable[..., Generator[FixtureValue, None, None]], fixturefunc
+ )
+ generator = fixturefunc(**kwargs)
+ try:
+ fixture_result = next(generator)
+ except StopIteration:
+ raise ValueError(f"{request.fixturename} did not yield a value") from None
+ finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
+ request.addfinalizer(finalizer)
+ else:
+ fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
+ fixture_result = fixturefunc(**kwargs)
+ return fixture_result
+
+
+def _teardown_yield_fixture(fixturefunc, it) -> None:
+ """Execute the teardown of a fixture function by advancing the iterator
+ after the yield and ensure the iteration ends (if not it means there is
+ more than one yield in the function)."""
+ try:
+ next(it)
+ except StopIteration:
+ pass
+ else:
+ fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'")
+
+
+def _eval_scope_callable(
+ scope_callable: "Callable[[str, Config], _ScopeName]",
+ fixture_name: str,
+ config: Config,
+) -> "_ScopeName":
+ try:
+ # Type ignored because there is no typing mechanism to specify
+ # keyword arguments, currently.
+ result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
+ except Exception as e:
+ raise TypeError(
+ "Error evaluating {} while defining fixture '{}'.\n"
+ "Expected a function with the signature (*, fixture_name, config)".format(
+ scope_callable, fixture_name
+ )
+ ) from e
+ if not isinstance(result, str):
+ fail(
+ "Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
+ "{!r}".format(scope_callable, fixture_name, result),
+ pytrace=False,
+ )
+ return result
+
+
+@final
+class FixtureDef(Generic[FixtureValue]):
+ """A container for a fixture definition."""
+
+ def __init__(
+ self,
+ fixturemanager: "FixtureManager",
+ baseid: Optional[str],
+ argname: str,
+ func: "_FixtureFunc[FixtureValue]",
+ scope: Union[Scope, "_ScopeName", Callable[[str, Config], "_ScopeName"], None],
+ params: Optional[Sequence[object]],
+ unittest: bool = False,
+ ids: Optional[
+ Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
+ ] = None,
+ ) -> None:
+ self._fixturemanager = fixturemanager
+ # The "base" node ID for the fixture.
+ #
+ # This is a node ID prefix. A fixture is only available to a node (e.g.
+ # a `Function` item) if the fixture's baseid is a parent of the node's
+ # nodeid (see the `iterparentnodeids` function for what constitutes a
+ # "parent" and a "prefix" in this context).
+ #
+ # For a fixture found in a Collector's object (e.g. a `Module`s module,
+ # a `Class`'s class), the baseid is the Collector's nodeid.
+ #
+ # For a fixture found in a conftest plugin, the baseid is the conftest's
+ # directory path relative to the rootdir.
+ #
+ # For other plugins, the baseid is the empty string (always matches).
+ self.baseid = baseid or ""
+ # Whether the fixture was found from a node or a conftest in the
+ # collection tree. Will be false for fixtures defined in non-conftest
+ # plugins.
+ self.has_location = baseid is not None
+ # The fixture factory function.
+ self.func = func
+ # The name by which the fixture may be requested.
+ self.argname = argname
+ if scope is None:
+ scope = Scope.Function
+ elif callable(scope):
+ scope = _eval_scope_callable(scope, argname, fixturemanager.config)
+ if isinstance(scope, str):
+ scope = Scope.from_user(
+ scope, descr=f"Fixture '{func.__name__}'", where=baseid
+ )
+ self._scope = scope
+ # If the fixture is directly parametrized, the parameter values.
+ self.params: Optional[Sequence[object]] = params
+ # If the fixture is directly parametrized, a tuple of explicit IDs to
+ # assign to the parameter values, or a callable to generate an ID given
+ # a parameter value.
+ self.ids = ids
+ # The names requested by the fixtures.
+ self.argnames = getfuncargnames(func, name=argname, is_method=unittest)
+ # Whether the fixture was collected from a unittest TestCase class.
+ # Note that it really only makes sense to define autouse fixtures in
+ # unittest TestCases.
+ self.unittest = unittest
+ # If the fixture was executed, the current value of the fixture.
+ # Can change if the fixture is executed with different parameters.
+ self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
+ self._finalizers: List[Callable[[], object]] = []
+
+ @property
+ def scope(self) -> "_ScopeName":
+ """Scope string, one of "function", "class", "module", "package", "session"."""
+ return self._scope.value
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ self._finalizers.append(finalizer)
+
+ def finish(self, request: SubRequest) -> None:
+ exc = None
+ try:
+ while self._finalizers:
+ try:
+ func = self._finalizers.pop()
+ func()
+ except BaseException as e:
+ # XXX Only first exception will be seen by user,
+ # ideally all should be reported.
+ if exc is None:
+ exc = e
+ if exc:
+ raise exc
+ finally:
+ ihook = request.node.ihook
+ ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
+ # Even if finalization fails, we invalidate the cached fixture
+ # value and remove all finalizers because they may be bound methods
+ # which will keep instances alive.
+ self.cached_result = None
+ self._finalizers = []
+
+ def execute(self, request: SubRequest) -> FixtureValue:
+ # Get required arguments and register our own finish()
+ # with their finalization.
+ for argname in self.argnames:
+ fixturedef = request._get_active_fixturedef(argname)
+ if argname != "request":
+ # PseudoFixtureDef is only for "request".
+ assert isinstance(fixturedef, FixtureDef)
+ fixturedef.addfinalizer(functools.partial(self.finish, request=request))
+
+ my_cache_key = self.cache_key(request)
+ if self.cached_result is not None:
+ # note: comparison with `==` can fail (or be expensive) for e.g.
+ # numpy arrays (#6497).
+ cache_key = self.cached_result[1]
+ if my_cache_key is cache_key:
+ if self.cached_result[2] is not None:
+ _, val, tb = self.cached_result[2]
+ raise val.with_traceback(tb)
+ else:
+ result = self.cached_result[0]
+ return result
+ # We have a previous but differently parametrized fixture instance
+ # so we need to tear it down before creating a new one.
+ self.finish(request)
+ assert self.cached_result is None
+
+ ihook = request.node.ihook
+ result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
+ return result
+
+ def cache_key(self, request: SubRequest) -> object:
+ return request.param_index if not hasattr(request, "param") else request.param
+
+ def __repr__(self) -> str:
+ return "".format(
+ self.argname, self.scope, self.baseid
+ )
+
+
+def resolve_fixture_function(
+ fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
+) -> "_FixtureFunc[FixtureValue]":
+ """Get the actual callable that can be called to obtain the fixture
+ value, dealing with unittest-specific instances and bound methods."""
+ fixturefunc = fixturedef.func
+ if fixturedef.unittest:
+ if request.instance is not None:
+ # Bind the unbound method to the TestCase instance.
+ fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
+ else:
+ # The fixture function needs to be bound to the actual
+ # request.instance so that code working with "fixturedef" behaves
+ # as expected.
+ if request.instance is not None:
+ # Handle the case where fixture is defined not in a test class, but some other class
+ # (for example a plugin class with a fixture), see #2270.
+ if hasattr(fixturefunc, "__self__") and not isinstance(
+ request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
+ ):
+ return fixturefunc
+ fixturefunc = getimfunc(fixturedef.func)
+ if fixturefunc != fixturedef.func:
+ fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr]
+ return fixturefunc
+
+
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[FixtureValue], request: SubRequest
+) -> FixtureValue:
+ """Execution of fixture setup."""
+ kwargs = {}
+ for argname in fixturedef.argnames:
+ fixdef = request._get_active_fixturedef(argname)
+ assert fixdef.cached_result is not None
+ result, arg_cache_key, exc = fixdef.cached_result
+ request._check_scope(argname, request._scope, fixdef._scope)
+ kwargs[argname] = result
+
+ fixturefunc = resolve_fixture_function(fixturedef, request)
+ my_cache_key = fixturedef.cache_key(request)
+ try:
+ result = call_fixture_func(fixturefunc, request, kwargs)
+ except TEST_OUTCOME:
+ exc_info = sys.exc_info()
+ assert exc_info[0] is not None
+ if isinstance(
+ exc_info[1], skip.Exception
+ ) and not fixturefunc.__name__.startswith("xunit_setup"):
+ exc_info[1]._use_item_location = True # type: ignore[attr-defined]
+ fixturedef.cached_result = (None, my_cache_key, exc_info)
+ raise
+ fixturedef.cached_result = (result, my_cache_key, None)
+ return result
+
+
+def _ensure_immutable_ids(
+ ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
+) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
+ if ids is None:
+ return None
+ if callable(ids):
+ return ids
+ return tuple(ids)
+
+
+def _params_converter(
+ params: Optional[Iterable[object]],
+) -> Optional[Tuple[object, ...]]:
+ return tuple(params) if params is not None else None
+
+
+def wrap_function_to_error_out_if_called_directly(
+ function: FixtureFunction,
+ fixture_marker: "FixtureFunctionMarker",
+) -> FixtureFunction:
+ """Wrap the given fixture function so we can raise an error about it being called directly,
+ instead of used as an argument in a test function."""
+ message = (
+ 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
+ "but are created automatically when test functions request them as parameters.\n"
+ "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
+ "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code."
+ ).format(name=fixture_marker.name or function.__name__)
+
+ @functools.wraps(function)
+ def result(*args, **kwargs):
+ fail(message, pytrace=False)
+
+ # Keep reference to the original function in our own custom attribute so we don't unwrap
+ # further than this point and lose useful wrappings like @mock.patch (#3774).
+ result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
+
+ return cast(FixtureFunction, result)
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class FixtureFunctionMarker:
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
+ params: Optional[Tuple[object, ...]]
+ autouse: bool = False
+ ids: Optional[
+ Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
+ ] = None
+ name: Optional[str] = None
+
+ _ispytest: dataclasses.InitVar[bool] = False
+
+ def __post_init__(self, _ispytest: bool) -> None:
+ check_ispytest(_ispytest)
+
+ def __call__(self, function: FixtureFunction) -> FixtureFunction:
+ if inspect.isclass(function):
+ raise ValueError("class fixtures not supported (maybe in the future)")
+
+ if getattr(function, "_pytestfixturefunction", False):
+ raise ValueError(
+ "fixture is being applied more than once to the same function"
+ )
+
+ function = wrap_function_to_error_out_if_called_directly(function, self)
+
+ name = self.name or function.__name__
+ if name == "request":
+ location = getlocation(function)
+ fail(
+ "'request' is a reserved word for fixtures, use another name:\n {}".format(
+ location
+ ),
+ pytrace=False,
+ )
+
+ # Type ignored because https://github.com/python/mypy/issues/2087.
+ function._pytestfixturefunction = self # type: ignore[attr-defined]
+ return function
+
+
+@overload
+def fixture(
+ fixture_function: FixtureFunction,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
+ params: Optional[Iterable[object]] = ...,
+ autouse: bool = ...,
+ ids: Optional[
+ Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
+ ] = ...,
+ name: Optional[str] = ...,
+) -> FixtureFunction:
+ ...
+
+
+@overload
+def fixture( # noqa: F811
+ fixture_function: None = ...,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
+ params: Optional[Iterable[object]] = ...,
+ autouse: bool = ...,
+ ids: Optional[
+ Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
+ ] = ...,
+ name: Optional[str] = None,
+) -> FixtureFunctionMarker:
+ ...
+
+
+def fixture( # noqa: F811
+ fixture_function: Optional[FixtureFunction] = None,
+ *,
+ scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
+ params: Optional[Iterable[object]] = None,
+ autouse: bool = False,
+ ids: Optional[
+ Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
+ ] = None,
+ name: Optional[str] = None,
+) -> Union[FixtureFunctionMarker, FixtureFunction]:
+ """Decorator to mark a fixture factory function.
+
+ This decorator can be used, with or without parameters, to define a
+ fixture function.
+
+ The name of the fixture function can later be referenced to cause its
+ invocation ahead of running tests: test modules or classes can use the
+ ``pytest.mark.usefixtures(fixturename)`` marker.
+
+ Test functions can directly use fixture names as input arguments in which
+ case the fixture instance returned from the fixture function will be
+ injected.
+
+ Fixtures can provide their values to test functions using ``return`` or
+ ``yield`` statements. When using ``yield`` the code block after the
+ ``yield`` statement is executed as teardown code regardless of the test
+ outcome, and must yield exactly once.
+
+ :param scope:
+ The scope for which this fixture is shared; one of ``"function"``
+ (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``.
+
+ This parameter may also be a callable which receives ``(fixture_name, config)``
+ as parameters, and must return a ``str`` with one of the values mentioned above.
+
+ See :ref:`dynamic scope` in the docs for more information.
+
+ :param params:
+ An optional list of parameters which will cause multiple invocations
+ of the fixture function and all of the tests using it. The current
+ parameter is available in ``request.param``.
+
+ :param autouse:
+ If True, the fixture func is activated for all tests that can see it.
+ If False (the default), an explicit reference is needed to activate
+ the fixture.
+
+ :param ids:
+ Sequence of ids each corresponding to the params so that they are
+ part of the test id. If no ids are provided they will be generated
+ automatically from the params.
+
+ :param name:
+ The name of the fixture. This defaults to the name of the decorated
+ function. If a fixture is used in the same module in which it is
+ defined, the function name of the fixture will be shadowed by the
+ function arg that requests the fixture; one way to resolve this is to
+ name the decorated function ``fixture_`` and then use
+ ``@pytest.fixture(name='')``.
+ """
+ fixture_marker = FixtureFunctionMarker(
+ scope=scope,
+ params=tuple(params) if params is not None else None,
+ autouse=autouse,
+ ids=None if ids is None else ids if callable(ids) else tuple(ids),
+ name=name,
+ _ispytest=True,
+ )
+
+ # Direct decoration.
+ if fixture_function:
+ return fixture_marker(fixture_function)
+
+ return fixture_marker
+
+
+def yield_fixture(
+ fixture_function=None,
+ *args,
+ scope="function",
+ params=None,
+ autouse=False,
+ ids=None,
+ name=None,
+):
+ """(Return a) decorator to mark a yield-fixture factory function.
+
+ .. deprecated:: 3.0
+ Use :py:func:`pytest.fixture` directly instead.
+ """
+ warnings.warn(YIELD_FIXTURE, stacklevel=2)
+ return fixture(
+ fixture_function,
+ *args,
+ scope=scope,
+ params=params,
+ autouse=autouse,
+ ids=ids,
+ name=name,
+ )
+
+
+@fixture(scope="session")
+def pytestconfig(request: FixtureRequest) -> Config:
+ """Session-scoped fixture that returns the session's :class:`pytest.Config`
+ object.
+
+ Example::
+
+ def test_foo(pytestconfig):
+ if pytestconfig.getoption("verbose") > 0:
+ ...
+
+ """
+ return request.config
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "usefixtures",
+ type="args",
+ default=[],
+ help="List of default fixtures to be used with this project",
+ )
+
+
+class FixtureManager:
+ """pytest fixture definitions and information is stored and managed
+ from this class.
+
+ During collection fm.parsefactories() is called multiple times to parse
+ fixture function definitions into FixtureDef objects and internal
+ data structures.
+
+ During collection of test functions, metafunc-mechanics instantiate
+ a FuncFixtureInfo object which is cached per node/func-name.
+ This FuncFixtureInfo object is later retrieved by Function nodes
+ which themselves offer a fixturenames attribute.
+
+ The FuncFixtureInfo object holds information about fixtures and FixtureDefs
+ relevant for a particular function. An initial list of fixtures is
+ assembled like this:
+
+ - ini-defined usefixtures
+ - autouse-marked fixtures along the collection chain up from the function
+ - usefixtures markers at module/class/function level
+ - test function funcargs
+
+ Subsequently the funcfixtureinfo.fixturenames attribute is computed
+ as the closure of the fixtures needed to setup the initial fixtures,
+ i.e. fixtures needed by fixture functions themselves are appended
+ to the fixturenames list.
+
+ Upon the test-setup phases all fixturenames are instantiated, retrieved
+ by a lookup of their FuncFixtureInfo.
+ """
+
+ FixtureLookupError = FixtureLookupError
+ FixtureLookupErrorRepr = FixtureLookupErrorRepr
+
+ def __init__(self, session: "Session") -> None:
+ self.session = session
+ self.config: Config = session.config
+ self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {}
+ self._holderobjseen: Set[object] = set()
+ # A mapping from a nodeid to a list of autouse fixtures it defines.
+ self._nodeid_autousenames: Dict[str, List[str]] = {
+ "": self.config.getini("usefixtures"),
+ }
+ session.config.pluginmanager.register(self, "funcmanage")
+
+ def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]:
+ """Return all direct parametrization arguments of a node, so we don't
+ mistake them for fixtures.
+
+ Check https://github.com/pytest-dev/pytest/issues/5036.
+
+ These things are done later as well when dealing with parametrization
+ so this could be improved.
+ """
+ parametrize_argnames: List[str] = []
+ for marker in node.iter_markers(name="parametrize"):
+ if not marker.kwargs.get("indirect", False):
+ p_argnames, _ = ParameterSet._parse_parametrize_args(
+ *marker.args, **marker.kwargs
+ )
+ parametrize_argnames.extend(p_argnames)
+
+ return parametrize_argnames
+
+ def getfixtureinfo(
+ self, node: nodes.Node, func, cls, funcargs: bool = True
+ ) -> FuncFixtureInfo:
+ if funcargs and not getattr(node, "nofuncargs", False):
+ argnames = getfuncargnames(func, name=node.name, cls=cls)
+ else:
+ argnames = ()
+
+ usefixtures = tuple(
+ arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
+ )
+ initialnames = usefixtures + argnames
+ fm = node.session._fixturemanager
+ initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
+ initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
+ )
+ return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
+
+ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
+ nodeid = None
+ try:
+ p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
+ except AttributeError:
+ pass
+ else:
+ # Construct the base nodeid which is later used to check
+ # what fixtures are visible for particular tests (as denoted
+ # by their test id).
+ if p.name.startswith("conftest.py"):
+ try:
+ nodeid = str(p.parent.relative_to(self.config.rootpath))
+ except ValueError:
+ nodeid = ""
+ if nodeid == ".":
+ nodeid = ""
+ if os.sep != nodes.SEP:
+ nodeid = nodeid.replace(os.sep, nodes.SEP)
+
+ self.parsefactories(plugin, nodeid)
+
+ def _getautousenames(self, nodeid: str) -> Iterator[str]:
+ """Return the names of autouse fixtures applicable to nodeid."""
+ for parentnodeid in nodes.iterparentnodeids(nodeid):
+ basenames = self._nodeid_autousenames.get(parentnodeid)
+ if basenames:
+ yield from basenames
+
+ def getfixtureclosure(
+ self,
+ fixturenames: Tuple[str, ...],
+ parentnode: nodes.Node,
+ ignore_args: Sequence[str] = (),
+ ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
+ # Collect the closure of all fixtures, starting with the given
+ # fixturenames as the initial set. As we have to visit all
+ # factory definitions anyway, we also return an arg2fixturedefs
+ # mapping so that the caller can reuse it and does not have
+ # to re-discover fixturedefs again for each fixturename
+ # (discovering matching fixtures for a given name/node is expensive).
+
+ parentid = parentnode.nodeid
+ fixturenames_closure = list(self._getautousenames(parentid))
+
+ def merge(otherlist: Iterable[str]) -> None:
+ for arg in otherlist:
+ if arg not in fixturenames_closure:
+ fixturenames_closure.append(arg)
+
+ merge(fixturenames)
+
+ # At this point, fixturenames_closure contains what we call "initialnames",
+ # which is a set of fixturenames the function immediately requests. We
+ # need to return it as well, so save this.
+ initialnames = tuple(fixturenames_closure)
+
+ arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
+ lastlen = -1
+ while lastlen != len(fixturenames_closure):
+ lastlen = len(fixturenames_closure)
+ for argname in fixturenames_closure:
+ if argname in ignore_args:
+ continue
+ if argname in arg2fixturedefs:
+ continue
+ fixturedefs = self.getfixturedefs(argname, parentid)
+ if fixturedefs:
+ arg2fixturedefs[argname] = fixturedefs
+ merge(fixturedefs[-1].argnames)
+
+ def sort_by_scope(arg_name: str) -> Scope:
+ try:
+ fixturedefs = arg2fixturedefs[arg_name]
+ except KeyError:
+ return Scope.Function
+ else:
+ return fixturedefs[-1]._scope
+
+ fixturenames_closure.sort(key=sort_by_scope, reverse=True)
+ return initialnames, fixturenames_closure, arg2fixturedefs
+
+ def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
+ """Generate new tests based on parametrized fixtures used by the given metafunc"""
+
+ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
+ args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs)
+ return args
+
+ for argname in metafunc.fixturenames:
+ # Get the FixtureDefs for the argname.
+ fixture_defs = metafunc._arg2fixturedefs.get(argname)
+ if not fixture_defs:
+ # Will raise FixtureLookupError at setup time if not parametrized somewhere
+ # else (e.g @pytest.mark.parametrize)
+ continue
+
+ # If the test itself parametrizes using this argname, give it
+ # precedence.
+ if any(
+ argname in get_parametrize_mark_argnames(mark)
+ for mark in metafunc.definition.iter_markers("parametrize")
+ ):
+ continue
+
+ # In the common case we only look at the fixture def with the
+ # closest scope (last in the list). But if the fixture overrides
+ # another fixture, while requesting the super fixture, keep going
+ # in case the super fixture is parametrized (#1953).
+ for fixturedef in reversed(fixture_defs):
+ # Fixture is parametrized, apply it and stop.
+ if fixturedef.params is not None:
+ metafunc.parametrize(
+ argname,
+ fixturedef.params,
+ indirect=True,
+ scope=fixturedef.scope,
+ ids=fixturedef.ids,
+ )
+ break
+
+ # Not requesting the overridden super fixture, stop.
+ if argname not in fixturedef.argnames:
+ break
+
+ # Try next super fixture, if any.
+
+ def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
+ # Separate parametrized setups.
+ items[:] = reorder_items(items)
+
+ @overload
+ def parsefactories(
+ self,
+ node_or_obj: nodes.Node,
+ *,
+ unittest: bool = ...,
+ ) -> None:
+ raise NotImplementedError()
+
+ @overload
+ def parsefactories( # noqa: F811
+ self,
+ node_or_obj: object,
+ nodeid: Optional[str],
+ *,
+ unittest: bool = ...,
+ ) -> None:
+ raise NotImplementedError()
+
+ def parsefactories( # noqa: F811
+ self,
+ node_or_obj: Union[nodes.Node, object],
+ nodeid: Union[str, NotSetType, None] = NOTSET,
+ *,
+ unittest: bool = False,
+ ) -> None:
+ """Collect fixtures from a collection node or object.
+
+ Found fixtures are parsed into `FixtureDef`s and saved.
+
+ If `node_or_object` is a collection node (with an underlying Python
+ object), the node's object is traversed and the node's nodeid is used to
+ determine the fixtures' visibilty. `nodeid` must not be specified in
+ this case.
+
+ If `node_or_object` is an object (e.g. a plugin), the object is
+ traversed and the given `nodeid` is used to determine the fixtures'
+ visibility. `nodeid` must be specified in this case; None and "" mean
+ total visibility.
+ """
+ if nodeid is not NOTSET:
+ holderobj = node_or_obj
+ else:
+ assert isinstance(node_or_obj, nodes.Node)
+ holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
+ assert isinstance(node_or_obj.nodeid, str)
+ nodeid = node_or_obj.nodeid
+ if holderobj in self._holderobjseen:
+ return
+
+ self._holderobjseen.add(holderobj)
+ autousenames = []
+ for name in dir(holderobj):
+ # ugly workaround for one of the fspath deprecated property of node
+ # todo: safely generalize
+ if isinstance(holderobj, nodes.Node) and name == "fspath":
+ continue
+
+ # The attribute can be an arbitrary descriptor, so the attribute
+ # access below can raise. safe_getatt() ignores such exceptions.
+ obj = safe_getattr(holderobj, name, None)
+ marker = getfixturemarker(obj)
+ if not isinstance(marker, FixtureFunctionMarker):
+ # Magic globals with __getattr__ might have got us a wrong
+ # fixture attribute.
+ continue
+
+ if marker.name:
+ name = marker.name
+
+ # During fixture definition we wrap the original fixture function
+ # to issue a warning if called directly, so here we unwrap it in
+ # order to not emit the warning when pytest itself calls the
+ # fixture function.
+ obj = get_real_method(obj, holderobj)
+
+ fixture_def = FixtureDef(
+ fixturemanager=self,
+ baseid=nodeid,
+ argname=name,
+ func=obj,
+ scope=marker.scope,
+ params=marker.params,
+ unittest=unittest,
+ ids=marker.ids,
+ )
+
+ faclist = self._arg2fixturedefs.setdefault(name, [])
+ if fixture_def.has_location:
+ faclist.append(fixture_def)
+ else:
+ # fixturedefs with no location are at the front
+ # so this inserts the current fixturedef after the
+ # existing fixturedefs from external plugins but
+ # before the fixturedefs provided in conftests.
+ i = len([f for f in faclist if not f.has_location])
+ faclist.insert(i, fixture_def)
+ if marker.autouse:
+ autousenames.append(name)
+
+ if autousenames:
+ self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
+
+ def getfixturedefs(
+ self, argname: str, nodeid: str
+ ) -> Optional[Sequence[FixtureDef[Any]]]:
+ """Get a list of fixtures which are applicable to the given node id.
+
+ :param str argname: Name of the fixture to search for.
+ :param str nodeid: Full node id of the requesting test.
+ :rtype: Sequence[FixtureDef]
+ """
+ try:
+ fixturedefs = self._arg2fixturedefs[argname]
+ except KeyError:
+ return None
+ return tuple(self._matchfactories(fixturedefs, nodeid))
+
+ def _matchfactories(
+ self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
+ ) -> Iterator[FixtureDef[Any]]:
+ parentnodeids = set(nodes.iterparentnodeids(nodeid))
+ for fixturedef in fixturedefs:
+ if fixturedef.baseid in parentnodeids:
+ yield fixturedef
diff --git a/venv/lib/python3.12/site-packages/_pytest/freeze_support.py b/venv/lib/python3.12/site-packages/_pytest/freeze_support.py
new file mode 100644
index 0000000..9f8ea23
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/freeze_support.py
@@ -0,0 +1,44 @@
+"""Provides a function to report all internal modules for using freezing
+tools."""
+import types
+from typing import Iterator
+from typing import List
+from typing import Union
+
+
+def freeze_includes() -> List[str]:
+ """Return a list of module names used by pytest that should be
+ included by cx_freeze."""
+ import _pytest
+
+ result = list(_iter_all_modules(_pytest))
+ return result
+
+
+def _iter_all_modules(
+ package: Union[str, types.ModuleType],
+ prefix: str = "",
+) -> Iterator[str]:
+ """Iterate over the names of all modules that can be found in the given
+ package, recursively.
+
+ >>> import _pytest
+ >>> list(_iter_all_modules(_pytest))
+ ['_pytest._argcomplete', '_pytest._code.code', ...]
+ """
+ import os
+ import pkgutil
+
+ if isinstance(package, str):
+ path = package
+ else:
+ # Type ignored because typeshed doesn't define ModuleType.__path__
+ # (only defined on packages).
+ package_path = package.__path__ # type: ignore[attr-defined]
+ path, prefix = package_path[0], package.__name__ + "."
+ for _, name, is_package in pkgutil.iter_modules([path]):
+ if is_package:
+ for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."):
+ yield prefix + m
+ else:
+ yield prefix + name
diff --git a/venv/lib/python3.12/site-packages/_pytest/helpconfig.py b/venv/lib/python3.12/site-packages/_pytest/helpconfig.py
new file mode 100644
index 0000000..ea16c43
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/helpconfig.py
@@ -0,0 +1,270 @@
+"""Version info, help messages, tracing configuration."""
+import os
+import sys
+from argparse import Action
+from typing import List
+from typing import Optional
+from typing import Union
+
+import pytest
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import PrintHelp
+from _pytest.config.argparsing import Parser
+from _pytest.terminal import TerminalReporter
+
+
+class HelpAction(Action):
+ """An argparse Action that will raise an exception in order to skip the
+ rest of the argument parsing when --help is passed.
+
+ This prevents argparse from quitting due to missing required arguments
+ when any are defined, for example by ``pytest_addoption``.
+ This is similar to the way that the builtin argparse --help option is
+ implemented by raising SystemExit.
+ """
+
+ def __init__(self, option_strings, dest=None, default=False, help=None):
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ const=True,
+ default=default,
+ nargs=0,
+ help=help,
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ setattr(namespace, self.dest, self.const)
+
+ # We should only skip the rest of the parsing after preparse is done.
+ if getattr(parser._parser, "after_preparse", False):
+ raise PrintHelp
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--version",
+ "-V",
+ action="count",
+ default=0,
+ dest="version",
+ help="Display pytest version and information about plugins. "
+ "When given twice, also display information about plugins.",
+ )
+ group._addoption(
+ "-h",
+ "--help",
+ action=HelpAction,
+ dest="help",
+ help="Show help message and configuration info",
+ )
+ group._addoption(
+ "-p",
+ action="append",
+ dest="plugins",
+ default=[],
+ metavar="name",
+ help="Early-load given plugin module name or entry point (multi-allowed). "
+ "To avoid loading of plugins, use the `no:` prefix, e.g. "
+ "`no:doctest`.",
+ )
+ group.addoption(
+ "--traceconfig",
+ "--trace-config",
+ action="store_true",
+ default=False,
+ help="Trace considerations of conftest.py files",
+ )
+ group.addoption(
+ "--debug",
+ action="store",
+ nargs="?",
+ const="pytestdebug.log",
+ dest="debug",
+ metavar="DEBUG_FILE_NAME",
+ help="Store internal tracing debug information in this log file. "
+ "This file is opened with 'w' and truncated as a result, care advised. "
+ "Default: pytestdebug.log.",
+ )
+ group._addoption(
+ "-o",
+ "--override-ini",
+ dest="override_ini",
+ action="append",
+ help='Override ini option with "option=value" style, '
+ "e.g. `-o xfail_strict=True -o cache_dir=cache`.",
+ )
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_cmdline_parse():
+ outcome = yield
+ config: Config = outcome.get_result()
+
+ if config.option.debug:
+ # --debug | --debug was provided.
+ path = config.option.debug
+ debugfile = open(path, "w", encoding="utf-8")
+ debugfile.write(
+ "versions pytest-%s, "
+ "python-%s\ncwd=%s\nargs=%s\n\n"
+ % (
+ pytest.__version__,
+ ".".join(map(str, sys.version_info)),
+ os.getcwd(),
+ config.invocation_params.args,
+ )
+ )
+ config.trace.root.setwriter(debugfile.write)
+ undo_tracing = config.pluginmanager.enable_tracing()
+ sys.stderr.write("writing pytest debug information to %s\n" % path)
+
+ def unset_tracing() -> None:
+ debugfile.close()
+ sys.stderr.write("wrote pytest debug information to %s\n" % debugfile.name)
+ config.trace.root.setwriter(None)
+ undo_tracing()
+
+ config.add_cleanup(unset_tracing)
+
+
+def showversion(config: Config) -> None:
+ if config.option.version > 1:
+ sys.stdout.write(
+ "This is pytest version {}, imported from {}\n".format(
+ pytest.__version__, pytest.__file__
+ )
+ )
+ plugininfo = getpluginversioninfo(config)
+ if plugininfo:
+ for line in plugininfo:
+ sys.stdout.write(line + "\n")
+ else:
+ sys.stdout.write(f"pytest {pytest.__version__}\n")
+
+
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.version > 0:
+ showversion(config)
+ return 0
+ elif config.option.help:
+ config._do_configure()
+ showhelp(config)
+ config._ensure_unconfigure()
+ return 0
+ return None
+
+
+def showhelp(config: Config) -> None:
+ import textwrap
+
+ reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
+ "terminalreporter"
+ )
+ assert reporter is not None
+ tw = reporter._tw
+ tw.write(config._parser.optparser.format_help())
+ tw.line()
+ tw.line(
+ "[pytest] ini-options in the first "
+ "pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
+ )
+ tw.line()
+
+ columns = tw.fullwidth # costly call
+ indent_len = 24 # based on argparse's max_help_position=24
+ indent = " " * indent_len
+ for name in config._parser._ininames:
+ help, type, default = config._parser._inidict[name]
+ if type is None:
+ type = "string"
+ if help is None:
+ raise TypeError(f"help argument cannot be None for {name}")
+ spec = f"{name} ({type}):"
+ tw.write(" %s" % spec)
+ spec_len = len(spec)
+ if spec_len > (indent_len - 3):
+ # Display help starting at a new line.
+ tw.line()
+ helplines = textwrap.wrap(
+ help,
+ columns,
+ initial_indent=indent,
+ subsequent_indent=indent,
+ break_on_hyphens=False,
+ )
+
+ for line in helplines:
+ tw.line(line)
+ else:
+ # Display help starting after the spec, following lines indented.
+ tw.write(" " * (indent_len - spec_len - 2))
+ wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False)
+
+ if wrapped:
+ tw.line(wrapped[0])
+ for line in wrapped[1:]:
+ tw.line(indent + line)
+
+ tw.line()
+ tw.line("Environment variables:")
+ vars = [
+ ("PYTEST_ADDOPTS", "Extra command line options"),
+ ("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"),
+ ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"),
+ ("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"),
+ ]
+ for name, help in vars:
+ tw.line(f" {name:<24} {help}")
+ tw.line()
+ tw.line()
+
+ tw.line("to see available markers type: pytest --markers")
+ tw.line("to see available fixtures type: pytest --fixtures")
+ tw.line(
+ "(shown according to specified file_or_dir or current dir "
+ "if not specified; fixtures with leading '_' are only shown "
+ "with the '-v' option"
+ )
+
+ for warningreport in reporter.stats.get("warnings", []):
+ tw.line("warning : " + warningreport.message, red=True)
+ return
+
+
+conftest_options = [("pytest_plugins", "list of plugin names to load")]
+
+
+def getpluginversioninfo(config: Config) -> List[str]:
+ lines = []
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
+ if plugininfo:
+ lines.append("setuptools registered plugins:")
+ for plugin, dist in plugininfo:
+ loc = getattr(plugin, "__file__", repr(plugin))
+ content = f"{dist.project_name}-{dist.version} at {loc}"
+ lines.append(" " + content)
+ return lines
+
+
+def pytest_report_header(config: Config) -> List[str]:
+ lines = []
+ if config.option.debug or config.option.traceconfig:
+ lines.append(f"using: pytest-{pytest.__version__}")
+
+ verinfo = getpluginversioninfo(config)
+ if verinfo:
+ lines.extend(verinfo)
+
+ if config.option.traceconfig:
+ lines.append("active plugins:")
+ items = config.pluginmanager.list_name_plugin()
+ for name, plugin in items:
+ if hasattr(plugin, "__file__"):
+ r = plugin.__file__
+ else:
+ r = repr(plugin)
+ lines.append(f" {name:<20}: {r}")
+ return lines
diff --git a/venv/lib/python3.12/site-packages/_pytest/hookspec.py b/venv/lib/python3.12/site-packages/_pytest/hookspec.py
new file mode 100644
index 0000000..1f7c368
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/hookspec.py
@@ -0,0 +1,979 @@
+"""Hook specifications for pytest plugins which are invoked by pytest itself
+and by builtin plugins."""
+from pathlib import Path
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from pluggy import HookspecMarker
+
+from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
+
+if TYPE_CHECKING:
+ import pdb
+ import warnings
+ from typing_extensions import Literal
+
+ from _pytest._code.code import ExceptionRepr
+ from _pytest._code.code import ExceptionInfo
+ from _pytest.config import Config
+ from _pytest.config import ExitCode
+ from _pytest.config import PytestPluginManager
+ from _pytest.config import _PluggyPlugin
+ from _pytest.config.argparsing import Parser
+ from _pytest.fixtures import FixtureDef
+ from _pytest.fixtures import SubRequest
+ from _pytest.main import Session
+ from _pytest.nodes import Collector
+ from _pytest.nodes import Item
+ from _pytest.outcomes import Exit
+ from _pytest.python import Class
+ from _pytest.python import Function
+ from _pytest.python import Metafunc
+ from _pytest.python import Module
+ from _pytest.reports import CollectReport
+ from _pytest.reports import TestReport
+ from _pytest.runner import CallInfo
+ from _pytest.terminal import TerminalReporter
+ from _pytest.terminal import TestShortLogReport
+ from _pytest.compat import LEGACY_PATH
+
+
+hookspec = HookspecMarker("pytest")
+
+# -------------------------------------------------------------------------
+# Initialization hooks called for every plugin
+# -------------------------------------------------------------------------
+
+
+@hookspec(historic=True)
+def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
+ """Called at plugin registration time to allow adding new hooks via a call to
+ ``pluginmanager.add_hookspecs(module_or_class, prefix)``.
+
+ :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+ """
+
+
+@hookspec(historic=True)
+def pytest_plugin_registered(
+ plugin: "_PluggyPlugin", manager: "PytestPluginManager"
+) -> None:
+ """A new pytest plugin got registered.
+
+ :param plugin: The plugin module or instance.
+ :param pytest.PytestPluginManager manager: pytest plugin manager.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+ """
+
+
+@hookspec(historic=True)
+def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None:
+ """Register argparse-style options and ini-style config values,
+ called once at the beginning of a test run.
+
+ .. note::
+
+ This function should be implemented only in plugins or ``conftest.py``
+ files situated at the tests root directory due to how pytest
+ :ref:`discovers plugins during startup `.
+
+ :param pytest.Parser parser:
+ To add command line options, call
+ :py:func:`parser.addoption(...) `.
+ To add ini-file values call :py:func:`parser.addini(...)
+ `.
+
+ :param pytest.PytestPluginManager pluginmanager:
+ The pytest plugin manager, which can be used to install :py:func:`hookspec`'s
+ or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks
+ to change how command line options are added.
+
+ Options can later be accessed through the
+ :py:class:`config ` object, respectively:
+
+ - :py:func:`config.getoption(name) ` to
+ retrieve the value of a command line option.
+
+ - :py:func:`config.getini(name) ` to retrieve
+ a value read from an ini-style file.
+
+ The config object is passed around on many internal objects via the ``.config``
+ attribute or can be retrieved as the ``pytestconfig`` fixture.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+ """
+
+
+@hookspec(historic=True)
+def pytest_configure(config: "Config") -> None:
+ """Allow plugins and conftest files to perform initial configuration.
+
+ This hook is called for every plugin and initial conftest file
+ after command line options have been parsed.
+
+ After that, the hook is called for other conftest files as they are
+ imported.
+
+ .. note::
+ This hook is incompatible with ``hookwrapper=True``.
+
+ :param pytest.Config config: The pytest config object.
+ """
+
+
+# -------------------------------------------------------------------------
+# Bootstrapping hooks called for plugins registered early enough:
+# internal and 3rd party plugins.
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_cmdline_parse(
+ pluginmanager: "PytestPluginManager", args: List[str]
+) -> Optional["Config"]:
+ """Return an initialized :class:`~pytest.Config`, parsing the specified args.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ .. note::
+ This hook will only be called for plugin classes passed to the
+ ``plugins`` arg when using `pytest.main`_ to perform an in-process
+ test run.
+
+ :param pluginmanager: The pytest plugin manager.
+ :param args: List of arguments passed on the command line.
+ :returns: A pytest config object.
+ """
+
+
+@hookspec(warn_on_impl=WARNING_CMDLINE_PREPARSE_HOOK)
+def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
+ """(**Deprecated**) modify command line arguments before option parsing.
+
+ This hook is considered deprecated and will be removed in a future pytest version. Consider
+ using :hook:`pytest_load_initial_conftests` instead.
+
+ .. note::
+ This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
+
+ :param config: The pytest config object.
+ :param args: Arguments passed on the command line.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
+ """Called for performing the main command line action. The default
+ implementation will invoke the configure hooks and runtest_mainloop.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param config: The pytest config object.
+ :returns: The exit code.
+ """
+
+
+def pytest_load_initial_conftests(
+ early_config: "Config", parser: "Parser", args: List[str]
+) -> None:
+ """Called to implement the loading of initial conftest files ahead
+ of command line option parsing.
+
+ .. note::
+ This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
+
+ :param early_config: The pytest config object.
+ :param args: Arguments passed on the command line.
+ :param parser: To add command line options.
+ """
+
+
+# -------------------------------------------------------------------------
+# collection hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_collection(session: "Session") -> Optional[object]:
+ """Perform the collection phase for the given session.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+
+ The default collection phase is this (see individual hooks for full details):
+
+ 1. Starting from ``session`` as the initial collector:
+
+ 1. ``pytest_collectstart(collector)``
+ 2. ``report = pytest_make_collect_report(collector)``
+ 3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred
+ 4. For each collected node:
+
+ 1. If an item, ``pytest_itemcollected(item)``
+ 2. If a collector, recurse into it.
+
+ 5. ``pytest_collectreport(report)``
+
+ 2. ``pytest_collection_modifyitems(session, config, items)``
+
+ 1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times)
+
+ 3. ``pytest_collection_finish(session)``
+ 4. Set ``session.items`` to the list of collected items
+ 5. Set ``session.testscollected`` to the number of collected items
+
+ You can implement this hook to only perform some action before collection,
+ for example the terminal plugin uses it to start displaying the collection
+ counter (and returns `None`).
+
+ :param session: The pytest session object.
+ """
+
+
+def pytest_collection_modifyitems(
+ session: "Session", config: "Config", items: List["Item"]
+) -> None:
+ """Called after collection has been performed. May filter or re-order
+ the items in-place.
+
+ :param session: The pytest session object.
+ :param config: The pytest config object.
+ :param items: List of item objects.
+ """
+
+
+def pytest_collection_finish(session: "Session") -> None:
+ """Called after collection has been performed and modified.
+
+ :param session: The pytest session object.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_ignore_collect(
+ collection_path: Path, path: "LEGACY_PATH", config: "Config"
+) -> Optional[bool]:
+ """Return True to prevent considering this path for collection.
+
+ This hook is consulted for all files and directories prior to calling
+ more specific hooks.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param collection_path: The path to analyze.
+ :param path: The path to analyze (deprecated).
+ :param config: The pytest config object.
+
+ .. versionchanged:: 7.0.0
+ The ``collection_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``path`` parameter. The ``path`` parameter
+ has been deprecated.
+ """
+
+
+def pytest_collect_file(
+ file_path: Path, path: "LEGACY_PATH", parent: "Collector"
+) -> "Optional[Collector]":
+ """Create a :class:`~pytest.Collector` for the given path, or None if not relevant.
+
+ The new node needs to have the specified ``parent`` as a parent.
+
+ :param file_path: The path to analyze.
+ :param path: The path to collect (deprecated).
+
+ .. versionchanged:: 7.0.0
+ The ``file_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``path`` parameter. The ``path`` parameter
+ has been deprecated.
+ """
+
+
+# logging hooks for collection
+
+
+def pytest_collectstart(collector: "Collector") -> None:
+ """Collector starts collecting.
+
+ :param collector:
+ The collector.
+ """
+
+
+def pytest_itemcollected(item: "Item") -> None:
+ """We just collected a test item.
+
+ :param item:
+ The item.
+ """
+
+
+def pytest_collectreport(report: "CollectReport") -> None:
+ """Collector finished collecting.
+
+ :param report:
+ The collect report.
+ """
+
+
+def pytest_deselected(items: Sequence["Item"]) -> None:
+ """Called for deselected test items, e.g. by keyword.
+
+ May be called multiple times.
+
+ :param items:
+ The items.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]":
+ """Perform :func:`collector.collect() ` and return
+ a :class:`~pytest.CollectReport`.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param collector:
+ The collector.
+ """
+
+
+# -------------------------------------------------------------------------
+# Python test function related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_pycollect_makemodule(
+ module_path: Path, path: "LEGACY_PATH", parent
+) -> Optional["Module"]:
+ """Return a :class:`pytest.Module` collector or None for the given path.
+
+ This hook will be called for each matching test module path.
+ The :hook:`pytest_collect_file` hook needs to be used if you want to
+ create test modules for files that do not match as a test module.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param module_path: The path of the module to collect.
+ :param path: The path of the module to collect (deprecated).
+
+ .. versionchanged:: 7.0.0
+ The ``module_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``path`` parameter.
+
+ The ``path`` parameter has been deprecated in favor of ``fspath``.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_pycollect_makeitem(
+ collector: Union["Module", "Class"], name: str, obj: object
+) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]:
+ """Return a custom item/collector for a Python object in a module, or None.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param collector:
+ The module/class collector.
+ :param name:
+ The name of the object in the module/class.
+ :param obj:
+ The object.
+ :returns:
+ The created items/collectors.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
+ """Call underlying test function.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param pyfuncitem:
+ The function item.
+ """
+
+
+def pytest_generate_tests(metafunc: "Metafunc") -> None:
+ """Generate (multiple) parametrized calls to a test function.
+
+ :param metafunc:
+ The :class:`~pytest.Metafunc` helper for the test function.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_make_parametrize_id(
+ config: "Config", val: object, argname: str
+) -> Optional[str]:
+ """Return a user-friendly string representation of the given ``val``
+ that will be used by @pytest.mark.parametrize calls, or None if the hook
+ doesn't know about ``val``.
+
+ The parameter name is available as ``argname``, if required.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param config: The pytest config object.
+ :param val: The parametrized value.
+ :param str argname: The automatic parameter name produced by pytest.
+ """
+
+
+# -------------------------------------------------------------------------
+# runtest related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_runtestloop(session: "Session") -> Optional[object]:
+ """Perform the main runtest loop (after collection finished).
+
+ The default hook implementation performs the runtest protocol for all items
+ collected in the session (``session.items``), unless the collection failed
+ or the ``collectonly`` pytest option is set.
+
+ If at any point :py:func:`pytest.exit` is called, the loop is
+ terminated immediately.
+
+ If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the
+ loop is terminated after the runtest protocol for the current item is finished.
+
+ :param session: The pytest session object.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_runtest_protocol(
+ item: "Item", nextitem: "Optional[Item]"
+) -> Optional[object]:
+ """Perform the runtest protocol for a single test item.
+
+ The default runtest protocol is this (see individual hooks for full details):
+
+ - ``pytest_runtest_logstart(nodeid, location)``
+
+ - Setup phase:
+ - ``call = pytest_runtest_setup(item)`` (wrapped in ``CallInfo(when="setup")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - Call phase, if the the setup passed and the ``setuponly`` pytest option is not set:
+ - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - Teardown phase:
+ - ``call = pytest_runtest_teardown(item, nextitem)`` (wrapped in ``CallInfo(when="teardown")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - ``pytest_runtest_logfinish(nodeid, location)``
+
+ :param item: Test item for which the runtest protocol is performed.
+ :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+ """
+
+
+def pytest_runtest_logstart(
+ nodeid: str, location: Tuple[str, Optional[int], str]
+) -> None:
+ """Called at the start of running the runtest protocol for a single item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ :param nodeid: Full node ID of the item.
+ :param location: A tuple of ``(filename, lineno, testname)``
+ where ``filename`` is a file path relative to ``config.rootpath``
+ and ``lineno`` is 0-based.
+ """
+
+
+def pytest_runtest_logfinish(
+ nodeid: str, location: Tuple[str, Optional[int], str]
+) -> None:
+ """Called at the end of running the runtest protocol for a single item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ :param nodeid: Full node ID of the item.
+ :param location: A tuple of ``(filename, lineno, testname)``
+ where ``filename`` is a file path relative to ``config.rootpath``
+ and ``lineno`` is 0-based.
+ """
+
+
+def pytest_runtest_setup(item: "Item") -> None:
+ """Called to perform the setup phase for a test item.
+
+ The default implementation runs ``setup()`` on ``item`` and all of its
+ parents (which haven't been setup yet). This includes obtaining the
+ values of fixtures required by the item (which haven't been obtained
+ yet).
+
+ :param item:
+ The item.
+ """
+
+
+def pytest_runtest_call(item: "Item") -> None:
+ """Called to run the test for test item (the call phase).
+
+ The default implementation calls ``item.runtest()``.
+
+ :param item:
+ The item.
+ """
+
+
+def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
+ """Called to perform the teardown phase for a test item.
+
+ The default implementation runs the finalizers and calls ``teardown()``
+ on ``item`` and all of its parents (which need to be torn down). This
+ includes running the teardown phase of fixtures required by the item (if
+ they go out of scope).
+
+ :param item:
+ The item.
+ :param nextitem:
+ The scheduled-to-be-next test item (None if no further test item is
+ scheduled). This argument is used to perform exact teardowns, i.e.
+ calling just enough finalizers so that nextitem only needs to call
+ setup functions.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_runtest_makereport(
+ item: "Item", call: "CallInfo[None]"
+) -> Optional["TestReport"]:
+ """Called to create a :class:`~pytest.TestReport` for each of
+ the setup, call and teardown runtest phases of a test item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ :param item: The item.
+ :param call: The :class:`~pytest.CallInfo` for the phase.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
+
+
+def pytest_runtest_logreport(report: "TestReport") -> None:
+ """Process the :class:`~pytest.TestReport` produced for each
+ of the setup, call and teardown runtest phases of an item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_to_serializable(
+ config: "Config",
+ report: Union["CollectReport", "TestReport"],
+) -> Optional[Dict[str, Any]]:
+ """Serialize the given report object into a data structure suitable for
+ sending over the wire, e.g. converted to JSON.
+
+ :param config: The pytest config object.
+ :param report: The report.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_from_serializable(
+ config: "Config",
+ data: Dict[str, Any],
+) -> Optional[Union["CollectReport", "TestReport"]]:
+ """Restore a report object previously serialized with
+ :hook:`pytest_report_to_serializable`.
+
+ :param config: The pytest config object.
+ """
+
+
+# -------------------------------------------------------------------------
+# Fixture related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_fixture_setup(
+ fixturedef: "FixtureDef[Any]", request: "SubRequest"
+) -> Optional[object]:
+ """Perform fixture setup execution.
+
+ :param fixturdef:
+ The fixture definition object.
+ :param request:
+ The fixture request object.
+ :returns:
+ The return value of the call to the fixture function.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ .. note::
+ If the fixture function returns None, other implementations of
+ this hook function will continue to be called, according to the
+ behavior of the :ref:`firstresult` option.
+ """
+
+
+def pytest_fixture_post_finalizer(
+ fixturedef: "FixtureDef[Any]", request: "SubRequest"
+) -> None:
+ """Called after fixture teardown, but before the cache is cleared, so
+ the fixture result ``fixturedef.cached_result`` is still available (not
+ ``None``).
+
+ :param fixturdef:
+ The fixture definition object.
+ :param request:
+ The fixture request object.
+ """
+
+
+# -------------------------------------------------------------------------
+# test session related hooks
+# -------------------------------------------------------------------------
+
+
+def pytest_sessionstart(session: "Session") -> None:
+ """Called after the ``Session`` object has been created and before performing collection
+ and entering the run test loop.
+
+ :param session: The pytest session object.
+ """
+
+
+def pytest_sessionfinish(
+ session: "Session",
+ exitstatus: Union[int, "ExitCode"],
+) -> None:
+ """Called after whole test run finished, right before returning the exit status to the system.
+
+ :param session: The pytest session object.
+ :param exitstatus: The status which pytest will return to the system.
+ """
+
+
+def pytest_unconfigure(config: "Config") -> None:
+ """Called before test process is exited.
+
+ :param config: The pytest config object.
+ """
+
+
+# -------------------------------------------------------------------------
+# hooks for customizing the assert methods
+# -------------------------------------------------------------------------
+
+
+def pytest_assertrepr_compare(
+ config: "Config", op: str, left: object, right: object
+) -> Optional[List[str]]:
+ """Return explanation for comparisons in failing assert expressions.
+
+ Return None for no custom explanation, otherwise return a list
+ of strings. The strings will be joined by newlines but any newlines
+ *in* a string will be escaped. Note that all but the first line will
+ be indented slightly, the intention is for the first line to be a summary.
+
+ :param config: The pytest config object.
+ :param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
+ :param left: The left operand.
+ :param right: The right operand.
+ """
+
+
+def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None:
+ """Called whenever an assertion passes.
+
+ .. versionadded:: 5.0
+
+ Use this hook to do some processing after a passing assertion.
+ The original assertion information is available in the `orig` string
+ and the pytest introspected assertion information is available in the
+ `expl` string.
+
+ This hook must be explicitly enabled by the ``enable_assertion_pass_hook``
+ ini-file option:
+
+ .. code-block:: ini
+
+ [pytest]
+ enable_assertion_pass_hook=true
+
+ You need to **clean the .pyc** files in your project directory and interpreter libraries
+ when enabling this option, as assertions will require to be re-written.
+
+ :param item: pytest item object of current test.
+ :param lineno: Line number of the assert statement.
+ :param orig: String with the original assertion.
+ :param expl: String with the assert explanation.
+ """
+
+
+# -------------------------------------------------------------------------
+# Hooks for influencing reporting (invoked from _pytest_terminal).
+# -------------------------------------------------------------------------
+
+
+def pytest_report_header( # type:ignore[empty-body]
+ config: "Config", start_path: Path, startdir: "LEGACY_PATH"
+) -> Union[str, List[str]]:
+ """Return a string or list of strings to be displayed as header info for terminal reporting.
+
+ :param config: The pytest config object.
+ :param start_path: The starting dir.
+ :param startdir: The starting dir (deprecated).
+
+ .. note::
+
+ Lines returned by a plugin are displayed before those of plugins which
+ ran before it.
+ If you want to have your line(s) displayed first, use
+ :ref:`trylast=True `.
+
+ .. note::
+
+ This function should be implemented only in plugins or ``conftest.py``
+ files situated at the tests root directory due to how pytest
+ :ref:`discovers plugins during startup `.
+
+ .. versionchanged:: 7.0.0
+ The ``start_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``startdir`` parameter. The ``startdir`` parameter
+ has been deprecated.
+ """
+
+
+def pytest_report_collectionfinish( # type:ignore[empty-body]
+ config: "Config",
+ start_path: Path,
+ startdir: "LEGACY_PATH",
+ items: Sequence["Item"],
+) -> Union[str, List[str]]:
+ """Return a string or list of strings to be displayed after collection
+ has finished successfully.
+
+ These strings will be displayed after the standard "collected X items" message.
+
+ .. versionadded:: 3.2
+
+ :param config: The pytest config object.
+ :param start_path: The starting dir.
+ :param startdir: The starting dir (deprecated).
+ :param items: List of pytest items that are going to be executed; this list should not be modified.
+
+ .. note::
+
+ Lines returned by a plugin are displayed before those of plugins which
+ ran before it.
+ If you want to have your line(s) displayed first, use
+ :ref:`trylast=True `.
+
+ .. versionchanged:: 7.0.0
+ The ``start_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``startdir`` parameter. The ``startdir`` parameter
+ has been deprecated.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_teststatus( # type:ignore[empty-body]
+ report: Union["CollectReport", "TestReport"], config: "Config"
+) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
+ """Return result-category, shortletter and verbose word for status
+ reporting.
+
+ The result-category is a category in which to count the result, for
+ example "passed", "skipped", "error" or the empty string.
+
+ The shortletter is shown as testing progresses, for example ".", "s",
+ "E" or the empty string.
+
+ The verbose word is shown as testing progresses in verbose mode, for
+ example "PASSED", "SKIPPED", "ERROR" or the empty string.
+
+ pytest may style these implicitly according to the report outcome.
+ To provide explicit styling, return a tuple for the verbose word,
+ for example ``"rerun", "R", ("RERUN", {"yellow": True})``.
+
+ :param report: The report object whose status is to be returned.
+ :param config: The pytest config object.
+ :returns: The test status.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ """
+
+
+def pytest_terminal_summary(
+ terminalreporter: "TerminalReporter",
+ exitstatus: "ExitCode",
+ config: "Config",
+) -> None:
+ """Add a section to terminal summary reporting.
+
+ :param terminalreporter: The internal terminal reporter object.
+ :param exitstatus: The exit status that will be reported back to the OS.
+ :param config: The pytest config object.
+
+ .. versionadded:: 4.2
+ The ``config`` parameter.
+ """
+
+
+@hookspec(historic=True)
+def pytest_warning_recorded(
+ warning_message: "warnings.WarningMessage",
+ when: "Literal['config', 'collect', 'runtest']",
+ nodeid: str,
+ location: Optional[Tuple[str, int, str]],
+) -> None:
+ """Process a warning captured by the internal pytest warnings plugin.
+
+ :param warning_message:
+ The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
+ the same attributes as the parameters of :py:func:`warnings.showwarning`.
+
+ :param when:
+ Indicates when the warning was captured. Possible values:
+
+ * ``"config"``: during pytest configuration/initialization stage.
+ * ``"collect"``: during test collection.
+ * ``"runtest"``: during test execution.
+
+ :param nodeid:
+ Full id of the item.
+
+ :param location:
+ When available, holds information about the execution context of the captured
+ warning (filename, linenumber, function). ``function`` evaluates to
+ when the execution context is at the module level.
+
+ .. versionadded:: 6.0
+ """
+
+
+# -------------------------------------------------------------------------
+# Hooks for influencing skipping
+# -------------------------------------------------------------------------
+
+
+def pytest_markeval_namespace( # type:ignore[empty-body]
+ config: "Config",
+) -> Dict[str, Any]:
+ """Called when constructing the globals dictionary used for
+ evaluating string conditions in xfail/skipif markers.
+
+ This is useful when the condition for a marker requires
+ objects that are expensive or impossible to obtain during
+ collection time, which is required by normal boolean
+ conditions.
+
+ .. versionadded:: 6.2
+
+ :param config: The pytest config object.
+ :returns: A dictionary of additional globals to add.
+ """
+
+
+# -------------------------------------------------------------------------
+# error handling and internal debugging hooks
+# -------------------------------------------------------------------------
+
+
+def pytest_internalerror(
+ excrepr: "ExceptionRepr",
+ excinfo: "ExceptionInfo[BaseException]",
+) -> Optional[bool]:
+ """Called for internal errors.
+
+ Return True to suppress the fallback handling of printing an
+ INTERNALERROR message directly to sys.stderr.
+
+ :param excrepr: The exception repr object.
+ :param excinfo: The exception info.
+ """
+
+
+def pytest_keyboard_interrupt(
+ excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
+) -> None:
+ """Called for keyboard interrupt.
+
+ :param excinfo: The exception info.
+ """
+
+
+def pytest_exception_interact(
+ node: Union["Item", "Collector"],
+ call: "CallInfo[Any]",
+ report: Union["CollectReport", "TestReport"],
+) -> None:
+ """Called when an exception was raised which can potentially be
+ interactively handled.
+
+ May be called during collection (see :hook:`pytest_make_collect_report`),
+ in which case ``report`` is a :class:`CollectReport`.
+
+ May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
+ in which case ``report`` is a :class:`TestReport`.
+
+ This hook is not called if the exception that was raised is an internal
+ exception like ``skip.Exception``.
+
+ :param node:
+ The item or collector.
+ :param call:
+ The call information. Contains the exception.
+ :param report:
+ The collection or test report.
+ """
+
+
+def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
+ """Called upon pdb.set_trace().
+
+ Can be used by plugins to take special action just before the python
+ debugger enters interactive mode.
+
+ :param config: The pytest config object.
+ :param pdb: The Pdb instance.
+ """
+
+
+def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
+ """Called when leaving pdb (e.g. with continue after pdb.set_trace()).
+
+ Can be used by plugins to take special action just after the python
+ debugger leaves interactive mode.
+
+ :param config: The pytest config object.
+ :param pdb: The Pdb instance.
+ """
diff --git a/venv/lib/python3.12/site-packages/_pytest/junitxml.py b/venv/lib/python3.12/site-packages/_pytest/junitxml.py
new file mode 100644
index 0000000..9ee35b8
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/junitxml.py
@@ -0,0 +1,700 @@
+"""Report test results in JUnit-XML format, for use with Jenkins and build
+integration servers.
+
+Based on initial code from Ross Lawley.
+
+Output conforms to
+https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
+"""
+import functools
+import os
+import platform
+import re
+import xml.etree.ElementTree as ET
+from datetime import datetime
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Match
+from typing import Optional
+from typing import Tuple
+from typing import Union
+
+import pytest
+from _pytest import nodes
+from _pytest import timing
+from _pytest._code.code import ExceptionRepr
+from _pytest._code.code import ReprFileLocation
+from _pytest.config import Config
+from _pytest.config import filename_arg
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureRequest
+from _pytest.reports import TestReport
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+
+
+xml_key = StashKey["LogXML"]()
+
+
+def bin_xml_escape(arg: object) -> str:
+ r"""Visually escape invalid XML characters.
+
+ For example, transforms
+ 'hello\aworld\b'
+ into
+ 'hello#x07world#x08'
+ Note that the #xABs are *not* XML escapes - missing the ampersand «.
+ The idea is to escape visually for the user rather than for XML itself.
+ """
+
+ def repl(matchobj: Match[str]) -> str:
+ i = ord(matchobj.group())
+ if i <= 0xFF:
+ return "#x%02X" % i
+ else:
+ return "#x%04X" % i
+
+ # The spec range of valid chars is:
+ # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ # For an unknown(?) reason, we disallow #x7F (DEL) as well.
+ illegal_xml_re = (
+ "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]"
+ )
+ return re.sub(illegal_xml_re, repl, str(arg))
+
+
+def merge_family(left, right) -> None:
+ result = {}
+ for kl, vl in left.items():
+ for kr, vr in right.items():
+ if not isinstance(vl, list):
+ raise TypeError(type(vl))
+ result[kl] = vl + vr
+ left.update(result)
+
+
+families = {}
+families["_base"] = {"testcase": ["classname", "name"]}
+families["_base_legacy"] = {"testcase": ["file", "line", "url"]}
+
+# xUnit 1.x inherits legacy attributes.
+families["xunit1"] = families["_base"].copy()
+merge_family(families["xunit1"], families["_base_legacy"])
+
+# xUnit 2.x uses strict base attributes.
+families["xunit2"] = families["_base"]
+
+
+class _NodeReporter:
+ def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None:
+ self.id = nodeid
+ self.xml = xml
+ self.add_stats = self.xml.add_stats
+ self.family = self.xml.family
+ self.duration = 0.0
+ self.properties: List[Tuple[str, str]] = []
+ self.nodes: List[ET.Element] = []
+ self.attrs: Dict[str, str] = {}
+
+ def append(self, node: ET.Element) -> None:
+ self.xml.add_stats(node.tag)
+ self.nodes.append(node)
+
+ def add_property(self, name: str, value: object) -> None:
+ self.properties.append((str(name), bin_xml_escape(value)))
+
+ def add_attribute(self, name: str, value: object) -> None:
+ self.attrs[str(name)] = bin_xml_escape(value)
+
+ def make_properties_node(self) -> Optional[ET.Element]:
+ """Return a Junit node containing custom properties, if any."""
+ if self.properties:
+ properties = ET.Element("properties")
+ for name, value in self.properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
+
+ def record_testreport(self, testreport: TestReport) -> None:
+ names = mangle_test_address(testreport.nodeid)
+ existing_attrs = self.attrs
+ classnames = names[:-1]
+ if self.xml.prefix:
+ classnames.insert(0, self.xml.prefix)
+ attrs: Dict[str, str] = {
+ "classname": ".".join(classnames),
+ "name": bin_xml_escape(names[-1]),
+ "file": testreport.location[0],
+ }
+ if testreport.location[1] is not None:
+ attrs["line"] = str(testreport.location[1])
+ if hasattr(testreport, "url"):
+ attrs["url"] = testreport.url
+ self.attrs = attrs
+ self.attrs.update(existing_attrs) # Restore any user-defined attributes.
+
+ # Preserve legacy testcase behavior.
+ if self.family == "xunit1":
+ return
+
+ # Filter out attributes not permitted by this test family.
+ # Including custom attributes because they are not valid here.
+ temp_attrs = {}
+ for key in self.attrs.keys():
+ if key in families[self.family]["testcase"]:
+ temp_attrs[key] = self.attrs[key]
+ self.attrs = temp_attrs
+
+ def to_xml(self) -> ET.Element:
+ testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration)
+ properties = self.make_properties_node()
+ if properties is not None:
+ testcase.append(properties)
+ testcase.extend(self.nodes)
+ return testcase
+
+ def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None:
+ node = ET.Element(tag, message=message)
+ node.text = bin_xml_escape(data)
+ self.append(node)
+
+ def write_captured_output(self, report: TestReport) -> None:
+ if not self.xml.log_passing_tests and report.passed:
+ return
+
+ content_out = report.capstdout
+ content_log = report.caplog
+ content_err = report.capstderr
+ if self.xml.logging == "no":
+ return
+ content_all = ""
+ if self.xml.logging in ["log", "all"]:
+ content_all = self._prepare_content(content_log, " Captured Log ")
+ if self.xml.logging in ["system-out", "out-err", "all"]:
+ content_all += self._prepare_content(content_out, " Captured Out ")
+ self._write_content(report, content_all, "system-out")
+ content_all = ""
+ if self.xml.logging in ["system-err", "out-err", "all"]:
+ content_all += self._prepare_content(content_err, " Captured Err ")
+ self._write_content(report, content_all, "system-err")
+ content_all = ""
+ if content_all:
+ self._write_content(report, content_all, "system-out")
+
+ def _prepare_content(self, content: str, header: str) -> str:
+ return "\n".join([header.center(80, "-"), content, ""])
+
+ def _write_content(self, report: TestReport, content: str, jheader: str) -> None:
+ tag = ET.Element(jheader)
+ tag.text = bin_xml_escape(content)
+ self.append(tag)
+
+ def append_pass(self, report: TestReport) -> None:
+ self.add_stats("passed")
+
+ def append_failure(self, report: TestReport) -> None:
+ # msg = str(report.longrepr.reprtraceback.extraline)
+ if hasattr(report, "wasxfail"):
+ self._add_simple("skipped", "xfail-marked test passes unexpectedly")
+ else:
+ assert report.longrepr is not None
+ reprcrash: Optional[ReprFileLocation] = getattr(
+ report.longrepr, "reprcrash", None
+ )
+ if reprcrash is not None:
+ message = reprcrash.message
+ else:
+ message = str(report.longrepr)
+ message = bin_xml_escape(message)
+ self._add_simple("failure", message, str(report.longrepr))
+
+ def append_collect_error(self, report: TestReport) -> None:
+ # msg = str(report.longrepr.reprtraceback.extraline)
+ assert report.longrepr is not None
+ self._add_simple("error", "collection failure", str(report.longrepr))
+
+ def append_collect_skipped(self, report: TestReport) -> None:
+ self._add_simple("skipped", "collection skipped", str(report.longrepr))
+
+ def append_error(self, report: TestReport) -> None:
+ assert report.longrepr is not None
+ reprcrash: Optional[ReprFileLocation] = getattr(
+ report.longrepr, "reprcrash", None
+ )
+ if reprcrash is not None:
+ reason = reprcrash.message
+ else:
+ reason = str(report.longrepr)
+
+ if report.when == "teardown":
+ msg = f'failed on teardown with "{reason}"'
+ else:
+ msg = f'failed on setup with "{reason}"'
+ self._add_simple("error", bin_xml_escape(msg), str(report.longrepr))
+
+ def append_skipped(self, report: TestReport) -> None:
+ if hasattr(report, "wasxfail"):
+ xfailreason = report.wasxfail
+ if xfailreason.startswith("reason: "):
+ xfailreason = xfailreason[8:]
+ xfailreason = bin_xml_escape(xfailreason)
+ skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason)
+ self.append(skipped)
+ else:
+ assert isinstance(report.longrepr, tuple)
+ filename, lineno, skipreason = report.longrepr
+ if skipreason.startswith("Skipped: "):
+ skipreason = skipreason[9:]
+ details = f"{filename}:{lineno}: {skipreason}"
+
+ skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
+ skipped.text = bin_xml_escape(details)
+ self.append(skipped)
+ self.write_captured_output(report)
+
+ def finalize(self) -> None:
+ data = self.to_xml()
+ self.__dict__.clear()
+ # Type ignored because mypy doesn't like overriding a method.
+ # Also the return value doesn't match...
+ self.to_xml = lambda: data # type: ignore[assignment]
+
+
+def _warn_incompatibility_with_xunit2(
+ request: FixtureRequest, fixture_name: str
+) -> None:
+ """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
+ from _pytest.warning_types import PytestWarning
+
+ xml = request.config.stash.get(xml_key, None)
+ if xml is not None and xml.family not in ("xunit1", "legacy"):
+ request.node.warn(
+ PytestWarning(
+ "{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format(
+ fixture_name=fixture_name, family=xml.family
+ )
+ )
+ )
+
+
+@pytest.fixture
+def record_property(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Add extra properties to the calling test.
+
+ User properties become part of the test report and are available to the
+ configured reporters, like JUnit XML.
+
+ The fixture is callable with ``name, value``. The value is automatically
+ XML-encoded.
+
+ Example::
+
+ def test_function(record_property):
+ record_property("example_key", 1)
+ """
+ _warn_incompatibility_with_xunit2(request, "record_property")
+
+ def append_property(name: str, value: object) -> None:
+ request.node.user_properties.append((name, value))
+
+ return append_property
+
+
+@pytest.fixture
+def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Add extra xml attributes to the tag for the calling test.
+
+ The fixture is callable with ``name, value``. The value is
+ automatically XML-encoded.
+ """
+ from _pytest.warning_types import PytestExperimentalApiWarning
+
+ request.node.warn(
+ PytestExperimentalApiWarning("record_xml_attribute is an experimental feature")
+ )
+
+ _warn_incompatibility_with_xunit2(request, "record_xml_attribute")
+
+ # Declare noop
+ def add_attr_noop(name: str, value: object) -> None:
+ pass
+
+ attr_func = add_attr_noop
+
+ xml = request.config.stash.get(xml_key, None)
+ if xml is not None:
+ node_reporter = xml.node_reporter(request.node.nodeid)
+ attr_func = node_reporter.add_attribute
+
+ return attr_func
+
+
+def _check_record_param_type(param: str, v: str) -> None:
+ """Used by record_testsuite_property to check that the given parameter name is of the proper
+ type."""
+ __tracebackhide__ = True
+ if not isinstance(v, str):
+ msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable]
+ raise TypeError(msg.format(param=param, g=type(v).__name__))
+
+
+@pytest.fixture(scope="session")
+def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Record a new ```` tag as child of the root ````.
+
+ This is suitable to writing global information regarding the entire test
+ suite, and is compatible with ``xunit2`` JUnit family.
+
+ This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
+
+ .. code-block:: python
+
+ def test_foo(record_testsuite_property):
+ record_testsuite_property("ARCH", "PPC")
+ record_testsuite_property("STORAGE_TYPE", "CEPH")
+
+ :param name:
+ The property name.
+ :param value:
+ The property value. Will be converted to a string.
+
+ .. warning::
+
+ Currently this fixture **does not work** with the
+ `pytest-xdist `__ plugin. See
+ :issue:`7767` for details.
+ """
+
+ __tracebackhide__ = True
+
+ def record_func(name: str, value: object) -> None:
+ """No-op function in case --junit-xml was not passed in the command-line."""
+ __tracebackhide__ = True
+ _check_record_param_type("name", name)
+
+ xml = request.config.stash.get(xml_key, None)
+ if xml is not None:
+ record_func = xml.add_global_property # noqa
+ return record_func
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting")
+ group.addoption(
+ "--junitxml",
+ "--junit-xml",
+ action="store",
+ dest="xmlpath",
+ metavar="path",
+ type=functools.partial(filename_arg, optname="--junitxml"),
+ default=None,
+ help="Create junit-xml style report file at given path",
+ )
+ group.addoption(
+ "--junitprefix",
+ "--junit-prefix",
+ action="store",
+ metavar="str",
+ default=None,
+ help="Prepend prefix to classnames in junit-xml output",
+ )
+ parser.addini(
+ "junit_suite_name", "Test suite name for JUnit report", default="pytest"
+ )
+ parser.addini(
+ "junit_logging",
+ "Write captured log messages to JUnit report: "
+ "one of no|log|system-out|system-err|out-err|all",
+ default="no",
+ )
+ parser.addini(
+ "junit_log_passing_tests",
+ "Capture log information for passing tests to JUnit report: ",
+ type="bool",
+ default=True,
+ )
+ parser.addini(
+ "junit_duration_report",
+ "Duration time to report: one of total|call",
+ default="total",
+ ) # choices=['total', 'call'])
+ parser.addini(
+ "junit_family",
+ "Emit XML for schema: one of legacy|xunit1|xunit2",
+ default="xunit2",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ xmlpath = config.option.xmlpath
+ # Prevent opening xmllog on worker nodes (xdist).
+ if xmlpath and not hasattr(config, "workerinput"):
+ junit_family = config.getini("junit_family")
+ config.stash[xml_key] = LogXML(
+ xmlpath,
+ config.option.junitprefix,
+ config.getini("junit_suite_name"),
+ config.getini("junit_logging"),
+ config.getini("junit_duration_report"),
+ junit_family,
+ config.getini("junit_log_passing_tests"),
+ )
+ config.pluginmanager.register(config.stash[xml_key])
+
+
+def pytest_unconfigure(config: Config) -> None:
+ xml = config.stash.get(xml_key, None)
+ if xml:
+ del config.stash[xml_key]
+ config.pluginmanager.unregister(xml)
+
+
+def mangle_test_address(address: str) -> List[str]:
+ path, possible_open_bracket, params = address.partition("[")
+ names = path.split("::")
+ # Convert file path to dotted path.
+ names[0] = names[0].replace(nodes.SEP, ".")
+ names[0] = re.sub(r"\.py$", "", names[0])
+ # Put any params back.
+ names[-1] += possible_open_bracket + params
+ return names
+
+
+class LogXML:
+ def __init__(
+ self,
+ logfile,
+ prefix: Optional[str],
+ suite_name: str = "pytest",
+ logging: str = "no",
+ report_duration: str = "total",
+ family="xunit1",
+ log_passing_tests: bool = True,
+ ) -> None:
+ logfile = os.path.expanduser(os.path.expandvars(logfile))
+ self.logfile = os.path.normpath(os.path.abspath(logfile))
+ self.prefix = prefix
+ self.suite_name = suite_name
+ self.logging = logging
+ self.log_passing_tests = log_passing_tests
+ self.report_duration = report_duration
+ self.family = family
+ self.stats: Dict[str, int] = dict.fromkeys(
+ ["error", "passed", "failure", "skipped"], 0
+ )
+ self.node_reporters: Dict[
+ Tuple[Union[str, TestReport], object], _NodeReporter
+ ] = {}
+ self.node_reporters_ordered: List[_NodeReporter] = []
+ self.global_properties: List[Tuple[str, str]] = []
+
+ # List of reports that failed on call but teardown is pending.
+ self.open_reports: List[TestReport] = []
+ self.cnt_double_fail_tests = 0
+
+ # Replaces convenience family with real family.
+ if self.family == "legacy":
+ self.family = "xunit1"
+
+ def finalize(self, report: TestReport) -> None:
+ nodeid = getattr(report, "nodeid", report)
+ # Local hack to handle xdist report order.
+ workernode = getattr(report, "node", None)
+ reporter = self.node_reporters.pop((nodeid, workernode))
+
+ for propname, propvalue in report.user_properties:
+ reporter.add_property(propname, str(propvalue))
+
+ if reporter is not None:
+ reporter.finalize()
+
+ def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
+ nodeid: Union[str, TestReport] = getattr(report, "nodeid", report)
+ # Local hack to handle xdist report order.
+ workernode = getattr(report, "node", None)
+
+ key = nodeid, workernode
+
+ if key in self.node_reporters:
+ # TODO: breaks for --dist=each
+ return self.node_reporters[key]
+
+ reporter = _NodeReporter(nodeid, self)
+
+ self.node_reporters[key] = reporter
+ self.node_reporters_ordered.append(reporter)
+
+ return reporter
+
+ def add_stats(self, key: str) -> None:
+ if key in self.stats:
+ self.stats[key] += 1
+
+ def _opentestcase(self, report: TestReport) -> _NodeReporter:
+ reporter = self.node_reporter(report)
+ reporter.record_testreport(report)
+ return reporter
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ """Handle a setup/call/teardown report, generating the appropriate
+ XML tags as necessary.
+
+ Note: due to plugins like xdist, this hook may be called in interlaced
+ order with reports from other nodes. For example:
+
+ Usual call order:
+ -> setup node1
+ -> call node1
+ -> teardown node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+
+ Possible call order in xdist:
+ -> setup node1
+ -> call node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+ -> teardown node1
+ """
+ close_report = None
+ if report.passed:
+ if report.when == "call": # ignore setup/teardown
+ reporter = self._opentestcase(report)
+ reporter.append_pass(report)
+ elif report.failed:
+ if report.when == "teardown":
+ # The following vars are needed when xdist plugin is used.
+ report_wid = getattr(report, "worker_id", None)
+ report_ii = getattr(report, "item_index", None)
+ close_report = next(
+ (
+ rep
+ for rep in self.open_reports
+ if (
+ rep.nodeid == report.nodeid
+ and getattr(rep, "item_index", None) == report_ii
+ and getattr(rep, "worker_id", None) == report_wid
+ )
+ ),
+ None,
+ )
+ if close_report:
+ # We need to open new testcase in case we have failure in
+ # call and error in teardown in order to follow junit
+ # schema.
+ self.finalize(close_report)
+ self.cnt_double_fail_tests += 1
+ reporter = self._opentestcase(report)
+ if report.when == "call":
+ reporter.append_failure(report)
+ self.open_reports.append(report)
+ if not self.log_passing_tests:
+ reporter.write_captured_output(report)
+ else:
+ reporter.append_error(report)
+ elif report.skipped:
+ reporter = self._opentestcase(report)
+ reporter.append_skipped(report)
+ self.update_testcase_duration(report)
+ if report.when == "teardown":
+ reporter = self._opentestcase(report)
+ reporter.write_captured_output(report)
+
+ self.finalize(report)
+ report_wid = getattr(report, "worker_id", None)
+ report_ii = getattr(report, "item_index", None)
+ close_report = next(
+ (
+ rep
+ for rep in self.open_reports
+ if (
+ rep.nodeid == report.nodeid
+ and getattr(rep, "item_index", None) == report_ii
+ and getattr(rep, "worker_id", None) == report_wid
+ )
+ ),
+ None,
+ )
+ if close_report:
+ self.open_reports.remove(close_report)
+
+ def update_testcase_duration(self, report: TestReport) -> None:
+ """Accumulate total duration for nodeid from given report and update
+ the Junit.testcase with the new total if already created."""
+ if self.report_duration == "total" or report.when == self.report_duration:
+ reporter = self.node_reporter(report)
+ reporter.duration += getattr(report, "duration", 0.0)
+
+ def pytest_collectreport(self, report: TestReport) -> None:
+ if not report.passed:
+ reporter = self._opentestcase(report)
+ if report.failed:
+ reporter.append_collect_error(report)
+ else:
+ reporter.append_collect_skipped(report)
+
+ def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
+ reporter = self.node_reporter("internal")
+ reporter.attrs.update(classname="pytest", name="internal")
+ reporter._add_simple("error", "internal error", str(excrepr))
+
+ def pytest_sessionstart(self) -> None:
+ self.suite_start_time = timing.time()
+
+ def pytest_sessionfinish(self) -> None:
+ dirname = os.path.dirname(os.path.abspath(self.logfile))
+ # exist_ok avoids filesystem race conditions between checking path existence and requesting creation
+ os.makedirs(dirname, exist_ok=True)
+
+ with open(self.logfile, "w", encoding="utf-8") as logfile:
+ suite_stop_time = timing.time()
+ suite_time_delta = suite_stop_time - self.suite_start_time
+
+ numtests = (
+ self.stats["passed"]
+ + self.stats["failure"]
+ + self.stats["skipped"]
+ + self.stats["error"]
+ - self.cnt_double_fail_tests
+ )
+ logfile.write('')
+
+ suite_node = ET.Element(
+ "testsuite",
+ name=self.suite_name,
+ errors=str(self.stats["error"]),
+ failures=str(self.stats["failure"]),
+ skipped=str(self.stats["skipped"]),
+ tests=str(numtests),
+ time="%.3f" % suite_time_delta,
+ timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(),
+ hostname=platform.node(),
+ )
+ global_properties = self._get_global_properties_node()
+ if global_properties is not None:
+ suite_node.append(global_properties)
+ for node_reporter in self.node_reporters_ordered:
+ suite_node.append(node_reporter.to_xml())
+ testsuites = ET.Element("testsuites")
+ testsuites.append(suite_node)
+ logfile.write(ET.tostring(testsuites, encoding="unicode"))
+
+ def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
+ terminalreporter.write_sep("-", f"generated xml file: {self.logfile}")
+
+ def add_global_property(self, name: str, value: object) -> None:
+ __tracebackhide__ = True
+ _check_record_param_type("name", name)
+ self.global_properties.append((name, bin_xml_escape(value)))
+
+ def _get_global_properties_node(self) -> Optional[ET.Element]:
+ """Return a Junit node containing custom properties, if any."""
+ if self.global_properties:
+ properties = ET.Element("properties")
+ for name, value in self.global_properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
diff --git a/venv/lib/python3.12/site-packages/_pytest/legacypath.py b/venv/lib/python3.12/site-packages/_pytest/legacypath.py
new file mode 100644
index 0000000..af1d0c0
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/legacypath.py
@@ -0,0 +1,479 @@
+"""Add backward compatibility support for the legacy py path type."""
+import dataclasses
+import shlex
+import subprocess
+from pathlib import Path
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from iniconfig import SectionWrapper
+
+from _pytest.cacheprovider import Cache
+from _pytest.compat import final
+from _pytest.compat import LEGACY_PATH
+from _pytest.compat import legacy_path
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.pytester import HookRecorder
+from _pytest.pytester import Pytester
+from _pytest.pytester import RunResult
+from _pytest.terminal import TerminalReporter
+from _pytest.tmpdir import TempPathFactory
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+
+ import pexpect
+
+
+@final
+class Testdir:
+ """
+ Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead.
+
+ All methods just forward to an internal :class:`Pytester` instance, converting results
+ to `legacy_path` objects as necessary.
+ """
+
+ __test__ = False
+
+ CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
+ TimeoutExpired: "Final" = Pytester.TimeoutExpired
+
+ def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._pytester = pytester
+
+ @property
+ def tmpdir(self) -> LEGACY_PATH:
+ """Temporary directory where tests are executed."""
+ return legacy_path(self._pytester.path)
+
+ @property
+ def test_tmproot(self) -> LEGACY_PATH:
+ return legacy_path(self._pytester._test_tmproot)
+
+ @property
+ def request(self):
+ return self._pytester._request
+
+ @property
+ def plugins(self):
+ return self._pytester.plugins
+
+ @plugins.setter
+ def plugins(self, plugins):
+ self._pytester.plugins = plugins
+
+ @property
+ def monkeypatch(self) -> MonkeyPatch:
+ return self._pytester._monkeypatch
+
+ def make_hook_recorder(self, pluginmanager) -> HookRecorder:
+ """See :meth:`Pytester.make_hook_recorder`."""
+ return self._pytester.make_hook_recorder(pluginmanager)
+
+ def chdir(self) -> None:
+ """See :meth:`Pytester.chdir`."""
+ return self._pytester.chdir()
+
+ def finalize(self) -> None:
+ """See :meth:`Pytester._finalize`."""
+ return self._pytester._finalize()
+
+ def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.makefile`."""
+ if ext and not ext.startswith("."):
+ # pytester.makefile is going to throw a ValueError in a way that
+ # testdir.makefile did not, because
+ # pathlib.Path is stricter suffixes than py.path
+ # This ext arguments is likely user error, but since testdir has
+ # allowed this, we will prepend "." as a workaround to avoid breaking
+ # testdir usage that worked before
+ ext = "." + ext
+ return legacy_path(self._pytester.makefile(ext, *args, **kwargs))
+
+ def makeconftest(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makeconftest`."""
+ return legacy_path(self._pytester.makeconftest(source))
+
+ def makeini(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makeini`."""
+ return legacy_path(self._pytester.makeini(source))
+
+ def getinicfg(self, source: str) -> SectionWrapper:
+ """See :meth:`Pytester.getinicfg`."""
+ return self._pytester.getinicfg(source)
+
+ def makepyprojecttoml(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makepyprojecttoml`."""
+ return legacy_path(self._pytester.makepyprojecttoml(source))
+
+ def makepyfile(self, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.makepyfile`."""
+ return legacy_path(self._pytester.makepyfile(*args, **kwargs))
+
+ def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.maketxtfile`."""
+ return legacy_path(self._pytester.maketxtfile(*args, **kwargs))
+
+ def syspathinsert(self, path=None) -> None:
+ """See :meth:`Pytester.syspathinsert`."""
+ return self._pytester.syspathinsert(path)
+
+ def mkdir(self, name) -> LEGACY_PATH:
+ """See :meth:`Pytester.mkdir`."""
+ return legacy_path(self._pytester.mkdir(name))
+
+ def mkpydir(self, name) -> LEGACY_PATH:
+ """See :meth:`Pytester.mkpydir`."""
+ return legacy_path(self._pytester.mkpydir(name))
+
+ def copy_example(self, name=None) -> LEGACY_PATH:
+ """See :meth:`Pytester.copy_example`."""
+ return legacy_path(self._pytester.copy_example(name))
+
+ def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
+ """See :meth:`Pytester.getnode`."""
+ return self._pytester.getnode(config, arg)
+
+ def getpathnode(self, path):
+ """See :meth:`Pytester.getpathnode`."""
+ return self._pytester.getpathnode(path)
+
+ def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]:
+ """See :meth:`Pytester.genitems`."""
+ return self._pytester.genitems(colitems)
+
+ def runitem(self, source):
+ """See :meth:`Pytester.runitem`."""
+ return self._pytester.runitem(source)
+
+ def inline_runsource(self, source, *cmdlineargs):
+ """See :meth:`Pytester.inline_runsource`."""
+ return self._pytester.inline_runsource(source, *cmdlineargs)
+
+ def inline_genitems(self, *args):
+ """See :meth:`Pytester.inline_genitems`."""
+ return self._pytester.inline_genitems(*args)
+
+ def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
+ """See :meth:`Pytester.inline_run`."""
+ return self._pytester.inline_run(
+ *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
+ )
+
+ def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
+ """See :meth:`Pytester.runpytest_inprocess`."""
+ return self._pytester.runpytest_inprocess(*args, **kwargs)
+
+ def runpytest(self, *args, **kwargs) -> RunResult:
+ """See :meth:`Pytester.runpytest`."""
+ return self._pytester.runpytest(*args, **kwargs)
+
+ def parseconfig(self, *args) -> Config:
+ """See :meth:`Pytester.parseconfig`."""
+ return self._pytester.parseconfig(*args)
+
+ def parseconfigure(self, *args) -> Config:
+ """See :meth:`Pytester.parseconfigure`."""
+ return self._pytester.parseconfigure(*args)
+
+ def getitem(self, source, funcname="test_func"):
+ """See :meth:`Pytester.getitem`."""
+ return self._pytester.getitem(source, funcname)
+
+ def getitems(self, source):
+ """See :meth:`Pytester.getitems`."""
+ return self._pytester.getitems(source)
+
+ def getmodulecol(self, source, configargs=(), withinit=False):
+ """See :meth:`Pytester.getmodulecol`."""
+ return self._pytester.getmodulecol(
+ source, configargs=configargs, withinit=withinit
+ )
+
+ def collect_by_name(
+ self, modcol: Collector, name: str
+ ) -> Optional[Union[Item, Collector]]:
+ """See :meth:`Pytester.collect_by_name`."""
+ return self._pytester.collect_by_name(modcol, name)
+
+ def popen(
+ self,
+ cmdargs,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=CLOSE_STDIN,
+ **kw,
+ ):
+ """See :meth:`Pytester.popen`."""
+ return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
+
+ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
+ """See :meth:`Pytester.run`."""
+ return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
+
+ def runpython(self, script) -> RunResult:
+ """See :meth:`Pytester.runpython`."""
+ return self._pytester.runpython(script)
+
+ def runpython_c(self, command):
+ """See :meth:`Pytester.runpython_c`."""
+ return self._pytester.runpython_c(command)
+
+ def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
+ """See :meth:`Pytester.runpytest_subprocess`."""
+ return self._pytester.runpytest_subprocess(*args, timeout=timeout)
+
+ def spawn_pytest(
+ self, string: str, expect_timeout: float = 10.0
+ ) -> "pexpect.spawn":
+ """See :meth:`Pytester.spawn_pytest`."""
+ return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
+
+ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
+ """See :meth:`Pytester.spawn`."""
+ return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
+
+ def __repr__(self) -> str:
+ return f""
+
+ def __str__(self) -> str:
+ return str(self.tmpdir)
+
+
+class LegacyTestdirPlugin:
+ @staticmethod
+ @fixture
+ def testdir(pytester: Pytester) -> Testdir:
+ """
+ Identical to :fixture:`pytester`, and provides an instance whose methods return
+ legacy ``LEGACY_PATH`` objects instead when applicable.
+
+ New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
+ """
+ return Testdir(pytester, _ispytest=True)
+
+
+@final
+@dataclasses.dataclass
+class TempdirFactory:
+ """Backward compatibility wrapper that implements :class:`py.path.local`
+ for :class:`TempPathFactory`.
+
+ .. note::
+ These days, it is preferred to use ``tmp_path_factory``.
+
+ :ref:`About the tmpdir and tmpdir_factory fixtures`.
+
+ """
+
+ _tmppath_factory: TempPathFactory
+
+ def __init__(
+ self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._tmppath_factory = tmppath_factory
+
+ def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
+ """Same as :meth:`TempPathFactory.mktemp`, but returns a :class:`py.path.local` object."""
+ return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
+
+ def getbasetemp(self) -> LEGACY_PATH:
+ """Same as :meth:`TempPathFactory.getbasetemp`, but returns a :class:`py.path.local` object."""
+ return legacy_path(self._tmppath_factory.getbasetemp().resolve())
+
+
+class LegacyTmpdirPlugin:
+ @staticmethod
+ @fixture(scope="session")
+ def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
+ """Return a :class:`pytest.TempdirFactory` instance for the test session."""
+ # Set dynamically by pytest_configure().
+ return request.config._tmpdirhandler # type: ignore
+
+ @staticmethod
+ @fixture
+ def tmpdir(tmp_path: Path) -> LEGACY_PATH:
+ """Return a temporary directory path object which is unique to each test
+ function invocation, created as a sub directory of the base temporary
+ directory.
+
+ By default, a new base temporary directory is created each test session,
+ and old bases are removed after 3 sessions, to aid in debugging. If
+ ``--basetemp`` is used then it is cleared each session. See :ref:`base
+ temporary directory`.
+
+ The returned object is a `legacy_path`_ object.
+
+ .. note::
+ These days, it is preferred to use ``tmp_path``.
+
+ :ref:`About the tmpdir and tmpdir_factory fixtures`.
+
+ .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
+ """
+ return legacy_path(tmp_path)
+
+
+def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH:
+ """Return a directory path object with the given name.
+
+ Same as :func:`mkdir`, but returns a legacy py path instance.
+ """
+ return legacy_path(self.mkdir(name))
+
+
+def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH:
+ """(deprecated) The file system path of the test module which collected this test."""
+ return legacy_path(self.path)
+
+
+def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH:
+ """The directory from which pytest was invoked.
+
+ Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(self.startpath)
+
+
+def Config_invocation_dir(self: Config) -> LEGACY_PATH:
+ """The directory from which pytest was invoked.
+
+ Prefer to use :attr:`invocation_params.dir `,
+ which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(str(self.invocation_params.dir))
+
+
+def Config_rootdir(self: Config) -> LEGACY_PATH:
+ """The path to the :ref:`rootdir `.
+
+ Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(str(self.rootpath))
+
+
+def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
+ """The path to the :ref:`configfile `.
+
+ Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
+
+ :type: Optional[LEGACY_PATH]
+ """
+ return legacy_path(str(self.inipath)) if self.inipath else None
+
+
+def Session_stardir(self: Session) -> LEGACY_PATH:
+ """The path from which pytest was invoked.
+
+ Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(self.startpath)
+
+
+def Config__getini_unknown_type(
+ self, name: str, type: str, value: Union[str, List[str]]
+):
+ if type == "pathlist":
+ # TODO: This assert is probably not valid in all cases.
+ assert self.inipath is not None
+ dp = self.inipath.parent
+ input_values = shlex.split(value) if isinstance(value, str) else value
+ return [legacy_path(str(dp / x)) for x in input_values]
+ else:
+ raise ValueError(f"unknown configuration type: {type}", value)
+
+
+def Node_fspath(self: Node) -> LEGACY_PATH:
+ """(deprecated) returns a legacy_path copy of self.path"""
+ return legacy_path(self.path)
+
+
+def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None:
+ self.path = Path(value)
+
+
+@hookimpl(tryfirst=True)
+def pytest_load_initial_conftests(early_config: Config) -> None:
+ """Monkeypatch legacy path attributes in several classes, as early as possible."""
+ mp = MonkeyPatch()
+ early_config.add_cleanup(mp.undo)
+
+ # Add Cache.makedir().
+ mp.setattr(Cache, "makedir", Cache_makedir, raising=False)
+
+ # Add FixtureRequest.fspath property.
+ mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False)
+
+ # Add TerminalReporter.startdir property.
+ mp.setattr(
+ TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False
+ )
+
+ # Add Config.{invocation_dir,rootdir,inifile} properties.
+ mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False)
+ mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False)
+ mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
+
+ # Add Session.startdir property.
+ mp.setattr(Session, "startdir", property(Session_stardir), raising=False)
+
+ # Add pathlist configuration type.
+ mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
+
+ # Add Node.fspath property.
+ mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
+
+
+@hookimpl
+def pytest_configure(config: Config) -> None:
+ """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed."""
+ if config.pluginmanager.has_plugin("tmpdir"):
+ mp = MonkeyPatch()
+ config.add_cleanup(mp.undo)
+ # Create TmpdirFactory and attach it to the config object.
+ #
+ # This is to comply with existing plugins which expect the handler to be
+ # available at pytest_configure time, but ideally should be moved entirely
+ # to the tmpdir_factory session fixture.
+ try:
+ tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined]
+ except AttributeError:
+ # tmpdir plugin is blocked.
+ pass
+ else:
+ _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True)
+ mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False)
+
+ config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir")
+
+
+@hookimpl
+def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None:
+ # pytester is not loaded by default and is commonly loaded from a conftest,
+ # so checking for it in `pytest_configure` is not enough.
+ is_pytester = plugin is manager.get_plugin("pytester")
+ if is_pytester and not manager.is_registered(LegacyTestdirPlugin):
+ manager.register(LegacyTestdirPlugin, "legacypath-pytester")
diff --git a/venv/lib/python3.12/site-packages/_pytest/logging.py b/venv/lib/python3.12/site-packages/_pytest/logging.py
new file mode 100644
index 0000000..9f2f1c7
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/logging.py
@@ -0,0 +1,920 @@
+"""Access and control log capturing."""
+import io
+import logging
+import os
+import re
+from contextlib import contextmanager
+from contextlib import nullcontext
+from datetime import datetime
+from datetime import timedelta
+from datetime import timezone
+from io import StringIO
+from logging import LogRecord
+from pathlib import Path
+from typing import AbstractSet
+from typing import Dict
+from typing import Generator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from _pytest import nodes
+from _pytest._io import TerminalWriter
+from _pytest.capture import CaptureManager
+from _pytest.compat import final
+from _pytest.config import _strtobool
+from _pytest.config import Config
+from _pytest.config import create_terminal_writer
+from _pytest.config import hookimpl
+from _pytest.config import UsageError
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+
+if TYPE_CHECKING:
+ logging_StreamHandler = logging.StreamHandler[StringIO]
+
+ from typing_extensions import Literal
+else:
+ logging_StreamHandler = logging.StreamHandler
+
+DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
+DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
+_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
+caplog_handler_key = StashKey["LogCaptureHandler"]()
+caplog_records_key = StashKey[Dict[str, List[logging.LogRecord]]]()
+
+
+def _remove_ansi_escape_sequences(text: str) -> str:
+ return _ANSI_ESCAPE_SEQ.sub("", text)
+
+
+class DatetimeFormatter(logging.Formatter):
+ """A logging formatter which formats record with
+ :func:`datetime.datetime.strftime` formatter instead of
+ :func:`time.strftime` in case of microseconds in format string.
+ """
+
+ def formatTime(self, record: LogRecord, datefmt=None) -> str:
+ if datefmt and "%f" in datefmt:
+ ct = self.converter(record.created)
+ tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
+ # Construct `datetime.datetime` object from `struct_time`
+ # and msecs information from `record`
+ dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
+ return dt.strftime(datefmt)
+ # Use `logging.Formatter` for non-microsecond formats
+ return super().formatTime(record, datefmt)
+
+
+class ColoredLevelFormatter(DatetimeFormatter):
+ """A logging formatter which colorizes the %(levelname)..s part of the
+ log format passed to __init__."""
+
+ LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = {
+ logging.CRITICAL: {"red"},
+ logging.ERROR: {"red", "bold"},
+ logging.WARNING: {"yellow"},
+ logging.WARN: {"yellow"},
+ logging.INFO: {"green"},
+ logging.DEBUG: {"purple"},
+ logging.NOTSET: set(),
+ }
+ LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*(?:\.\d+)?s)")
+
+ def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self._terminalwriter = terminalwriter
+ self._original_fmt = self._style._fmt
+ self._level_to_fmt_mapping: Dict[int, str] = {}
+
+ for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
+ self.add_color_level(level, *color_opts)
+
+ def add_color_level(self, level: int, *color_opts: str) -> None:
+ """Add or update color opts for a log level.
+
+ :param level:
+ Log level to apply a style to, e.g. ``logging.INFO``.
+ :param color_opts:
+ ANSI escape sequence color options. Capitalized colors indicates
+ background color, i.e. ``'green', 'Yellow', 'bold'`` will give bold
+ green text on yellow background.
+
+ .. warning::
+ This is an experimental API.
+ """
+
+ assert self._fmt is not None
+ levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
+ if not levelname_fmt_match:
+ return
+ levelname_fmt = levelname_fmt_match.group()
+
+ formatted_levelname = levelname_fmt % {"levelname": logging.getLevelName(level)}
+
+ # add ANSI escape sequences around the formatted levelname
+ color_kwargs = {name: True for name in color_opts}
+ colorized_formatted_levelname = self._terminalwriter.markup(
+ formatted_levelname, **color_kwargs
+ )
+ self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
+ colorized_formatted_levelname, self._fmt
+ )
+
+ def format(self, record: logging.LogRecord) -> str:
+ fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt)
+ self._style._fmt = fmt
+ return super().format(record)
+
+
+class PercentStyleMultiline(logging.PercentStyle):
+ """A logging style with special support for multiline messages.
+
+ If the message of a record consists of multiple lines, this style
+ formats the message as if each line were logged separately.
+ """
+
+ def __init__(self, fmt: str, auto_indent: Union[int, str, bool, None]) -> None:
+ super().__init__(fmt)
+ self._auto_indent = self._get_auto_indent(auto_indent)
+
+ @staticmethod
+ def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int:
+ """Determine the current auto indentation setting.
+
+ Specify auto indent behavior (on/off/fixed) by passing in
+ extra={"auto_indent": [value]} to the call to logging.log() or
+ using a --log-auto-indent [value] command line or the
+ log_auto_indent [value] config option.
+
+ Default behavior is auto-indent off.
+
+ Using the string "True" or "on" or the boolean True as the value
+ turns auto indent on, using the string "False" or "off" or the
+ boolean False or the int 0 turns it off, and specifying a
+ positive integer fixes the indentation position to the value
+ specified.
+
+ Any other values for the option are invalid, and will silently be
+ converted to the default.
+
+ :param None|bool|int|str auto_indent_option:
+ User specified option for indentation from command line, config
+ or extra kwarg. Accepts int, bool or str. str option accepts the
+ same range of values as boolean config options, as well as
+ positive integers represented in str form.
+
+ :returns:
+ Indentation value, which can be
+ -1 (automatically determine indentation) or
+ 0 (auto-indent turned off) or
+ >0 (explicitly set indentation position).
+ """
+
+ if auto_indent_option is None:
+ return 0
+ elif isinstance(auto_indent_option, bool):
+ if auto_indent_option:
+ return -1
+ else:
+ return 0
+ elif isinstance(auto_indent_option, int):
+ return int(auto_indent_option)
+ elif isinstance(auto_indent_option, str):
+ try:
+ return int(auto_indent_option)
+ except ValueError:
+ pass
+ try:
+ if _strtobool(auto_indent_option):
+ return -1
+ except ValueError:
+ return 0
+
+ return 0
+
+ def format(self, record: logging.LogRecord) -> str:
+ if "\n" in record.message:
+ if hasattr(record, "auto_indent"):
+ # Passed in from the "extra={}" kwarg on the call to logging.log().
+ auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined]
+ else:
+ auto_indent = self._auto_indent
+
+ if auto_indent:
+ lines = record.message.splitlines()
+ formatted = self._fmt % {**record.__dict__, "message": lines[0]}
+
+ if auto_indent < 0:
+ indentation = _remove_ansi_escape_sequences(formatted).find(
+ lines[0]
+ )
+ else:
+ # Optimizes logging by allowing a fixed indentation.
+ indentation = auto_indent
+ lines[0] = formatted
+ return ("\n" + " " * indentation).join(lines)
+ return self._fmt % record.__dict__
+
+
+def get_option_ini(config: Config, *names: str):
+ for name in names:
+ ret = config.getoption(name) # 'default' arg won't work as expected
+ if ret is None:
+ ret = config.getini(name)
+ if ret:
+ return ret
+
+
+def pytest_addoption(parser: Parser) -> None:
+ """Add options to control log capturing."""
+ group = parser.getgroup("logging")
+
+ def add_option_ini(option, dest, default=None, type=None, **kwargs):
+ parser.addini(
+ dest, default=default, type=type, help="Default value for " + option
+ )
+ group.addoption(option, dest=dest, **kwargs)
+
+ add_option_ini(
+ "--log-level",
+ dest="log_level",
+ default=None,
+ metavar="LEVEL",
+ help=(
+ "Level of messages to catch/display."
+ " Not set by default, so it depends on the root/parent log handler's"
+ ' effective level, where it is "WARNING" by default.'
+ ),
+ )
+ add_option_ini(
+ "--log-format",
+ dest="log_format",
+ default=DEFAULT_LOG_FORMAT,
+ help="Log format used by the logging module",
+ )
+ add_option_ini(
+ "--log-date-format",
+ dest="log_date_format",
+ default=DEFAULT_LOG_DATE_FORMAT,
+ help="Log date format used by the logging module",
+ )
+ parser.addini(
+ "log_cli",
+ default=False,
+ type="bool",
+ help='Enable log display during test run (also known as "live logging")',
+ )
+ add_option_ini(
+ "--log-cli-level", dest="log_cli_level", default=None, help="CLI logging level"
+ )
+ add_option_ini(
+ "--log-cli-format",
+ dest="log_cli_format",
+ default=None,
+ help="Log format used by the logging module",
+ )
+ add_option_ini(
+ "--log-cli-date-format",
+ dest="log_cli_date_format",
+ default=None,
+ help="Log date format used by the logging module",
+ )
+ add_option_ini(
+ "--log-file",
+ dest="log_file",
+ default=None,
+ help="Path to a file when logging will be written to",
+ )
+ add_option_ini(
+ "--log-file-level",
+ dest="log_file_level",
+ default=None,
+ help="Log file logging level",
+ )
+ add_option_ini(
+ "--log-file-format",
+ dest="log_file_format",
+ default=DEFAULT_LOG_FORMAT,
+ help="Log format used by the logging module",
+ )
+ add_option_ini(
+ "--log-file-date-format",
+ dest="log_file_date_format",
+ default=DEFAULT_LOG_DATE_FORMAT,
+ help="Log date format used by the logging module",
+ )
+ add_option_ini(
+ "--log-auto-indent",
+ dest="log_auto_indent",
+ default=None,
+ help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
+ )
+ group.addoption(
+ "--log-disable",
+ action="append",
+ default=[],
+ dest="logger_disable",
+ help="Disable a logger by name. Can be passed multiple times.",
+ )
+
+
+_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
+
+
+# Not using @contextmanager for performance reasons.
+class catching_logs:
+ """Context manager that prepares the whole logging machinery properly."""
+
+ __slots__ = ("handler", "level", "orig_level")
+
+ def __init__(self, handler: _HandlerType, level: Optional[int] = None) -> None:
+ self.handler = handler
+ self.level = level
+
+ def __enter__(self):
+ root_logger = logging.getLogger()
+ if self.level is not None:
+ self.handler.setLevel(self.level)
+ root_logger.addHandler(self.handler)
+ if self.level is not None:
+ self.orig_level = root_logger.level
+ root_logger.setLevel(min(self.orig_level, self.level))
+ return self.handler
+
+ def __exit__(self, type, value, traceback):
+ root_logger = logging.getLogger()
+ if self.level is not None:
+ root_logger.setLevel(self.orig_level)
+ root_logger.removeHandler(self.handler)
+
+
+class LogCaptureHandler(logging_StreamHandler):
+ """A logging handler that stores log records and the log text."""
+
+ def __init__(self) -> None:
+ """Create a new log handler."""
+ super().__init__(StringIO())
+ self.records: List[logging.LogRecord] = []
+
+ def emit(self, record: logging.LogRecord) -> None:
+ """Keep the log records in a list in addition to the log text."""
+ self.records.append(record)
+ super().emit(record)
+
+ def reset(self) -> None:
+ self.records = []
+ self.stream = StringIO()
+
+ def clear(self) -> None:
+ self.records.clear()
+ self.stream = StringIO()
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ if logging.raiseExceptions:
+ # Fail the test if the log message is bad (emit failed).
+ # The default behavior of logging is to print "Logging error"
+ # to stderr with the call stack and some extra details.
+ # pytest wants to make such mistakes visible during testing.
+ raise
+
+
+@final
+class LogCaptureFixture:
+ """Provides access and control of log capturing."""
+
+ def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._item = item
+ self._initial_handler_level: Optional[int] = None
+ # Dict of log name -> log level.
+ self._initial_logger_levels: Dict[Optional[str], int] = {}
+ self._initial_disabled_logging_level: Optional[int] = None
+
+ def _finalize(self) -> None:
+ """Finalize the fixture.
+
+ This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
+ """
+ # Restore log levels.
+ if self._initial_handler_level is not None:
+ self.handler.setLevel(self._initial_handler_level)
+ for logger_name, level in self._initial_logger_levels.items():
+ logger = logging.getLogger(logger_name)
+ logger.setLevel(level)
+ # Disable logging at the original disabled logging level.
+ if self._initial_disabled_logging_level is not None:
+ logging.disable(self._initial_disabled_logging_level)
+ self._initial_disabled_logging_level = None
+
+ @property
+ def handler(self) -> LogCaptureHandler:
+ """Get the logging handler used by the fixture."""
+ return self._item.stash[caplog_handler_key]
+
+ def get_records(
+ self, when: "Literal['setup', 'call', 'teardown']"
+ ) -> List[logging.LogRecord]:
+ """Get the logging records for one of the possible test phases.
+
+ :param when:
+ Which test phase to obtain the records from.
+ Valid values are: "setup", "call" and "teardown".
+
+ :returns: The list of captured records at the given stage.
+
+ .. versionadded:: 3.4
+ """
+ return self._item.stash[caplog_records_key].get(when, [])
+
+ @property
+ def text(self) -> str:
+ """The formatted log text."""
+ return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
+
+ @property
+ def records(self) -> List[logging.LogRecord]:
+ """The list of log records."""
+ return self.handler.records
+
+ @property
+ def record_tuples(self) -> List[Tuple[str, int, str]]:
+ """A list of a stripped down version of log records intended
+ for use in assertion comparison.
+
+ The format of the tuple is:
+
+ (logger_name, log_level, message)
+ """
+ return [(r.name, r.levelno, r.getMessage()) for r in self.records]
+
+ @property
+ def messages(self) -> List[str]:
+ """A list of format-interpolated log messages.
+
+ Unlike 'records', which contains the format string and parameters for
+ interpolation, log messages in this list are all interpolated.
+
+ Unlike 'text', which contains the output from the handler, log
+ messages in this list are unadorned with levels, timestamps, etc,
+ making exact comparisons more reliable.
+
+ Note that traceback or stack info (from :func:`logging.exception` or
+ the `exc_info` or `stack_info` arguments to the logging functions) is
+ not included, as this is added by the formatter in the handler.
+
+ .. versionadded:: 3.7
+ """
+ return [r.getMessage() for r in self.records]
+
+ def clear(self) -> None:
+ """Reset the list of log records and the captured log text."""
+ self.handler.clear()
+
+ def _force_enable_logging(
+ self, level: Union[int, str], logger_obj: logging.Logger
+ ) -> int:
+ """Enable the desired logging level if the global level was disabled via ``logging.disabled``.
+
+ Only enables logging levels greater than or equal to the requested ``level``.
+
+ Does nothing if the desired ``level`` wasn't disabled.
+
+ :param level:
+ The logger level caplog should capture.
+ All logging is enabled if a non-standard logging level string is supplied.
+ Valid level strings are in :data:`logging._nameToLevel`.
+ :param logger_obj: The logger object to check.
+
+ :return: The original disabled logging level.
+ """
+ original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
+
+ if isinstance(level, str):
+ # Try to translate the level string to an int for `logging.disable()`
+ level = logging.getLevelName(level)
+
+ if not isinstance(level, int):
+ # The level provided was not valid, so just un-disable all logging.
+ logging.disable(logging.NOTSET)
+ elif not logger_obj.isEnabledFor(level):
+ # Each level is `10` away from other levels.
+ # https://docs.python.org/3/library/logging.html#logging-levels
+ disable_level = max(level - 10, logging.NOTSET)
+ logging.disable(disable_level)
+
+ return original_disable_level
+
+ def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
+ """Set the threshold level of a logger for the duration of a test.
+
+ Logging messages which are less severe than this level will not be captured.
+
+ .. versionchanged:: 3.4
+ The levels of the loggers changed by this function will be
+ restored to their initial values at the end of the test.
+
+ Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
+
+ :param level: The level.
+ :param logger: The logger to update. If not given, the root logger.
+ """
+ logger_obj = logging.getLogger(logger)
+ # Save the original log-level to restore it during teardown.
+ self._initial_logger_levels.setdefault(logger, logger_obj.level)
+ logger_obj.setLevel(level)
+ if self._initial_handler_level is None:
+ self._initial_handler_level = self.handler.level
+ self.handler.setLevel(level)
+ initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
+ if self._initial_disabled_logging_level is None:
+ self._initial_disabled_logging_level = initial_disabled_logging_level
+
+ @contextmanager
+ def at_level(
+ self, level: Union[int, str], logger: Optional[str] = None
+ ) -> Generator[None, None, None]:
+ """Context manager that sets the level for capturing of logs. After
+ the end of the 'with' statement the level is restored to its original
+ value.
+
+ Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
+
+ :param level: The level.
+ :param logger: The logger to update. If not given, the root logger.
+ """
+ logger_obj = logging.getLogger(logger)
+ orig_level = logger_obj.level
+ logger_obj.setLevel(level)
+ handler_orig_level = self.handler.level
+ self.handler.setLevel(level)
+ original_disable_level = self._force_enable_logging(level, logger_obj)
+ try:
+ yield
+ finally:
+ logger_obj.setLevel(orig_level)
+ self.handler.setLevel(handler_orig_level)
+ logging.disable(original_disable_level)
+
+
+@fixture
+def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
+ """Access and control log capturing.
+
+ Captured logs are available through the following properties/methods::
+
+ * caplog.messages -> list of format-interpolated log messages
+ * caplog.text -> string containing formatted log output
+ * caplog.records -> list of logging.LogRecord instances
+ * caplog.record_tuples -> list of (logger_name, level, message) tuples
+ * caplog.clear() -> clear captured records and formatted log output string
+ """
+ result = LogCaptureFixture(request.node, _ispytest=True)
+ yield result
+ result._finalize()
+
+
+def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[int]:
+ for setting_name in setting_names:
+ log_level = config.getoption(setting_name)
+ if log_level is None:
+ log_level = config.getini(setting_name)
+ if log_level:
+ break
+ else:
+ return None
+
+ if isinstance(log_level, str):
+ log_level = log_level.upper()
+ try:
+ return int(getattr(logging, log_level, log_level))
+ except ValueError as e:
+ # Python logging does not recognise this as a logging level
+ raise UsageError(
+ "'{}' is not recognized as a logging level name for "
+ "'{}'. Please consider passing the "
+ "logging level num instead.".format(log_level, setting_name)
+ ) from e
+
+
+# run after terminalreporter/capturemanager are configured
+@hookimpl(trylast=True)
+def pytest_configure(config: Config) -> None:
+ config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
+
+
+class LoggingPlugin:
+ """Attaches to the logging module and captures log messages for each test."""
+
+ def __init__(self, config: Config) -> None:
+ """Create a new plugin to capture log messages.
+
+ The formatter can be safely shared across all handlers so
+ create a single one for the entire test session here.
+ """
+ self._config = config
+
+ # Report logging.
+ self.formatter = self._create_formatter(
+ get_option_ini(config, "log_format"),
+ get_option_ini(config, "log_date_format"),
+ get_option_ini(config, "log_auto_indent"),
+ )
+ self.log_level = get_log_level_for_setting(config, "log_level")
+ self.caplog_handler = LogCaptureHandler()
+ self.caplog_handler.setFormatter(self.formatter)
+ self.report_handler = LogCaptureHandler()
+ self.report_handler.setFormatter(self.formatter)
+
+ # File logging.
+ self.log_file_level = get_log_level_for_setting(config, "log_file_level")
+ log_file = get_option_ini(config, "log_file") or os.devnull
+ if log_file != os.devnull:
+ directory = os.path.dirname(os.path.abspath(log_file))
+ if not os.path.isdir(directory):
+ os.makedirs(directory)
+
+ self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
+ log_file_format = get_option_ini(config, "log_file_format", "log_format")
+ log_file_date_format = get_option_ini(
+ config, "log_file_date_format", "log_date_format"
+ )
+
+ log_file_formatter = DatetimeFormatter(
+ log_file_format, datefmt=log_file_date_format
+ )
+ self.log_file_handler.setFormatter(log_file_formatter)
+
+ # CLI/live logging.
+ self.log_cli_level = get_log_level_for_setting(
+ config, "log_cli_level", "log_level"
+ )
+ if self._log_cli_enabled():
+ terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
+ # Guaranteed by `_log_cli_enabled()`.
+ assert terminal_reporter is not None
+ capture_manager = config.pluginmanager.get_plugin("capturemanager")
+ # if capturemanager plugin is disabled, live logging still works.
+ self.log_cli_handler: Union[
+ _LiveLoggingStreamHandler, _LiveLoggingNullHandler
+ ] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
+ else:
+ self.log_cli_handler = _LiveLoggingNullHandler()
+ log_cli_formatter = self._create_formatter(
+ get_option_ini(config, "log_cli_format", "log_format"),
+ get_option_ini(config, "log_cli_date_format", "log_date_format"),
+ get_option_ini(config, "log_auto_indent"),
+ )
+ self.log_cli_handler.setFormatter(log_cli_formatter)
+ self._disable_loggers(loggers_to_disable=config.option.logger_disable)
+
+ def _disable_loggers(self, loggers_to_disable: List[str]) -> None:
+ if not loggers_to_disable:
+ return
+
+ for name in loggers_to_disable:
+ logger = logging.getLogger(name)
+ logger.disabled = True
+
+ def _create_formatter(self, log_format, log_date_format, auto_indent):
+ # Color option doesn't exist if terminal plugin is disabled.
+ color = getattr(self._config.option, "color", "no")
+ if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
+ log_format
+ ):
+ formatter: logging.Formatter = ColoredLevelFormatter(
+ create_terminal_writer(self._config), log_format, log_date_format
+ )
+ else:
+ formatter = DatetimeFormatter(log_format, log_date_format)
+
+ formatter._style = PercentStyleMultiline(
+ formatter._style._fmt, auto_indent=auto_indent
+ )
+
+ return formatter
+
+ def set_log_path(self, fname: str) -> None:
+ """Set the filename parameter for Logging.FileHandler().
+
+ Creates parent directory if it does not exist.
+
+ .. warning::
+ This is an experimental API.
+ """
+ fpath = Path(fname)
+
+ if not fpath.is_absolute():
+ fpath = self._config.rootpath / fpath
+
+ if not fpath.parent.exists():
+ fpath.parent.mkdir(exist_ok=True, parents=True)
+
+ # https://github.com/python/mypy/issues/11193
+ stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
+ old_stream = self.log_file_handler.setStream(stream)
+ if old_stream:
+ old_stream.close()
+
+ def _log_cli_enabled(self):
+ """Return whether live logging is enabled."""
+ enabled = self._config.getoption(
+ "--log-cli-level"
+ ) is not None or self._config.getini("log_cli")
+ if not enabled:
+ return False
+
+ terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
+ if terminal_reporter is None:
+ # terminal reporter is disabled e.g. by pytest-xdist.
+ return False
+
+ return True
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_sessionstart(self) -> Generator[None, None, None]:
+ self.log_cli_handler.set_when("sessionstart")
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ yield
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_collection(self) -> Generator[None, None, None]:
+ self.log_cli_handler.set_when("collection")
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ yield
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
+ if session.config.option.collectonly:
+ yield
+ return
+
+ if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
+ # The verbose flag is needed to avoid messy test progress output.
+ self._config.option.verbose = 1
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ yield # Run all the tests.
+
+ @hookimpl
+ def pytest_runtest_logstart(self) -> None:
+ self.log_cli_handler.reset()
+ self.log_cli_handler.set_when("start")
+
+ @hookimpl
+ def pytest_runtest_logreport(self) -> None:
+ self.log_cli_handler.set_when("logreport")
+
+ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]:
+ """Implement the internals of the pytest_runtest_xxx() hooks."""
+ with catching_logs(
+ self.caplog_handler,
+ level=self.log_level,
+ ) as caplog_handler, catching_logs(
+ self.report_handler,
+ level=self.log_level,
+ ) as report_handler:
+ caplog_handler.reset()
+ report_handler.reset()
+ item.stash[caplog_records_key][when] = caplog_handler.records
+ item.stash[caplog_handler_key] = caplog_handler
+
+ yield
+
+ log = report_handler.stream.getvalue().strip()
+ item.add_report_section(when, "log", log)
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
+ self.log_cli_handler.set_when("setup")
+
+ empty: Dict[str, List[logging.LogRecord]] = {}
+ item.stash[caplog_records_key] = empty
+ yield from self._runtest_for(item, "setup")
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
+ self.log_cli_handler.set_when("call")
+
+ yield from self._runtest_for(item, "call")
+
+ @hookimpl(hookwrapper=True)
+ def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
+ self.log_cli_handler.set_when("teardown")
+
+ yield from self._runtest_for(item, "teardown")
+ del item.stash[caplog_records_key]
+ del item.stash[caplog_handler_key]
+
+ @hookimpl
+ def pytest_runtest_logfinish(self) -> None:
+ self.log_cli_handler.set_when("finish")
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_sessionfinish(self) -> Generator[None, None, None]:
+ self.log_cli_handler.set_when("sessionfinish")
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ yield
+
+ @hookimpl
+ def pytest_unconfigure(self) -> None:
+ # Close the FileHandler explicitly.
+ # (logging.shutdown might have lost the weakref?!)
+ self.log_file_handler.close()
+
+
+class _FileHandler(logging.FileHandler):
+ """A logging FileHandler with pytest tweaks."""
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
+
+
+class _LiveLoggingStreamHandler(logging_StreamHandler):
+ """A logging StreamHandler used by the live logging feature: it will
+ write a newline before the first log message in each test.
+
+ During live logging we must also explicitly disable stdout/stderr
+ capturing otherwise it will get captured and won't appear in the
+ terminal.
+ """
+
+ # Officially stream needs to be a IO[str], but TerminalReporter
+ # isn't. So force it.
+ stream: TerminalReporter = None # type: ignore
+
+ def __init__(
+ self,
+ terminal_reporter: TerminalReporter,
+ capture_manager: Optional[CaptureManager],
+ ) -> None:
+ super().__init__(stream=terminal_reporter) # type: ignore[arg-type]
+ self.capture_manager = capture_manager
+ self.reset()
+ self.set_when(None)
+ self._test_outcome_written = False
+
+ def reset(self) -> None:
+ """Reset the handler; should be called before the start of each test."""
+ self._first_record_emitted = False
+
+ def set_when(self, when: Optional[str]) -> None:
+ """Prepare for the given test phase (setup/call/teardown)."""
+ self._when = when
+ self._section_name_shown = False
+ if when == "start":
+ self._test_outcome_written = False
+
+ def emit(self, record: logging.LogRecord) -> None:
+ ctx_manager = (
+ self.capture_manager.global_and_fixture_disabled()
+ if self.capture_manager
+ else nullcontext()
+ )
+ with ctx_manager:
+ if not self._first_record_emitted:
+ self.stream.write("\n")
+ self._first_record_emitted = True
+ elif self._when in ("teardown", "finish"):
+ if not self._test_outcome_written:
+ self._test_outcome_written = True
+ self.stream.write("\n")
+ if not self._section_name_shown and self._when:
+ self.stream.section("live log " + self._when, sep="-", bold=True)
+ self._section_name_shown = True
+ super().emit(record)
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
+
+
+class _LiveLoggingNullHandler(logging.NullHandler):
+ """A logging handler used when live logging is disabled."""
+
+ def reset(self) -> None:
+ pass
+
+ def set_when(self, when: str) -> None:
+ pass
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
diff --git a/venv/lib/python3.12/site-packages/_pytest/main.py b/venv/lib/python3.12/site-packages/_pytest/main.py
new file mode 100644
index 0000000..ea89a63
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/main.py
@@ -0,0 +1,913 @@
+"""Core implementation of the testing process: init, session, runtest loop."""
+import argparse
+import dataclasses
+import fnmatch
+import functools
+import importlib
+import os
+import sys
+from pathlib import Path
+from typing import Callable
+from typing import Dict
+from typing import FrozenSet
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+import _pytest._code
+from _pytest import nodes
+from _pytest.compat import final
+from _pytest.compat import overload
+from _pytest.config import Config
+from _pytest.config import directory_arg
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.config import UsageError
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureManager
+from _pytest.outcomes import exit
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import fnmatch_ex
+from _pytest.pathlib import safe_exists
+from _pytest.pathlib import visit
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+from _pytest.runner import collect_one_node
+from _pytest.runner import SetupState
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "norecursedirs",
+ "Directory patterns to avoid for recursion",
+ type="args",
+ default=[
+ "*.egg",
+ ".*",
+ "_darcs",
+ "build",
+ "CVS",
+ "dist",
+ "node_modules",
+ "venv",
+ "{arch}",
+ ],
+ )
+ parser.addini(
+ "testpaths",
+ "Directories to search for tests when no files or directories are given on the "
+ "command line",
+ type="args",
+ default=[],
+ )
+ group = parser.getgroup("general", "Running and selection options")
+ group._addoption(
+ "-x",
+ "--exitfirst",
+ action="store_const",
+ dest="maxfail",
+ const=1,
+ help="Exit instantly on first error or failed test",
+ )
+ group = parser.getgroup("pytest-warnings")
+ group.addoption(
+ "-W",
+ "--pythonwarnings",
+ action="append",
+ help="Set which warnings to report, see -W option of Python itself",
+ )
+ parser.addini(
+ "filterwarnings",
+ type="linelist",
+ help="Each line specifies a pattern for "
+ "warnings.filterwarnings. "
+ "Processed after -W/--pythonwarnings.",
+ )
+ group._addoption(
+ "--maxfail",
+ metavar="num",
+ action="store",
+ type=int,
+ dest="maxfail",
+ default=0,
+ help="Exit after first num failures or errors",
+ )
+ group._addoption(
+ "--strict-config",
+ action="store_true",
+ help="Any warnings encountered while parsing the `pytest` section of the "
+ "configuration file raise errors",
+ )
+ group._addoption(
+ "--strict-markers",
+ action="store_true",
+ help="Markers not registered in the `markers` section of the configuration "
+ "file raise errors",
+ )
+ group._addoption(
+ "--strict",
+ action="store_true",
+ help="(Deprecated) alias to --strict-markers",
+ )
+ group._addoption(
+ "-c",
+ "--config-file",
+ metavar="FILE",
+ type=str,
+ dest="inifilename",
+ help="Load configuration from `FILE` instead of trying to locate one of the "
+ "implicit configuration files.",
+ )
+ group._addoption(
+ "--continue-on-collection-errors",
+ action="store_true",
+ default=False,
+ dest="continue_on_collection_errors",
+ help="Force test execution even if collection errors occur",
+ )
+ group._addoption(
+ "--rootdir",
+ action="store",
+ dest="rootdir",
+ help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "
+ "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "
+ "'$HOME/root_dir'.",
+ )
+
+ group = parser.getgroup("collect", "collection")
+ group.addoption(
+ "--collectonly",
+ "--collect-only",
+ "--co",
+ action="store_true",
+ help="Only collect tests, don't execute them",
+ )
+ group.addoption(
+ "--pyargs",
+ action="store_true",
+ help="Try to interpret all arguments as Python packages",
+ )
+ group.addoption(
+ "--ignore",
+ action="append",
+ metavar="path",
+ help="Ignore path during collection (multi-allowed)",
+ )
+ group.addoption(
+ "--ignore-glob",
+ action="append",
+ metavar="path",
+ help="Ignore path pattern during collection (multi-allowed)",
+ )
+ group.addoption(
+ "--deselect",
+ action="append",
+ metavar="nodeid_prefix",
+ help="Deselect item (via node id prefix) during collection (multi-allowed)",
+ )
+ group.addoption(
+ "--confcutdir",
+ dest="confcutdir",
+ default=None,
+ metavar="dir",
+ type=functools.partial(directory_arg, optname="--confcutdir"),
+ help="Only load conftest.py's relative to specified dir",
+ )
+ group.addoption(
+ "--noconftest",
+ action="store_true",
+ dest="noconftest",
+ default=False,
+ help="Don't load any conftest.py files",
+ )
+ group.addoption(
+ "--keepduplicates",
+ "--keep-duplicates",
+ action="store_true",
+ dest="keepduplicates",
+ default=False,
+ help="Keep duplicate tests",
+ )
+ group.addoption(
+ "--collect-in-virtualenv",
+ action="store_true",
+ dest="collect_in_virtualenv",
+ default=False,
+ help="Don't ignore tests in a local virtualenv directory",
+ )
+ group.addoption(
+ "--import-mode",
+ default="prepend",
+ choices=["prepend", "append", "importlib"],
+ dest="importmode",
+ help="Prepend/append to sys.path when importing test modules and conftest "
+ "files. Default: prepend.",
+ )
+
+ group = parser.getgroup("debugconfig", "test session debugging and configuration")
+ group.addoption(
+ "--basetemp",
+ dest="basetemp",
+ default=None,
+ type=validate_basetemp,
+ metavar="dir",
+ help=(
+ "Base temporary directory for this test run. "
+ "(Warning: this directory is removed if it exists.)"
+ ),
+ )
+
+
+def validate_basetemp(path: str) -> str:
+ # GH 7119
+ msg = "basetemp must not be empty, the current working directory or any parent directory of it"
+
+ # empty path
+ if not path:
+ raise argparse.ArgumentTypeError(msg)
+
+ def is_ancestor(base: Path, query: Path) -> bool:
+ """Return whether query is an ancestor of base."""
+ if base == query:
+ return True
+ return query in base.parents
+
+ # check if path is an ancestor of cwd
+ if is_ancestor(Path.cwd(), Path(path).absolute()):
+ raise argparse.ArgumentTypeError(msg)
+
+ # check symlinks for ancestors
+ if is_ancestor(Path.cwd().resolve(), Path(path).resolve()):
+ raise argparse.ArgumentTypeError(msg)
+
+ return path
+
+
+def wrap_session(
+ config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
+) -> Union[int, ExitCode]:
+ """Skeleton command line program."""
+ session = Session.from_config(config)
+ session.exitstatus = ExitCode.OK
+ initstate = 0
+ try:
+ try:
+ config._do_configure()
+ initstate = 1
+ config.hook.pytest_sessionstart(session=session)
+ initstate = 2
+ session.exitstatus = doit(config, session) or 0
+ except UsageError:
+ session.exitstatus = ExitCode.USAGE_ERROR
+ raise
+ except Failed:
+ session.exitstatus = ExitCode.TESTS_FAILED
+ except (KeyboardInterrupt, exit.Exception):
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED
+ if isinstance(excinfo.value, exit.Exception):
+ if excinfo.value.returncode is not None:
+ exitstatus = excinfo.value.returncode
+ if initstate < 2:
+ sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")
+ config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
+ session.exitstatus = exitstatus
+ except BaseException:
+ session.exitstatus = ExitCode.INTERNAL_ERROR
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ try:
+ config.notify_exception(excinfo, config.option)
+ except exit.Exception as exc:
+ if exc.returncode is not None:
+ session.exitstatus = exc.returncode
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
+ else:
+ if isinstance(excinfo.value, SystemExit):
+ sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
+
+ finally:
+ # Explicitly break reference cycle.
+ excinfo = None # type: ignore
+ os.chdir(session.startpath)
+ if initstate >= 2:
+ try:
+ config.hook.pytest_sessionfinish(
+ session=session, exitstatus=session.exitstatus
+ )
+ except exit.Exception as exc:
+ if exc.returncode is not None:
+ session.exitstatus = exc.returncode
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
+ config._ensure_unconfigure()
+ return session.exitstatus
+
+
+def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:
+ return wrap_session(config, _main)
+
+
+def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
+ """Default command line protocol for initialization, session,
+ running tests and reporting."""
+ config.hook.pytest_collection(session=session)
+ config.hook.pytest_runtestloop(session=session)
+
+ if session.testsfailed:
+ return ExitCode.TESTS_FAILED
+ elif session.testscollected == 0:
+ return ExitCode.NO_TESTS_COLLECTED
+ return None
+
+
+def pytest_collection(session: "Session") -> None:
+ session.perform_collect()
+
+
+def pytest_runtestloop(session: "Session") -> bool:
+ if session.testsfailed and not session.config.option.continue_on_collection_errors:
+ raise session.Interrupted(
+ "%d error%s during collection"
+ % (session.testsfailed, "s" if session.testsfailed != 1 else "")
+ )
+
+ if session.config.option.collectonly:
+ return True
+
+ for i, item in enumerate(session.items):
+ nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
+ item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
+ if session.shouldfail:
+ raise session.Failed(session.shouldfail)
+ if session.shouldstop:
+ raise session.Interrupted(session.shouldstop)
+ return True
+
+
+def _in_venv(path: Path) -> bool:
+ """Attempt to detect if ``path`` is the root of a Virtual Environment by
+ checking for the existence of the appropriate activate script."""
+ bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin")
+ try:
+ if not bindir.is_dir():
+ return False
+ except OSError:
+ return False
+ activates = (
+ "activate",
+ "activate.csh",
+ "activate.fish",
+ "Activate",
+ "Activate.bat",
+ "Activate.ps1",
+ )
+ return any(fname.name in activates for fname in bindir.iterdir())
+
+
+def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
+ ignore_paths = config._getconftest_pathlist(
+ "collect_ignore", path=collection_path.parent, rootpath=config.rootpath
+ )
+ ignore_paths = ignore_paths or []
+ excludeopt = config.getoption("ignore")
+ if excludeopt:
+ ignore_paths.extend(absolutepath(x) for x in excludeopt)
+
+ if collection_path in ignore_paths:
+ return True
+
+ ignore_globs = config._getconftest_pathlist(
+ "collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath
+ )
+ ignore_globs = ignore_globs or []
+ excludeglobopt = config.getoption("ignore_glob")
+ if excludeglobopt:
+ ignore_globs.extend(absolutepath(x) for x in excludeglobopt)
+
+ if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs):
+ return True
+
+ allow_in_venv = config.getoption("collect_in_virtualenv")
+ if not allow_in_venv and _in_venv(collection_path):
+ return True
+
+ if collection_path.is_dir():
+ norecursepatterns = config.getini("norecursedirs")
+ if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
+ return True
+
+ return None
+
+
+def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None:
+ deselect_prefixes = tuple(config.getoption("deselect") or [])
+ if not deselect_prefixes:
+ return
+
+ remaining = []
+ deselected = []
+ for colitem in items:
+ if colitem.nodeid.startswith(deselect_prefixes):
+ deselected.append(colitem)
+ else:
+ remaining.append(colitem)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+class FSHookProxy:
+ def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
+ self.pm = pm
+ self.remove_mods = remove_mods
+
+ def __getattr__(self, name: str):
+ x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
+ self.__dict__[name] = x
+ return x
+
+
+class Interrupted(KeyboardInterrupt):
+ """Signals that the test run was interrupted."""
+
+ __module__ = "builtins" # For py3.
+
+
+class Failed(Exception):
+ """Signals a stop as failed test run."""
+
+
+@dataclasses.dataclass
+class _bestrelpath_cache(Dict[Path, str]):
+ __slots__ = ("path",)
+
+ path: Path
+
+ def __missing__(self, path: Path) -> str:
+ r = bestrelpath(self.path, path)
+ self[path] = r
+ return r
+
+
+@final
+class Session(nodes.FSCollector):
+ """The root of the collection tree.
+
+ ``Session`` collects the initial paths given as arguments to pytest.
+ """
+
+ Interrupted = Interrupted
+ Failed = Failed
+ # Set on the session by runner.pytest_sessionstart.
+ _setupstate: SetupState
+ # Set on the session by fixtures.pytest_sessionstart.
+ _fixturemanager: FixtureManager
+ exitstatus: Union[int, ExitCode]
+
+ def __init__(self, config: Config) -> None:
+ super().__init__(
+ path=config.rootpath,
+ fspath=None,
+ parent=None,
+ config=config,
+ session=self,
+ nodeid="",
+ )
+ self.testsfailed = 0
+ self.testscollected = 0
+ self.shouldstop: Union[bool, str] = False
+ self.shouldfail: Union[bool, str] = False
+ self.trace = config.trace.root.get("collection")
+ self._initialpaths: FrozenSet[Path] = frozenset()
+
+ self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
+
+ self.config.pluginmanager.register(self, name="session")
+
+ @classmethod
+ def from_config(cls, config: Config) -> "Session":
+ session: Session = cls._create(config=config)
+ return session
+
+ def __repr__(self) -> str:
+ return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % (
+ self.__class__.__name__,
+ self.name,
+ getattr(self, "exitstatus", ""),
+ self.testsfailed,
+ self.testscollected,
+ )
+
+ @property
+ def startpath(self) -> Path:
+ """The path from which pytest was invoked.
+
+ .. versionadded:: 7.0.0
+ """
+ return self.config.invocation_params.dir
+
+ def _node_location_to_relpath(self, node_path: Path) -> str:
+ # bestrelpath is a quite slow function.
+ return self._bestrelpathcache[node_path]
+
+ @hookimpl(tryfirst=True)
+ def pytest_collectstart(self) -> None:
+ if self.shouldfail:
+ raise self.Failed(self.shouldfail)
+ if self.shouldstop:
+ raise self.Interrupted(self.shouldstop)
+
+ @hookimpl(tryfirst=True)
+ def pytest_runtest_logreport(
+ self, report: Union[TestReport, CollectReport]
+ ) -> None:
+ if report.failed and not hasattr(report, "wasxfail"):
+ self.testsfailed += 1
+ maxfail = self.config.getvalue("maxfail")
+ if maxfail and self.testsfailed >= maxfail:
+ self.shouldfail = "stopping after %d failures" % (self.testsfailed)
+
+ pytest_collectreport = pytest_runtest_logreport
+
+ def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
+ # Optimization: Path(Path(...)) is much slower than isinstance.
+ path_ = path if isinstance(path, Path) else Path(path)
+ return path_ in self._initialpaths
+
+ def gethookproxy(self, fspath: "os.PathLike[str]"):
+ # Optimization: Path(Path(...)) is much slower than isinstance.
+ path = fspath if isinstance(fspath, Path) else Path(fspath)
+ pm = self.config.pluginmanager
+ # Check if we have the common case of running
+ # hooks with all conftest.py files.
+ my_conftestmodules = pm._getconftestmodules(
+ path,
+ self.config.getoption("importmode"),
+ rootpath=self.config.rootpath,
+ )
+ remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+ if remove_mods:
+ # One or more conftests are not in use at this fspath.
+ from .config.compat import PathAwareHookProxy
+
+ proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods))
+ else:
+ # All plugins are active for this fspath.
+ proxy = self.config.hook
+ return proxy
+
+ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
+ if direntry.name == "__pycache__":
+ return False
+ fspath = Path(direntry.path)
+ ihook = self.gethookproxy(fspath.parent)
+ if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
+ return False
+ return True
+
+ def _collectfile(
+ self, fspath: Path, handle_dupes: bool = True
+ ) -> Sequence[nodes.Collector]:
+ assert (
+ fspath.is_file()
+ ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
+ fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
+ )
+ ihook = self.gethookproxy(fspath)
+ if not self.isinitpath(fspath):
+ if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
+ return ()
+
+ if handle_dupes:
+ keepduplicates = self.config.getoption("keepduplicates")
+ if not keepduplicates:
+ duplicate_paths = self.config.pluginmanager._duplicatepaths
+ if fspath in duplicate_paths:
+ return ()
+ else:
+ duplicate_paths.add(fspath)
+
+ return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return]
+
+ @overload
+ def perform_collect(
+ self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
+ ) -> Sequence[nodes.Item]:
+ ...
+
+ @overload
+ def perform_collect( # noqa: F811
+ self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
+ ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
+ ...
+
+ def perform_collect( # noqa: F811
+ self, args: Optional[Sequence[str]] = None, genitems: bool = True
+ ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
+ """Perform the collection phase for this session.
+
+ This is called by the default :hook:`pytest_collection` hook
+ implementation; see the documentation of this hook for more details.
+ For testing purposes, it may also be called directly on a fresh
+ ``Session``.
+
+ This function normally recursively expands any collectors collected
+ from the session to their items, and only items are returned. For
+ testing purposes, this may be suppressed by passing ``genitems=False``,
+ in which case the return value contains these collectors unexpanded,
+ and ``session.items`` is empty.
+ """
+ if args is None:
+ args = self.config.args
+
+ self.trace("perform_collect", self, args)
+ self.trace.root.indent += 1
+
+ self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
+ self._initial_parts: List[Tuple[Path, List[str]]] = []
+ self.items: List[nodes.Item] = []
+
+ hook = self.config.hook
+
+ items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items
+ try:
+ initialpaths: List[Path] = []
+ for arg in args:
+ fspath, parts = resolve_collection_argument(
+ self.config.invocation_params.dir,
+ arg,
+ as_pypath=self.config.option.pyargs,
+ )
+ self._initial_parts.append((fspath, parts))
+ initialpaths.append(fspath)
+ self._initialpaths = frozenset(initialpaths)
+ rep = collect_one_node(self)
+ self.ihook.pytest_collectreport(report=rep)
+ self.trace.root.indent -= 1
+ if self._notfound:
+ errors = []
+ for arg, collectors in self._notfound:
+ if collectors:
+ errors.append(
+ f"not found: {arg}\n(no name {arg!r} in any of {collectors!r})"
+ )
+ else:
+ errors.append(f"found no collectors for {arg}")
+
+ raise UsageError(*errors)
+ if not genitems:
+ items = rep.result
+ else:
+ if rep.passed:
+ for node in rep.result:
+ self.items.extend(self.genitems(node))
+
+ self.config.pluginmanager.check_pending()
+ hook.pytest_collection_modifyitems(
+ session=self, config=self.config, items=items
+ )
+ finally:
+ hook.pytest_collection_finish(session=self)
+
+ self.testscollected = len(items)
+ return items
+
+ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
+ from _pytest.python import Package
+
+ # Keep track of any collected nodes in here, so we don't duplicate fixtures.
+ node_cache1: Dict[Path, Sequence[nodes.Collector]] = {}
+ node_cache2: Dict[Tuple[Type[nodes.Collector], Path], nodes.Collector] = {}
+
+ # Keep track of any collected collectors in matchnodes paths, so they
+ # are not collected more than once.
+ matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
+
+ # Directories of pkgs with dunder-init files.
+ pkg_roots: Dict[Path, Package] = {}
+
+ for argpath, names in self._initial_parts:
+ self.trace("processing argument", (argpath, names))
+ self.trace.root.indent += 1
+
+ # Start with a Session root, and delve to argpath item (dir or file)
+ # and stack all Packages found on the way.
+ # No point in finding packages when collecting doctests.
+ if not self.config.getoption("doctestmodules", False):
+ pm = self.config.pluginmanager
+ for parent in (argpath, *argpath.parents):
+ if not pm._is_in_confcutdir(argpath):
+ break
+
+ if parent.is_dir():
+ pkginit = parent / "__init__.py"
+ if pkginit.is_file() and pkginit not in node_cache1:
+ col = self._collectfile(pkginit, handle_dupes=False)
+ if col:
+ if isinstance(col[0], Package):
+ pkg_roots[parent] = col[0]
+ node_cache1[col[0].path] = [col[0]]
+
+ # If it's a directory argument, recurse and look for any Subpackages.
+ # Let the Package collector deal with subnodes, don't collect here.
+ if argpath.is_dir():
+ assert not names, f"invalid arg {(argpath, names)!r}"
+
+ seen_dirs: Set[Path] = set()
+ for direntry in visit(argpath, self._recurse):
+ if not direntry.is_file():
+ continue
+
+ path = Path(direntry.path)
+ dirpath = path.parent
+
+ if dirpath not in seen_dirs:
+ # Collect packages first.
+ seen_dirs.add(dirpath)
+ pkginit = dirpath / "__init__.py"
+ if pkginit.exists():
+ for x in self._collectfile(pkginit):
+ yield x
+ if isinstance(x, Package):
+ pkg_roots[dirpath] = x
+ if dirpath in pkg_roots:
+ # Do not collect packages here.
+ continue
+
+ for x in self._collectfile(path):
+ key2 = (type(x), x.path)
+ if key2 in node_cache2:
+ yield node_cache2[key2]
+ else:
+ node_cache2[key2] = x
+ yield x
+ else:
+ assert argpath.is_file()
+
+ if argpath in node_cache1:
+ col = node_cache1[argpath]
+ else:
+ collect_root = pkg_roots.get(argpath.parent, self)
+ col = collect_root._collectfile(argpath, handle_dupes=False)
+ if col:
+ node_cache1[argpath] = col
+
+ matching = []
+ work: List[
+ Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]
+ ] = [(col, names)]
+ while work:
+ self.trace("matchnodes", col, names)
+ self.trace.root.indent += 1
+
+ matchnodes, matchnames = work.pop()
+ for node in matchnodes:
+ if not matchnames:
+ matching.append(node)
+ continue
+ if not isinstance(node, nodes.Collector):
+ continue
+ key = (type(node), node.nodeid)
+ if key in matchnodes_cache:
+ rep = matchnodes_cache[key]
+ else:
+ rep = collect_one_node(node)
+ matchnodes_cache[key] = rep
+ if rep.passed:
+ submatchnodes = []
+ for r in rep.result:
+ # TODO: Remove parametrized workaround once collection structure contains
+ # parametrization.
+ if (
+ r.name == matchnames[0]
+ or r.name.split("[")[0] == matchnames[0]
+ ):
+ submatchnodes.append(r)
+ if submatchnodes:
+ work.append((submatchnodes, matchnames[1:]))
+ else:
+ # Report collection failures here to avoid failing to run some test
+ # specified in the command line because the module could not be
+ # imported (#134).
+ node.ihook.pytest_collectreport(report=rep)
+
+ self.trace("matchnodes finished -> ", len(matching), "nodes")
+ self.trace.root.indent -= 1
+
+ if not matching:
+ report_arg = "::".join((str(argpath), *names))
+ self._notfound.append((report_arg, col))
+ continue
+
+ # If __init__.py was the only file requested, then the matched
+ # node will be the corresponding Package (by default), and the
+ # first yielded item will be the __init__ Module itself, so
+ # just use that. If this special case isn't taken, then all the
+ # files in the package will be yielded.
+ if argpath.name == "__init__.py" and isinstance(matching[0], Package):
+ try:
+ yield next(iter(matching[0].collect()))
+ except StopIteration:
+ # The package collects nothing with only an __init__.py
+ # file in it, which gets ignored by the default
+ # "python_files" option.
+ pass
+ continue
+
+ yield from matching
+
+ self.trace.root.indent -= 1
+
+ def genitems(
+ self, node: Union[nodes.Item, nodes.Collector]
+ ) -> Iterator[nodes.Item]:
+ self.trace("genitems", node)
+ if isinstance(node, nodes.Item):
+ node.ihook.pytest_itemcollected(item=node)
+ yield node
+ else:
+ assert isinstance(node, nodes.Collector)
+ rep = collect_one_node(node)
+ if rep.passed:
+ for subnode in rep.result:
+ yield from self.genitems(subnode)
+ node.ihook.pytest_collectreport(report=rep)
+
+
+def search_pypath(module_name: str) -> str:
+ """Search sys.path for the given a dotted module name, and return its file system path."""
+ try:
+ spec = importlib.util.find_spec(module_name)
+ # AttributeError: looks like package module, but actually filename
+ # ImportError: module does not exist
+ # ValueError: not a module name
+ except (AttributeError, ImportError, ValueError):
+ return module_name
+ if spec is None or spec.origin is None or spec.origin == "namespace":
+ return module_name
+ elif spec.submodule_search_locations:
+ return os.path.dirname(spec.origin)
+ else:
+ return spec.origin
+
+
+def resolve_collection_argument(
+ invocation_path: Path, arg: str, *, as_pypath: bool = False
+) -> Tuple[Path, List[str]]:
+ """Parse path arguments optionally containing selection parts and return (fspath, names).
+
+ Command-line arguments can point to files and/or directories, and optionally contain
+ parts for specific tests selection, for example:
+
+ "pkg/tests/test_foo.py::TestClass::test_foo"
+
+ This function ensures the path exists, and returns a tuple:
+
+ (Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
+
+ When as_pypath is True, expects that the command-line argument actually contains
+ module paths instead of file-system paths:
+
+ "pkg.tests.test_foo::TestClass::test_foo"
+
+ In which case we search sys.path for a matching module, and then return the *path* to the
+ found module.
+
+ If the path doesn't exist, raise UsageError.
+ If the path is a directory and selection parts are present, raise UsageError.
+ """
+ base, squacket, rest = str(arg).partition("[")
+ strpath, *parts = base.split("::")
+ if parts:
+ parts[-1] = f"{parts[-1]}{squacket}{rest}"
+ if as_pypath:
+ strpath = search_pypath(strpath)
+ fspath = invocation_path / strpath
+ fspath = absolutepath(fspath)
+ if not safe_exists(fspath):
+ msg = (
+ "module or package not found: {arg} (missing __init__.py?)"
+ if as_pypath
+ else "file or directory not found: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ if parts and fspath.is_dir():
+ msg = (
+ "package argument cannot contain :: selection parts: {arg}"
+ if as_pypath
+ else "directory argument cannot contain :: selection parts: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ return fspath, parts
diff --git a/venv/lib/python3.12/site-packages/_pytest/mark/__init__.py b/venv/lib/python3.12/site-packages/_pytest/mark/__init__.py
new file mode 100644
index 0000000..de46b4c
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/mark/__init__.py
@@ -0,0 +1,269 @@
+"""Generic mechanism for marking and selecting python functions."""
+import dataclasses
+from typing import AbstractSet
+from typing import Collection
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from .expression import Expression
+from .expression import ParseError
+from .structures import EMPTY_PARAMETERSET_OPTION
+from .structures import get_empty_parameterset_mark
+from .structures import Mark
+from .structures import MARK_GEN
+from .structures import MarkDecorator
+from .structures import MarkGenerator
+from .structures import ParameterSet
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import UsageError
+from _pytest.config.argparsing import Parser
+from _pytest.stash import StashKey
+
+if TYPE_CHECKING:
+ from _pytest.nodes import Item
+
+
+__all__ = [
+ "MARK_GEN",
+ "Mark",
+ "MarkDecorator",
+ "MarkGenerator",
+ "ParameterSet",
+ "get_empty_parameterset_mark",
+]
+
+
+old_mark_config_key = StashKey[Optional[Config]]()
+
+
+def param(
+ *values: object,
+ marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
+ id: Optional[str] = None,
+) -> ParameterSet:
+ """Specify a parameter in `pytest.mark.parametrize`_ calls or
+ :ref:`parametrized fixtures `.
+
+ .. code-block:: python
+
+ @pytest.mark.parametrize(
+ "test_input,expected",
+ [
+ ("3+5", 8),
+ pytest.param("6*9", 42, marks=pytest.mark.xfail),
+ ],
+ )
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+ :param values: Variable args of the values of the parameter set, in order.
+ :param marks: A single mark or a list of marks to be applied to this parameter set.
+ :param id: The id to attribute to this parameter set.
+ """
+ return ParameterSet.param(*values, marks=marks, id=id)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption(
+ "-k",
+ action="store",
+ dest="keyword",
+ default="",
+ metavar="EXPRESSION",
+ help="Only run tests which match the given substring expression. "
+ "An expression is a Python evaluatable expression "
+ "where all names are substring-matched against test names "
+ "and their parent classes. Example: -k 'test_method or test_"
+ "other' matches all test functions and classes whose name "
+ "contains 'test_method' or 'test_other', while -k 'not test_method' "
+ "matches those that don't contain 'test_method' in their names. "
+ "-k 'not test_method and not test_other' will eliminate the matches. "
+ "Additionally keywords are matched to classes and functions "
+ "containing extra names in their 'extra_keyword_matches' set, "
+ "as well as functions which have names assigned directly to them. "
+ "The matching is case-insensitive.",
+ )
+
+ group._addoption(
+ "-m",
+ action="store",
+ dest="markexpr",
+ default="",
+ metavar="MARKEXPR",
+ help="Only run tests matching given mark expression. "
+ "For example: -m 'mark1 and not mark2'.",
+ )
+
+ group.addoption(
+ "--markers",
+ action="store_true",
+ help="show markers (builtin, plugin and per-project ones).",
+ )
+
+ parser.addini("markers", "Markers for test functions", "linelist")
+ parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
+
+
+@hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ import _pytest.config
+
+ if config.option.markers:
+ config._do_configure()
+ tw = _pytest.config.create_terminal_writer(config)
+ for line in config.getini("markers"):
+ parts = line.split(":", 1)
+ name = parts[0]
+ rest = parts[1] if len(parts) == 2 else ""
+ tw.write("@pytest.mark.%s:" % name, bold=True)
+ tw.line(rest)
+ tw.line()
+ config._ensure_unconfigure()
+ return 0
+
+ return None
+
+
+@dataclasses.dataclass
+class KeywordMatcher:
+ """A matcher for keywords.
+
+ Given a list of names, matches any substring of one of these names. The
+ string inclusion check is case-insensitive.
+
+ Will match on the name of colitem, including the names of its parents.
+ Only matches names of items which are either a :class:`Class` or a
+ :class:`Function`.
+
+ Additionally, matches on names in the 'extra_keyword_matches' set of
+ any item, as well as names directly assigned to test functions.
+ """
+
+ __slots__ = ("_names",)
+
+ _names: AbstractSet[str]
+
+ @classmethod
+ def from_item(cls, item: "Item") -> "KeywordMatcher":
+ mapped_names = set()
+
+ # Add the names of the current item and any parent items.
+ import pytest
+
+ for node in item.listchain():
+ if not isinstance(node, pytest.Session):
+ mapped_names.add(node.name)
+
+ # Add the names added as extra keywords to current or parent items.
+ mapped_names.update(item.listextrakeywords())
+
+ # Add the names attached to the current function through direct assignment.
+ function_obj = getattr(item, "function", None)
+ if function_obj:
+ mapped_names.update(function_obj.__dict__)
+
+ # Add the markers to the keywords as we no longer handle them correctly.
+ mapped_names.update(mark.name for mark in item.iter_markers())
+
+ return cls(mapped_names)
+
+ def __call__(self, subname: str) -> bool:
+ subname = subname.lower()
+ names = (name.lower() for name in self._names)
+
+ for name in names:
+ if subname in name:
+ return True
+ return False
+
+
+def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
+ keywordexpr = config.option.keyword.lstrip()
+ if not keywordexpr:
+ return
+
+ expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")
+
+ remaining = []
+ deselected = []
+ for colitem in items:
+ if not expr.evaluate(KeywordMatcher.from_item(colitem)):
+ deselected.append(colitem)
+ else:
+ remaining.append(colitem)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+@dataclasses.dataclass
+class MarkMatcher:
+ """A matcher for markers which are present.
+
+ Tries to match on any marker names, attached to the given colitem.
+ """
+
+ __slots__ = ("own_mark_names",)
+
+ own_mark_names: AbstractSet[str]
+
+ @classmethod
+ def from_item(cls, item: "Item") -> "MarkMatcher":
+ mark_names = {mark.name for mark in item.iter_markers()}
+ return cls(mark_names)
+
+ def __call__(self, name: str) -> bool:
+ return name in self.own_mark_names
+
+
+def deselect_by_mark(items: "List[Item]", config: Config) -> None:
+ matchexpr = config.option.markexpr
+ if not matchexpr:
+ return
+
+ expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")
+ remaining: List[Item] = []
+ deselected: List[Item] = []
+ for item in items:
+ if expr.evaluate(MarkMatcher.from_item(item)):
+ remaining.append(item)
+ else:
+ deselected.append(item)
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+def _parse_expression(expr: str, exc_message: str) -> Expression:
+ try:
+ return Expression.compile(expr)
+ except ParseError as e:
+ raise UsageError(f"{exc_message}: {expr}: {e}") from None
+
+
+def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None:
+ deselect_by_keyword(items, config)
+ deselect_by_mark(items, config)
+
+
+def pytest_configure(config: Config) -> None:
+ config.stash[old_mark_config_key] = MARK_GEN._config
+ MARK_GEN._config = config
+
+ empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
+
+ if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
+ raise UsageError(
+ "{!s} must be one of skip, xfail or fail_at_collect"
+ " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
+ )
+
+
+def pytest_unconfigure(config: Config) -> None:
+ MARK_GEN._config = config.stash.get(old_mark_config_key, None)
diff --git a/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..b056a9d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/expression.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/expression.cpython-312.pyc
new file mode 100644
index 0000000..ae63914
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/expression.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/structures.cpython-312.pyc b/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/structures.cpython-312.pyc
new file mode 100644
index 0000000..252934a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_pytest/mark/__pycache__/structures.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/_pytest/mark/expression.py b/venv/lib/python3.12/site-packages/_pytest/mark/expression.py
new file mode 100644
index 0000000..9287bce
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/mark/expression.py
@@ -0,0 +1,228 @@
+r"""Evaluate match expressions, as used by `-k` and `-m`.
+
+The grammar is:
+
+expression: expr? EOF
+expr: and_expr ('or' and_expr)*
+and_expr: not_expr ('and' not_expr)*
+not_expr: 'not' not_expr | '(' expr ')' | ident
+ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
+
+The semantics are:
+
+- Empty expression evaluates to False.
+- ident evaluates to True of False according to a provided matcher function.
+- or/and/not evaluate according to the usual boolean semantics.
+"""
+import ast
+import dataclasses
+import enum
+import re
+import sys
+import types
+from typing import Callable
+from typing import Iterator
+from typing import Mapping
+from typing import NoReturn
+from typing import Optional
+from typing import Sequence
+
+if sys.version_info >= (3, 8):
+ astNameConstant = ast.Constant
+else:
+ astNameConstant = ast.NameConstant
+
+
+__all__ = [
+ "Expression",
+ "ParseError",
+]
+
+
+class TokenType(enum.Enum):
+ LPAREN = "left parenthesis"
+ RPAREN = "right parenthesis"
+ OR = "or"
+ AND = "and"
+ NOT = "not"
+ IDENT = "identifier"
+ EOF = "end of input"
+
+
+@dataclasses.dataclass(frozen=True)
+class Token:
+ __slots__ = ("type", "value", "pos")
+ type: TokenType
+ value: str
+ pos: int
+
+
+class ParseError(Exception):
+ """The expression contains invalid syntax.
+
+ :param column: The column in the line where the error occurred (1-based).
+ :param message: A description of the error.
+ """
+
+ def __init__(self, column: int, message: str) -> None:
+ self.column = column
+ self.message = message
+
+ def __str__(self) -> str:
+ return f"at column {self.column}: {self.message}"
+
+
+class Scanner:
+ __slots__ = ("tokens", "current")
+
+ def __init__(self, input: str) -> None:
+ self.tokens = self.lex(input)
+ self.current = next(self.tokens)
+
+ def lex(self, input: str) -> Iterator[Token]:
+ pos = 0
+ while pos < len(input):
+ if input[pos] in (" ", "\t"):
+ pos += 1
+ elif input[pos] == "(":
+ yield Token(TokenType.LPAREN, "(", pos)
+ pos += 1
+ elif input[pos] == ")":
+ yield Token(TokenType.RPAREN, ")", pos)
+ pos += 1
+ else:
+ match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
+ if match:
+ value = match.group(0)
+ if value == "or":
+ yield Token(TokenType.OR, value, pos)
+ elif value == "and":
+ yield Token(TokenType.AND, value, pos)
+ elif value == "not":
+ yield Token(TokenType.NOT, value, pos)
+ else:
+ yield Token(TokenType.IDENT, value, pos)
+ pos += len(value)
+ else:
+ raise ParseError(
+ pos + 1,
+ f'unexpected character "{input[pos]}"',
+ )
+ yield Token(TokenType.EOF, "", pos)
+
+ def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]:
+ if self.current.type is type:
+ token = self.current
+ if token.type is not TokenType.EOF:
+ self.current = next(self.tokens)
+ return token
+ if reject:
+ self.reject((type,))
+ return None
+
+ def reject(self, expected: Sequence[TokenType]) -> NoReturn:
+ raise ParseError(
+ self.current.pos + 1,
+ "expected {}; got {}".format(
+ " OR ".join(type.value for type in expected),
+ self.current.type.value,
+ ),
+ )
+
+
+# True, False and None are legal match expression identifiers,
+# but illegal as Python identifiers. To fix this, this prefix
+# is added to identifiers in the conversion to Python AST.
+IDENT_PREFIX = "$"
+
+
+def expression(s: Scanner) -> ast.Expression:
+ if s.accept(TokenType.EOF):
+ ret: ast.expr = astNameConstant(False)
+ else:
+ ret = expr(s)
+ s.accept(TokenType.EOF, reject=True)
+ return ast.fix_missing_locations(ast.Expression(ret))
+
+
+def expr(s: Scanner) -> ast.expr:
+ ret = and_expr(s)
+ while s.accept(TokenType.OR):
+ rhs = and_expr(s)
+ ret = ast.BoolOp(ast.Or(), [ret, rhs])
+ return ret
+
+
+def and_expr(s: Scanner) -> ast.expr:
+ ret = not_expr(s)
+ while s.accept(TokenType.AND):
+ rhs = not_expr(s)
+ ret = ast.BoolOp(ast.And(), [ret, rhs])
+ return ret
+
+
+def not_expr(s: Scanner) -> ast.expr:
+ if s.accept(TokenType.NOT):
+ return ast.UnaryOp(ast.Not(), not_expr(s))
+ if s.accept(TokenType.LPAREN):
+ ret = expr(s)
+ s.accept(TokenType.RPAREN, reject=True)
+ return ret
+ ident = s.accept(TokenType.IDENT)
+ if ident:
+ return ast.Name(IDENT_PREFIX + ident.value, ast.Load())
+ s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
+
+
+class MatcherAdapter(Mapping[str, bool]):
+ """Adapts a matcher function to a locals mapping as required by eval()."""
+
+ def __init__(self, matcher: Callable[[str], bool]) -> None:
+ self.matcher = matcher
+
+ def __getitem__(self, key: str) -> bool:
+ return self.matcher(key[len(IDENT_PREFIX) :])
+
+ def __iter__(self) -> Iterator[str]:
+ raise NotImplementedError()
+
+ def __len__(self) -> int:
+ raise NotImplementedError()
+
+
+class Expression:
+ """A compiled match expression as used by -k and -m.
+
+ The expression can be evaluated against different matchers.
+ """
+
+ __slots__ = ("code",)
+
+ def __init__(self, code: types.CodeType) -> None:
+ self.code = code
+
+ @classmethod
+ def compile(self, input: str) -> "Expression":
+ """Compile a match expression.
+
+ :param input: The input expression - one line.
+ """
+ astexpr = expression(Scanner(input))
+ code: types.CodeType = compile(
+ astexpr,
+ filename="",
+ mode="eval",
+ )
+ return Expression(code)
+
+ def evaluate(self, matcher: Callable[[str], bool]) -> bool:
+ """Evaluate the match expression.
+
+ :param matcher:
+ Given an identifier, should return whether it matches or not.
+ Should be prepared to handle arbitrary strings as input.
+
+ :returns: Whether the expression matches or not.
+ """
+ ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))
+ return ret
diff --git a/venv/lib/python3.12/site-packages/_pytest/mark/structures.py b/venv/lib/python3.12/site-packages/_pytest/mark/structures.py
new file mode 100644
index 0000000..55620f0
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/mark/structures.py
@@ -0,0 +1,618 @@
+import collections.abc
+import dataclasses
+import inspect
+import warnings
+from typing import Any
+from typing import Callable
+from typing import Collection
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import MutableMapping
+from typing import NamedTuple
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from .._code import getfslineno
+from ..compat import ascii_escaped
+from ..compat import final
+from ..compat import NOTSET
+from ..compat import NotSetType
+from _pytest.config import Config
+from _pytest.deprecated import check_ispytest
+from _pytest.outcomes import fail
+from _pytest.warning_types import PytestUnknownMarkWarning
+
+if TYPE_CHECKING:
+ from ..nodes import Node
+
+
+EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
+
+
+def istestfunc(func) -> bool:
+ return callable(func) and getattr(func, "__name__", "") != ""
+
+
+def get_empty_parameterset_mark(
+ config: Config, argnames: Sequence[str], func
+) -> "MarkDecorator":
+ from ..nodes import Collector
+
+ fs, lineno = getfslineno(func)
+ reason = "got empty parameter set %r, function %s at %s:%d" % (
+ argnames,
+ func.__name__,
+ fs,
+ lineno,
+ )
+
+ requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
+ if requested_mark in ("", None, "skip"):
+ mark = MARK_GEN.skip(reason=reason)
+ elif requested_mark == "xfail":
+ mark = MARK_GEN.xfail(reason=reason, run=False)
+ elif requested_mark == "fail_at_collect":
+ f_name = func.__name__
+ _, lineno = getfslineno(func)
+ raise Collector.CollectError(
+ "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1)
+ )
+ else:
+ raise LookupError(requested_mark)
+ return mark
+
+
+class ParameterSet(NamedTuple):
+ values: Sequence[Union[object, NotSetType]]
+ marks: Collection[Union["MarkDecorator", "Mark"]]
+ id: Optional[str]
+
+ @classmethod
+ def param(
+ cls,
+ *values: object,
+ marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
+ id: Optional[str] = None,
+ ) -> "ParameterSet":
+ if isinstance(marks, MarkDecorator):
+ marks = (marks,)
+ else:
+ assert isinstance(marks, collections.abc.Collection)
+
+ if id is not None:
+ if not isinstance(id, str):
+ raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")
+ id = ascii_escaped(id)
+ return cls(values, marks, id)
+
+ @classmethod
+ def extract_from(
+ cls,
+ parameterset: Union["ParameterSet", Sequence[object], object],
+ force_tuple: bool = False,
+ ) -> "ParameterSet":
+ """Extract from an object or objects.
+
+ :param parameterset:
+ A legacy style parameterset that may or may not be a tuple,
+ and may or may not be wrapped into a mess of mark objects.
+
+ :param force_tuple:
+ Enforce tuple wrapping so single argument tuple values
+ don't get decomposed and break tests.
+ """
+
+ if isinstance(parameterset, cls):
+ return parameterset
+ if force_tuple:
+ return cls.param(parameterset)
+ else:
+ # TODO: Refactor to fix this type-ignore. Currently the following
+ # passes type-checking but crashes:
+ #
+ # @pytest.mark.parametrize(('x', 'y'), [1, 2])
+ # def test_foo(x, y): pass
+ return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
+
+ @staticmethod
+ def _parse_parametrize_args(
+ argnames: Union[str, Sequence[str]],
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ *args,
+ **kwargs,
+ ) -> Tuple[Sequence[str], bool]:
+ if isinstance(argnames, str):
+ argnames = [x.strip() for x in argnames.split(",") if x.strip()]
+ force_tuple = len(argnames) == 1
+ else:
+ force_tuple = False
+ return argnames, force_tuple
+
+ @staticmethod
+ def _parse_parametrize_parameters(
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ force_tuple: bool,
+ ) -> List["ParameterSet"]:
+ return [
+ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
+ ]
+
+ @classmethod
+ def _for_parametrize(
+ cls,
+ argnames: Union[str, Sequence[str]],
+ argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
+ func,
+ config: Config,
+ nodeid: str,
+ ) -> Tuple[Sequence[str], List["ParameterSet"]]:
+ argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
+ parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
+ del argvalues
+
+ if parameters:
+ # Check all parameter sets have the correct number of values.
+ for param in parameters:
+ if len(param.values) != len(argnames):
+ msg = (
+ '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
+ " {names}\n"
+ "must be equal to the number of values ({values_len}):\n"
+ " {values}"
+ )
+ fail(
+ msg.format(
+ nodeid=nodeid,
+ values=param.values,
+ names=argnames,
+ names_len=len(argnames),
+ values_len=len(param.values),
+ ),
+ pytrace=False,
+ )
+ else:
+ # Empty parameter set (likely computed at runtime): create a single
+ # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
+ mark = get_empty_parameterset_mark(config, argnames, func)
+ parameters.append(
+ ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
+ )
+ return argnames, parameters
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class Mark:
+ """A pytest mark."""
+
+ #: Name of the mark.
+ name: str
+ #: Positional arguments of the mark decorator.
+ args: Tuple[Any, ...]
+ #: Keyword arguments of the mark decorator.
+ kwargs: Mapping[str, Any]
+
+ #: Source Mark for ids with parametrize Marks.
+ _param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
+ #: Resolved/generated ids with parametrize Marks.
+ _param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
+ default=None, repr=False
+ )
+
+ def __init__(
+ self,
+ name: str,
+ args: Tuple[Any, ...],
+ kwargs: Mapping[str, Any],
+ param_ids_from: Optional["Mark"] = None,
+ param_ids_generated: Optional[Sequence[str]] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ """:meta private:"""
+ check_ispytest(_ispytest)
+ # Weirdness to bypass frozen=True.
+ object.__setattr__(self, "name", name)
+ object.__setattr__(self, "args", args)
+ object.__setattr__(self, "kwargs", kwargs)
+ object.__setattr__(self, "_param_ids_from", param_ids_from)
+ object.__setattr__(self, "_param_ids_generated", param_ids_generated)
+
+ def _has_param_ids(self) -> bool:
+ return "ids" in self.kwargs or len(self.args) >= 4
+
+ def combined_with(self, other: "Mark") -> "Mark":
+ """Return a new Mark which is a combination of this
+ Mark and another Mark.
+
+ Combines by appending args and merging kwargs.
+
+ :param Mark other: The mark to combine with.
+ :rtype: Mark
+ """
+ assert self.name == other.name
+
+ # Remember source of ids with parametrize Marks.
+ param_ids_from: Optional[Mark] = None
+ if self.name == "parametrize":
+ if other._has_param_ids():
+ param_ids_from = other
+ elif self._has_param_ids():
+ param_ids_from = self
+
+ return Mark(
+ self.name,
+ self.args + other.args,
+ dict(self.kwargs, **other.kwargs),
+ param_ids_from=param_ids_from,
+ _ispytest=True,
+ )
+
+
+# A generic parameter designating an object to which a Mark may
+# be applied -- a test function (callable) or class.
+# Note: a lambda is not allowed, but this can't be represented.
+Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
+
+
+@dataclasses.dataclass
+class MarkDecorator:
+ """A decorator for applying a mark on test functions and classes.
+
+ ``MarkDecorators`` are created with ``pytest.mark``::
+
+ mark1 = pytest.mark.NAME # Simple MarkDecorator
+ mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
+
+ and can then be applied as decorators to test functions::
+
+ @mark2
+ def test_function():
+ pass
+
+ When a ``MarkDecorator`` is called, it does the following:
+
+ 1. If called with a single class as its only positional argument and no
+ additional keyword arguments, it attaches the mark to the class so it
+ gets applied automatically to all test cases found in that class.
+
+ 2. If called with a single function as its only positional argument and
+ no additional keyword arguments, it attaches the mark to the function,
+ containing all the arguments already stored internally in the
+ ``MarkDecorator``.
+
+ 3. When called in any other case, it returns a new ``MarkDecorator``
+ instance with the original ``MarkDecorator``'s content updated with
+ the arguments passed to this call.
+
+ Note: The rules above prevent a ``MarkDecorator`` from storing only a
+ single function or class reference as its positional argument with no
+ additional keyword or positional arguments. You can work around this by
+ using `with_args()`.
+ """
+
+ mark: Mark
+
+ def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
+ """:meta private:"""
+ check_ispytest(_ispytest)
+ self.mark = mark
+
+ @property
+ def name(self) -> str:
+ """Alias for mark.name."""
+ return self.mark.name
+
+ @property
+ def args(self) -> Tuple[Any, ...]:
+ """Alias for mark.args."""
+ return self.mark.args
+
+ @property
+ def kwargs(self) -> Mapping[str, Any]:
+ """Alias for mark.kwargs."""
+ return self.mark.kwargs
+
+ @property
+ def markname(self) -> str:
+ """:meta private:"""
+ return self.name # for backward-compat (2.4.1 had this attr)
+
+ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
+ """Return a MarkDecorator with extra arguments added.
+
+ Unlike calling the MarkDecorator, with_args() can be used even
+ if the sole argument is a callable/class.
+ """
+ mark = Mark(self.name, args, kwargs, _ispytest=True)
+ return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
+
+ # Type ignored because the overloads overlap with an incompatible
+ # return type. Not much we can do about that. Thankfully mypy picks
+ # the first match so it works out even if we break the rules.
+ @overload
+ def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
+ pass
+
+ @overload
+ def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
+ pass
+
+ def __call__(self, *args: object, **kwargs: object):
+ """Call the MarkDecorator."""
+ if args and not kwargs:
+ func = args[0]
+ is_class = inspect.isclass(func)
+ if len(args) == 1 and (istestfunc(func) or is_class):
+ store_mark(func, self.mark)
+ return func
+ return self.with_args(*args, **kwargs)
+
+
+def get_unpacked_marks(
+ obj: Union[object, type],
+ *,
+ consider_mro: bool = True,
+) -> List[Mark]:
+ """Obtain the unpacked marks that are stored on an object.
+
+ If obj is a class and consider_mro is true, return marks applied to
+ this class and all of its super-classes in MRO order. If consider_mro
+ is false, only return marks applied directly to this class.
+ """
+ if isinstance(obj, type):
+ if not consider_mro:
+ mark_lists = [obj.__dict__.get("pytestmark", [])]
+ else:
+ mark_lists = [
+ x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
+ ]
+ mark_list = []
+ for item in mark_lists:
+ if isinstance(item, list):
+ mark_list.extend(item)
+ else:
+ mark_list.append(item)
+ else:
+ mark_attribute = getattr(obj, "pytestmark", [])
+ if isinstance(mark_attribute, list):
+ mark_list = mark_attribute
+ else:
+ mark_list = [mark_attribute]
+ return list(normalize_mark_list(mark_list))
+
+
+def normalize_mark_list(
+ mark_list: Iterable[Union[Mark, MarkDecorator]]
+) -> Iterable[Mark]:
+ """
+ Normalize an iterable of Mark or MarkDecorator objects into a list of marks
+ by retrieving the `mark` attribute on MarkDecorator instances.
+
+ :param mark_list: marks to normalize
+ :returns: A new list of the extracted Mark objects
+ """
+ for mark in mark_list:
+ mark_obj = getattr(mark, "mark", mark)
+ if not isinstance(mark_obj, Mark):
+ raise TypeError(f"got {repr(mark_obj)} instead of Mark")
+ yield mark_obj
+
+
+def store_mark(obj, mark: Mark) -> None:
+ """Store a Mark on an object.
+
+ This is used to implement the Mark declarations/decorators correctly.
+ """
+ assert isinstance(mark, Mark), mark
+ # Always reassign name to avoid updating pytestmark in a reference that
+ # was only borrowed.
+ obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
+
+
+# Typing for builtin pytest marks. This is cheating; it gives builtin marks
+# special privilege, and breaks modularity. But practicality beats purity...
+if TYPE_CHECKING:
+ from _pytest.scope import _ScopeName
+
+ class _SkipMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,misc,no-overload-impl]
+ def __call__(self, arg: Markable) -> Markable:
+ ...
+
+ @overload
+ def __call__(self, reason: str = ...) -> "MarkDecorator":
+ ...
+
+ class _SkipifMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ condition: Union[str, bool] = ...,
+ *conditions: Union[str, bool],
+ reason: str = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _XfailMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,misc,no-overload-impl]
+ def __call__(self, arg: Markable) -> Markable:
+ ...
+
+ @overload
+ def __call__(
+ self,
+ condition: Union[str, bool] = ...,
+ *conditions: Union[str, bool],
+ reason: str = ...,
+ run: bool = ...,
+ raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
+ strict: bool = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _ParametrizeMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ argnames: Union[str, Sequence[str]],
+ argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
+ *,
+ indirect: Union[bool, Sequence[str]] = ...,
+ ids: Optional[
+ Union[
+ Iterable[Union[None, str, float, int, bool]],
+ Callable[[Any], Optional[object]],
+ ]
+ ] = ...,
+ scope: Optional[_ScopeName] = ...,
+ ) -> MarkDecorator:
+ ...
+
+ class _UsefixturesMarkDecorator(MarkDecorator):
+ def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
+ ...
+
+ class _FilterwarningsMarkDecorator(MarkDecorator):
+ def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override]
+ ...
+
+
+@final
+class MarkGenerator:
+ """Factory for :class:`MarkDecorator` objects - exposed as
+ a ``pytest.mark`` singleton instance.
+
+ Example::
+
+ import pytest
+
+ @pytest.mark.slowtest
+ def test_function():
+ pass
+
+ applies a 'slowtest' :class:`Mark` on ``test_function``.
+ """
+
+ # See TYPE_CHECKING above.
+ if TYPE_CHECKING:
+ skip: _SkipMarkDecorator
+ skipif: _SkipifMarkDecorator
+ xfail: _XfailMarkDecorator
+ parametrize: _ParametrizeMarkDecorator
+ usefixtures: _UsefixturesMarkDecorator
+ filterwarnings: _FilterwarningsMarkDecorator
+
+ def __init__(self, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._config: Optional[Config] = None
+ self._markers: Set[str] = set()
+
+ def __getattr__(self, name: str) -> MarkDecorator:
+ """Generate a new :class:`MarkDecorator` with the given name."""
+ if name[0] == "_":
+ raise AttributeError("Marker name must NOT start with underscore")
+
+ if self._config is not None:
+ # We store a set of markers as a performance optimisation - if a mark
+ # name is in the set we definitely know it, but a mark may be known and
+ # not in the set. We therefore start by updating the set!
+ if name not in self._markers:
+ for line in self._config.getini("markers"):
+ # example lines: "skipif(condition): skip the given test if..."
+ # or "hypothesis: tests which use Hypothesis", so to get the
+ # marker name we split on both `:` and `(`.
+ marker = line.split(":")[0].split("(")[0].strip()
+ self._markers.add(marker)
+
+ # If the name is not in the set of known marks after updating,
+ # then it really is time to issue a warning or an error.
+ if name not in self._markers:
+ if self._config.option.strict_markers or self._config.option.strict:
+ fail(
+ f"{name!r} not found in `markers` configuration option",
+ pytrace=False,
+ )
+
+ # Raise a specific error for common misspellings of "parametrize".
+ if name in ["parameterize", "parametrise", "parameterise"]:
+ __tracebackhide__ = True
+ fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
+
+ warnings.warn(
+ "Unknown pytest.mark.%s - is this a typo? You can register "
+ "custom marks to avoid this warning - for details, see "
+ "https://docs.pytest.org/en/stable/how-to/mark.html" % name,
+ PytestUnknownMarkWarning,
+ 2,
+ )
+
+ return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
+
+
+MARK_GEN = MarkGenerator(_ispytest=True)
+
+
+@final
+class NodeKeywords(MutableMapping[str, Any]):
+ __slots__ = ("node", "parent", "_markers")
+
+ def __init__(self, node: "Node") -> None:
+ self.node = node
+ self.parent = node.parent
+ self._markers = {node.name: True}
+
+ def __getitem__(self, key: str) -> Any:
+ try:
+ return self._markers[key]
+ except KeyError:
+ if self.parent is None:
+ raise
+ return self.parent.keywords[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self._markers[key] = value
+
+ # Note: we could've avoided explicitly implementing some of the methods
+ # below and use the collections.abc fallback, but that would be slow.
+
+ def __contains__(self, key: object) -> bool:
+ return (
+ key in self._markers
+ or self.parent is not None
+ and key in self.parent.keywords
+ )
+
+ def update( # type: ignore[override]
+ self,
+ other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),
+ **kwds: Any,
+ ) -> None:
+ self._markers.update(other)
+ self._markers.update(kwds)
+
+ def __delitem__(self, key: str) -> None:
+ raise ValueError("cannot delete key in keywords dict")
+
+ def __iter__(self) -> Iterator[str]:
+ # Doesn't need to be fast.
+ yield from self._markers
+ if self.parent is not None:
+ for keyword in self.parent.keywords:
+ # self._marks and self.parent.keywords can have duplicates.
+ if keyword not in self._markers:
+ yield keyword
+
+ def __len__(self) -> int:
+ # Doesn't need to be fast.
+ return sum(1 for keyword in self)
+
+ def __repr__(self) -> str:
+ return f""
diff --git a/venv/lib/python3.12/site-packages/_pytest/monkeypatch.py b/venv/lib/python3.12/site-packages/_pytest/monkeypatch.py
new file mode 100644
index 0000000..9e51ff3
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/monkeypatch.py
@@ -0,0 +1,421 @@
+"""Monkeypatching and mocking functionality."""
+import os
+import re
+import sys
+import warnings
+from contextlib import contextmanager
+from typing import Any
+from typing import Generator
+from typing import List
+from typing import Mapping
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+from typing import Tuple
+from typing import TypeVar
+from typing import Union
+
+from _pytest.compat import final
+from _pytest.fixtures import fixture
+from _pytest.warning_types import PytestWarning
+
+RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
+
+
+K = TypeVar("K")
+V = TypeVar("V")
+
+
+@fixture
+def monkeypatch() -> Generator["MonkeyPatch", None, None]:
+ """A convenient fixture for monkey-patching.
+
+ The fixture provides these methods to modify objects, dictionaries, or
+ :data:`os.environ`:
+
+ * :meth:`monkeypatch.setattr(obj, name, value, raising=True) `
+ * :meth:`monkeypatch.delattr(obj, name, raising=True) `
+ * :meth:`monkeypatch.setitem(mapping, name, value) `
+ * :meth:`monkeypatch.delitem(obj, name, raising=True) `
+ * :meth:`monkeypatch.setenv(name, value, prepend=None) `
+ * :meth:`monkeypatch.delenv(name, raising=True) `
+ * :meth:`monkeypatch.syspath_prepend(path) `
+ * :meth:`monkeypatch.chdir(path) `
+ * :meth:`monkeypatch.context() `
+
+ All modifications will be undone after the requesting test function or
+ fixture has finished. The ``raising`` parameter determines if a :class:`KeyError`
+ or :class:`AttributeError` will be raised if the set/deletion operation does not have the
+ specified target.
+
+ To undo modifications done by the fixture in a contained scope,
+ use :meth:`context() `.
+ """
+ mpatch = MonkeyPatch()
+ yield mpatch
+ mpatch.undo()
+
+
+def resolve(name: str) -> object:
+ # Simplified from zope.dottedname.
+ parts = name.split(".")
+
+ used = parts.pop(0)
+ found: object = __import__(used)
+ for part in parts:
+ used += "." + part
+ try:
+ found = getattr(found, part)
+ except AttributeError:
+ pass
+ else:
+ continue
+ # We use explicit un-nesting of the handling block in order
+ # to avoid nested exceptions.
+ try:
+ __import__(used)
+ except ImportError as ex:
+ expected = str(ex).split()[-1]
+ if expected == used:
+ raise
+ else:
+ raise ImportError(f"import error in {used}: {ex}") from ex
+ found = annotated_getattr(found, part, used)
+ return found
+
+
+def annotated_getattr(obj: object, name: str, ann: str) -> object:
+ try:
+ obj = getattr(obj, name)
+ except AttributeError as e:
+ raise AttributeError(
+ "{!r} object at {} has no attribute {!r}".format(
+ type(obj).__name__, ann, name
+ )
+ ) from e
+ return obj
+
+
+def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
+ if not isinstance(import_path, str) or "." not in import_path:
+ raise TypeError(f"must be absolute import path string, not {import_path!r}")
+ module, attr = import_path.rsplit(".", 1)
+ target = resolve(module)
+ if raising:
+ annotated_getattr(target, attr, ann=module)
+ return attr, target
+
+
+class Notset:
+ def __repr__(self) -> str:
+ return ""
+
+
+notset = Notset()
+
+
+@final
+class MonkeyPatch:
+ """Helper to conveniently monkeypatch attributes/items/environment
+ variables/syspath.
+
+ Returned by the :fixture:`monkeypatch` fixture.
+
+ .. versionchanged:: 6.2
+ Can now also be used directly as `pytest.MonkeyPatch()`, for when
+ the fixture is not available. In this case, use
+ :meth:`with MonkeyPatch.context() as mp: ` or remember to call
+ :meth:`undo` explicitly.
+ """
+
+ def __init__(self) -> None:
+ self._setattr: List[Tuple[object, str, object]] = []
+ self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
+ self._cwd: Optional[str] = None
+ self._savesyspath: Optional[List[str]] = None
+
+ @classmethod
+ @contextmanager
+ def context(cls) -> Generator["MonkeyPatch", None, None]:
+ """Context manager that returns a new :class:`MonkeyPatch` object
+ which undoes any patching done inside the ``with`` block upon exit.
+
+ Example:
+
+ .. code-block:: python
+
+ import functools
+
+
+ def test_partial(monkeypatch):
+ with monkeypatch.context() as m:
+ m.setattr(functools, "partial", 3)
+
+ Useful in situations where it is desired to undo some patches before the test ends,
+ such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
+ of this see :issue:`3290`).
+ """
+ m = cls()
+ try:
+ yield m
+ finally:
+ m.undo()
+
+ @overload
+ def setattr(
+ self,
+ target: str,
+ name: object,
+ value: Notset = ...,
+ raising: bool = ...,
+ ) -> None:
+ ...
+
+ @overload
+ def setattr(
+ self,
+ target: object,
+ name: str,
+ value: object,
+ raising: bool = ...,
+ ) -> None:
+ ...
+
+ def setattr(
+ self,
+ target: Union[str, object],
+ name: Union[object, str],
+ value: object = notset,
+ raising: bool = True,
+ ) -> None:
+ """
+ Set attribute value on target, memorizing the old value.
+
+ For example:
+
+ .. code-block:: python
+
+ import os
+
+ monkeypatch.setattr(os, "getcwd", lambda: "/")
+
+ The code above replaces the :func:`os.getcwd` function by a ``lambda`` which
+ always returns ``"/"``.
+
+ For convenience, you can specify a string as ``target`` which
+ will be interpreted as a dotted import path, with the last part
+ being the attribute name:
+
+ .. code-block:: python
+
+ monkeypatch.setattr("os.getcwd", lambda: "/")
+
+ Raises :class:`AttributeError` if the attribute does not exist, unless
+ ``raising`` is set to False.
+
+ **Where to patch**
+
+ ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one.
+ There can be many names pointing to any individual object, so for patching to work you must ensure
+ that you patch the name used by the system under test.
+
+ See the section :ref:`Where to patch ` in the :mod:`unittest.mock`
+ docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but
+ applies to ``monkeypatch.setattr`` as well.
+ """
+ __tracebackhide__ = True
+ import inspect
+
+ if isinstance(value, Notset):
+ if not isinstance(target, str):
+ raise TypeError(
+ "use setattr(target, name, value) or "
+ "setattr(target, value) with target being a dotted "
+ "import string"
+ )
+ value = name
+ name, target = derive_importpath(target, raising)
+ else:
+ if not isinstance(name, str):
+ raise TypeError(
+ "use setattr(target, name, value) with name being a string or "
+ "setattr(target, value) with target being a dotted "
+ "import string"
+ )
+
+ oldval = getattr(target, name, notset)
+ if raising and oldval is notset:
+ raise AttributeError(f"{target!r} has no attribute {name!r}")
+
+ # avoid class descriptors like staticmethod/classmethod
+ if inspect.isclass(target):
+ oldval = target.__dict__.get(name, notset)
+ self._setattr.append((target, name, oldval))
+ setattr(target, name, value)
+
+ def delattr(
+ self,
+ target: Union[object, str],
+ name: Union[str, Notset] = notset,
+ raising: bool = True,
+ ) -> None:
+ """Delete attribute ``name`` from ``target``.
+
+ If no ``name`` is specified and ``target`` is a string
+ it will be interpreted as a dotted import path with the
+ last part being the attribute name.
+
+ Raises AttributeError it the attribute does not exist, unless
+ ``raising`` is set to False.
+ """
+ __tracebackhide__ = True
+ import inspect
+
+ if isinstance(name, Notset):
+ if not isinstance(target, str):
+ raise TypeError(
+ "use delattr(target, name) or "
+ "delattr(target) with target being a dotted "
+ "import string"
+ )
+ name, target = derive_importpath(target, raising)
+
+ if not hasattr(target, name):
+ if raising:
+ raise AttributeError(name)
+ else:
+ oldval = getattr(target, name, notset)
+ # Avoid class descriptors like staticmethod/classmethod.
+ if inspect.isclass(target):
+ oldval = target.__dict__.get(name, notset)
+ self._setattr.append((target, name, oldval))
+ delattr(target, name)
+
+ def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
+ """Set dictionary entry ``name`` to value."""
+ self._setitem.append((dic, name, dic.get(name, notset)))
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ dic[name] = value # type: ignore[index]
+
+ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
+ """Delete ``name`` from dict.
+
+ Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
+ False.
+ """
+ if name not in dic:
+ if raising:
+ raise KeyError(name)
+ else:
+ self._setitem.append((dic, name, dic.get(name, notset)))
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ del dic[name] # type: ignore[attr-defined]
+
+ def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
+ """Set environment variable ``name`` to ``value``.
+
+ If ``prepend`` is a character, read the current environment variable
+ value and prepend the ``value`` adjoined with the ``prepend``
+ character.
+ """
+ if not isinstance(value, str):
+ warnings.warn( # type: ignore[unreachable]
+ PytestWarning(
+ "Value of environment variable {name} type should be str, but got "
+ "{value!r} (type: {type}); converted to str implicitly".format(
+ name=name, value=value, type=type(value).__name__
+ )
+ ),
+ stacklevel=2,
+ )
+ value = str(value)
+ if prepend and name in os.environ:
+ value = value + prepend + os.environ[name]
+ self.setitem(os.environ, name, value)
+
+ def delenv(self, name: str, raising: bool = True) -> None:
+ """Delete ``name`` from the environment.
+
+ Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
+ False.
+ """
+ environ: MutableMapping[str, str] = os.environ
+ self.delitem(environ, name, raising=raising)
+
+ def syspath_prepend(self, path) -> None:
+ """Prepend ``path`` to ``sys.path`` list of import locations."""
+
+ if self._savesyspath is None:
+ self._savesyspath = sys.path[:]
+ sys.path.insert(0, str(path))
+
+ # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
+ # this is only needed when pkg_resources was already loaded by the namespace package
+ if "pkg_resources" in sys.modules:
+ from pkg_resources import fixup_namespace_packages
+
+ fixup_namespace_packages(str(path))
+
+ # A call to syspathinsert() usually means that the caller wants to
+ # import some dynamically created files, thus with python3 we
+ # invalidate its import caches.
+ # This is especially important when any namespace package is in use,
+ # since then the mtime based FileFinder cache (that gets created in
+ # this case already) gets not invalidated when writing the new files
+ # quickly afterwards.
+ from importlib import invalidate_caches
+
+ invalidate_caches()
+
+ def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None:
+ """Change the current working directory to the specified path.
+
+ :param path:
+ The path to change into.
+ """
+ if self._cwd is None:
+ self._cwd = os.getcwd()
+ os.chdir(path)
+
+ def undo(self) -> None:
+ """Undo previous changes.
+
+ This call consumes the undo stack. Calling it a second time has no
+ effect unless you do more monkeypatching after the undo call.
+
+ There is generally no need to call `undo()`, since it is
+ called automatically during tear-down.
+
+ .. note::
+ The same `monkeypatch` fixture is used across a
+ single test function invocation. If `monkeypatch` is used both by
+ the test function itself and one of the test fixtures,
+ calling `undo()` will undo all of the changes made in
+ both functions.
+
+ Prefer to use :meth:`context() ` instead.
+ """
+ for obj, name, value in reversed(self._setattr):
+ if value is not notset:
+ setattr(obj, name, value)
+ else:
+ delattr(obj, name)
+ self._setattr[:] = []
+ for dictionary, key, value in reversed(self._setitem):
+ if value is notset:
+ try:
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ del dictionary[key] # type: ignore[attr-defined]
+ except KeyError:
+ pass # Was already deleted, so we have the desired state.
+ else:
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ dictionary[key] = value # type: ignore[index]
+ self._setitem[:] = []
+ if self._savesyspath is not None:
+ sys.path[:] = self._savesyspath
+ self._savesyspath = None
+
+ if self._cwd is not None:
+ os.chdir(self._cwd)
+ self._cwd = None
diff --git a/venv/lib/python3.12/site-packages/_pytest/nodes.py b/venv/lib/python3.12/site-packages/_pytest/nodes.py
new file mode 100644
index 0000000..a5313cb
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/nodes.py
@@ -0,0 +1,783 @@
+import os
+import warnings
+from inspect import signature
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+import _pytest._code
+from _pytest._code import getfslineno
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest._code.code import Traceback
+from _pytest.compat import cached_property
+from _pytest.compat import LEGACY_PATH
+from _pytest.config import Config
+from _pytest.config import ConftestImportFailure
+from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
+from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
+from _pytest.mark.structures import Mark
+from _pytest.mark.structures import MarkDecorator
+from _pytest.mark.structures import NodeKeywords
+from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import commonpath
+from _pytest.stash import Stash
+from _pytest.warning_types import PytestWarning
+
+if TYPE_CHECKING:
+ # Imported here due to circular import.
+ from _pytest.main import Session
+ from _pytest._code.code import _TracebackStyle
+
+
+SEP = "/"
+
+tracebackcutdir = Path(_pytest.__file__).parent
+
+
+def iterparentnodeids(nodeid: str) -> Iterator[str]:
+ """Return the parent node IDs of a given node ID, inclusive.
+
+ For the node ID
+
+ "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
+
+ the result would be
+
+ ""
+ "testing"
+ "testing/code"
+ "testing/code/test_excinfo.py"
+ "testing/code/test_excinfo.py::TestFormattedExcinfo"
+ "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
+
+ Note that / components are only considered until the first ::.
+ """
+ pos = 0
+ first_colons: Optional[int] = nodeid.find("::")
+ if first_colons == -1:
+ first_colons = None
+ # The root Session node - always present.
+ yield ""
+ # Eagerly consume SEP parts until first colons.
+ while True:
+ at = nodeid.find(SEP, pos, first_colons)
+ if at == -1:
+ break
+ if at > 0:
+ yield nodeid[:at]
+ pos = at + len(SEP)
+ # Eagerly consume :: parts.
+ while True:
+ at = nodeid.find("::", pos)
+ if at == -1:
+ break
+ if at > 0:
+ yield nodeid[:at]
+ pos = at + len("::")
+ # The node ID itself.
+ if nodeid:
+ yield nodeid
+
+
+def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
+ if Path(fspath) != path:
+ raise ValueError(
+ f"Path({fspath!r}) != {path!r}\n"
+ "if both path and fspath are given they need to be equal"
+ )
+
+
+def _imply_path(
+ node_type: Type["Node"],
+ path: Optional[Path],
+ fspath: Optional[LEGACY_PATH],
+) -> Path:
+ if fspath is not None:
+ warnings.warn(
+ NODE_CTOR_FSPATH_ARG.format(
+ node_type_name=node_type.__name__,
+ ),
+ stacklevel=6,
+ )
+ if path is not None:
+ if fspath is not None:
+ _check_path(path, fspath)
+ return path
+ else:
+ assert fspath is not None
+ return Path(fspath)
+
+
+_NodeType = TypeVar("_NodeType", bound="Node")
+
+
+class NodeMeta(type):
+ def __call__(self, *k, **kw):
+ msg = (
+ "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
+ "See "
+ "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
+ " for more details."
+ ).format(name=f"{self.__module__}.{self.__name__}")
+ fail(msg, pytrace=False)
+
+ def _create(self, *k, **kw):
+ try:
+ return super().__call__(*k, **kw)
+ except TypeError:
+ sig = signature(getattr(self, "__init__"))
+ known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
+ from .warning_types import PytestDeprecationWarning
+
+ warnings.warn(
+ PytestDeprecationWarning(
+ f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html"
+ "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
+ "for more details."
+ )
+ )
+
+ return super().__call__(*k, **known_kw)
+
+
+class Node(metaclass=NodeMeta):
+ r"""Base class of :class:`Collector` and :class:`Item`, the components of
+ the test collection tree.
+
+ ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
+ leaf nodes.
+ """
+
+ # Implemented in the legacypath plugin.
+ #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
+ #: for methods not migrated to ``pathlib.Path`` yet, such as
+ #: :meth:`Item.reportinfo`. Will be deprecated in a future release, prefer
+ #: using :attr:`path` instead.
+ fspath: LEGACY_PATH
+
+ # Use __slots__ to make attribute access faster.
+ # Note that __dict__ is still available.
+ __slots__ = (
+ "name",
+ "parent",
+ "config",
+ "session",
+ "path",
+ "_nodeid",
+ "_store",
+ "__dict__",
+ )
+
+ def __init__(
+ self,
+ name: str,
+ parent: "Optional[Node]" = None,
+ config: Optional[Config] = None,
+ session: "Optional[Session]" = None,
+ fspath: Optional[LEGACY_PATH] = None,
+ path: Optional[Path] = None,
+ nodeid: Optional[str] = None,
+ ) -> None:
+ #: A unique name within the scope of the parent node.
+ self.name: str = name
+
+ #: The parent collector node.
+ self.parent = parent
+
+ if config:
+ #: The pytest config object.
+ self.config: Config = config
+ else:
+ if not parent:
+ raise TypeError("config or parent must be provided")
+ self.config = parent.config
+
+ if session:
+ #: The pytest session this node is part of.
+ self.session: Session = session
+ else:
+ if not parent:
+ raise TypeError("session or parent must be provided")
+ self.session = parent.session
+
+ if path is None and fspath is None:
+ path = getattr(parent, "path", None)
+ #: Filesystem path where this node was collected from (can be None).
+ self.path: Path = _imply_path(type(self), path, fspath=fspath)
+
+ # The explicit annotation is to avoid publicly exposing NodeKeywords.
+ #: Keywords/markers collected from all scopes.
+ self.keywords: MutableMapping[str, Any] = NodeKeywords(self)
+
+ #: The marker objects belonging to this node.
+ self.own_markers: List[Mark] = []
+
+ #: Allow adding of extra keywords to use for matching.
+ self.extra_keyword_matches: Set[str] = set()
+
+ if nodeid is not None:
+ assert "::()" not in nodeid
+ self._nodeid = nodeid
+ else:
+ if not self.parent:
+ raise TypeError("nodeid or parent must be provided")
+ self._nodeid = self.parent.nodeid + "::" + self.name
+
+ #: A place where plugins can store information on the node for their
+ #: own use.
+ self.stash: Stash = Stash()
+ # Deprecated alias. Was never public. Can be removed in a few releases.
+ self._store = self.stash
+
+ @classmethod
+ def from_parent(cls, parent: "Node", **kw):
+ """Public constructor for Nodes.
+
+ This indirection got introduced in order to enable removing
+ the fragile logic from the node constructors.
+
+ Subclasses can use ``super().from_parent(...)`` when overriding the
+ construction.
+
+ :param parent: The parent node of this Node.
+ """
+ if "config" in kw:
+ raise TypeError("config is not a valid argument for from_parent")
+ if "session" in kw:
+ raise TypeError("session is not a valid argument for from_parent")
+ return cls._create(parent=parent, **kw)
+
+ @property
+ def ihook(self):
+ """fspath-sensitive hook proxy used to call pytest hooks."""
+ return self.session.gethookproxy(self.path)
+
+ def __repr__(self) -> str:
+ return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
+
+ def warn(self, warning: Warning) -> None:
+ """Issue a warning for this Node.
+
+ Warnings will be displayed after the test session, unless explicitly suppressed.
+
+ :param Warning warning:
+ The warning instance to issue.
+
+ :raises ValueError: If ``warning`` instance is not a subclass of Warning.
+
+ Example usage:
+
+ .. code-block:: python
+
+ node.warn(PytestWarning("some message"))
+ node.warn(UserWarning("some message"))
+
+ .. versionchanged:: 6.2
+ Any subclass of :class:`Warning` is now accepted, rather than only
+ :class:`PytestWarning ` subclasses.
+ """
+ # enforce type checks here to avoid getting a generic type error later otherwise.
+ if not isinstance(warning, Warning):
+ raise ValueError(
+ "warning must be an instance of Warning or subclass, got {!r}".format(
+ warning
+ )
+ )
+ path, lineno = get_fslocation_from_item(self)
+ assert lineno is not None
+ warnings.warn_explicit(
+ warning,
+ category=None,
+ filename=str(path),
+ lineno=lineno + 1,
+ )
+
+ # Methods for ordering nodes.
+
+ @property
+ def nodeid(self) -> str:
+ """A ::-separated string denoting its collection tree address."""
+ return self._nodeid
+
+ def __hash__(self) -> int:
+ return hash(self._nodeid)
+
+ def setup(self) -> None:
+ pass
+
+ def teardown(self) -> None:
+ pass
+
+ def listchain(self) -> List["Node"]:
+ """Return list of all parent collectors up to self, starting from
+ the root of collection tree.
+
+ :returns: The nodes.
+ """
+ chain = []
+ item: Optional[Node] = self
+ while item is not None:
+ chain.append(item)
+ item = item.parent
+ chain.reverse()
+ return chain
+
+ def add_marker(
+ self, marker: Union[str, MarkDecorator], append: bool = True
+ ) -> None:
+ """Dynamically add a marker object to the node.
+
+ :param marker:
+ The marker.
+ :param append:
+ Whether to append the marker, or prepend it.
+ """
+ from _pytest.mark import MARK_GEN
+
+ if isinstance(marker, MarkDecorator):
+ marker_ = marker
+ elif isinstance(marker, str):
+ marker_ = getattr(MARK_GEN, marker)
+ else:
+ raise ValueError("is not a string or pytest.mark.* Marker")
+ self.keywords[marker_.name] = marker_
+ if append:
+ self.own_markers.append(marker_.mark)
+ else:
+ self.own_markers.insert(0, marker_.mark)
+
+ def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
+ """Iterate over all markers of the node.
+
+ :param name: If given, filter the results by the name attribute.
+ :returns: An iterator of the markers of the node.
+ """
+ return (x[1] for x in self.iter_markers_with_node(name=name))
+
+ def iter_markers_with_node(
+ self, name: Optional[str] = None
+ ) -> Iterator[Tuple["Node", Mark]]:
+ """Iterate over all markers of the node.
+
+ :param name: If given, filter the results by the name attribute.
+ :returns: An iterator of (node, mark) tuples.
+ """
+ for node in reversed(self.listchain()):
+ for mark in node.own_markers:
+ if name is None or getattr(mark, "name", None) == name:
+ yield node, mark
+
+ @overload
+ def get_closest_marker(self, name: str) -> Optional[Mark]:
+ ...
+
+ @overload
+ def get_closest_marker(self, name: str, default: Mark) -> Mark:
+ ...
+
+ def get_closest_marker(
+ self, name: str, default: Optional[Mark] = None
+ ) -> Optional[Mark]:
+ """Return the first marker matching the name, from closest (for
+ example function) to farther level (for example module level).
+
+ :param default: Fallback return value if no marker was found.
+ :param name: Name to filter by.
+ """
+ return next(self.iter_markers(name=name), default)
+
+ def listextrakeywords(self) -> Set[str]:
+ """Return a set of all extra keywords in self and any parents."""
+ extra_keywords: Set[str] = set()
+ for item in self.listchain():
+ extra_keywords.update(item.extra_keyword_matches)
+ return extra_keywords
+
+ def listnames(self) -> List[str]:
+ return [x.name for x in self.listchain()]
+
+ def addfinalizer(self, fin: Callable[[], object]) -> None:
+ """Register a function to be called without arguments when this node is
+ finalized.
+
+ This method can only be called when this node is active
+ in a setup chain, for example during self.setup().
+ """
+ self.session._setupstate.addfinalizer(fin, self)
+
+ def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
+ """Get the next parent node (including self) which is an instance of
+ the given class.
+
+ :param cls: The node class to search for.
+ :returns: The node, if found.
+ """
+ current: Optional[Node] = self
+ while current and not isinstance(current, cls):
+ current = current.parent
+ assert current is None or isinstance(current, cls)
+ return current
+
+ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
+ return excinfo.traceback
+
+ def _repr_failure_py(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ style: "Optional[_TracebackStyle]" = None,
+ ) -> TerminalRepr:
+ from _pytest.fixtures import FixtureLookupError
+
+ if isinstance(excinfo.value, ConftestImportFailure):
+ excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
+ if isinstance(excinfo.value, fail.Exception):
+ if not excinfo.value.pytrace:
+ style = "value"
+ if isinstance(excinfo.value, FixtureLookupError):
+ return excinfo.value.formatrepr()
+
+ tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
+ if self.config.getoption("fulltrace", False):
+ style = "long"
+ tbfilter = False
+ else:
+ tbfilter = self._traceback_filter
+ if style == "auto":
+ style = "long"
+ # XXX should excinfo.getrepr record all data and toterminal() process it?
+ if style is None:
+ if self.config.getoption("tbstyle", "auto") == "short":
+ style = "short"
+ else:
+ style = "long"
+
+ if self.config.getoption("verbose", 0) > 1:
+ truncate_locals = False
+ else:
+ truncate_locals = True
+
+ # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
+ # It is possible for a fixture/test to change the CWD while this code runs, which
+ # would then result in the user seeing confusing paths in the failure message.
+ # To fix this, if the CWD changed, always display the full absolute path.
+ # It will be better to just always display paths relative to invocation_dir, but
+ # this requires a lot of plumbing (#6428).
+ try:
+ abspath = Path(os.getcwd()) != self.config.invocation_params.dir
+ except OSError:
+ abspath = True
+
+ return excinfo.getrepr(
+ funcargs=True,
+ abspath=abspath,
+ showlocals=self.config.getoption("showlocals", False),
+ style=style,
+ tbfilter=tbfilter,
+ truncate_locals=truncate_locals,
+ )
+
+ def repr_failure(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ style: "Optional[_TracebackStyle]" = None,
+ ) -> Union[str, TerminalRepr]:
+ """Return a representation of a collection or test failure.
+
+ .. seealso:: :ref:`non-python tests`
+
+ :param excinfo: Exception information for the failure.
+ """
+ return self._repr_failure_py(excinfo, style)
+
+
+def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[int]]:
+ """Try to extract the actual location from a node, depending on available attributes:
+
+ * "location": a pair (path, lineno)
+ * "obj": a Python object that the node wraps.
+ * "fspath": just a path
+
+ :rtype: A tuple of (str|Path, int) with filename and 0-based line number.
+ """
+ # See Item.location.
+ location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
+ if location is not None:
+ return location[:2]
+ obj = getattr(node, "obj", None)
+ if obj is not None:
+ return getfslineno(obj)
+ return getattr(node, "fspath", "unknown location"), -1
+
+
+class Collector(Node):
+ """Base class of all collectors.
+
+ Collector create children through `collect()` and thus iteratively build
+ the collection tree.
+ """
+
+ class CollectError(Exception):
+ """An error during collection, contains a custom message."""
+
+ def collect(self) -> Iterable[Union["Item", "Collector"]]:
+ """Collect children (items and collectors) for this collector."""
+ raise NotImplementedError("abstract")
+
+ # TODO: This omits the style= parameter which breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self, excinfo: ExceptionInfo[BaseException]
+ ) -> Union[str, TerminalRepr]:
+ """Return a representation of a collection failure.
+
+ :param excinfo: Exception information for the failure.
+ """
+ if isinstance(excinfo.value, self.CollectError) and not self.config.getoption(
+ "fulltrace", False
+ ):
+ exc = excinfo.value
+ return str(exc.args[0])
+
+ # Respect explicit tbstyle option, but default to "short"
+ # (_repr_failure_py uses "long" with "fulltrace" option always).
+ tbstyle = self.config.getoption("tbstyle", "auto")
+ if tbstyle == "auto":
+ tbstyle = "short"
+
+ return self._repr_failure_py(excinfo, style=tbstyle)
+
+ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
+ if hasattr(self, "path"):
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=self.path)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
+ return ntraceback.filter(excinfo)
+ return excinfo.traceback
+
+
+def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
+ for initial_path in session._initialpaths:
+ if commonpath(path, initial_path) == initial_path:
+ rel = str(path.relative_to(initial_path))
+ return "" if rel == "." else rel
+ return None
+
+
+class FSCollector(Collector):
+ """Base class for filesystem collectors."""
+
+ def __init__(
+ self,
+ fspath: Optional[LEGACY_PATH] = None,
+ path_or_parent: Optional[Union[Path, Node]] = None,
+ path: Optional[Path] = None,
+ name: Optional[str] = None,
+ parent: Optional[Node] = None,
+ config: Optional[Config] = None,
+ session: Optional["Session"] = None,
+ nodeid: Optional[str] = None,
+ ) -> None:
+ if path_or_parent:
+ if isinstance(path_or_parent, Node):
+ assert parent is None
+ parent = cast(FSCollector, path_or_parent)
+ elif isinstance(path_or_parent, Path):
+ assert path is None
+ path = path_or_parent
+
+ path = _imply_path(type(self), path, fspath=fspath)
+ if name is None:
+ name = path.name
+ if parent is not None and parent.path != path:
+ try:
+ rel = path.relative_to(parent.path)
+ except ValueError:
+ pass
+ else:
+ name = str(rel)
+ name = name.replace(os.sep, SEP)
+ self.path = path
+
+ if session is None:
+ assert parent is not None
+ session = parent.session
+
+ if nodeid is None:
+ try:
+ nodeid = str(self.path.relative_to(session.config.rootpath))
+ except ValueError:
+ nodeid = _check_initialpaths_for_relpath(session, path)
+
+ if nodeid and os.sep != SEP:
+ nodeid = nodeid.replace(os.sep, SEP)
+
+ super().__init__(
+ name=name,
+ parent=parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ path=path,
+ )
+
+ @classmethod
+ def from_parent(
+ cls,
+ parent,
+ *,
+ fspath: Optional[LEGACY_PATH] = None,
+ path: Optional[Path] = None,
+ **kw,
+ ):
+ """The public constructor."""
+ return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
+
+ def gethookproxy(self, fspath: "os.PathLike[str]"):
+ warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+ return self.session.gethookproxy(fspath)
+
+ def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
+ warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
+ return self.session.isinitpath(path)
+
+
+class File(FSCollector):
+ """Base class for collecting tests from a file.
+
+ :ref:`non-python tests`.
+ """
+
+
+class Item(Node):
+ """Base class of all test invocation items.
+
+ Note that for a single function there might be multiple test invocation items.
+ """
+
+ nextitem = None
+
+ def __init__(
+ self,
+ name,
+ parent=None,
+ config: Optional[Config] = None,
+ session: Optional["Session"] = None,
+ nodeid: Optional[str] = None,
+ **kw,
+ ) -> None:
+ # The first two arguments are intentionally passed positionally,
+ # to keep plugins who define a node type which inherits from
+ # (pytest.Item, pytest.File) working (see issue #8435).
+ # They can be made kwargs when the deprecation above is done.
+ super().__init__(
+ name,
+ parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ **kw,
+ )
+ self._report_sections: List[Tuple[str, str, str]] = []
+
+ #: A list of tuples (name, value) that holds user defined properties
+ #: for this test.
+ self.user_properties: List[Tuple[str, object]] = []
+
+ self._check_item_and_collector_diamond_inheritance()
+
+ def _check_item_and_collector_diamond_inheritance(self) -> None:
+ """
+ Check if the current type inherits from both File and Collector
+ at the same time, emitting a warning accordingly (#8447).
+ """
+ cls = type(self)
+
+ # We inject an attribute in the type to avoid issuing this warning
+ # for the same class more than once, which is not helpful.
+ # It is a hack, but was deemed acceptable in order to avoid
+ # flooding the user in the common case.
+ attr_name = "_pytest_diamond_inheritance_warning_shown"
+ if getattr(cls, attr_name, False):
+ return
+ setattr(cls, attr_name, True)
+
+ problems = ", ".join(
+ base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
+ )
+ if problems:
+ warnings.warn(
+ f"{cls.__name__} is an Item subclass and should not be a collector, "
+ f"however its bases {problems} are collectors.\n"
+ "Please split the Collectors and the Item into separate node types.\n"
+ "Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
+ "example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
+ PytestWarning,
+ )
+
+ def runtest(self) -> None:
+ """Run the test case for this item.
+
+ Must be implemented by subclasses.
+
+ .. seealso:: :ref:`non-python tests`
+ """
+ raise NotImplementedError("runtest must be implemented by Item subclass")
+
+ def add_report_section(self, when: str, key: str, content: str) -> None:
+ """Add a new report section, similar to what's done internally to add
+ stdout and stderr captured output::
+
+ item.add_report_section("call", "stdout", "report section contents")
+
+ :param str when:
+ One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
+ :param str key:
+ Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
+ ``"stderr"`` internally.
+ :param str content:
+ The full contents as a string.
+ """
+ if content:
+ self._report_sections.append((when, key, content))
+
+ def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
+ """Get location information for this item for test reports.
+
+ Returns a tuple with three elements:
+
+ - The path of the test (default ``self.path``)
+ - The 0-based line number of the test (default ``None``)
+ - A name of the test to be shown (default ``""``)
+
+ .. seealso:: :ref:`non-python tests`
+ """
+ return self.path, None, ""
+
+ @cached_property
+ def location(self) -> Tuple[str, Optional[int], str]:
+ """
+ Returns a tuple of ``(relfspath, lineno, testname)`` for this item
+ where ``relfspath`` is file path relative to ``config.rootpath``
+ and lineno is a 0-based line number.
+ """
+ location = self.reportinfo()
+ path = absolutepath(os.fspath(location[0]))
+ relfspath = self.session._node_location_to_relpath(path)
+ assert type(location[2]) is str
+ return (relfspath, location[1], location[2])
diff --git a/venv/lib/python3.12/site-packages/_pytest/nose.py b/venv/lib/python3.12/site-packages/_pytest/nose.py
new file mode 100644
index 0000000..273bd04
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/nose.py
@@ -0,0 +1,50 @@
+"""Run testsuites written for nose."""
+import warnings
+
+from _pytest.config import hookimpl
+from _pytest.deprecated import NOSE_SUPPORT
+from _pytest.fixtures import getfixturemarker
+from _pytest.nodes import Item
+from _pytest.python import Function
+from _pytest.unittest import TestCaseFunction
+
+
+@hookimpl(trylast=True)
+def pytest_runtest_setup(item: Item) -> None:
+ if not isinstance(item, Function):
+ return
+ # Don't do nose style setup/teardown on direct unittest style classes.
+ if isinstance(item, TestCaseFunction):
+ return
+
+ # Capture the narrowed type of item for the teardown closure,
+ # see https://github.com/python/mypy/issues/2608
+ func = item
+
+ call_optional(func.obj, "setup", func.nodeid)
+ func.addfinalizer(lambda: call_optional(func.obj, "teardown", func.nodeid))
+
+ # NOTE: Module- and class-level fixtures are handled in python.py
+ # with `pluginmanager.has_plugin("nose")` checks.
+ # It would have been nicer to implement them outside of core, but
+ # it's not straightforward.
+
+
+def call_optional(obj: object, name: str, nodeid: str) -> bool:
+ method = getattr(obj, name, None)
+ if method is None:
+ return False
+ is_fixture = getfixturemarker(method) is not None
+ if is_fixture:
+ return False
+ if not callable(method):
+ return False
+ # Warn about deprecation of this plugin.
+ method_name = getattr(method, "__name__", str(method))
+ warnings.warn(
+ NOSE_SUPPORT.format(nodeid=nodeid, method=method_name, stage=name), stacklevel=2
+ )
+ # If there are any problems allow the exception to raise rather than
+ # silently ignoring it.
+ method()
+ return True
diff --git a/venv/lib/python3.12/site-packages/_pytest/outcomes.py b/venv/lib/python3.12/site-packages/_pytest/outcomes.py
new file mode 100644
index 0000000..53c3e15
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/outcomes.py
@@ -0,0 +1,311 @@
+"""Exception classes and constants handling test outcomes as well as
+functions creating them."""
+import sys
+import warnings
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import NoReturn
+from typing import Optional
+from typing import Type
+from typing import TypeVar
+
+from _pytest.deprecated import KEYWORD_MSG_ARG
+
+TYPE_CHECKING = False # Avoid circular import through compat.
+
+if TYPE_CHECKING:
+ from typing_extensions import Protocol
+else:
+ # typing.Protocol is only available starting from Python 3.8. It is also
+ # available from typing_extensions, but we don't want a runtime dependency
+ # on that. So use a dummy runtime implementation.
+ from typing import Generic
+
+ Protocol = Generic
+
+
+class OutcomeException(BaseException):
+ """OutcomeException and its subclass instances indicate and contain info
+ about test and collection outcomes."""
+
+ def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
+ if msg is not None and not isinstance(msg, str):
+ error_msg = ( # type: ignore[unreachable]
+ "{} expected string as 'msg' parameter, got '{}' instead.\n"
+ "Perhaps you meant to use a mark?"
+ )
+ raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
+ super().__init__(msg)
+ self.msg = msg
+ self.pytrace = pytrace
+
+ def __repr__(self) -> str:
+ if self.msg is not None:
+ return self.msg
+ return f"<{self.__class__.__name__} instance>"
+
+ __str__ = __repr__
+
+
+TEST_OUTCOME = (OutcomeException, Exception)
+
+
+class Skipped(OutcomeException):
+ # XXX hackish: on 3k we fake to live in the builtins
+ # in order to have Skipped exception printing shorter/nicer
+ __module__ = "builtins"
+
+ def __init__(
+ self,
+ msg: Optional[str] = None,
+ pytrace: bool = True,
+ allow_module_level: bool = False,
+ *,
+ _use_item_location: bool = False,
+ ) -> None:
+ super().__init__(msg=msg, pytrace=pytrace)
+ self.allow_module_level = allow_module_level
+ # If true, the skip location is reported as the item's location,
+ # instead of the place that raises the exception/calls skip().
+ self._use_item_location = _use_item_location
+
+
+class Failed(OutcomeException):
+ """Raised from an explicit call to pytest.fail()."""
+
+ __module__ = "builtins"
+
+
+class Exit(Exception):
+ """Raised for immediate program exits (no tracebacks/summaries)."""
+
+ def __init__(
+ self, msg: str = "unknown reason", returncode: Optional[int] = None
+ ) -> None:
+ self.msg = msg
+ self.returncode = returncode
+ super().__init__(msg)
+
+
+# Elaborate hack to work around https://github.com/python/mypy/issues/2087.
+# Ideally would just be `exit.Exception = Exit` etc.
+
+_F = TypeVar("_F", bound=Callable[..., object])
+_ET = TypeVar("_ET", bound=Type[BaseException])
+
+
+class _WithException(Protocol[_F, _ET]):
+ Exception: _ET
+ __call__: _F
+
+
+def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
+ def decorate(func: _F) -> _WithException[_F, _ET]:
+ func_with_exception = cast(_WithException[_F, _ET], func)
+ func_with_exception.Exception = exception_type
+ return func_with_exception
+
+ return decorate
+
+
+# Exposed helper methods.
+
+
+@_with_exception(Exit)
+def exit(
+ reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None
+) -> NoReturn:
+ """Exit testing process.
+
+ :param reason:
+ The message to show as the reason for exiting pytest. reason has a default value
+ only because `msg` is deprecated.
+
+ :param returncode:
+ Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
+
+ :param msg:
+ Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
+ """
+ __tracebackhide__ = True
+ from _pytest.config import UsageError
+
+ if reason and msg:
+ raise UsageError(
+ "cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`."
+ )
+ if not reason:
+ if msg is None:
+ raise UsageError("exit() requires a reason argument")
+ warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2)
+ reason = msg
+ raise Exit(reason, returncode)
+
+
+@_with_exception(Skipped)
+def skip(
+ reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None
+) -> NoReturn:
+ """Skip an executing test with the given message.
+
+ This function should be called only during testing (setup, call or teardown) or
+ during collection by using the ``allow_module_level`` flag. This function can
+ be called in doctests as well.
+
+ :param reason:
+ The message to show the user as reason for the skip.
+
+ :param allow_module_level:
+ Allows this function to be called at module level.
+ Raising the skip exception at module level will stop
+ the execution of the module and prevent the collection of all tests in the module,
+ even those defined before the `skip` call.
+
+ Defaults to False.
+
+ :param msg:
+ Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
+
+ .. note::
+ It is better to use the :ref:`pytest.mark.skipif ref` marker when
+ possible to declare a test to be skipped under certain conditions
+ like mismatching platforms or dependencies.
+ Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`)
+ to skip a doctest statically.
+ """
+ __tracebackhide__ = True
+ reason = _resolve_msg_to_reason("skip", reason, msg)
+ raise Skipped(msg=reason, allow_module_level=allow_module_level)
+
+
+@_with_exception(Failed)
+def fail(reason: str = "", pytrace: bool = True, msg: Optional[str] = None) -> NoReturn:
+ """Explicitly fail an executing test with the given message.
+
+ :param reason:
+ The message to show the user as reason for the failure.
+
+ :param pytrace:
+ If False, msg represents the full failure information and no
+ python traceback will be reported.
+
+ :param msg:
+ Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
+ """
+ __tracebackhide__ = True
+ reason = _resolve_msg_to_reason("fail", reason, msg)
+ raise Failed(msg=reason, pytrace=pytrace)
+
+
+def _resolve_msg_to_reason(
+ func_name: str, reason: str, msg: Optional[str] = None
+) -> str:
+ """
+ Handles converting the deprecated msg parameter if provided into
+ reason, raising a deprecation warning. This function will be removed
+ when the optional msg argument is removed from here in future.
+
+ :param str func_name:
+ The name of the offending function, this is formatted into the deprecation message.
+
+ :param str reason:
+ The reason= passed into either pytest.fail() or pytest.skip()
+
+ :param str msg:
+ The msg= passed into either pytest.fail() or pytest.skip(). This will
+ be converted into reason if it is provided to allow pytest.skip(msg=) or
+ pytest.fail(msg=) to continue working in the interim period.
+
+ :returns:
+ The value to use as reason.
+
+ """
+ __tracebackhide__ = True
+ if msg is not None:
+ if reason:
+ from pytest import UsageError
+
+ raise UsageError(
+ f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted."
+ )
+ warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3)
+ reason = msg
+ return reason
+
+
+class XFailed(Failed):
+ """Raised from an explicit call to pytest.xfail()."""
+
+
+@_with_exception(XFailed)
+def xfail(reason: str = "") -> NoReturn:
+ """Imperatively xfail an executing test or setup function with the given reason.
+
+ This function should be called only during testing (setup, call or teardown).
+
+ :param reason:
+ The message to show the user as reason for the xfail.
+
+ .. note::
+ It is better to use the :ref:`pytest.mark.xfail ref` marker when
+ possible to declare a test to be xfailed under certain conditions
+ like known bugs or missing features.
+ """
+ __tracebackhide__ = True
+ raise XFailed(reason)
+
+
+def importorskip(
+ modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
+) -> Any:
+ """Import and return the requested module ``modname``, or skip the
+ current test if the module cannot be imported.
+
+ :param modname:
+ The name of the module to import.
+ :param minversion:
+ If given, the imported module's ``__version__`` attribute must be at
+ least this minimal version, otherwise the test is still skipped.
+ :param reason:
+ If given, this reason is shown as the message when the module cannot
+ be imported.
+
+ :returns:
+ The imported module. This should be assigned to its canonical name.
+
+ Example::
+
+ docutils = pytest.importorskip("docutils")
+ """
+ import warnings
+
+ __tracebackhide__ = True
+ compile(modname, "", "eval") # to catch syntaxerrors
+
+ with warnings.catch_warnings():
+ # Make sure to ignore ImportWarnings that might happen because
+ # of existing directories with the same name we're trying to
+ # import but without a __init__.py file.
+ warnings.simplefilter("ignore")
+ try:
+ __import__(modname)
+ except ImportError as exc:
+ if reason is None:
+ reason = f"could not import {modname!r}: {exc}"
+ raise Skipped(reason, allow_module_level=True) from None
+ mod = sys.modules[modname]
+ if minversion is None:
+ return mod
+ verattr = getattr(mod, "__version__", None)
+ if minversion is not None:
+ # Imported lazily to improve start-up time.
+ from packaging.version import Version
+
+ if verattr is None or Version(verattr) < Version(minversion):
+ raise Skipped(
+ "module %r has __version__ %r, required is: %r"
+ % (modname, verattr, minversion),
+ allow_module_level=True,
+ )
+ return mod
diff --git a/venv/lib/python3.12/site-packages/_pytest/pastebin.py b/venv/lib/python3.12/site-packages/_pytest/pastebin.py
new file mode 100644
index 0000000..22c7a62
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/pastebin.py
@@ -0,0 +1,110 @@
+"""Submit failure or test session information to a pastebin service."""
+import tempfile
+from io import StringIO
+from typing import IO
+from typing import Union
+
+import pytest
+from _pytest.config import Config
+from _pytest.config import create_terminal_writer
+from _pytest.config.argparsing import Parser
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+
+
+pastebinfile_key = StashKey[IO[bytes]]()
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting")
+ group._addoption(
+ "--pastebin",
+ metavar="mode",
+ action="store",
+ dest="pastebin",
+ default=None,
+ choices=["failed", "all"],
+ help="Send failed|all info to bpaste.net pastebin service",
+ )
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_configure(config: Config) -> None:
+ if config.option.pastebin == "all":
+ tr = config.pluginmanager.getplugin("terminalreporter")
+ # If no terminal reporter plugin is present, nothing we can do here;
+ # this can happen when this function executes in a worker node
+ # when using pytest-xdist, for example.
+ if tr is not None:
+ # pastebin file will be UTF-8 encoded binary file.
+ config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b")
+ oldwrite = tr._tw.write
+
+ def tee_write(s, **kwargs):
+ oldwrite(s, **kwargs)
+ if isinstance(s, str):
+ s = s.encode("utf-8")
+ config.stash[pastebinfile_key].write(s)
+
+ tr._tw.write = tee_write
+
+
+def pytest_unconfigure(config: Config) -> None:
+ if pastebinfile_key in config.stash:
+ pastebinfile = config.stash[pastebinfile_key]
+ # Get terminal contents and delete file.
+ pastebinfile.seek(0)
+ sessionlog = pastebinfile.read()
+ pastebinfile.close()
+ del config.stash[pastebinfile_key]
+ # Undo our patching in the terminal reporter.
+ tr = config.pluginmanager.getplugin("terminalreporter")
+ del tr._tw.__dict__["write"]
+ # Write summary.
+ tr.write_sep("=", "Sending information to Paste Service")
+ pastebinurl = create_new_paste(sessionlog)
+ tr.write_line("pastebin session-log: %s\n" % pastebinurl)
+
+
+def create_new_paste(contents: Union[str, bytes]) -> str:
+ """Create a new paste using the bpaste.net service.
+
+ :contents: Paste contents string.
+ :returns: URL to the pasted contents, or an error message.
+ """
+ import re
+ from urllib.request import urlopen
+ from urllib.parse import urlencode
+
+ params = {"code": contents, "lexer": "text", "expiry": "1week"}
+ url = "https://bpa.st"
+ try:
+ response: str = (
+ urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
+ )
+ except OSError as exc_info: # urllib errors
+ return "bad response: %s" % exc_info
+ m = re.search(r'href="/raw/(\w+)"', response)
+ if m:
+ return f"{url}/show/{m.group(1)}"
+ else:
+ return "bad response: invalid format ('" + response + "')"
+
+
+def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
+ if terminalreporter.config.option.pastebin != "failed":
+ return
+ if "failed" in terminalreporter.stats:
+ terminalreporter.write_sep("=", "Sending information to Paste Service")
+ for rep in terminalreporter.stats["failed"]:
+ try:
+ msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
+ except AttributeError:
+ msg = terminalreporter._getfailureheadline(rep)
+ file = StringIO()
+ tw = create_terminal_writer(terminalreporter.config, file)
+ rep.toterminal(tw)
+ s = file.getvalue()
+ assert len(s)
+ pastebinurl = create_new_paste(s)
+ terminalreporter.write_line(f"{msg} --> {pastebinurl}")
diff --git a/venv/lib/python3.12/site-packages/_pytest/pathlib.py b/venv/lib/python3.12/site-packages/_pytest/pathlib.py
new file mode 100644
index 0000000..c2f8535
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/pathlib.py
@@ -0,0 +1,804 @@
+import atexit
+import contextlib
+import fnmatch
+import importlib.util
+import itertools
+import os
+import shutil
+import sys
+import types
+import uuid
+import warnings
+from enum import Enum
+from errno import EBADF
+from errno import ELOOP
+from errno import ENOENT
+from errno import ENOTDIR
+from functools import partial
+from os.path import expanduser
+from os.path import expandvars
+from os.path import isabs
+from os.path import sep
+from pathlib import Path
+from pathlib import PurePath
+from posixpath import sep as posix_sep
+from types import ModuleType
+from typing import Callable
+from typing import Dict
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TypeVar
+from typing import Union
+
+from _pytest.compat import assert_never
+from _pytest.outcomes import skip
+from _pytest.warning_types import PytestWarning
+
+LOCK_TIMEOUT = 60 * 60 * 24 * 3
+
+
+_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
+
+# The following function, variables and comments were
+# copied from cpython 3.9 Lib/pathlib.py file.
+
+# EBADF - guard against macOS `stat` throwing EBADF
+_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
+
+_IGNORED_WINERRORS = (
+ 21, # ERROR_NOT_READY - drive exists but is not accessible
+ 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
+)
+
+
+def _ignore_error(exception):
+ return (
+ getattr(exception, "errno", None) in _IGNORED_ERRORS
+ or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
+ )
+
+
+def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
+ return path.joinpath(".lock")
+
+
+def on_rm_rf_error(
+ func,
+ path: str,
+ excinfo: Union[
+ BaseException,
+ Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
+ ],
+ *,
+ start_path: Path,
+) -> bool:
+ """Handle known read-only errors during rmtree.
+
+ The returned value is used only by our own tests.
+ """
+ if isinstance(excinfo, BaseException):
+ exc = excinfo
+ else:
+ exc = excinfo[1]
+
+ # Another process removed the file in the middle of the "rm_rf" (xdist for example).
+ # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
+ if isinstance(exc, FileNotFoundError):
+ return False
+
+ if not isinstance(exc, PermissionError):
+ warnings.warn(
+ PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
+ )
+ return False
+
+ if func not in (os.rmdir, os.remove, os.unlink):
+ if func not in (os.open,):
+ warnings.warn(
+ PytestWarning(
+ "(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
+ func, path, type(exc), exc
+ )
+ )
+ )
+ return False
+
+ # Chmod + retry.
+ import stat
+
+ def chmod_rw(p: str) -> None:
+ mode = os.stat(p).st_mode
+ os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
+
+ # For files, we need to recursively go upwards in the directories to
+ # ensure they all are also writable.
+ p = Path(path)
+ if p.is_file():
+ for parent in p.parents:
+ chmod_rw(str(parent))
+ # Stop when we reach the original path passed to rm_rf.
+ if parent == start_path:
+ break
+ chmod_rw(str(path))
+
+ func(path)
+ return True
+
+
+def ensure_extended_length_path(path: Path) -> Path:
+ """Get the extended-length version of a path (Windows).
+
+ On Windows, by default, the maximum length of a path (MAX_PATH) is 260
+ characters, and operations on paths longer than that fail. But it is possible
+ to overcome this by converting the path to "extended-length" form before
+ performing the operation:
+ https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
+
+ On Windows, this function returns the extended-length absolute version of path.
+ On other platforms it returns path unchanged.
+ """
+ if sys.platform.startswith("win32"):
+ path = path.resolve()
+ path = Path(get_extended_length_path_str(str(path)))
+ return path
+
+
+def get_extended_length_path_str(path: str) -> str:
+ """Convert a path to a Windows extended length path."""
+ long_path_prefix = "\\\\?\\"
+ unc_long_path_prefix = "\\\\?\\UNC\\"
+ if path.startswith((long_path_prefix, unc_long_path_prefix)):
+ return path
+ # UNC
+ if path.startswith("\\\\"):
+ return unc_long_path_prefix + path[2:]
+ return long_path_prefix + path
+
+
+def rm_rf(path: Path) -> None:
+ """Remove the path contents recursively, even if some elements
+ are read-only."""
+ path = ensure_extended_length_path(path)
+ onerror = partial(on_rm_rf_error, start_path=path)
+ if sys.version_info >= (3, 12):
+ shutil.rmtree(str(path), onexc=onerror)
+ else:
+ shutil.rmtree(str(path), onerror=onerror)
+
+
+def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
+ """Find all elements in root that begin with the prefix, case insensitive."""
+ l_prefix = prefix.lower()
+ for x in root.iterdir():
+ if x.name.lower().startswith(l_prefix):
+ yield x
+
+
+def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
+ """Return the parts of the paths following the prefix.
+
+ :param iter: Iterator over path names.
+ :param prefix: Expected prefix of the path names.
+ """
+ p_len = len(prefix)
+ for p in iter:
+ yield p.name[p_len:]
+
+
+def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
+ """Combine find_prefixes and extract_suffixes."""
+ return extract_suffixes(find_prefixed(root, prefix), prefix)
+
+
+def parse_num(maybe_num) -> int:
+ """Parse number path suffixes, returns -1 on error."""
+ try:
+ return int(maybe_num)
+ except ValueError:
+ return -1
+
+
+def _force_symlink(
+ root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
+) -> None:
+ """Helper to create the current symlink.
+
+ It's full of race conditions that are reasonably OK to ignore
+ for the context of best effort linking to the latest test run.
+
+ The presumption being that in case of much parallelism
+ the inaccuracy is going to be acceptable.
+ """
+ current_symlink = root.joinpath(target)
+ try:
+ current_symlink.unlink()
+ except OSError:
+ pass
+ try:
+ current_symlink.symlink_to(link_to)
+ except Exception:
+ pass
+
+
+def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path:
+ """Create a directory with an increased number as suffix for the given prefix."""
+ for i in range(10):
+ # try up to 10 times to create the folder
+ max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
+ new_number = max_existing + 1
+ new_path = root.joinpath(f"{prefix}{new_number}")
+ try:
+ new_path.mkdir(mode=mode)
+ except Exception:
+ pass
+ else:
+ _force_symlink(root, prefix + "current", new_path)
+ return new_path
+ else:
+ raise OSError(
+ "could not create numbered dir with prefix "
+ "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root)
+ )
+
+
+def create_cleanup_lock(p: Path) -> Path:
+ """Create a lock to prevent premature folder cleanup."""
+ lock_path = get_lock_path(p)
+ try:
+ fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
+ except FileExistsError as e:
+ raise OSError(f"cannot create lockfile in {p}") from e
+ else:
+ pid = os.getpid()
+ spid = str(pid).encode()
+ os.write(fd, spid)
+ os.close(fd)
+ if not lock_path.is_file():
+ raise OSError("lock path got renamed after successful creation")
+ return lock_path
+
+
+def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
+ """Register a cleanup function for removing a lock, by default on atexit."""
+ pid = os.getpid()
+
+ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
+ current_pid = os.getpid()
+ if current_pid != original_pid:
+ # fork
+ return
+ try:
+ lock_path.unlink()
+ except OSError:
+ pass
+
+ return register(cleanup_on_exit)
+
+
+def maybe_delete_a_numbered_dir(path: Path) -> None:
+ """Remove a numbered directory if its lock can be obtained and it does
+ not seem to be in use."""
+ path = ensure_extended_length_path(path)
+ lock_path = None
+ try:
+ lock_path = create_cleanup_lock(path)
+ parent = path.parent
+
+ garbage = parent.joinpath(f"garbage-{uuid.uuid4()}")
+ path.rename(garbage)
+ rm_rf(garbage)
+ except OSError:
+ # known races:
+ # * other process did a cleanup at the same time
+ # * deletable folder was found
+ # * process cwd (Windows)
+ return
+ finally:
+ # If we created the lock, ensure we remove it even if we failed
+ # to properly remove the numbered dir.
+ if lock_path is not None:
+ try:
+ lock_path.unlink()
+ except OSError:
+ pass
+
+
+def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
+ """Check if `path` is deletable based on whether the lock file is expired."""
+ if path.is_symlink():
+ return False
+ lock = get_lock_path(path)
+ try:
+ if not lock.is_file():
+ return True
+ except OSError:
+ # we might not have access to the lock file at all, in this case assume
+ # we don't have access to the entire directory (#7491).
+ return False
+ try:
+ lock_time = lock.stat().st_mtime
+ except Exception:
+ return False
+ else:
+ if lock_time < consider_lock_dead_if_created_before:
+ # We want to ignore any errors while trying to remove the lock such as:
+ # - PermissionDenied, like the file permissions have changed since the lock creation;
+ # - FileNotFoundError, in case another pytest process got here first;
+ # and any other cause of failure.
+ with contextlib.suppress(OSError):
+ lock.unlink()
+ return True
+ return False
+
+
+def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
+ """Try to cleanup a folder if we can ensure it's deletable."""
+ if ensure_deletable(path, consider_lock_dead_if_created_before):
+ maybe_delete_a_numbered_dir(path)
+
+
+def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
+ """List candidates for numbered directories to be removed - follows py.path."""
+ max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
+ max_delete = max_existing - keep
+ paths = find_prefixed(root, prefix)
+ paths, paths2 = itertools.tee(paths)
+ numbers = map(parse_num, extract_suffixes(paths2, prefix))
+ for path, number in zip(paths, numbers):
+ if number <= max_delete:
+ yield path
+
+
+def cleanup_dead_symlinks(root: Path):
+ for left_dir in root.iterdir():
+ if left_dir.is_symlink():
+ if not left_dir.resolve().exists():
+ left_dir.unlink()
+
+
+def cleanup_numbered_dir(
+ root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
+) -> None:
+ """Cleanup for lock driven numbered directories."""
+ if not root.exists():
+ return
+ for path in cleanup_candidates(root, prefix, keep):
+ try_cleanup(path, consider_lock_dead_if_created_before)
+ for path in root.glob("garbage-*"):
+ try_cleanup(path, consider_lock_dead_if_created_before)
+
+ cleanup_dead_symlinks(root)
+
+
+def make_numbered_dir_with_cleanup(
+ root: Path,
+ prefix: str,
+ keep: int,
+ lock_timeout: float,
+ mode: int,
+) -> Path:
+ """Create a numbered dir with a cleanup lock and remove old ones."""
+ e = None
+ for i in range(10):
+ try:
+ p = make_numbered_dir(root, prefix, mode)
+ # Only lock the current dir when keep is not 0
+ if keep != 0:
+ lock_path = create_cleanup_lock(p)
+ register_cleanup_lock_removal(lock_path)
+ except Exception as exc:
+ e = exc
+ else:
+ consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout
+ # Register a cleanup for program exit
+ atexit.register(
+ cleanup_numbered_dir,
+ root,
+ prefix,
+ keep,
+ consider_lock_dead_if_created_before,
+ )
+ return p
+ assert e is not None
+ raise e
+
+
+def resolve_from_str(input: str, rootpath: Path) -> Path:
+ input = expanduser(input)
+ input = expandvars(input)
+ if isabs(input):
+ return Path(input)
+ else:
+ return rootpath.joinpath(input)
+
+
+def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool:
+ """A port of FNMatcher from py.path.common which works with PurePath() instances.
+
+ The difference between this algorithm and PurePath.match() is that the
+ latter matches "**" glob expressions for each part of the path, while
+ this algorithm uses the whole path instead.
+
+ For example:
+ "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py"
+ with this algorithm, but not with PurePath.match().
+
+ This algorithm was ported to keep backward-compatibility with existing
+ settings which assume paths match according this logic.
+
+ References:
+ * https://bugs.python.org/issue29249
+ * https://bugs.python.org/issue34731
+ """
+ path = PurePath(path)
+ iswin32 = sys.platform.startswith("win")
+
+ if iswin32 and sep not in pattern and posix_sep in pattern:
+ # Running on Windows, the pattern has no Windows path separators,
+ # and the pattern has one or more Posix path separators. Replace
+ # the Posix path separators with the Windows path separator.
+ pattern = pattern.replace(posix_sep, sep)
+
+ if sep not in pattern:
+ name = path.name
+ else:
+ name = str(path)
+ if path.is_absolute() and not os.path.isabs(pattern):
+ pattern = f"*{os.sep}{pattern}"
+ return fnmatch.fnmatch(name, pattern)
+
+
+def parts(s: str) -> Set[str]:
+ parts = s.split(sep)
+ return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
+
+
+def symlink_or_skip(src, dst, **kwargs):
+ """Make a symlink, or skip the test in case symlinks are not supported."""
+ try:
+ os.symlink(str(src), str(dst), **kwargs)
+ except OSError as e:
+ skip(f"symlinks not supported: {e}")
+
+
+class ImportMode(Enum):
+ """Possible values for `mode` parameter of `import_path`."""
+
+ prepend = "prepend"
+ append = "append"
+ importlib = "importlib"
+
+
+class ImportPathMismatchError(ImportError):
+ """Raised on import_path() if there is a mismatch of __file__'s.
+
+ This can happen when `import_path` is called multiple times with different filenames that has
+ the same basename but reside in packages
+ (for example "/tests1/test_foo.py" and "/tests2/test_foo.py").
+ """
+
+
+def import_path(
+ p: Union[str, "os.PathLike[str]"],
+ *,
+ mode: Union[str, ImportMode] = ImportMode.prepend,
+ root: Path,
+) -> ModuleType:
+ """Import and return a module from the given path, which can be a file (a module) or
+ a directory (a package).
+
+ The import mechanism used is controlled by the `mode` parameter:
+
+ * `mode == ImportMode.prepend`: the directory containing the module (or package, taking
+ `__init__.py` files into account) will be put at the *start* of `sys.path` before
+ being imported with `importlib.import_module`.
+
+ * `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
+ to the end of `sys.path`, if not already in `sys.path`.
+
+ * `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
+ to import the module, which avoids having to muck with `sys.path` at all. It effectively
+ allows having same-named test modules in different places.
+
+ :param root:
+ Used as an anchor when mode == ImportMode.importlib to obtain
+ a unique name for the module being imported so it can safely be stored
+ into ``sys.modules``.
+
+ :raises ImportPathMismatchError:
+ If after importing the given `path` and the module `__file__`
+ are different. Only raised in `prepend` and `append` modes.
+ """
+ mode = ImportMode(mode)
+
+ path = Path(p)
+
+ if not path.exists():
+ raise ImportError(path)
+
+ if mode is ImportMode.importlib:
+ module_name = module_name_from_path(path, root)
+ with contextlib.suppress(KeyError):
+ return sys.modules[module_name]
+
+ for meta_importer in sys.meta_path:
+ spec = meta_importer.find_spec(module_name, [str(path.parent)])
+ if spec is not None:
+ break
+ else:
+ spec = importlib.util.spec_from_file_location(module_name, str(path))
+
+ if spec is None:
+ raise ImportError(f"Can't find module {module_name} at location {path}")
+ mod = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = mod
+ spec.loader.exec_module(mod) # type: ignore[union-attr]
+ insert_missing_modules(sys.modules, module_name)
+ return mod
+
+ pkg_path = resolve_package_path(path)
+ if pkg_path is not None:
+ pkg_root = pkg_path.parent
+ names = list(path.with_suffix("").relative_to(pkg_root).parts)
+ if names[-1] == "__init__":
+ names.pop()
+ module_name = ".".join(names)
+ else:
+ pkg_root = path.parent
+ module_name = path.stem
+
+ # Change sys.path permanently: restoring it at the end of this function would cause surprising
+ # problems because of delayed imports: for example, a conftest.py file imported by this function
+ # might have local imports, which would fail at runtime if we restored sys.path.
+ if mode is ImportMode.append:
+ if str(pkg_root) not in sys.path:
+ sys.path.append(str(pkg_root))
+ elif mode is ImportMode.prepend:
+ if str(pkg_root) != sys.path[0]:
+ sys.path.insert(0, str(pkg_root))
+ else:
+ assert_never(mode)
+
+ importlib.import_module(module_name)
+
+ mod = sys.modules[module_name]
+ if path.name == "__init__.py":
+ return mod
+
+ ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
+ if ignore != "1":
+ module_file = mod.__file__
+ if module_file is None:
+ raise ImportPathMismatchError(module_name, module_file, path)
+
+ if module_file.endswith((".pyc", ".pyo")):
+ module_file = module_file[:-1]
+ if module_file.endswith(os.sep + "__init__.py"):
+ module_file = module_file[: -(len(os.sep + "__init__.py"))]
+
+ try:
+ is_same = _is_same(str(path), module_file)
+ except FileNotFoundError:
+ is_same = False
+
+ if not is_same:
+ raise ImportPathMismatchError(module_name, module_file, path)
+
+ return mod
+
+
+# Implement a special _is_same function on Windows which returns True if the two filenames
+# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
+if sys.platform.startswith("win"):
+
+ def _is_same(f1: str, f2: str) -> bool:
+ return Path(f1) == Path(f2) or os.path.samefile(f1, f2)
+
+else:
+
+ def _is_same(f1: str, f2: str) -> bool:
+ return os.path.samefile(f1, f2)
+
+
+def module_name_from_path(path: Path, root: Path) -> str:
+ """
+ Return a dotted module name based on the given path, anchored on root.
+
+ For example: path="projects/src/tests/test_foo.py" and root="/projects", the
+ resulting module name will be "src.tests.test_foo".
+ """
+ path = path.with_suffix("")
+ try:
+ relative_path = path.relative_to(root)
+ except ValueError:
+ # If we can't get a relative path to root, use the full path, except
+ # for the first part ("d:\\" or "/" depending on the platform, for example).
+ path_parts = path.parts[1:]
+ else:
+ # Use the parts for the relative path to the root path.
+ path_parts = relative_path.parts
+
+ # Module name for packages do not contain the __init__ file, unless
+ # the `__init__.py` file is at the root.
+ if len(path_parts) >= 2 and path_parts[-1] == "__init__":
+ path_parts = path_parts[:-1]
+
+ return ".".join(path_parts)
+
+
+def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> None:
+ """
+ Used by ``import_path`` to create intermediate modules when using mode=importlib.
+
+ When we want to import a module as "src.tests.test_foo" for example, we need
+ to create empty modules "src" and "src.tests" after inserting "src.tests.test_foo",
+ otherwise "src.tests.test_foo" is not importable by ``__import__``.
+ """
+ module_parts = module_name.split(".")
+ child_module: Union[ModuleType, None] = None
+ module: Union[ModuleType, None] = None
+ child_name: str = ""
+ while module_name:
+ if module_name not in modules:
+ try:
+ # If sys.meta_path is empty, calling import_module will issue
+ # a warning and raise ModuleNotFoundError. To avoid the
+ # warning, we check sys.meta_path explicitly and raise the error
+ # ourselves to fall back to creating a dummy module.
+ if not sys.meta_path:
+ raise ModuleNotFoundError
+ module = importlib.import_module(module_name)
+ except ModuleNotFoundError:
+ module = ModuleType(
+ module_name,
+ doc="Empty module created by pytest's importmode=importlib.",
+ )
+ else:
+ module = modules[module_name]
+ if child_module:
+ # Add child attribute to the parent that can reference the child
+ # modules.
+ if not hasattr(module, child_name):
+ setattr(module, child_name, child_module)
+ modules[module_name] = module
+ # Keep track of the child module while moving up the tree.
+ child_module, child_name = module, module_name.rpartition(".")[-1]
+ module_parts.pop(-1)
+ module_name = ".".join(module_parts)
+
+
+def resolve_package_path(path: Path) -> Optional[Path]:
+ """Return the Python package path by looking for the last
+ directory upwards which still contains an __init__.py.
+
+ Returns None if it can not be determined.
+ """
+ result = None
+ for parent in itertools.chain((path,), path.parents):
+ if parent.is_dir():
+ if not parent.joinpath("__init__.py").is_file():
+ break
+ if not parent.name.isidentifier():
+ break
+ result = parent
+ return result
+
+
+def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
+ """Scan a directory recursively, in breadth-first order.
+
+ The returned entries are sorted.
+ """
+ entries = []
+ with os.scandir(path) as s:
+ # Skip entries with symlink loops and other brokenness, so the caller
+ # doesn't have to deal with it.
+ for entry in s:
+ try:
+ entry.is_file()
+ except OSError as err:
+ if _ignore_error(err):
+ continue
+ raise
+ entries.append(entry)
+ entries.sort(key=lambda entry: entry.name)
+ return entries
+
+
+def visit(
+ path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
+) -> Iterator["os.DirEntry[str]"]:
+ """Walk a directory recursively, in breadth-first order.
+
+ The `recurse` predicate determines whether a directory is recursed.
+
+ Entries at each directory level are sorted.
+ """
+ entries = scandir(path)
+ yield from entries
+ for entry in entries:
+ if entry.is_dir() and recurse(entry):
+ yield from visit(entry.path, recurse)
+
+
+def absolutepath(path: Union[Path, str]) -> Path:
+ """Convert a path to an absolute path using os.path.abspath.
+
+ Prefer this over Path.resolve() (see #6523).
+ Prefer this over Path.absolute() (not public, doesn't normalize).
+ """
+ return Path(os.path.abspath(str(path)))
+
+
+def commonpath(path1: Path, path2: Path) -> Optional[Path]:
+ """Return the common part shared with the other path, or None if there is
+ no common part.
+
+ If one path is relative and one is absolute, returns None.
+ """
+ try:
+ return Path(os.path.commonpath((str(path1), str(path2))))
+ except ValueError:
+ return None
+
+
+def bestrelpath(directory: Path, dest: Path) -> str:
+ """Return a string which is a relative path from directory to dest such
+ that directory/bestrelpath == dest.
+
+ The paths must be either both absolute or both relative.
+
+ If no such path can be determined, returns dest.
+ """
+ assert isinstance(directory, Path)
+ assert isinstance(dest, Path)
+ if dest == directory:
+ return os.curdir
+ # Find the longest common directory.
+ base = commonpath(directory, dest)
+ # Can be the case on Windows for two absolute paths on different drives.
+ # Can be the case for two relative paths without common prefix.
+ # Can be the case for a relative path and an absolute path.
+ if not base:
+ return str(dest)
+ reldirectory = directory.relative_to(base)
+ reldest = dest.relative_to(base)
+ return os.path.join(
+ # Back from directory to base.
+ *([os.pardir] * len(reldirectory.parts)),
+ # Forward from base to dest.
+ *reldest.parts,
+ )
+
+
+# Originates from py. path.local.copy(), with siginficant trims and adjustments.
+# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True)
+def copytree(source: Path, target: Path) -> None:
+ """Recursively copy a source directory to target."""
+ assert source.is_dir()
+ for entry in visit(source, recurse=lambda entry: not entry.is_symlink()):
+ x = Path(entry)
+ relpath = x.relative_to(source)
+ newx = target / relpath
+ newx.parent.mkdir(exist_ok=True)
+ if x.is_symlink():
+ newx.symlink_to(os.readlink(x))
+ elif x.is_file():
+ shutil.copyfile(x, newx)
+ elif x.is_dir():
+ newx.mkdir(exist_ok=True)
+
+
+def safe_exists(p: Path) -> bool:
+ """Like Path.exists(), but account for input arguments that might be too long (#11394)."""
+ try:
+ return p.exists()
+ except (ValueError, OSError):
+ # ValueError: stat: path too long for Windows
+ # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
+ return False
diff --git a/venv/lib/python3.12/site-packages/_pytest/py.typed b/venv/lib/python3.12/site-packages/_pytest/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/_pytest/pytester.py b/venv/lib/python3.12/site-packages/_pytest/pytester.py
new file mode 100644
index 0000000..0771065
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/pytester.py
@@ -0,0 +1,1789 @@
+"""(Disabled by default) support for testing pytest and pytest plugins.
+
+PYTEST_DONT_REWRITE
+"""
+import collections.abc
+import contextlib
+import gc
+import importlib
+import locale
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import traceback
+from fnmatch import fnmatch
+from io import StringIO
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generator
+from typing import IO
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import TextIO
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+from weakref import WeakKeyDictionary
+
+from iniconfig import IniConfig
+from iniconfig import SectionWrapper
+
+from _pytest import timing
+from _pytest._code import Source
+from _pytest.capture import _get_multicapture
+from _pytest.compat import final
+from _pytest.compat import NOTSET
+from _pytest.compat import NotSetType
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import main
+from _pytest.config import PytestPluginManager
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import fail
+from _pytest.outcomes import importorskip
+from _pytest.outcomes import skip
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import copytree
+from _pytest.pathlib import make_numbered_dir
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+from _pytest.tmpdir import TempPathFactory
+from _pytest.warning_types import PytestWarning
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Final
+ from typing_extensions import Literal
+
+ import pexpect
+
+
+pytest_plugins = ["pytester_assertions"]
+
+
+IGNORE_PAM = [ # filenames added when obtaining details about the current user
+ "/var/lib/sss/mc/passwd"
+]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addoption(
+ "--lsof",
+ action="store_true",
+ dest="lsof",
+ default=False,
+ help="Run FD checks if lsof is available",
+ )
+
+ parser.addoption(
+ "--runpytest",
+ default="inprocess",
+ dest="runpytest",
+ choices=("inprocess", "subprocess"),
+ help=(
+ "Run pytest sub runs in tests using an 'inprocess' "
+ "or 'subprocess' (python -m main) method"
+ ),
+ )
+
+ parser.addini(
+ "pytester_example_dir", help="Directory to take the pytester example files from"
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ if config.getvalue("lsof"):
+ checker = LsofFdLeakChecker()
+ if checker.matching_platform():
+ config.pluginmanager.register(checker)
+
+ config.addinivalue_line(
+ "markers",
+ "pytester_example_path(*path_segments): join the given path "
+ "segments to `pytester_example_dir` for this test.",
+ )
+
+
+class LsofFdLeakChecker:
+ def get_open_files(self) -> List[Tuple[str, str]]:
+ out = subprocess.run(
+ ("lsof", "-Ffn0", "-p", str(os.getpid())),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ text=True,
+ encoding=locale.getpreferredencoding(False),
+ ).stdout
+
+ def isopen(line: str) -> bool:
+ return line.startswith("f") and (
+ "deleted" not in line
+ and "mem" not in line
+ and "txt" not in line
+ and "cwd" not in line
+ )
+
+ open_files = []
+
+ for line in out.split("\n"):
+ if isopen(line):
+ fields = line.split("\0")
+ fd = fields[0][1:]
+ filename = fields[1][1:]
+ if filename in IGNORE_PAM:
+ continue
+ if filename.startswith("/"):
+ open_files.append((fd, filename))
+
+ return open_files
+
+ def matching_platform(self) -> bool:
+ try:
+ subprocess.run(("lsof", "-v"), check=True)
+ except (OSError, subprocess.CalledProcessError):
+ return False
+ else:
+ return True
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]:
+ lines1 = self.get_open_files()
+ yield
+ if hasattr(sys, "pypy_version_info"):
+ gc.collect()
+ lines2 = self.get_open_files()
+
+ new_fds = {t[0] for t in lines2} - {t[0] for t in lines1}
+ leaked_files = [t for t in lines2 if t[0] in new_fds]
+ if leaked_files:
+ error = [
+ "***** %s FD leakage detected" % len(leaked_files),
+ *(str(f) for f in leaked_files),
+ "*** Before:",
+ *(str(f) for f in lines1),
+ "*** After:",
+ *(str(f) for f in lines2),
+ "***** %s FD leakage detected" % len(leaked_files),
+ "*** function %s:%s: %s " % item.location,
+ "See issue #2366",
+ ]
+ item.warn(PytestWarning("\n".join(error)))
+
+
+# used at least by pytest-xdist plugin
+
+
+@fixture
+def _pytest(request: FixtureRequest) -> "PytestArg":
+ """Return a helper which offers a gethookrecorder(hook) method which
+ returns a HookRecorder instance which helps to make assertions about called
+ hooks."""
+ return PytestArg(request)
+
+
+class PytestArg:
+ def __init__(self, request: FixtureRequest) -> None:
+ self._request = request
+
+ def gethookrecorder(self, hook) -> "HookRecorder":
+ hookrecorder = HookRecorder(hook._pm)
+ self._request.addfinalizer(hookrecorder.finish_recording)
+ return hookrecorder
+
+
+def get_public_names(values: Iterable[str]) -> List[str]:
+ """Only return names from iterator values without a leading underscore."""
+ return [x for x in values if x[0] != "_"]
+
+
+@final
+class RecordedHookCall:
+ """A recorded call to a hook.
+
+ The arguments to the hook call are set as attributes.
+ For example:
+
+ .. code-block:: python
+
+ calls = hook_recorder.getcalls("pytest_runtest_setup")
+ # Suppose pytest_runtest_setup was called once with `item=an_item`.
+ assert calls[0].item is an_item
+ """
+
+ def __init__(self, name: str, kwargs) -> None:
+ self.__dict__.update(kwargs)
+ self._name = name
+
+ def __repr__(self) -> str:
+ d = self.__dict__.copy()
+ del d["_name"]
+ return f""
+
+ if TYPE_CHECKING:
+ # The class has undetermined attributes, this tells mypy about it.
+ def __getattr__(self, key: str):
+ ...
+
+
+@final
+class HookRecorder:
+ """Record all hooks called in a plugin manager.
+
+ Hook recorders are created by :class:`Pytester`.
+
+ This wraps all the hook calls in the plugin manager, recording each call
+ before propagating the normal calls.
+ """
+
+ def __init__(
+ self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+
+ self._pluginmanager = pluginmanager
+ self.calls: List[RecordedHookCall] = []
+ self.ret: Optional[Union[int, ExitCode]] = None
+
+ def before(hook_name: str, hook_impls, kwargs) -> None:
+ self.calls.append(RecordedHookCall(hook_name, kwargs))
+
+ def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
+ pass
+
+ self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
+
+ def finish_recording(self) -> None:
+ self._undo_wrapping()
+
+ def getcalls(self, names: Union[str, Iterable[str]]) -> List[RecordedHookCall]:
+ """Get all recorded calls to hooks with the given names (or name)."""
+ if isinstance(names, str):
+ names = names.split()
+ return [call for call in self.calls if call._name in names]
+
+ def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None:
+ __tracebackhide__ = True
+ i = 0
+ entries = list(entries)
+ backlocals = sys._getframe(1).f_locals
+ while entries:
+ name, check = entries.pop(0)
+ for ind, call in enumerate(self.calls[i:]):
+ if call._name == name:
+ print("NAMEMATCH", name, call)
+ if eval(check, backlocals, call.__dict__):
+ print("CHECKERMATCH", repr(check), "->", call)
+ else:
+ print("NOCHECKERMATCH", repr(check), "-", call)
+ continue
+ i += ind + 1
+ break
+ print("NONAMEMATCH", name, "with", call)
+ else:
+ fail(f"could not find {name!r} check {check!r}")
+
+ def popcall(self, name: str) -> RecordedHookCall:
+ __tracebackhide__ = True
+ for i, call in enumerate(self.calls):
+ if call._name == name:
+ del self.calls[i]
+ return call
+ lines = [f"could not find call {name!r}, in:"]
+ lines.extend([" %s" % x for x in self.calls])
+ fail("\n".join(lines))
+
+ def getcall(self, name: str) -> RecordedHookCall:
+ values = self.getcalls(name)
+ assert len(values) == 1, (name, values)
+ return values[0]
+
+ # functionality for test reports
+
+ @overload
+ def getreports(
+ self,
+ names: "Literal['pytest_collectreport']",
+ ) -> Sequence[CollectReport]:
+ ...
+
+ @overload
+ def getreports(
+ self,
+ names: "Literal['pytest_runtest_logreport']",
+ ) -> Sequence[TestReport]:
+ ...
+
+ @overload
+ def getreports(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ ...
+
+ def getreports(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ return [x.report for x in self.getcalls(names)]
+
+ def matchreport(
+ self,
+ inamepart: str = "",
+ names: Union[str, Iterable[str]] = (
+ "pytest_runtest_logreport",
+ "pytest_collectreport",
+ ),
+ when: Optional[str] = None,
+ ) -> Union[CollectReport, TestReport]:
+ """Return a testreport whose dotted import path matches."""
+ values = []
+ for rep in self.getreports(names=names):
+ if not when and rep.when != "call" and rep.passed:
+ # setup/teardown passing reports - let's ignore those
+ continue
+ if when and rep.when != when:
+ continue
+ if not inamepart or inamepart in rep.nodeid.split("::"):
+ values.append(rep)
+ if not values:
+ raise ValueError(
+ "could not find test report matching %r: "
+ "no test reports at all!" % (inamepart,)
+ )
+ if len(values) > 1:
+ raise ValueError(
+ "found 2 or more testreports matching {!r}: {}".format(
+ inamepart, values
+ )
+ )
+ return values[0]
+
+ @overload
+ def getfailures(
+ self,
+ names: "Literal['pytest_collectreport']",
+ ) -> Sequence[CollectReport]:
+ ...
+
+ @overload
+ def getfailures(
+ self,
+ names: "Literal['pytest_runtest_logreport']",
+ ) -> Sequence[TestReport]:
+ ...
+
+ @overload
+ def getfailures(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ ...
+
+ def getfailures(
+ self,
+ names: Union[str, Iterable[str]] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[Union[CollectReport, TestReport]]:
+ return [rep for rep in self.getreports(names) if rep.failed]
+
+ def getfailedcollections(self) -> Sequence[CollectReport]:
+ return self.getfailures("pytest_collectreport")
+
+ def listoutcomes(
+ self,
+ ) -> Tuple[
+ Sequence[TestReport],
+ Sequence[Union[CollectReport, TestReport]],
+ Sequence[Union[CollectReport, TestReport]],
+ ]:
+ passed = []
+ skipped = []
+ failed = []
+ for rep in self.getreports(
+ ("pytest_collectreport", "pytest_runtest_logreport")
+ ):
+ if rep.passed:
+ if rep.when == "call":
+ assert isinstance(rep, TestReport)
+ passed.append(rep)
+ elif rep.skipped:
+ skipped.append(rep)
+ else:
+ assert rep.failed, f"Unexpected outcome: {rep!r}"
+ failed.append(rep)
+ return passed, skipped, failed
+
+ def countoutcomes(self) -> List[int]:
+ return [len(x) for x in self.listoutcomes()]
+
+ def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
+ __tracebackhide__ = True
+ from _pytest.pytester_assertions import assertoutcome
+
+ outcomes = self.listoutcomes()
+ assertoutcome(
+ outcomes,
+ passed=passed,
+ skipped=skipped,
+ failed=failed,
+ )
+
+ def clear(self) -> None:
+ self.calls[:] = []
+
+
+@fixture
+def linecomp() -> "LineComp":
+ """A :class: `LineComp` instance for checking that an input linearly
+ contains a sequence of strings."""
+ return LineComp()
+
+
+@fixture(name="LineMatcher")
+def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
+ """A reference to the :class: `LineMatcher`.
+
+ This is instantiable with a list of lines (without their trailing newlines).
+ This is useful for testing large texts, such as the output of commands.
+ """
+ return LineMatcher
+
+
+@fixture
+def pytester(
+ request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
+) -> "Pytester":
+ """
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
+ against expected output, perfect for black-box testing of pytest plugins.
+
+ It attempts to isolate the test run from external factors as much as possible, modifying
+ the current working directory to ``path`` and environment variables during initialization.
+
+ It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
+ fixture but provides methods which aid in testing pytest itself.
+ """
+ return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True)
+
+
+@fixture
+def _sys_snapshot() -> Generator[None, None, None]:
+ snappaths = SysPathsSnapshot()
+ snapmods = SysModulesSnapshot()
+ yield
+ snapmods.restore()
+ snappaths.restore()
+
+
+@fixture
+def _config_for_test() -> Generator[Config, None, None]:
+ from _pytest.config import get_config
+
+ config = get_config()
+ yield config
+ config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
+
+
+# Regex to match the session duration string in the summary: "74.34s".
+rex_session_duration = re.compile(r"\d+\.\d\ds")
+# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped".
+rex_outcome = re.compile(r"(\d+) (\w+)")
+
+
+@final
+class RunResult:
+ """The result of running a command from :class:`~pytest.Pytester`."""
+
+ def __init__(
+ self,
+ ret: Union[int, ExitCode],
+ outlines: List[str],
+ errlines: List[str],
+ duration: float,
+ ) -> None:
+ try:
+ self.ret: Union[int, ExitCode] = ExitCode(ret)
+ """The return value."""
+ except ValueError:
+ self.ret = ret
+ self.outlines = outlines
+ """List of lines captured from stdout."""
+ self.errlines = errlines
+ """List of lines captured from stderr."""
+ self.stdout = LineMatcher(outlines)
+ """:class:`~pytest.LineMatcher` of stdout.
+
+ Use e.g. :func:`str(stdout) ` to reconstruct stdout, or the commonly used
+ :func:`stdout.fnmatch_lines() ` method.
+ """
+ self.stderr = LineMatcher(errlines)
+ """:class:`~pytest.LineMatcher` of stderr."""
+ self.duration = duration
+ """Duration in seconds."""
+
+ def __repr__(self) -> str:
+ return (
+ ""
+ % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
+ )
+
+ def parseoutcomes(self) -> Dict[str, int]:
+ """Return a dictionary of outcome noun -> count from parsing the terminal
+ output that the test process produced.
+
+ The returned nouns will always be in plural form::
+
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
+
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
+ """
+ return self.parse_summary_nouns(self.outlines)
+
+ @classmethod
+ def parse_summary_nouns(cls, lines) -> Dict[str, int]:
+ """Extract the nouns from a pytest terminal summary line.
+
+ It always returns the plural noun for consistency::
+
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
+
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
+ """
+ for line in reversed(lines):
+ if rex_session_duration.search(line):
+ outcomes = rex_outcome.findall(line)
+ ret = {noun: int(count) for (count, noun) in outcomes}
+ break
+ else:
+ raise ValueError("Pytest terminal summary report not found")
+
+ to_plural = {
+ "warning": "warnings",
+ "error": "errors",
+ }
+ return {to_plural.get(k, k): v for k, v in ret.items()}
+
+ def assert_outcomes(
+ self,
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+ errors: int = 0,
+ xpassed: int = 0,
+ xfailed: int = 0,
+ warnings: Optional[int] = None,
+ deselected: Optional[int] = None,
+ ) -> None:
+ """
+ Assert that the specified outcomes appear with the respective
+ numbers (0 means it didn't occur) in the text output from a test run.
+
+ ``warnings`` and ``deselected`` are only checked if not None.
+ """
+ __tracebackhide__ = True
+ from _pytest.pytester_assertions import assert_outcomes
+
+ outcomes = self.parseoutcomes()
+ assert_outcomes(
+ outcomes,
+ passed=passed,
+ skipped=skipped,
+ failed=failed,
+ errors=errors,
+ xpassed=xpassed,
+ xfailed=xfailed,
+ warnings=warnings,
+ deselected=deselected,
+ )
+
+
+class CwdSnapshot:
+ def __init__(self) -> None:
+ self.__saved = os.getcwd()
+
+ def restore(self) -> None:
+ os.chdir(self.__saved)
+
+
+class SysModulesSnapshot:
+ def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None:
+ self.__preserve = preserve
+ self.__saved = dict(sys.modules)
+
+ def restore(self) -> None:
+ if self.__preserve:
+ self.__saved.update(
+ (k, m) for k, m in sys.modules.items() if self.__preserve(k)
+ )
+ sys.modules.clear()
+ sys.modules.update(self.__saved)
+
+
+class SysPathsSnapshot:
+ def __init__(self) -> None:
+ self.__saved = list(sys.path), list(sys.meta_path)
+
+ def restore(self) -> None:
+ sys.path[:], sys.meta_path[:] = self.__saved
+
+
+@final
+class Pytester:
+ """
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
+ against expected output, perfect for black-box testing of pytest plugins.
+
+ It attempts to isolate the test run from external factors as much as possible, modifying
+ the current working directory to :attr:`path` and environment variables during initialization.
+ """
+
+ __test__ = False
+
+ CLOSE_STDIN: "Final" = NOTSET
+
+ class TimeoutExpired(Exception):
+ pass
+
+ def __init__(
+ self,
+ request: FixtureRequest,
+ tmp_path_factory: TempPathFactory,
+ monkeypatch: MonkeyPatch,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._request = request
+ self._mod_collections: WeakKeyDictionary[
+ Collector, List[Union[Item, Collector]]
+ ] = WeakKeyDictionary()
+ if request.function:
+ name: str = request.function.__name__
+ else:
+ name = request.node.name
+ self._name = name
+ self._path: Path = tmp_path_factory.mktemp(name, numbered=True)
+ #: A list of plugins to use with :py:meth:`parseconfig` and
+ #: :py:meth:`runpytest`. Initially this is an empty list but plugins can
+ #: be added to the list. The type of items to add to the list depends on
+ #: the method using them so refer to them for details.
+ self.plugins: List[Union[str, _PluggyPlugin]] = []
+ self._cwd_snapshot = CwdSnapshot()
+ self._sys_path_snapshot = SysPathsSnapshot()
+ self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
+ self.chdir()
+ self._request.addfinalizer(self._finalize)
+ self._method = self._request.config.getoption("--runpytest")
+ self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
+
+ self._monkeypatch = mp = monkeypatch
+ mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
+ # Ensure no unexpected caching via tox.
+ mp.delenv("TOX_ENV_DIR", raising=False)
+ # Discard outer pytest options.
+ mp.delenv("PYTEST_ADDOPTS", raising=False)
+ # Ensure no user config is used.
+ tmphome = str(self.path)
+ mp.setenv("HOME", tmphome)
+ mp.setenv("USERPROFILE", tmphome)
+ # Do not use colors for inner runs by default.
+ mp.setenv("PY_COLORS", "0")
+
+ @property
+ def path(self) -> Path:
+ """Temporary directory path used to create files/run tests from, etc."""
+ return self._path
+
+ def __repr__(self) -> str:
+ return f""
+
+ def _finalize(self) -> None:
+ """
+ Clean up global state artifacts.
+
+ Some methods modify the global interpreter state and this tries to
+ clean this up. It does not remove the temporary directory however so
+ it can be looked at after the test run has finished.
+ """
+ self._sys_modules_snapshot.restore()
+ self._sys_path_snapshot.restore()
+ self._cwd_snapshot.restore()
+
+ def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
+ # Some zope modules used by twisted-related tests keep internal state
+ # and can't be deleted; we had some trouble in the past with
+ # `zope.interface` for example.
+ #
+ # Preserve readline due to https://bugs.python.org/issue41033.
+ # pexpect issues a SIGWINCH.
+ def preserve_module(name):
+ return name.startswith(("zope", "readline"))
+
+ return SysModulesSnapshot(preserve=preserve_module)
+
+ def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
+ """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
+ pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
+ self._request.addfinalizer(reprec.finish_recording)
+ return reprec
+
+ def chdir(self) -> None:
+ """Cd into the temporary directory.
+
+ This is done automatically upon instantiation.
+ """
+ os.chdir(self.path)
+
+ def _makefile(
+ self,
+ ext: str,
+ lines: Sequence[Union[Any, bytes]],
+ files: Dict[str, str],
+ encoding: str = "utf-8",
+ ) -> Path:
+ items = list(files.items())
+
+ if ext and not ext.startswith("."):
+ raise ValueError(
+ f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
+ )
+
+ def to_text(s: Union[Any, bytes]) -> str:
+ return s.decode(encoding) if isinstance(s, bytes) else str(s)
+
+ if lines:
+ source = "\n".join(to_text(x) for x in lines)
+ basename = self._name
+ items.insert(0, (basename, source))
+
+ ret = None
+ for basename, value in items:
+ p = self.path.joinpath(basename).with_suffix(ext)
+ p.parent.mkdir(parents=True, exist_ok=True)
+ source_ = Source(value)
+ source = "\n".join(to_text(line) for line in source_.lines)
+ p.write_text(source.strip(), encoding=encoding)
+ if ret is None:
+ ret = p
+ assert ret is not None
+ return ret
+
+ def makefile(self, ext: str, *args: str, **kwargs: str) -> Path:
+ r"""Create new text file(s) in the test directory.
+
+ :param ext:
+ The extension the file(s) should use, including the dot, e.g. `.py`.
+ :param args:
+ All args are treated as strings and joined using newlines.
+ The result is written as contents to the file. The name of the
+ file is based on the test function requesting this fixture.
+ :param kwargs:
+ Each keyword is the name of a file, while the value of it will
+ be written as contents of the file.
+ :returns:
+ The first created file.
+
+ Examples:
+
+ .. code-block:: python
+
+ pytester.makefile(".txt", "line1", "line2")
+
+ pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n")
+
+ To create binary files, use :meth:`pathlib.Path.write_bytes` directly:
+
+ .. code-block:: python
+
+ filename = pytester.path.joinpath("foo.bin")
+ filename.write_bytes(b"...")
+ """
+ return self._makefile(ext, args, kwargs)
+
+ def makeconftest(self, source: str) -> Path:
+ """Write a contest.py file.
+
+ :param source: The contents.
+ :returns: The conftest.py file.
+ """
+ return self.makepyfile(conftest=source)
+
+ def makeini(self, source: str) -> Path:
+ """Write a tox.ini file.
+
+ :param source: The contents.
+ :returns: The tox.ini file.
+ """
+ return self.makefile(".ini", tox=source)
+
+ def getinicfg(self, source: str) -> SectionWrapper:
+ """Return the pytest section from the tox.ini config file."""
+ p = self.makeini(source)
+ return IniConfig(str(p))["pytest"]
+
+ def makepyprojecttoml(self, source: str) -> Path:
+ """Write a pyproject.toml file.
+
+ :param source: The contents.
+ :returns: The pyproject.ini file.
+
+ .. versionadded:: 6.0
+ """
+ return self.makefile(".toml", pyproject=source)
+
+ def makepyfile(self, *args, **kwargs) -> Path:
+ r"""Shortcut for .makefile() with a .py extension.
+
+ Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
+ existing files.
+
+ Examples:
+
+ .. code-block:: python
+
+ def test_something(pytester):
+ # Initial file is created test_something.py.
+ pytester.makepyfile("foobar")
+ # To create multiple files, pass kwargs accordingly.
+ pytester.makepyfile(custom="foobar")
+ # At this point, both 'test_something.py' & 'custom.py' exist in the test directory.
+
+ """
+ return self._makefile(".py", args, kwargs)
+
+ def maketxtfile(self, *args, **kwargs) -> Path:
+ r"""Shortcut for .makefile() with a .txt extension.
+
+ Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
+ existing files.
+
+ Examples:
+
+ .. code-block:: python
+
+ def test_something(pytester):
+ # Initial file is created test_something.txt.
+ pytester.maketxtfile("foobar")
+ # To create multiple files, pass kwargs accordingly.
+ pytester.maketxtfile(custom="foobar")
+ # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory.
+
+ """
+ return self._makefile(".txt", args, kwargs)
+
+ def syspathinsert(
+ self, path: Optional[Union[str, "os.PathLike[str]"]] = None
+ ) -> None:
+ """Prepend a directory to sys.path, defaults to :attr:`path`.
+
+ This is undone automatically when this object dies at the end of each
+ test.
+
+ :param path:
+ The path.
+ """
+ if path is None:
+ path = self.path
+
+ self._monkeypatch.syspath_prepend(str(path))
+
+ def mkdir(self, name: Union[str, "os.PathLike[str]"]) -> Path:
+ """Create a new (sub)directory.
+
+ :param name:
+ The name of the directory, relative to the pytester path.
+ :returns:
+ The created directory.
+ """
+ p = self.path / name
+ p.mkdir()
+ return p
+
+ def mkpydir(self, name: Union[str, "os.PathLike[str]"]) -> Path:
+ """Create a new python package.
+
+ This creates a (sub)directory with an empty ``__init__.py`` file so it
+ gets recognised as a Python package.
+ """
+ p = self.path / name
+ p.mkdir()
+ p.joinpath("__init__.py").touch()
+ return p
+
+ def copy_example(self, name: Optional[str] = None) -> Path:
+ """Copy file from project's directory into the testdir.
+
+ :param name:
+ The name of the file to copy.
+ :return:
+ Path to the copied directory (inside ``self.path``).
+ """
+ example_dir_ = self._request.config.getini("pytester_example_dir")
+ if example_dir_ is None:
+ raise ValueError("pytester_example_dir is unset, can't copy examples")
+ example_dir: Path = self._request.config.rootpath / example_dir_
+
+ for extra_element in self._request.node.iter_markers("pytester_example_path"):
+ assert extra_element.args
+ example_dir = example_dir.joinpath(*extra_element.args)
+
+ if name is None:
+ func_name = self._name
+ maybe_dir = example_dir / func_name
+ maybe_file = example_dir / (func_name + ".py")
+
+ if maybe_dir.is_dir():
+ example_path = maybe_dir
+ elif maybe_file.is_file():
+ example_path = maybe_file
+ else:
+ raise LookupError(
+ f"{func_name} can't be found as module or package in {example_dir}"
+ )
+ else:
+ example_path = example_dir.joinpath(name)
+
+ if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
+ copytree(example_path, self.path)
+ return self.path
+ elif example_path.is_file():
+ result = self.path.joinpath(example_path.name)
+ shutil.copy(example_path, result)
+ return result
+ else:
+ raise LookupError(
+ f'example "{example_path}" is not found as a file or directory'
+ )
+
+ def getnode(
+ self, config: Config, arg: Union[str, "os.PathLike[str]"]
+ ) -> Union[Collector, Item]:
+ """Get the collection node of a file.
+
+ :param config:
+ A pytest config.
+ See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
+ :param arg:
+ Path to the file.
+ :returns:
+ The node.
+ """
+ session = Session.from_config(config)
+ assert "::" not in str(arg)
+ p = Path(os.path.abspath(arg))
+ config.hook.pytest_sessionstart(session=session)
+ res = session.perform_collect([str(p)], genitems=False)[0]
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
+ return res
+
+ def getpathnode(
+ self, path: Union[str, "os.PathLike[str]"]
+ ) -> Union[Collector, Item]:
+ """Return the collection node of a file.
+
+ This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
+ create the (configured) pytest Config instance.
+
+ :param path:
+ Path to the file.
+ :returns:
+ The node.
+ """
+ path = Path(path)
+ config = self.parseconfigure(path)
+ session = Session.from_config(config)
+ x = bestrelpath(session.path, path)
+ config.hook.pytest_sessionstart(session=session)
+ res = session.perform_collect([x], genitems=False)[0]
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
+ return res
+
+ def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]:
+ """Generate all test items from a collection node.
+
+ This recurses into the collection node and returns a list of all the
+ test items contained within.
+
+ :param colitems:
+ The collection nodes.
+ :returns:
+ The collected items.
+ """
+ session = colitems[0].session
+ result: List[Item] = []
+ for colitem in colitems:
+ result.extend(session.genitems(colitem))
+ return result
+
+ def runitem(self, source: str) -> Any:
+ """Run the "test_func" Item.
+
+ The calling test instance (class containing the test method) must
+ provide a ``.getrunner()`` method which should return a runner which
+ can run the test protocol for a single item, e.g.
+ :py:func:`_pytest.runner.runtestprotocol`.
+ """
+ # used from runner functional tests
+ item = self.getitem(source)
+ # the test class where we are called from wants to provide the runner
+ testclassinstance = self._request.instance
+ runner = testclassinstance.getrunner()
+ return runner(item)
+
+ def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder:
+ """Run a test module in process using ``pytest.main()``.
+
+ This run writes "source" into a temporary file and runs
+ ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
+ for the result.
+
+ :param source: The source code of the test module.
+ :param cmdlineargs: Any extra command line arguments to use.
+ """
+ p = self.makepyfile(source)
+ values = list(cmdlineargs) + [p]
+ return self.inline_run(*values)
+
+ def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
+ """Run ``pytest.main(['--collect-only'])`` in-process.
+
+ Runs the :py:func:`pytest.main` function to run all of pytest inside
+ the test process itself like :py:meth:`inline_run`, but returns a
+ tuple of the collected items and a :py:class:`HookRecorder` instance.
+ """
+ rec = self.inline_run("--collect-only", *args)
+ items = [x.item for x in rec.getcalls("pytest_itemcollected")]
+ return items, rec
+
+ def inline_run(
+ self,
+ *args: Union[str, "os.PathLike[str]"],
+ plugins=(),
+ no_reraise_ctrlc: bool = False,
+ ) -> HookRecorder:
+ """Run ``pytest.main()`` in-process, returning a HookRecorder.
+
+ Runs the :py:func:`pytest.main` function to run all of pytest inside
+ the test process itself. This means it can return a
+ :py:class:`HookRecorder` instance which gives more detailed results
+ from that run than can be done by matching stdout/stderr from
+ :py:meth:`runpytest`.
+
+ :param args:
+ Command line arguments to pass to :py:func:`pytest.main`.
+ :param plugins:
+ Extra plugin instances the ``pytest.main()`` instance should use.
+ :param no_reraise_ctrlc:
+ Typically we reraise keyboard interrupts from the child run. If
+ True, the KeyboardInterrupt exception is captured.
+ """
+ # (maybe a cpython bug?) the importlib cache sometimes isn't updated
+ # properly between file creation and inline_run (especially if imports
+ # are interspersed with file creation)
+ importlib.invalidate_caches()
+
+ plugins = list(plugins)
+ finalizers = []
+ try:
+ # Any sys.module or sys.path changes done while running pytest
+ # inline should be reverted after the test run completes to avoid
+ # clashing with later inline tests run within the same pytest test,
+ # e.g. just because they use matching test module names.
+ finalizers.append(self.__take_sys_modules_snapshot().restore)
+ finalizers.append(SysPathsSnapshot().restore)
+
+ # Important note:
+ # - our tests should not leave any other references/registrations
+ # laying around other than possibly loaded test modules
+ # referenced from sys.modules, as nothing will clean those up
+ # automatically
+
+ rec = []
+
+ class Collect:
+ def pytest_configure(x, config: Config) -> None:
+ rec.append(self.make_hook_recorder(config.pluginmanager))
+
+ plugins.append(Collect())
+ ret = main([str(x) for x in args], plugins=plugins)
+ if len(rec) == 1:
+ reprec = rec.pop()
+ else:
+
+ class reprec: # type: ignore
+ pass
+
+ reprec.ret = ret
+
+ # Typically we reraise keyboard interrupts from the child run
+ # because it's our user requesting interruption of the testing.
+ if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
+ calls = reprec.getcalls("pytest_keyboard_interrupt")
+ if calls and calls[-1].excinfo.type == KeyboardInterrupt:
+ raise KeyboardInterrupt()
+ return reprec
+ finally:
+ for finalizer in finalizers:
+ finalizer()
+
+ def runpytest_inprocess(
+ self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
+ ) -> RunResult:
+ """Return result of running pytest in-process, providing a similar
+ interface to what self.runpytest() provides."""
+ syspathinsert = kwargs.pop("syspathinsert", False)
+
+ if syspathinsert:
+ self.syspathinsert()
+ now = timing.time()
+ capture = _get_multicapture("sys")
+ capture.start_capturing()
+ try:
+ try:
+ reprec = self.inline_run(*args, **kwargs)
+ except SystemExit as e:
+ ret = e.args[0]
+ try:
+ ret = ExitCode(e.args[0])
+ except ValueError:
+ pass
+
+ class reprec: # type: ignore
+ ret = ret
+
+ except Exception:
+ traceback.print_exc()
+
+ class reprec: # type: ignore
+ ret = ExitCode(3)
+
+ finally:
+ out, err = capture.readouterr()
+ capture.stop_capturing()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+ assert reprec.ret is not None
+ res = RunResult(
+ reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now
+ )
+ res.reprec = reprec # type: ignore
+ return res
+
+ def runpytest(
+ self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
+ ) -> RunResult:
+ """Run pytest inline or in a subprocess, depending on the command line
+ option "--runpytest" and return a :py:class:`~pytest.RunResult`."""
+ new_args = self._ensure_basetemp(args)
+ if self._method == "inprocess":
+ return self.runpytest_inprocess(*new_args, **kwargs)
+ elif self._method == "subprocess":
+ return self.runpytest_subprocess(*new_args, **kwargs)
+ raise RuntimeError(f"Unrecognized runpytest option: {self._method}")
+
+ def _ensure_basetemp(
+ self, args: Sequence[Union[str, "os.PathLike[str]"]]
+ ) -> List[Union[str, "os.PathLike[str]"]]:
+ new_args = list(args)
+ for x in new_args:
+ if str(x).startswith("--basetemp"):
+ break
+ else:
+ new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp"))
+ return new_args
+
+ def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
+ """Return a new pytest :class:`pytest.Config` instance from given
+ commandline args.
+
+ This invokes the pytest bootstrapping code in _pytest.config to create a
+ new :py:class:`pytest.PytestPluginManager` and call the
+ :hook:`pytest_cmdline_parse` hook to create a new :class:`pytest.Config`
+ instance.
+
+ If :attr:`plugins` has been populated they should be plugin modules
+ to be registered with the plugin manager.
+ """
+ import _pytest.config
+
+ new_args = self._ensure_basetemp(args)
+ new_args = [str(x) for x in new_args]
+
+ config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type]
+ # we don't know what the test will do with this half-setup config
+ # object and thus we make sure it gets unconfigured properly in any
+ # case (otherwise capturing could still be active, for example)
+ self._request.addfinalizer(config._ensure_unconfigure)
+ return config
+
+ def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
+ """Return a new pytest configured Config instance.
+
+ Returns a new :py:class:`pytest.Config` instance like
+ :py:meth:`parseconfig`, but also calls the :hook:`pytest_configure`
+ hook.
+ """
+ config = self.parseconfig(*args)
+ config._do_configure()
+ return config
+
+ def getitem(
+ self, source: Union[str, "os.PathLike[str]"], funcname: str = "test_func"
+ ) -> Item:
+ """Return the test item for a test function.
+
+ Writes the source to a python file and runs pytest's collection on
+ the resulting module, returning the test item for the requested
+ function name.
+
+ :param source:
+ The module source.
+ :param funcname:
+ The name of the test function for which to return a test item.
+ :returns:
+ The test item.
+ """
+ items = self.getitems(source)
+ for item in items:
+ if item.name == funcname:
+ return item
+ assert 0, "{!r} item not found in module:\n{}\nitems: {}".format(
+ funcname, source, items
+ )
+
+ def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]:
+ """Return all test items collected from the module.
+
+ Writes the source to a Python file and runs pytest's collection on
+ the resulting module, returning all test items contained within.
+ """
+ modcol = self.getmodulecol(source)
+ return self.genitems([modcol])
+
+ def getmodulecol(
+ self,
+ source: Union[str, "os.PathLike[str]"],
+ configargs=(),
+ *,
+ withinit: bool = False,
+ ):
+ """Return the module collection node for ``source``.
+
+ Writes ``source`` to a file using :py:meth:`makepyfile` and then
+ runs the pytest collection on it, returning the collection node for the
+ test module.
+
+ :param source:
+ The source code of the module to collect.
+
+ :param configargs:
+ Any extra arguments to pass to :py:meth:`parseconfigure`.
+
+ :param withinit:
+ Whether to also write an ``__init__.py`` file to the same
+ directory to ensure it is a package.
+ """
+ if isinstance(source, os.PathLike):
+ path = self.path.joinpath(source)
+ assert not withinit, "not supported for paths"
+ else:
+ kw = {self._name: str(source)}
+ path = self.makepyfile(**kw)
+ if withinit:
+ self.makepyfile(__init__="#")
+ self.config = config = self.parseconfigure(path, *configargs)
+ return self.getnode(config, path)
+
+ def collect_by_name(
+ self, modcol: Collector, name: str
+ ) -> Optional[Union[Item, Collector]]:
+ """Return the collection node for name from the module collection.
+
+ Searches a module collection node for a collection node matching the
+ given name.
+
+ :param modcol: A module collection node; see :py:meth:`getmodulecol`.
+ :param name: The name of the node to return.
+ """
+ if modcol not in self._mod_collections:
+ self._mod_collections[modcol] = list(modcol.collect())
+ for colitem in self._mod_collections[modcol]:
+ if colitem.name == name:
+ return colitem
+ return None
+
+ def popen(
+ self,
+ cmdargs: Sequence[Union[str, "os.PathLike[str]"]],
+ stdout: Union[int, TextIO] = subprocess.PIPE,
+ stderr: Union[int, TextIO] = subprocess.PIPE,
+ stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN,
+ **kw,
+ ):
+ """Invoke :py:class:`subprocess.Popen`.
+
+ Calls :py:class:`subprocess.Popen` making sure the current working
+ directory is in ``PYTHONPATH``.
+
+ You probably want to use :py:meth:`run` instead.
+ """
+ env = os.environ.copy()
+ env["PYTHONPATH"] = os.pathsep.join(
+ filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
+ )
+ kw["env"] = env
+
+ if stdin is self.CLOSE_STDIN:
+ kw["stdin"] = subprocess.PIPE
+ elif isinstance(stdin, bytes):
+ kw["stdin"] = subprocess.PIPE
+ else:
+ kw["stdin"] = stdin
+
+ popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
+ if stdin is self.CLOSE_STDIN:
+ assert popen.stdin is not None
+ popen.stdin.close()
+ elif isinstance(stdin, bytes):
+ assert popen.stdin is not None
+ popen.stdin.write(stdin)
+
+ return popen
+
+ def run(
+ self,
+ *cmdargs: Union[str, "os.PathLike[str]"],
+ timeout: Optional[float] = None,
+ stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN,
+ ) -> RunResult:
+ """Run a command with arguments.
+
+ Run a process using :py:class:`subprocess.Popen` saving the stdout and
+ stderr.
+
+ :param cmdargs:
+ The sequence of arguments to pass to :py:class:`subprocess.Popen`,
+ with path-like objects being converted to :py:class:`str`
+ automatically.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Pytester.TimeoutExpired`.
+ :param stdin:
+ Optional standard input.
+
+ - If it is :py:attr:`CLOSE_STDIN` (Default), then this method calls
+ :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and
+ the standard input is closed immediately after the new command is
+ started.
+
+ - If it is of type :py:class:`bytes`, these bytes are sent to the
+ standard input of the command.
+
+ - Otherwise, it is passed through to :py:class:`subprocess.Popen`.
+ For further information in this case, consult the document of the
+ ``stdin`` parameter in :py:class:`subprocess.Popen`.
+ :returns:
+ The result.
+ """
+ __tracebackhide__ = True
+
+ cmdargs = tuple(os.fspath(arg) for arg in cmdargs)
+ p1 = self.path.joinpath("stdout")
+ p2 = self.path.joinpath("stderr")
+ print("running:", *cmdargs)
+ print(" in:", Path.cwd())
+
+ with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2:
+ now = timing.time()
+ popen = self.popen(
+ cmdargs,
+ stdin=stdin,
+ stdout=f1,
+ stderr=f2,
+ close_fds=(sys.platform != "win32"),
+ )
+ if popen.stdin is not None:
+ popen.stdin.close()
+
+ def handle_timeout() -> None:
+ __tracebackhide__ = True
+
+ timeout_message = (
+ "{seconds} second timeout expired running:"
+ " {command}".format(seconds=timeout, command=cmdargs)
+ )
+
+ popen.kill()
+ popen.wait()
+ raise self.TimeoutExpired(timeout_message)
+
+ if timeout is None:
+ ret = popen.wait()
+ else:
+ try:
+ ret = popen.wait(timeout)
+ except subprocess.TimeoutExpired:
+ handle_timeout()
+
+ with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2:
+ out = f1.read().splitlines()
+ err = f2.read().splitlines()
+
+ self._dump_lines(out, sys.stdout)
+ self._dump_lines(err, sys.stderr)
+
+ with contextlib.suppress(ValueError):
+ ret = ExitCode(ret)
+ return RunResult(ret, out, err, timing.time() - now)
+
+ def _dump_lines(self, lines, fp):
+ try:
+ for line in lines:
+ print(line, file=fp)
+ except UnicodeEncodeError:
+ print(f"couldn't print to {fp} because of encoding")
+
+ def _getpytestargs(self) -> Tuple[str, ...]:
+ return sys.executable, "-mpytest"
+
+ def runpython(self, script: "os.PathLike[str]") -> RunResult:
+ """Run a python script using sys.executable as interpreter."""
+ return self.run(sys.executable, script)
+
+ def runpython_c(self, command: str) -> RunResult:
+ """Run ``python -c "command"``."""
+ return self.run(sys.executable, "-c", command)
+
+ def runpytest_subprocess(
+ self, *args: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None
+ ) -> RunResult:
+ """Run pytest as a subprocess with given arguments.
+
+ Any plugins added to the :py:attr:`plugins` list will be added using the
+ ``-p`` command line option. Additionally ``--basetemp`` is used to put
+ any temporary files and directories in a numbered directory prefixed
+ with "runpytest-" to not conflict with the normal numbered pytest
+ location for temporary files and directories.
+
+ :param args:
+ The sequence of arguments to pass to the pytest subprocess.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Pytester.TimeoutExpired`.
+ :returns:
+ The result.
+ """
+ __tracebackhide__ = True
+ p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
+ args = ("--basetemp=%s" % p,) + args
+ plugins = [x for x in self.plugins if isinstance(x, str)]
+ if plugins:
+ args = ("-p", plugins[0]) + args
+ args = self._getpytestargs() + args
+ return self.run(*args, timeout=timeout)
+
+ def spawn_pytest(
+ self, string: str, expect_timeout: float = 10.0
+ ) -> "pexpect.spawn":
+ """Run pytest using pexpect.
+
+ This makes sure to use the right pytest and sets up the temporary
+ directory locations.
+
+ The pexpect child is returned.
+ """
+ basetemp = self.path / "temp-pexpect"
+ basetemp.mkdir(mode=0o700)
+ invoke = " ".join(map(str, self._getpytestargs()))
+ cmd = f"{invoke} --basetemp={basetemp} {string}"
+ return self.spawn(cmd, expect_timeout=expect_timeout)
+
+ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
+ """Run a command using pexpect.
+
+ The pexpect child is returned.
+ """
+ pexpect = importorskip("pexpect", "3.0")
+ if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
+ skip("pypy-64 bit not supported")
+ if not hasattr(pexpect, "spawn"):
+ skip("pexpect.spawn not available")
+ logfile = self.path.joinpath("spawn.out").open("wb")
+
+ child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout)
+ self._request.addfinalizer(logfile.close)
+ return child
+
+
+class LineComp:
+ def __init__(self) -> None:
+ self.stringio = StringIO()
+ """:class:`python:io.StringIO()` instance used for input."""
+
+ def assert_contains_lines(self, lines2: Sequence[str]) -> None:
+ """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
+
+ Lines are matched using :func:`LineMatcher.fnmatch_lines `.
+ """
+ __tracebackhide__ = True
+ val = self.stringio.getvalue()
+ self.stringio.truncate(0)
+ self.stringio.seek(0)
+ lines1 = val.split("\n")
+ LineMatcher(lines1).fnmatch_lines(lines2)
+
+
+class LineMatcher:
+ """Flexible matching of text.
+
+ This is a convenience class to test large texts like the output of
+ commands.
+
+ The constructor takes a list of lines without their trailing newlines, i.e.
+ ``text.splitlines()``.
+ """
+
+ def __init__(self, lines: List[str]) -> None:
+ self.lines = lines
+ self._log_output: List[str] = []
+
+ def __str__(self) -> str:
+ """Return the entire original text.
+
+ .. versionadded:: 6.2
+ You can use :meth:`str` in older versions.
+ """
+ return "\n".join(self.lines)
+
+ def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]:
+ if isinstance(lines2, str):
+ lines2 = Source(lines2)
+ if isinstance(lines2, Source):
+ lines2 = lines2.strip().lines
+ return lines2
+
+ def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
+ """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`)."""
+ __tracebackhide__ = True
+ self._match_lines_random(lines2, fnmatch)
+
+ def re_match_lines_random(self, lines2: Sequence[str]) -> None:
+ """Check lines exist in the output in any order (using :func:`python:re.match`)."""
+ __tracebackhide__ = True
+ self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
+
+ def _match_lines_random(
+ self, lines2: Sequence[str], match_func: Callable[[str, str], bool]
+ ) -> None:
+ __tracebackhide__ = True
+ lines2 = self._getlines(lines2)
+ for line in lines2:
+ for x in self.lines:
+ if line == x or match_func(x, line):
+ self._log("matched: ", repr(line))
+ break
+ else:
+ msg = "line %r not found in output" % line
+ self._log(msg)
+ self._fail(msg)
+
+ def get_lines_after(self, fnline: str) -> Sequence[str]:
+ """Return all lines following the given line in the text.
+
+ The given line can contain glob wildcards.
+ """
+ for i, line in enumerate(self.lines):
+ if fnline == line or fnmatch(line, fnline):
+ return self.lines[i + 1 :]
+ raise ValueError("line %r not found in output" % fnline)
+
+ def _log(self, *args) -> None:
+ self._log_output.append(" ".join(str(x) for x in args))
+
+ @property
+ def _log_text(self) -> str:
+ return "\n".join(self._log_output)
+
+ def fnmatch_lines(
+ self, lines2: Sequence[str], *, consecutive: bool = False
+ ) -> None:
+ """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).
+
+ The argument is a list of lines which have to match and can use glob
+ wildcards. If they do not match a pytest.fail() is called. The
+ matches and non-matches are also shown as part of the error message.
+
+ :param lines2: String patterns to match.
+ :param consecutive: Match lines consecutively?
+ """
+ __tracebackhide__ = True
+ self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
+
+ def re_match_lines(
+ self, lines2: Sequence[str], *, consecutive: bool = False
+ ) -> None:
+ """Check lines exist in the output (using :func:`python:re.match`).
+
+ The argument is a list of lines which have to match using ``re.match``.
+ If they do not match a pytest.fail() is called.
+
+ The matches and non-matches are also shown as part of the error message.
+
+ :param lines2: string patterns to match.
+ :param consecutive: match lines consecutively?
+ """
+ __tracebackhide__ = True
+ self._match_lines(
+ lines2,
+ lambda name, pat: bool(re.match(pat, name)),
+ "re.match",
+ consecutive=consecutive,
+ )
+
+ def _match_lines(
+ self,
+ lines2: Sequence[str],
+ match_func: Callable[[str, str], bool],
+ match_nickname: str,
+ *,
+ consecutive: bool = False,
+ ) -> None:
+ """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
+
+ :param Sequence[str] lines2:
+ List of string patterns to match. The actual format depends on
+ ``match_func``.
+ :param match_func:
+ A callable ``match_func(line, pattern)`` where line is the
+ captured line from stdout/stderr and pattern is the matching
+ pattern.
+ :param str match_nickname:
+ The nickname for the match function that will be logged to stdout
+ when a match occurs.
+ :param consecutive:
+ Match lines consecutively?
+ """
+ if not isinstance(lines2, collections.abc.Sequence):
+ raise TypeError(f"invalid type for lines2: {type(lines2).__name__}")
+ lines2 = self._getlines(lines2)
+ lines1 = self.lines[:]
+ extralines = []
+ __tracebackhide__ = True
+ wnick = len(match_nickname) + 1
+ started = False
+ for line in lines2:
+ nomatchprinted = False
+ while lines1:
+ nextline = lines1.pop(0)
+ if line == nextline:
+ self._log("exact match:", repr(line))
+ started = True
+ break
+ elif match_func(nextline, line):
+ self._log("%s:" % match_nickname, repr(line))
+ self._log(
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
+ )
+ started = True
+ break
+ else:
+ if consecutive and started:
+ msg = f"no consecutive match: {line!r}"
+ self._log(msg)
+ self._log(
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
+ )
+ self._fail(msg)
+ if not nomatchprinted:
+ self._log(
+ "{:>{width}}".format("nomatch:", width=wnick), repr(line)
+ )
+ nomatchprinted = True
+ self._log("{:>{width}}".format("and:", width=wnick), repr(nextline))
+ extralines.append(nextline)
+ else:
+ msg = f"remains unmatched: {line!r}"
+ self._log(msg)
+ self._fail(msg)
+ self._log_output = []
+
+ def no_fnmatch_line(self, pat: str) -> None:
+ """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
+
+ :param str pat: The pattern to match lines.
+ """
+ __tracebackhide__ = True
+ self._no_match_line(pat, fnmatch, "fnmatch")
+
+ def no_re_match_line(self, pat: str) -> None:
+ """Ensure captured lines do not match the given pattern, using ``re.match``.
+
+ :param str pat: The regular expression to match lines.
+ """
+ __tracebackhide__ = True
+ self._no_match_line(
+ pat, lambda name, pat: bool(re.match(pat, name)), "re.match"
+ )
+
+ def _no_match_line(
+ self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
+ ) -> None:
+ """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``.
+
+ :param str pat: The pattern to match lines.
+ """
+ __tracebackhide__ = True
+ nomatch_printed = False
+ wnick = len(match_nickname) + 1
+ for line in self.lines:
+ if match_func(line, pat):
+ msg = f"{match_nickname}: {pat!r}"
+ self._log(msg)
+ self._log("{:>{width}}".format("with:", width=wnick), repr(line))
+ self._fail(msg)
+ else:
+ if not nomatch_printed:
+ self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat))
+ nomatch_printed = True
+ self._log("{:>{width}}".format("and:", width=wnick), repr(line))
+ self._log_output = []
+
+ def _fail(self, msg: str) -> None:
+ __tracebackhide__ = True
+ log_text = self._log_text
+ self._log_output = []
+ fail(log_text)
+
+ def str(self) -> str:
+ """Return the entire original text."""
+ return str(self)
diff --git a/venv/lib/python3.12/site-packages/_pytest/pytester_assertions.py b/venv/lib/python3.12/site-packages/_pytest/pytester_assertions.py
new file mode 100644
index 0000000..657e4db
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/pytester_assertions.py
@@ -0,0 +1,75 @@
+"""Helper plugin for pytester; should not be loaded on its own."""
+# This plugin contains assertions used by pytester. pytester cannot
+# contain them itself, since it is imported by the `pytest` module,
+# hence cannot be subject to assertion rewriting, which requires a
+# module to not be already imported.
+from typing import Dict
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Union
+
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+
+
+def assertoutcome(
+ outcomes: Tuple[
+ Sequence[TestReport],
+ Sequence[Union[CollectReport, TestReport]],
+ Sequence[Union[CollectReport, TestReport]],
+ ],
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+) -> None:
+ __tracebackhide__ = True
+
+ realpassed, realskipped, realfailed = outcomes
+ obtained = {
+ "passed": len(realpassed),
+ "skipped": len(realskipped),
+ "failed": len(realfailed),
+ }
+ expected = {"passed": passed, "skipped": skipped, "failed": failed}
+ assert obtained == expected, outcomes
+
+
+def assert_outcomes(
+ outcomes: Dict[str, int],
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+ errors: int = 0,
+ xpassed: int = 0,
+ xfailed: int = 0,
+ warnings: Optional[int] = None,
+ deselected: Optional[int] = None,
+) -> None:
+ """Assert that the specified outcomes appear with the respective
+ numbers (0 means it didn't occur) in the text output from a test run."""
+ __tracebackhide__ = True
+
+ obtained = {
+ "passed": outcomes.get("passed", 0),
+ "skipped": outcomes.get("skipped", 0),
+ "failed": outcomes.get("failed", 0),
+ "errors": outcomes.get("errors", 0),
+ "xpassed": outcomes.get("xpassed", 0),
+ "xfailed": outcomes.get("xfailed", 0),
+ }
+ expected = {
+ "passed": passed,
+ "skipped": skipped,
+ "failed": failed,
+ "errors": errors,
+ "xpassed": xpassed,
+ "xfailed": xfailed,
+ }
+ if warnings is not None:
+ obtained["warnings"] = outcomes.get("warnings", 0)
+ expected["warnings"] = warnings
+ if deselected is not None:
+ obtained["deselected"] = outcomes.get("deselected", 0)
+ expected["deselected"] = deselected
+ assert obtained == expected
diff --git a/venv/lib/python3.12/site-packages/_pytest/python.py b/venv/lib/python3.12/site-packages/_pytest/python.py
new file mode 100644
index 0000000..5f8be5d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/python.py
@@ -0,0 +1,1843 @@
+"""Python test discovery, setup and run of test functions."""
+import dataclasses
+import enum
+import fnmatch
+import inspect
+import itertools
+import os
+import sys
+import types
+import warnings
+from collections import Counter
+from collections import defaultdict
+from functools import partial
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Generator
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Pattern
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+import _pytest
+from _pytest import fixtures
+from _pytest import nodes
+from _pytest._code import filter_traceback
+from _pytest._code import getfslineno
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest._code.code import Traceback
+from _pytest._io import TerminalWriter
+from _pytest._io.saferepr import saferepr
+from _pytest.compat import ascii_escaped
+from _pytest.compat import assert_never
+from _pytest.compat import final
+from _pytest.compat import get_default_arg_names
+from _pytest.compat import get_real_func
+from _pytest.compat import getimfunc
+from _pytest.compat import getlocation
+from _pytest.compat import is_async_function
+from _pytest.compat import is_generator
+from _pytest.compat import LEGACY_PATH
+from _pytest.compat import NOTSET
+from _pytest.compat import safe_getattr
+from _pytest.compat import safe_isclass
+from _pytest.compat import STRING_TYPES
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.deprecated import INSTANCE_COLLECTOR
+from _pytest.deprecated import NOSE_SUPPORT_METHOD
+from _pytest.fixtures import FuncFixtureInfo
+from _pytest.main import Session
+from _pytest.mark import MARK_GEN
+from _pytest.mark import ParameterSet
+from _pytest.mark.structures import get_unpacked_marks
+from _pytest.mark.structures import Mark
+from _pytest.mark.structures import MarkDecorator
+from _pytest.mark.structures import normalize_mark_list
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import fnmatch_ex
+from _pytest.pathlib import import_path
+from _pytest.pathlib import ImportPathMismatchError
+from _pytest.pathlib import parts
+from _pytest.pathlib import visit
+from _pytest.scope import Scope
+from _pytest.warning_types import PytestCollectionWarning
+from _pytest.warning_types import PytestReturnNotNoneWarning
+from _pytest.warning_types import PytestUnhandledCoroutineWarning
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from _pytest.scope import _ScopeName
+
+
+_PYTEST_DIR = Path(_pytest.__file__).parent
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--fixtures",
+ "--funcargs",
+ action="store_true",
+ dest="showfixtures",
+ default=False,
+ help="Show available fixtures, sorted by plugin appearance "
+ "(fixtures with leading '_' are only shown with '-v')",
+ )
+ group.addoption(
+ "--fixtures-per-test",
+ action="store_true",
+ dest="show_fixtures_per_test",
+ default=False,
+ help="Show fixtures per test",
+ )
+ parser.addini(
+ "python_files",
+ type="args",
+ # NOTE: default is also used in AssertionRewritingHook.
+ default=["test_*.py", "*_test.py"],
+ help="Glob-style file patterns for Python test module discovery",
+ )
+ parser.addini(
+ "python_classes",
+ type="args",
+ default=["Test"],
+ help="Prefixes or glob names for Python test class discovery",
+ )
+ parser.addini(
+ "python_functions",
+ type="args",
+ default=["test"],
+ help="Prefixes or glob names for Python test function and method discovery",
+ )
+ parser.addini(
+ "disable_test_id_escaping_and_forfeit_all_rights_to_community_support",
+ type="bool",
+ default=False,
+ help="Disable string escape non-ASCII characters, might cause unwanted "
+ "side effects(use at your own risk)",
+ )
+
+
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.showfixtures:
+ showfixtures(config)
+ return 0
+ if config.option.show_fixtures_per_test:
+ show_fixtures_per_test(config)
+ return 0
+ return None
+
+
+def pytest_generate_tests(metafunc: "Metafunc") -> None:
+ for marker in metafunc.definition.iter_markers(name="parametrize"):
+ metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
+
+
+def pytest_configure(config: Config) -> None:
+ config.addinivalue_line(
+ "markers",
+ "parametrize(argnames, argvalues): call a test function multiple "
+ "times passing in different arguments in turn. argvalues generally "
+ "needs to be a list of values if argnames specifies only one name "
+ "or a list of tuples of values if argnames specifies multiple names. "
+ "Example: @parametrize('arg1', [1,2]) would lead to two calls of the "
+ "decorated test function, one with arg1=1 and another with arg1=2."
+ "see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info "
+ "and examples.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "usefixtures(fixturename1, fixturename2, ...): mark tests as needing "
+ "all of the specified fixtures. see "
+ "https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures ",
+ )
+
+
+def async_warn_and_skip(nodeid: str) -> None:
+ msg = "async def functions are not natively supported and have been skipped.\n"
+ msg += (
+ "You need to install a suitable plugin for your async framework, for example:\n"
+ )
+ msg += " - anyio\n"
+ msg += " - pytest-asyncio\n"
+ msg += " - pytest-tornasync\n"
+ msg += " - pytest-trio\n"
+ msg += " - pytest-twisted"
+ warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))
+ skip(reason="async def function and no async plugin installed (see warnings)")
+
+
+@hookimpl(trylast=True)
+def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
+ testfunction = pyfuncitem.obj
+ if is_async_function(testfunction):
+ async_warn_and_skip(pyfuncitem.nodeid)
+ funcargs = pyfuncitem.funcargs
+ testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
+ result = testfunction(**testargs)
+ if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
+ async_warn_and_skip(pyfuncitem.nodeid)
+ elif result is not None:
+ warnings.warn(
+ PytestReturnNotNoneWarning(
+ f"Expected None, but {pyfuncitem.nodeid} returned {result!r}, which will be an error in a "
+ "future version of pytest. Did you mean to use `assert` instead of `return`?"
+ )
+ )
+ return True
+
+
+def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]:
+ if file_path.suffix == ".py":
+ if not parent.session.isinitpath(file_path):
+ if not path_matches_patterns(
+ file_path, parent.config.getini("python_files") + ["__init__.py"]
+ ):
+ return None
+ ihook = parent.session.gethookproxy(file_path)
+ module: Module = ihook.pytest_pycollect_makemodule(
+ module_path=file_path, parent=parent
+ )
+ return module
+ return None
+
+
+def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
+ """Return whether path matches any of the patterns in the list of globs given."""
+ return any(fnmatch_ex(pattern, path) for pattern in patterns)
+
+
+def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
+ if module_path.name == "__init__.py":
+ pkg: Package = Package.from_parent(parent, path=module_path)
+ return pkg
+ mod: Module = Module.from_parent(parent, path=module_path)
+ return mod
+
+
+@hookimpl(trylast=True)
+def pytest_pycollect_makeitem(
+ collector: Union["Module", "Class"], name: str, obj: object
+) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]:
+ assert isinstance(collector, (Class, Module)), type(collector)
+ # Nothing was collected elsewhere, let's do it here.
+ if safe_isclass(obj):
+ if collector.istestclass(obj, name):
+ klass: Class = Class.from_parent(collector, name=name, obj=obj)
+ return klass
+ elif collector.istestfunction(obj, name):
+ # mock seems to store unbound methods (issue473), normalize it.
+ obj = getattr(obj, "__func__", obj)
+ # We need to try and unwrap the function if it's a functools.partial
+ # or a functools.wrapped.
+ # We mustn't if it's been wrapped with mock.patch (python 2 only).
+ if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
+ filename, lineno = getfslineno(obj)
+ warnings.warn_explicit(
+ message=PytestCollectionWarning(
+ "cannot collect %r because it is not a function." % name
+ ),
+ category=None,
+ filename=str(filename),
+ lineno=lineno + 1,
+ )
+ elif getattr(obj, "__test__", True):
+ if is_generator(obj):
+ res: Function = Function.from_parent(collector, name=name)
+ reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format(
+ name=name
+ )
+ res.add_marker(MARK_GEN.xfail(run=False, reason=reason))
+ res.warn(PytestCollectionWarning(reason))
+ return res
+ else:
+ return list(collector._genfunctions(name, obj))
+ return None
+
+
+class PyobjMixin(nodes.Node):
+ """this mix-in inherits from Node to carry over the typing information
+
+ as its intended to always mix in before a node
+ its position in the mro is unaffected"""
+
+ _ALLOW_MARKERS = True
+
+ @property
+ def module(self):
+ """Python module object this node was collected from (can be None)."""
+ node = self.getparent(Module)
+ return node.obj if node is not None else None
+
+ @property
+ def cls(self):
+ """Python class object this node was collected from (can be None)."""
+ node = self.getparent(Class)
+ return node.obj if node is not None else None
+
+ @property
+ def instance(self):
+ """Python instance object the function is bound to.
+
+ Returns None if not a test method, e.g. for a standalone test function,
+ a staticmethod, a class or a module.
+ """
+ node = self.getparent(Function)
+ return getattr(node.obj, "__self__", None) if node is not None else None
+
+ @property
+ def obj(self):
+ """Underlying Python object."""
+ obj = getattr(self, "_obj", None)
+ if obj is None:
+ self._obj = obj = self._getobj()
+ # XXX evil hack
+ # used to avoid Function marker duplication
+ if self._ALLOW_MARKERS:
+ self.own_markers.extend(get_unpacked_marks(self.obj))
+ # This assumes that `obj` is called before there is a chance
+ # to add custom keys to `self.keywords`, so no fear of overriding.
+ self.keywords.update((mark.name, mark) for mark in self.own_markers)
+ return obj
+
+ @obj.setter
+ def obj(self, value):
+ self._obj = value
+
+ def _getobj(self):
+ """Get the underlying Python object. May be overwritten by subclasses."""
+ # TODO: Improve the type of `parent` such that assert/ignore aren't needed.
+ assert self.parent is not None
+ obj = self.parent.obj # type: ignore[attr-defined]
+ return getattr(obj, self.name)
+
+ def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
+ """Return Python path relative to the containing module."""
+ chain = self.listchain()
+ chain.reverse()
+ parts = []
+ for node in chain:
+ name = node.name
+ if isinstance(node, Module):
+ name = os.path.splitext(name)[0]
+ if stopatmodule:
+ if includemodule:
+ parts.append(name)
+ break
+ parts.append(name)
+ parts.reverse()
+ return ".".join(parts)
+
+ def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
+ # XXX caching?
+ obj = self.obj
+ compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
+ if isinstance(compat_co_firstlineno, int):
+ # nose compatibility
+ file_path = sys.modules[obj.__module__].__file__
+ assert file_path is not None
+ if file_path.endswith(".pyc"):
+ file_path = file_path[:-1]
+ path: Union["os.PathLike[str]", str] = file_path
+ lineno = compat_co_firstlineno
+ else:
+ path, lineno = getfslineno(obj)
+ modpath = self.getmodpath()
+ assert isinstance(lineno, int)
+ return path, lineno, modpath
+
+
+# As an optimization, these builtin attribute names are pre-ignored when
+# iterating over an object during collection -- the pytest_pycollect_makeitem
+# hook is not called for them.
+# fmt: off
+class _EmptyClass: pass # noqa: E701
+IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
+ frozenset(),
+ # Module.
+ dir(types.ModuleType("empty_module")),
+ # Some extra module attributes the above doesn't catch.
+ {"__builtins__", "__file__", "__cached__"},
+ # Class.
+ dir(_EmptyClass),
+ # Instance.
+ dir(_EmptyClass()),
+)
+del _EmptyClass
+# fmt: on
+
+
+class PyCollector(PyobjMixin, nodes.Collector):
+ def funcnamefilter(self, name: str) -> bool:
+ return self._matches_prefix_or_glob_option("python_functions", name)
+
+ def isnosetest(self, obj: object) -> bool:
+ """Look for the __test__ attribute, which is applied by the
+ @nose.tools.istest decorator.
+ """
+ # We explicitly check for "is True" here to not mistakenly treat
+ # classes with a custom __getattr__ returning something truthy (like a
+ # function) as test classes.
+ return safe_getattr(obj, "__test__", False) is True
+
+ def classnamefilter(self, name: str) -> bool:
+ return self._matches_prefix_or_glob_option("python_classes", name)
+
+ def istestfunction(self, obj: object, name: str) -> bool:
+ if self.funcnamefilter(name) or self.isnosetest(obj):
+ if isinstance(obj, (staticmethod, classmethod)):
+ # staticmethods and classmethods need to be unwrapped.
+ obj = safe_getattr(obj, "__func__", False)
+ return callable(obj) and fixtures.getfixturemarker(obj) is None
+ else:
+ return False
+
+ def istestclass(self, obj: object, name: str) -> bool:
+ return self.classnamefilter(name) or self.isnosetest(obj)
+
+ def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
+ """Check if the given name matches the prefix or glob-pattern defined
+ in ini configuration."""
+ for option in self.config.getini(option_name):
+ if name.startswith(option):
+ return True
+ # Check that name looks like a glob-string before calling fnmatch
+ # because this is called for every name in each collected module,
+ # and fnmatch is somewhat expensive to call.
+ elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
+ name, option
+ ):
+ return True
+ return False
+
+ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
+ if not getattr(self.obj, "__test__", True):
+ return []
+
+ # Avoid random getattrs and peek in the __dict__ instead.
+ dicts = [getattr(self.obj, "__dict__", {})]
+ if isinstance(self.obj, type):
+ for basecls in self.obj.__mro__:
+ dicts.append(basecls.__dict__)
+
+ # In each class, nodes should be definition ordered.
+ # __dict__ is definition ordered.
+ seen: Set[str] = set()
+ dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
+ ihook = self.ihook
+ for dic in dicts:
+ values: List[Union[nodes.Item, nodes.Collector]] = []
+ # Note: seems like the dict can change during iteration -
+ # be careful not to remove the list() without consideration.
+ for name, obj in list(dic.items()):
+ if name in IGNORED_ATTRIBUTES:
+ continue
+ if name in seen:
+ continue
+ seen.add(name)
+ res = ihook.pytest_pycollect_makeitem(
+ collector=self, name=name, obj=obj
+ )
+ if res is None:
+ continue
+ elif isinstance(res, list):
+ values.extend(res)
+ else:
+ values.append(res)
+ dict_values.append(values)
+
+ # Between classes in the class hierarchy, reverse-MRO order -- nodes
+ # inherited from base classes should come before subclasses.
+ result = []
+ for values in reversed(dict_values):
+ result.extend(values)
+ return result
+
+ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
+ modulecol = self.getparent(Module)
+ assert modulecol is not None
+ module = modulecol.obj
+ clscol = self.getparent(Class)
+ cls = clscol and clscol.obj or None
+
+ definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
+ fixtureinfo = definition._fixtureinfo
+
+ # pytest_generate_tests impls call metafunc.parametrize() which fills
+ # metafunc._calls, the outcome of the hook.
+ metafunc = Metafunc(
+ definition=definition,
+ fixtureinfo=fixtureinfo,
+ config=self.config,
+ cls=cls,
+ module=module,
+ _ispytest=True,
+ )
+ methods = []
+ if hasattr(module, "pytest_generate_tests"):
+ methods.append(module.pytest_generate_tests)
+ if cls is not None and hasattr(cls, "pytest_generate_tests"):
+ methods.append(cls().pytest_generate_tests)
+ self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
+
+ if not metafunc._calls:
+ yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
+ else:
+ # Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
+ fm = self.session._fixturemanager
+ fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
+
+ # Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
+ # with direct parametrization, so make sure we update what the
+ # function really needs.
+ fixtureinfo.prune_dependency_tree()
+
+ for callspec in metafunc._calls:
+ subname = f"{name}[{callspec.id}]"
+ yield Function.from_parent(
+ self,
+ name=subname,
+ callspec=callspec,
+ fixtureinfo=fixtureinfo,
+ keywords={callspec.id: True},
+ originalname=name,
+ )
+
+
+class Module(nodes.File, PyCollector):
+ """Collector for test classes and functions in a Python module."""
+
+ def _getobj(self):
+ return self._importtestmodule()
+
+ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
+ self._inject_setup_module_fixture()
+ self._inject_setup_function_fixture()
+ self.session._fixturemanager.parsefactories(self)
+ return super().collect()
+
+ def _inject_setup_module_fixture(self) -> None:
+ """Inject a hidden autouse, module scoped fixture into the collected module object
+ that invokes setUpModule/tearDownModule if either or both are available.
+
+ Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ has_nose = self.config.pluginmanager.has_plugin("nose")
+ setup_module = _get_first_non_fixture_func(
+ self.obj, ("setUpModule", "setup_module")
+ )
+ if setup_module is None and has_nose:
+ # The name "setup" is too common - only treat as fixture if callable.
+ setup_module = _get_first_non_fixture_func(self.obj, ("setup",))
+ if not callable(setup_module):
+ setup_module = None
+ teardown_module = _get_first_non_fixture_func(
+ self.obj, ("tearDownModule", "teardown_module")
+ )
+ if teardown_module is None and has_nose:
+ teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",))
+ # Same as "setup" above - only treat as fixture if callable.
+ if not callable(teardown_module):
+ teardown_module = None
+
+ if setup_module is None and teardown_module is None:
+ return
+
+ @fixtures.fixture(
+ autouse=True,
+ scope="module",
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
+ )
+ def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
+ if setup_module is not None:
+ _call_with_optional_argument(setup_module, request.module)
+ yield
+ if teardown_module is not None:
+ _call_with_optional_argument(teardown_module, request.module)
+
+ self.obj.__pytest_setup_module = xunit_setup_module_fixture
+
+ def _inject_setup_function_fixture(self) -> None:
+ """Inject a hidden autouse, function scoped fixture into the collected module object
+ that invokes setup_function/teardown_function if either or both are available.
+
+ Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",))
+ teardown_function = _get_first_non_fixture_func(
+ self.obj, ("teardown_function",)
+ )
+ if setup_function is None and teardown_function is None:
+ return
+
+ @fixtures.fixture(
+ autouse=True,
+ scope="function",
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
+ )
+ def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
+ if request.instance is not None:
+ # in this case we are bound to an instance, so we need to let
+ # setup_method handle this
+ yield
+ return
+ if setup_function is not None:
+ _call_with_optional_argument(setup_function, request.function)
+ yield
+ if teardown_function is not None:
+ _call_with_optional_argument(teardown_function, request.function)
+
+ self.obj.__pytest_setup_function = xunit_setup_function_fixture
+
+ def _importtestmodule(self):
+ # We assume we are only called once per module.
+ importmode = self.config.getoption("--import-mode")
+ try:
+ mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
+ except SyntaxError as e:
+ raise self.CollectError(
+ ExceptionInfo.from_current().getrepr(style="short")
+ ) from e
+ except ImportPathMismatchError as e:
+ raise self.CollectError(
+ "import file mismatch:\n"
+ "imported module %r has this __file__ attribute:\n"
+ " %s\n"
+ "which is not the same as the test file we want to collect:\n"
+ " %s\n"
+ "HINT: remove __pycache__ / .pyc files and/or use a "
+ "unique basename for your test file modules" % e.args
+ ) from e
+ except ImportError as e:
+ exc_info = ExceptionInfo.from_current()
+ if self.config.getoption("verbose") < 2:
+ exc_info.traceback = exc_info.traceback.filter(filter_traceback)
+ exc_repr = (
+ exc_info.getrepr(style="short")
+ if exc_info.traceback
+ else exc_info.exconly()
+ )
+ formatted_tb = str(exc_repr)
+ raise self.CollectError(
+ "ImportError while importing test module '{path}'.\n"
+ "Hint: make sure your test modules/packages have valid Python names.\n"
+ "Traceback:\n"
+ "{traceback}".format(path=self.path, traceback=formatted_tb)
+ ) from e
+ except skip.Exception as e:
+ if e.allow_module_level:
+ raise
+ raise self.CollectError(
+ "Using pytest.skip outside of a test will skip the entire module. "
+ "If that's your intention, pass `allow_module_level=True`. "
+ "If you want to skip a specific test or an entire class, "
+ "use the @pytest.mark.skip or @pytest.mark.skipif decorators."
+ ) from e
+ self.config.pluginmanager.consider_module(mod)
+ return mod
+
+
+class Package(Module):
+ """Collector for files and directories in a Python packages -- directories
+ with an `__init__.py` file."""
+
+ def __init__(
+ self,
+ fspath: Optional[LEGACY_PATH],
+ parent: nodes.Collector,
+ # NOTE: following args are unused:
+ config=None,
+ session=None,
+ nodeid=None,
+ path: Optional[Path] = None,
+ ) -> None:
+ # NOTE: Could be just the following, but kept as-is for compat.
+ # nodes.FSCollector.__init__(self, fspath, parent=parent)
+ session = parent.session
+ nodes.FSCollector.__init__(
+ self,
+ fspath=fspath,
+ path=path,
+ parent=parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ )
+ self.name = self.path.parent.name
+
+ def setup(self) -> None:
+ # Not using fixtures to call setup_module here because autouse fixtures
+ # from packages are not called automatically (#4085).
+ setup_module = _get_first_non_fixture_func(
+ self.obj, ("setUpModule", "setup_module")
+ )
+ if setup_module is not None:
+ _call_with_optional_argument(setup_module, self.obj)
+
+ teardown_module = _get_first_non_fixture_func(
+ self.obj, ("tearDownModule", "teardown_module")
+ )
+ if teardown_module is not None:
+ func = partial(_call_with_optional_argument, teardown_module, self.obj)
+ self.addfinalizer(func)
+
+ def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
+ if direntry.name == "__pycache__":
+ return False
+ fspath = Path(direntry.path)
+ ihook = self.session.gethookproxy(fspath.parent)
+ if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
+ return False
+ return True
+
+ def _collectfile(
+ self, fspath: Path, handle_dupes: bool = True
+ ) -> Sequence[nodes.Collector]:
+ assert (
+ fspath.is_file()
+ ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format(
+ fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink()
+ )
+ ihook = self.session.gethookproxy(fspath)
+ if not self.session.isinitpath(fspath):
+ if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
+ return ()
+
+ if handle_dupes:
+ keepduplicates = self.config.getoption("keepduplicates")
+ if not keepduplicates:
+ duplicate_paths = self.config.pluginmanager._duplicatepaths
+ if fspath in duplicate_paths:
+ return ()
+ else:
+ duplicate_paths.add(fspath)
+
+ return ihook.pytest_collect_file(file_path=fspath, parent=self) # type: ignore[no-any-return]
+
+ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
+ this_path = self.path.parent
+
+ # Always collect the __init__ first.
+ if path_matches_patterns(self.path, self.config.getini("python_files")):
+ yield Module.from_parent(self, path=self.path)
+
+ pkg_prefixes: Set[Path] = set()
+ for direntry in visit(str(this_path), recurse=self._recurse):
+ path = Path(direntry.path)
+
+ # We will visit our own __init__.py file, in which case we skip it.
+ if direntry.is_file():
+ if direntry.name == "__init__.py" and path.parent == this_path:
+ continue
+
+ parts_ = parts(direntry.path)
+ if any(
+ str(pkg_prefix) in parts_ and pkg_prefix / "__init__.py" != path
+ for pkg_prefix in pkg_prefixes
+ ):
+ continue
+
+ if direntry.is_file():
+ yield from self._collectfile(path)
+ elif not direntry.is_dir():
+ # Broken symlink or invalid/missing file.
+ continue
+ elif path.joinpath("__init__.py").is_file():
+ pkg_prefixes.add(path)
+
+
+def _call_with_optional_argument(func, arg) -> None:
+ """Call the given function with the given argument if func accepts one argument, otherwise
+ calls func without arguments."""
+ arg_count = func.__code__.co_argcount
+ if inspect.ismethod(func):
+ arg_count -= 1
+ if arg_count:
+ func(arg)
+ else:
+ func()
+
+
+def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
+ """Return the attribute from the given object to be used as a setup/teardown
+ xunit-style function, but only if not marked as a fixture to avoid calling it twice.
+ """
+ for name in names:
+ meth: Optional[object] = getattr(obj, name, None)
+ if meth is not None and fixtures.getfixturemarker(meth) is None:
+ return meth
+ return None
+
+
+class Class(PyCollector):
+ """Collector for test methods (and nested classes) in a Python class."""
+
+ @classmethod
+ def from_parent(cls, parent, *, name, obj=None, **kw):
+ """The public constructor."""
+ return super().from_parent(name=name, parent=parent, **kw)
+
+ def newinstance(self):
+ return self.obj()
+
+ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
+ if not safe_getattr(self.obj, "__test__", True):
+ return []
+ if hasinit(self.obj):
+ assert self.parent is not None
+ self.warn(
+ PytestCollectionWarning(
+ "cannot collect test class %r because it has a "
+ "__init__ constructor (from: %s)"
+ % (self.obj.__name__, self.parent.nodeid)
+ )
+ )
+ return []
+ elif hasnew(self.obj):
+ assert self.parent is not None
+ self.warn(
+ PytestCollectionWarning(
+ "cannot collect test class %r because it has a "
+ "__new__ constructor (from: %s)"
+ % (self.obj.__name__, self.parent.nodeid)
+ )
+ )
+ return []
+
+ self._inject_setup_class_fixture()
+ self._inject_setup_method_fixture()
+
+ self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
+
+ return super().collect()
+
+ def _inject_setup_class_fixture(self) -> None:
+ """Inject a hidden autouse, class scoped fixture into the collected class object
+ that invokes setup_class/teardown_class if either or both are available.
+
+ Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
+ teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
+ if setup_class is None and teardown_class is None:
+ return
+
+ @fixtures.fixture(
+ autouse=True,
+ scope="class",
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
+ )
+ def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
+ if setup_class is not None:
+ func = getimfunc(setup_class)
+ _call_with_optional_argument(func, self.obj)
+ yield
+ if teardown_class is not None:
+ func = getimfunc(teardown_class)
+ _call_with_optional_argument(func, self.obj)
+
+ self.obj.__pytest_setup_class = xunit_setup_class_fixture
+
+ def _inject_setup_method_fixture(self) -> None:
+ """Inject a hidden autouse, function scoped fixture into the collected class object
+ that invokes setup_method/teardown_method if either or both are available.
+
+ Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ has_nose = self.config.pluginmanager.has_plugin("nose")
+ setup_name = "setup_method"
+ setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
+ emit_nose_setup_warning = False
+ if setup_method is None and has_nose:
+ setup_name = "setup"
+ emit_nose_setup_warning = True
+ setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
+ teardown_name = "teardown_method"
+ teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
+ emit_nose_teardown_warning = False
+ if teardown_method is None and has_nose:
+ teardown_name = "teardown"
+ emit_nose_teardown_warning = True
+ teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
+ if setup_method is None and teardown_method is None:
+ return
+
+ @fixtures.fixture(
+ autouse=True,
+ scope="function",
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
+ )
+ def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
+ method = request.function
+ if setup_method is not None:
+ func = getattr(self, setup_name)
+ _call_with_optional_argument(func, method)
+ if emit_nose_setup_warning:
+ warnings.warn(
+ NOSE_SUPPORT_METHOD.format(
+ nodeid=request.node.nodeid, method="setup"
+ ),
+ stacklevel=2,
+ )
+ yield
+ if teardown_method is not None:
+ func = getattr(self, teardown_name)
+ _call_with_optional_argument(func, method)
+ if emit_nose_teardown_warning:
+ warnings.warn(
+ NOSE_SUPPORT_METHOD.format(
+ nodeid=request.node.nodeid, method="teardown"
+ ),
+ stacklevel=2,
+ )
+
+ self.obj.__pytest_setup_method = xunit_setup_method_fixture
+
+
+class InstanceDummy:
+ """Instance used to be a node type between Class and Function. It has been
+ removed in pytest 7.0. Some plugins exist which reference `pytest.Instance`
+ only to ignore it; this dummy class keeps them working. This will be removed
+ in pytest 8."""
+
+
+def __getattr__(name: str) -> object:
+ if name == "Instance":
+ warnings.warn(INSTANCE_COLLECTOR, 2)
+ return InstanceDummy
+ raise AttributeError(f"module {__name__} has no attribute {name}")
+
+
+def hasinit(obj: object) -> bool:
+ init: object = getattr(obj, "__init__", None)
+ if init:
+ return init != object.__init__
+ return False
+
+
+def hasnew(obj: object) -> bool:
+ new: object = getattr(obj, "__new__", None)
+ if new:
+ return new != object.__new__
+ return False
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class IdMaker:
+ """Make IDs for a parametrization."""
+
+ __slots__ = (
+ "argnames",
+ "parametersets",
+ "idfn",
+ "ids",
+ "config",
+ "nodeid",
+ "func_name",
+ )
+
+ # The argnames of the parametrization.
+ argnames: Sequence[str]
+ # The ParameterSets of the parametrization.
+ parametersets: Sequence[ParameterSet]
+ # Optionally, a user-provided callable to make IDs for parameters in a
+ # ParameterSet.
+ idfn: Optional[Callable[[Any], Optional[object]]]
+ # Optionally, explicit IDs for ParameterSets by index.
+ ids: Optional[Sequence[Optional[object]]]
+ # Optionally, the pytest config.
+ # Used for controlling ASCII escaping, and for calling the
+ # :hook:`pytest_make_parametrize_id` hook.
+ config: Optional[Config]
+ # Optionally, the ID of the node being parametrized.
+ # Used only for clearer error messages.
+ nodeid: Optional[str]
+ # Optionally, the ID of the function being parametrized.
+ # Used only for clearer error messages.
+ func_name: Optional[str]
+
+ def make_unique_parameterset_ids(self) -> List[str]:
+ """Make a unique identifier for each ParameterSet, that may be used to
+ identify the parametrization in a node ID.
+
+ Format is -...-[counter], where prm_x_token is
+ - user-provided id, if given
+ - else an id derived from the value, applicable for certain types
+ - else
+ The counter suffix is appended only in case a string wouldn't be unique
+ otherwise.
+ """
+ resolved_ids = list(self._resolve_ids())
+ # All IDs must be unique!
+ if len(resolved_ids) != len(set(resolved_ids)):
+ # Record the number of occurrences of each ID.
+ id_counts = Counter(resolved_ids)
+ # Map the ID to its next suffix.
+ id_suffixes: Dict[str, int] = defaultdict(int)
+ # Suffix non-unique IDs to make them unique.
+ for index, id in enumerate(resolved_ids):
+ if id_counts[id] > 1:
+ resolved_ids[index] = f"{id}{id_suffixes[id]}"
+ id_suffixes[id] += 1
+ return resolved_ids
+
+ def _resolve_ids(self) -> Iterable[str]:
+ """Resolve IDs for all ParameterSets (may contain duplicates)."""
+ for idx, parameterset in enumerate(self.parametersets):
+ if parameterset.id is not None:
+ # ID provided directly - pytest.param(..., id="...")
+ yield parameterset.id
+ elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
+ # ID provided in the IDs list - parametrize(..., ids=[...]).
+ yield self._idval_from_value_required(self.ids[idx], idx)
+ else:
+ # ID not provided - generate it.
+ yield "-".join(
+ self._idval(val, argname, idx)
+ for val, argname in zip(parameterset.values, self.argnames)
+ )
+
+ def _idval(self, val: object, argname: str, idx: int) -> str:
+ """Make an ID for a parameter in a ParameterSet."""
+ idval = self._idval_from_function(val, argname, idx)
+ if idval is not None:
+ return idval
+ idval = self._idval_from_hook(val, argname)
+ if idval is not None:
+ return idval
+ idval = self._idval_from_value(val)
+ if idval is not None:
+ return idval
+ return self._idval_from_argname(argname, idx)
+
+ def _idval_from_function(
+ self, val: object, argname: str, idx: int
+ ) -> Optional[str]:
+ """Try to make an ID for a parameter in a ParameterSet using the
+ user-provided id callable, if given."""
+ if self.idfn is None:
+ return None
+ try:
+ id = self.idfn(val)
+ except Exception as e:
+ prefix = f"{self.nodeid}: " if self.nodeid is not None else ""
+ msg = "error raised while trying to determine id of parameter '{}' at position {}"
+ msg = prefix + msg.format(argname, idx)
+ raise ValueError(msg) from e
+ if id is None:
+ return None
+ return self._idval_from_value(id)
+
+ def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
+ """Try to make an ID for a parameter in a ParameterSet by calling the
+ :hook:`pytest_make_parametrize_id` hook."""
+ if self.config:
+ id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
+ config=self.config, val=val, argname=argname
+ )
+ return id
+ return None
+
+ def _idval_from_value(self, val: object) -> Optional[str]:
+ """Try to make an ID for a parameter in a ParameterSet from its value,
+ if the value type is supported."""
+ if isinstance(val, STRING_TYPES):
+ return _ascii_escaped_by_config(val, self.config)
+ elif val is None or isinstance(val, (float, int, bool, complex)):
+ return str(val)
+ elif isinstance(val, Pattern):
+ return ascii_escaped(val.pattern)
+ elif val is NOTSET:
+ # Fallback to default. Note that NOTSET is an enum.Enum.
+ pass
+ elif isinstance(val, enum.Enum):
+ return str(val)
+ elif isinstance(getattr(val, "__name__", None), str):
+ # Name of a class, function, module, etc.
+ name: str = getattr(val, "__name__")
+ return name
+ return None
+
+ def _idval_from_value_required(self, val: object, idx: int) -> str:
+ """Like _idval_from_value(), but fails if the type is not supported."""
+ id = self._idval_from_value(val)
+ if id is not None:
+ return id
+
+ # Fail.
+ if self.func_name is not None:
+ prefix = f"In {self.func_name}: "
+ elif self.nodeid is not None:
+ prefix = f"In {self.nodeid}: "
+ else:
+ prefix = ""
+ msg = (
+ f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
+ "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
+ )
+ fail(msg, pytrace=False)
+
+ @staticmethod
+ def _idval_from_argname(argname: str, idx: int) -> str:
+ """Make an ID for a parameter in a ParameterSet from the argument name
+ and the index of the ParameterSet."""
+ return str(argname) + str(idx)
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class CallSpec2:
+ """A planned parameterized invocation of a test function.
+
+ Calculated during collection for a given test function's Metafunc.
+ Once collection is over, each callspec is turned into a single Item
+ and stored in item.callspec.
+ """
+
+ # arg name -> arg value which will be passed to the parametrized test
+ # function (direct parameterization).
+ funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
+ # arg name -> arg value which will be passed to a fixture of the same name
+ # (indirect parametrization).
+ params: Dict[str, object] = dataclasses.field(default_factory=dict)
+ # arg name -> arg index.
+ indices: Dict[str, int] = dataclasses.field(default_factory=dict)
+ # Used for sorting parametrized resources.
+ _arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
+ # Parts which will be added to the item's name in `[..]` separated by "-".
+ _idlist: List[str] = dataclasses.field(default_factory=list)
+ # Marks which will be applied to the item.
+ marks: List[Mark] = dataclasses.field(default_factory=list)
+
+ def setmulti(
+ self,
+ *,
+ valtypes: Mapping[str, "Literal['params', 'funcargs']"],
+ argnames: Iterable[str],
+ valset: Iterable[object],
+ id: str,
+ marks: Iterable[Union[Mark, MarkDecorator]],
+ scope: Scope,
+ param_index: int,
+ ) -> "CallSpec2":
+ funcargs = self.funcargs.copy()
+ params = self.params.copy()
+ indices = self.indices.copy()
+ arg2scope = self._arg2scope.copy()
+ for arg, val in zip(argnames, valset):
+ if arg in params or arg in funcargs:
+ raise ValueError(f"duplicate parametrization of {arg!r}")
+ valtype_for_arg = valtypes[arg]
+ if valtype_for_arg == "params":
+ params[arg] = val
+ elif valtype_for_arg == "funcargs":
+ funcargs[arg] = val
+ else:
+ assert_never(valtype_for_arg)
+ indices[arg] = param_index
+ arg2scope[arg] = scope
+ return CallSpec2(
+ funcargs=funcargs,
+ params=params,
+ indices=indices,
+ _arg2scope=arg2scope,
+ _idlist=[*self._idlist, id],
+ marks=[*self.marks, *normalize_mark_list(marks)],
+ )
+
+ def getparam(self, name: str) -> object:
+ try:
+ return self.params[name]
+ except KeyError as e:
+ raise ValueError(name) from e
+
+ @property
+ def id(self) -> str:
+ return "-".join(self._idlist)
+
+
+@final
+class Metafunc:
+ """Objects passed to the :hook:`pytest_generate_tests` hook.
+
+ They help to inspect a test function and to generate tests according to
+ test configuration or values specified in the class or module where a
+ test function is defined.
+ """
+
+ def __init__(
+ self,
+ definition: "FunctionDefinition",
+ fixtureinfo: fixtures.FuncFixtureInfo,
+ config: Config,
+ cls=None,
+ module=None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+
+ #: Access to the underlying :class:`_pytest.python.FunctionDefinition`.
+ self.definition = definition
+
+ #: Access to the :class:`pytest.Config` object for the test session.
+ self.config = config
+
+ #: The module object where the test function is defined in.
+ self.module = module
+
+ #: Underlying Python test function.
+ self.function = definition.obj
+
+ #: Set of fixture names required by the test function.
+ self.fixturenames = fixtureinfo.names_closure
+
+ #: Class object where the test function is defined in or ``None``.
+ self.cls = cls
+
+ self._arg2fixturedefs = fixtureinfo.name2fixturedefs
+
+ # Result of parametrize().
+ self._calls: List[CallSpec2] = []
+
+ def parametrize(
+ self,
+ argnames: Union[str, Sequence[str]],
+ argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
+ indirect: Union[bool, Sequence[str]] = False,
+ ids: Optional[
+ Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
+ ] = None,
+ scope: "Optional[_ScopeName]" = None,
+ *,
+ _param_mark: Optional[Mark] = None,
+ ) -> None:
+ """Add new invocations to the underlying test function using the list
+ of argvalues for the given argnames. Parametrization is performed
+ during the collection phase. If you need to setup expensive resources
+ see about setting indirect to do it rather than at test setup time.
+
+ Can be called multiple times per test function (but only on different
+ argument names), in which case each call parametrizes all previous
+ parametrizations, e.g.
+
+ ::
+
+ unparametrized: t
+ parametrize ["x", "y"]: t[x], t[y]
+ parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2]
+
+ :param argnames:
+ A comma-separated string denoting one or more argument names, or
+ a list/tuple of argument strings.
+
+ :param argvalues:
+ The list of argvalues determines how often a test is invoked with
+ different argument values.
+
+ If only one argname was specified argvalues is a list of values.
+ If N argnames were specified, argvalues must be a list of
+ N-tuples, where each tuple-element specifies a value for its
+ respective argname.
+
+ :param indirect:
+ A list of arguments' names (subset of argnames) or a boolean.
+ If True the list contains all names from the argnames. Each
+ argvalue corresponding to an argname in this list will
+ be passed as request.param to its respective argname fixture
+ function so that it can perform more expensive setups during the
+ setup phase of a test rather than at collection time.
+
+ :param ids:
+ Sequence of (or generator for) ids for ``argvalues``,
+ or a callable to return part of the id for each argvalue.
+
+ With sequences (and generators like ``itertools.count()``) the
+ returned ids should be of type ``string``, ``int``, ``float``,
+ ``bool``, or ``None``.
+ They are mapped to the corresponding index in ``argvalues``.
+ ``None`` means to use the auto-generated id.
+
+ If it is a callable it will be called for each entry in
+ ``argvalues``, and the return value is used as part of the
+ auto-generated id for the whole set (where parts are joined with
+ dashes ("-")).
+ This is useful to provide more specific ids for certain items, e.g.
+ dates. Returning ``None`` will use an auto-generated id.
+
+ If no ids are provided they will be generated automatically from
+ the argvalues.
+
+ :param scope:
+ If specified it denotes the scope of the parameters.
+ The scope is used for grouping tests by parameter instances.
+ It will also override any fixture-function defined scope, allowing
+ to set a dynamic scope using test context or configuration.
+ """
+ argnames, parametersets = ParameterSet._for_parametrize(
+ argnames,
+ argvalues,
+ self.function,
+ self.config,
+ nodeid=self.definition.nodeid,
+ )
+ del argvalues
+
+ if "request" in argnames:
+ fail(
+ "'request' is a reserved name and cannot be used in @pytest.mark.parametrize",
+ pytrace=False,
+ )
+
+ if scope is not None:
+ scope_ = Scope.from_user(
+ scope, descr=f"parametrize() call in {self.function.__name__}"
+ )
+ else:
+ scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
+
+ self._validate_if_using_arg_names(argnames, indirect)
+
+ arg_values_types = self._resolve_arg_value_types(argnames, indirect)
+
+ # Use any already (possibly) generated ids with parametrize Marks.
+ if _param_mark and _param_mark._param_ids_from:
+ generated_ids = _param_mark._param_ids_from._param_ids_generated
+ if generated_ids is not None:
+ ids = generated_ids
+
+ ids = self._resolve_parameter_set_ids(
+ argnames, ids, parametersets, nodeid=self.definition.nodeid
+ )
+
+ # Store used (possibly generated) ids with parametrize Marks.
+ if _param_mark and _param_mark._param_ids_from and generated_ids is None:
+ object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
+
+ # Create the new calls: if we are parametrize() multiple times (by applying the decorator
+ # more than once) then we accumulate those calls generating the cartesian product
+ # of all calls.
+ newcalls = []
+ for callspec in self._calls or [CallSpec2()]:
+ for param_index, (param_id, param_set) in enumerate(
+ zip(ids, parametersets)
+ ):
+ newcallspec = callspec.setmulti(
+ valtypes=arg_values_types,
+ argnames=argnames,
+ valset=param_set.values,
+ id=param_id,
+ marks=param_set.marks,
+ scope=scope_,
+ param_index=param_index,
+ )
+ newcalls.append(newcallspec)
+ self._calls = newcalls
+
+ def _resolve_parameter_set_ids(
+ self,
+ argnames: Sequence[str],
+ ids: Optional[
+ Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
+ ],
+ parametersets: Sequence[ParameterSet],
+ nodeid: str,
+ ) -> List[str]:
+ """Resolve the actual ids for the given parameter sets.
+
+ :param argnames:
+ Argument names passed to ``parametrize()``.
+ :param ids:
+ The `ids` parameter of the ``parametrize()`` call (see docs).
+ :param parametersets:
+ The parameter sets, each containing a set of values corresponding
+ to ``argnames``.
+ :param nodeid str:
+ The nodeid of the definition item that generated this
+ parametrization.
+ :returns:
+ List with ids for each parameter set given.
+ """
+ if ids is None:
+ idfn = None
+ ids_ = None
+ elif callable(ids):
+ idfn = ids
+ ids_ = None
+ else:
+ idfn = None
+ ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
+ id_maker = IdMaker(
+ argnames,
+ parametersets,
+ idfn,
+ ids_,
+ self.config,
+ nodeid=nodeid,
+ func_name=self.function.__name__,
+ )
+ return id_maker.make_unique_parameterset_ids()
+
+ def _validate_ids(
+ self,
+ ids: Iterable[Optional[object]],
+ parametersets: Sequence[ParameterSet],
+ func_name: str,
+ ) -> List[Optional[object]]:
+ try:
+ num_ids = len(ids) # type: ignore[arg-type]
+ except TypeError:
+ try:
+ iter(ids)
+ except TypeError as e:
+ raise TypeError("ids must be a callable or an iterable") from e
+ num_ids = len(parametersets)
+
+ # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
+ if num_ids != len(parametersets) and num_ids != 0:
+ msg = "In {}: {} parameter sets specified, with different number of ids: {}"
+ fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
+
+ return list(itertools.islice(ids, num_ids))
+
+ def _resolve_arg_value_types(
+ self,
+ argnames: Sequence[str],
+ indirect: Union[bool, Sequence[str]],
+ ) -> Dict[str, "Literal['params', 'funcargs']"]:
+ """Resolve if each parametrized argument must be considered a
+ parameter to a fixture or a "funcarg" to the function, based on the
+ ``indirect`` parameter of the parametrized() call.
+
+ :param List[str] argnames: List of argument names passed to ``parametrize()``.
+ :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
+ :rtype: Dict[str, str]
+ A dict mapping each arg name to either:
+ * "params" if the argname should be the parameter of a fixture of the same name.
+ * "funcargs" if the argname should be a parameter to the parametrized test function.
+ """
+ if isinstance(indirect, bool):
+ valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys(
+ argnames, "params" if indirect else "funcargs"
+ )
+ elif isinstance(indirect, Sequence):
+ valtypes = dict.fromkeys(argnames, "funcargs")
+ for arg in indirect:
+ if arg not in argnames:
+ fail(
+ "In {}: indirect fixture '{}' doesn't exist".format(
+ self.function.__name__, arg
+ ),
+ pytrace=False,
+ )
+ valtypes[arg] = "params"
+ else:
+ fail(
+ "In {func}: expected Sequence or boolean for indirect, got {type}".format(
+ type=type(indirect).__name__, func=self.function.__name__
+ ),
+ pytrace=False,
+ )
+ return valtypes
+
+ def _validate_if_using_arg_names(
+ self,
+ argnames: Sequence[str],
+ indirect: Union[bool, Sequence[str]],
+ ) -> None:
+ """Check if all argnames are being used, by default values, or directly/indirectly.
+
+ :param List[str] argnames: List of argument names passed to ``parametrize()``.
+ :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
+ :raises ValueError: If validation fails.
+ """
+ default_arg_names = set(get_default_arg_names(self.function))
+ func_name = self.function.__name__
+ for arg in argnames:
+ if arg not in self.fixturenames:
+ if arg in default_arg_names:
+ fail(
+ "In {}: function already takes an argument '{}' with a default value".format(
+ func_name, arg
+ ),
+ pytrace=False,
+ )
+ else:
+ if isinstance(indirect, Sequence):
+ name = "fixture" if arg in indirect else "argument"
+ else:
+ name = "fixture" if indirect else "argument"
+ fail(
+ f"In {func_name}: function uses no {name} '{arg}'",
+ pytrace=False,
+ )
+
+
+def _find_parametrized_scope(
+ argnames: Sequence[str],
+ arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
+ indirect: Union[bool, Sequence[str]],
+) -> Scope:
+ """Find the most appropriate scope for a parametrized call based on its arguments.
+
+ When there's at least one direct argument, always use "function" scope.
+
+ When a test function is parametrized and all its arguments are indirect
+ (e.g. fixtures), return the most narrow scope based on the fixtures used.
+
+ Related to issue #1832, based on code posted by @Kingdread.
+ """
+ if isinstance(indirect, Sequence):
+ all_arguments_are_fixtures = len(indirect) == len(argnames)
+ else:
+ all_arguments_are_fixtures = bool(indirect)
+
+ if all_arguments_are_fixtures:
+ fixturedefs = arg2fixturedefs or {}
+ used_scopes = [
+ fixturedef[0]._scope
+ for name, fixturedef in fixturedefs.items()
+ if name in argnames
+ ]
+ # Takes the most narrow scope from used fixtures.
+ return min(used_scopes, default=Scope.Function)
+
+ return Scope.Function
+
+
+def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str:
+ if config is None:
+ escape_option = False
+ else:
+ escape_option = config.getini(
+ "disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
+ )
+ # TODO: If escaping is turned off and the user passes bytes,
+ # will return a bytes. For now we ignore this but the
+ # code *probably* doesn't handle this case.
+ return val if escape_option else ascii_escaped(val) # type: ignore
+
+
+def _pretty_fixture_path(func) -> str:
+ cwd = Path.cwd()
+ loc = Path(getlocation(func, str(cwd)))
+ prefix = Path("...", "_pytest")
+ try:
+ return str(prefix / loc.relative_to(_PYTEST_DIR))
+ except ValueError:
+ return bestrelpath(cwd, loc)
+
+
+def show_fixtures_per_test(config):
+ from _pytest.main import wrap_session
+
+ return wrap_session(config, _show_fixtures_per_test)
+
+
+def _show_fixtures_per_test(config: Config, session: Session) -> None:
+ import _pytest.config
+
+ session.perform_collect()
+ curdir = Path.cwd()
+ tw = _pytest.config.create_terminal_writer(config)
+ verbose = config.getvalue("verbose")
+
+ def get_best_relpath(func) -> str:
+ loc = getlocation(func, str(curdir))
+ return bestrelpath(curdir, Path(loc))
+
+ def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
+ argname = fixture_def.argname
+ if verbose <= 0 and argname.startswith("_"):
+ return
+ prettypath = _pretty_fixture_path(fixture_def.func)
+ tw.write(f"{argname}", green=True)
+ tw.write(f" -- {prettypath}", yellow=True)
+ tw.write("\n")
+ fixture_doc = inspect.getdoc(fixture_def.func)
+ if fixture_doc:
+ write_docstring(
+ tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
+ )
+ else:
+ tw.line(" no docstring available", red=True)
+
+ def write_item(item: nodes.Item) -> None:
+ # Not all items have _fixtureinfo attribute.
+ info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
+ if info is None or not info.name2fixturedefs:
+ # This test item does not use any fixtures.
+ return
+ tw.line()
+ tw.sep("-", f"fixtures used by {item.name}")
+ # TODO: Fix this type ignore.
+ tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
+ # dict key not used in loop but needed for sorting.
+ for _, fixturedefs in sorted(info.name2fixturedefs.items()):
+ assert fixturedefs is not None
+ if not fixturedefs:
+ continue
+ # Last item is expected to be the one used by the test item.
+ write_fixture(fixturedefs[-1])
+
+ for session_item in session.items:
+ write_item(session_item)
+
+
+def showfixtures(config: Config) -> Union[int, ExitCode]:
+ from _pytest.main import wrap_session
+
+ return wrap_session(config, _showfixtures_main)
+
+
+def _showfixtures_main(config: Config, session: Session) -> None:
+ import _pytest.config
+
+ session.perform_collect()
+ curdir = Path.cwd()
+ tw = _pytest.config.create_terminal_writer(config)
+ verbose = config.getvalue("verbose")
+
+ fm = session._fixturemanager
+
+ available = []
+ seen: Set[Tuple[str, str]] = set()
+
+ for argname, fixturedefs in fm._arg2fixturedefs.items():
+ assert fixturedefs is not None
+ if not fixturedefs:
+ continue
+ for fixturedef in fixturedefs:
+ loc = getlocation(fixturedef.func, str(curdir))
+ if (fixturedef.argname, loc) in seen:
+ continue
+ seen.add((fixturedef.argname, loc))
+ available.append(
+ (
+ len(fixturedef.baseid),
+ fixturedef.func.__module__,
+ _pretty_fixture_path(fixturedef.func),
+ fixturedef.argname,
+ fixturedef,
+ )
+ )
+
+ available.sort()
+ currentmodule = None
+ for baseid, module, prettypath, argname, fixturedef in available:
+ if currentmodule != module:
+ if not module.startswith("_pytest."):
+ tw.line()
+ tw.sep("-", f"fixtures defined from {module}")
+ currentmodule = module
+ if verbose <= 0 and argname.startswith("_"):
+ continue
+ tw.write(f"{argname}", green=True)
+ if fixturedef.scope != "function":
+ tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
+ tw.write(f" -- {prettypath}", yellow=True)
+ tw.write("\n")
+ doc = inspect.getdoc(fixturedef.func)
+ if doc:
+ write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
+ else:
+ tw.line(" no docstring available", red=True)
+ tw.line()
+
+
+def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
+ for line in doc.split("\n"):
+ tw.line(indent + line)
+
+
+class Function(PyobjMixin, nodes.Item):
+ """Item responsible for setting up and executing a Python test function.
+
+ :param name:
+ The full function name, including any decorations like those
+ added by parametrization (``my_func[my_param]``).
+ :param parent:
+ The parent Node.
+ :param config:
+ The pytest Config object.
+ :param callspec:
+ If given, this is function has been parametrized and the callspec contains
+ meta information about the parametrization.
+ :param callobj:
+ If given, the object which will be called when the Function is invoked,
+ otherwise the callobj will be obtained from ``parent`` using ``originalname``.
+ :param keywords:
+ Keywords bound to the function object for "-k" matching.
+ :param session:
+ The pytest Session object.
+ :param fixtureinfo:
+ Fixture information already resolved at this fixture node..
+ :param originalname:
+ The attribute name to use for accessing the underlying function object.
+ Defaults to ``name``. Set this if name is different from the original name,
+ for example when it contains decorations like those added by parametrization
+ (``my_func[my_param]``).
+ """
+
+ # Disable since functions handle it themselves.
+ _ALLOW_MARKERS = False
+
+ def __init__(
+ self,
+ name: str,
+ parent,
+ config: Optional[Config] = None,
+ callspec: Optional[CallSpec2] = None,
+ callobj=NOTSET,
+ keywords: Optional[Mapping[str, Any]] = None,
+ session: Optional[Session] = None,
+ fixtureinfo: Optional[FuncFixtureInfo] = None,
+ originalname: Optional[str] = None,
+ ) -> None:
+ super().__init__(name, parent, config=config, session=session)
+
+ if callobj is not NOTSET:
+ self.obj = callobj
+
+ #: Original function name, without any decorations (for example
+ #: parametrization adds a ``"[...]"`` suffix to function names), used to access
+ #: the underlying function object from ``parent`` (in case ``callobj`` is not given
+ #: explicitly).
+ #:
+ #: .. versionadded:: 3.0
+ self.originalname = originalname or name
+
+ # Note: when FunctionDefinition is introduced, we should change ``originalname``
+ # to a readonly property that returns FunctionDefinition.name.
+
+ self.own_markers.extend(get_unpacked_marks(self.obj))
+ if callspec:
+ self.callspec = callspec
+ self.own_markers.extend(callspec.marks)
+
+ # todo: this is a hell of a hack
+ # https://github.com/pytest-dev/pytest/issues/4569
+ # Note: the order of the updates is important here; indicates what
+ # takes priority (ctor argument over function attributes over markers).
+ # Take own_markers only; NodeKeywords handles parent traversal on its own.
+ self.keywords.update((mark.name, mark) for mark in self.own_markers)
+ self.keywords.update(self.obj.__dict__)
+ if keywords:
+ self.keywords.update(keywords)
+
+ if fixtureinfo is None:
+ fixtureinfo = self.session._fixturemanager.getfixtureinfo(
+ self, self.obj, self.cls, funcargs=True
+ )
+ self._fixtureinfo: FuncFixtureInfo = fixtureinfo
+ self.fixturenames = fixtureinfo.names_closure
+ self._initrequest()
+
+ @classmethod
+ def from_parent(cls, parent, **kw): # todo: determine sound type limitations
+ """The public constructor."""
+ return super().from_parent(parent=parent, **kw)
+
+ def _initrequest(self) -> None:
+ self.funcargs: Dict[str, object] = {}
+ self._request = fixtures.FixtureRequest(self, _ispytest=True)
+
+ @property
+ def function(self):
+ """Underlying python 'function' object."""
+ return getimfunc(self.obj)
+
+ def _getobj(self):
+ assert self.parent is not None
+ if isinstance(self.parent, Class):
+ # Each Function gets a fresh class instance.
+ parent_obj = self.parent.newinstance()
+ else:
+ parent_obj = self.parent.obj # type: ignore[attr-defined]
+ return getattr(parent_obj, self.originalname)
+
+ @property
+ def _pyfuncitem(self):
+ """(compatonly) for code expecting pytest-2.2 style request objects."""
+ return self
+
+ def runtest(self) -> None:
+ """Execute the underlying test function."""
+ self.ihook.pytest_pyfunc_call(pyfuncitem=self)
+
+ def setup(self) -> None:
+ self._request._fillfixtures()
+
+ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
+ if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
+ code = _pytest._code.Code.from_function(get_real_func(self.obj))
+ path, firstlineno = code.path, code.firstlineno
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.cut(path=path)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.filter(filter_traceback)
+ if not ntraceback:
+ ntraceback = traceback
+ ntraceback = ntraceback.filter(excinfo)
+
+ # issue364: mark all but first and last frames to
+ # only show a single-line message for each frame.
+ if self.config.getoption("tbstyle", "auto") == "auto":
+ if len(ntraceback) > 2:
+ ntraceback = Traceback(
+ entry
+ if i == 0 or i == len(ntraceback) - 1
+ else entry.with_repr_style("short")
+ for i, entry in enumerate(ntraceback)
+ )
+
+ return ntraceback
+ return excinfo.traceback
+
+ # TODO: Type ignored -- breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ ) -> Union[str, TerminalRepr]:
+ style = self.config.getoption("tbstyle", "auto")
+ if style == "auto":
+ style = "long"
+ return self._repr_failure_py(excinfo, style=style)
+
+
+class FunctionDefinition(Function):
+ """This class is a stop gap solution until we evolve to have actual function
+ definition nodes and manage to get rid of ``metafunc``."""
+
+ def runtest(self) -> None:
+ raise RuntimeError("function definitions are not supposed to be run as tests")
+
+ setup = runtest
diff --git a/venv/lib/python3.12/site-packages/_pytest/python_api.py b/venv/lib/python3.12/site-packages/_pytest/python_api.py
new file mode 100644
index 0000000..1833561
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/python_api.py
@@ -0,0 +1,996 @@
+import math
+import pprint
+from collections.abc import Collection
+from collections.abc import Sized
+from decimal import Decimal
+from numbers import Complex
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ContextManager
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Pattern
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+if TYPE_CHECKING:
+ from numpy import ndarray
+
+
+import _pytest._code
+from _pytest.compat import final
+from _pytest.compat import STRING_TYPES
+from _pytest.compat import overload
+from _pytest.outcomes import fail
+
+
+def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
+ at_str = f" at {at}" if at else ""
+ return TypeError(
+ "cannot make approximate comparisons to non-numeric values: {!r} {}".format(
+ value, at_str
+ )
+ )
+
+
+def _compare_approx(
+ full_object: object,
+ message_data: Sequence[Tuple[str, str, str]],
+ number_of_elements: int,
+ different_ids: Sequence[object],
+ max_abs_diff: float,
+ max_rel_diff: float,
+) -> List[str]:
+ message_list = list(message_data)
+ message_list.insert(0, ("Index", "Obtained", "Expected"))
+ max_sizes = [0, 0, 0]
+ for index, obtained, expected in message_list:
+ max_sizes[0] = max(max_sizes[0], len(index))
+ max_sizes[1] = max(max_sizes[1], len(obtained))
+ max_sizes[2] = max(max_sizes[2], len(expected))
+ explanation = [
+ f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:",
+ f"Max absolute difference: {max_abs_diff}",
+ f"Max relative difference: {max_rel_diff}",
+ ] + [
+ f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}"
+ for indexes, obtained, expected in message_list
+ ]
+ return explanation
+
+
+# builtin pytest.approx helper
+
+
+class ApproxBase:
+ """Provide shared utilities for making approximate comparisons between
+ numbers or sequences of numbers."""
+
+ # Tell numpy to use our `__eq__` operator instead of its.
+ __array_ufunc__ = None
+ __array_priority__ = 100
+
+ def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None:
+ __tracebackhide__ = True
+ self.expected = expected
+ self.abs = abs
+ self.rel = rel
+ self.nan_ok = nan_ok
+ self._check_type()
+
+ def __repr__(self) -> str:
+ raise NotImplementedError
+
+ def _repr_compare(self, other_side: Any) -> List[str]:
+ return [
+ "comparison failed",
+ f"Obtained: {other_side}",
+ f"Expected: {self}",
+ ]
+
+ def __eq__(self, actual) -> bool:
+ return all(
+ a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)
+ )
+
+ def __bool__(self):
+ __tracebackhide__ = True
+ raise AssertionError(
+ "approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?"
+ )
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ def __ne__(self, actual) -> bool:
+ return not (actual == self)
+
+ def _approx_scalar(self, x) -> "ApproxScalar":
+ if isinstance(x, Decimal):
+ return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
+ return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
+
+ def _yield_comparisons(self, actual):
+ """Yield all the pairs of numbers to be compared.
+
+ This is used to implement the `__eq__` method.
+ """
+ raise NotImplementedError
+
+ def _check_type(self) -> None:
+ """Raise a TypeError if the expected value is not a valid type."""
+ # This is only a concern if the expected value is a sequence. In every
+ # other case, the approx() function ensures that the expected value has
+ # a numeric type. For this reason, the default is to do nothing. The
+ # classes that deal with sequences should reimplement this method to
+ # raise if there are any non-numeric elements in the sequence.
+
+
+def _recursive_sequence_map(f, x):
+ """Recursively map a function over a sequence of arbitrary depth"""
+ if isinstance(x, (list, tuple)):
+ seq_type = type(x)
+ return seq_type(_recursive_sequence_map(f, xi) for xi in x)
+ else:
+ return f(x)
+
+
+class ApproxNumpy(ApproxBase):
+ """Perform approximate comparisons where the expected value is numpy array."""
+
+ def __repr__(self) -> str:
+ list_scalars = _recursive_sequence_map(
+ self._approx_scalar, self.expected.tolist()
+ )
+ return f"approx({list_scalars!r})"
+
+ def _repr_compare(self, other_side: "ndarray") -> List[str]:
+ import itertools
+ import math
+
+ def get_value_from_nested_list(
+ nested_list: List[Any], nd_index: Tuple[Any, ...]
+ ) -> Any:
+ """
+ Helper function to get the value out of a nested list, given an n-dimensional index.
+ This mimics numpy's indexing, but for raw nested python lists.
+ """
+ value: Any = nested_list
+ for i in nd_index:
+ value = value[i]
+ return value
+
+ np_array_shape = self.expected.shape
+ approx_side_as_seq = _recursive_sequence_map(
+ self._approx_scalar, self.expected.tolist()
+ )
+
+ if np_array_shape != other_side.shape:
+ return [
+ "Impossible to compare arrays with different shapes.",
+ f"Shapes: {np_array_shape} and {other_side.shape}",
+ ]
+
+ number_of_elements = self.expected.size
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for index in itertools.product(*(range(i) for i in np_array_shape)):
+ approx_value = get_value_from_nested_list(approx_side_as_seq, index)
+ other_value = get_value_from_nested_list(other_side, index)
+ if approx_value != other_value:
+ abs_diff = abs(approx_value.expected - other_value)
+ max_abs_diff = max(max_abs_diff, abs_diff)
+ if other_value == 0.0:
+ max_rel_diff = math.inf
+ else:
+ max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
+ different_ids.append(index)
+
+ message_data = [
+ (
+ str(index),
+ str(get_value_from_nested_list(other_side, index)),
+ str(get_value_from_nested_list(approx_side_as_seq, index)),
+ )
+ for index in different_ids
+ ]
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ import numpy as np
+
+ # self.expected is supposed to always be an array here.
+
+ if not np.isscalar(actual):
+ try:
+ actual = np.asarray(actual)
+ except Exception as e:
+ raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e
+
+ if not np.isscalar(actual) and actual.shape != self.expected.shape:
+ return False
+
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ import numpy as np
+
+ # `actual` can either be a numpy array or a scalar, it is treated in
+ # `__eq__` before being passed to `ApproxBase.__eq__`, which is the
+ # only method that calls this one.
+
+ if np.isscalar(actual):
+ for i in np.ndindex(self.expected.shape):
+ yield actual, self.expected[i].item()
+ else:
+ for i in np.ndindex(self.expected.shape):
+ yield actual[i].item(), self.expected[i].item()
+
+
+class ApproxMapping(ApproxBase):
+ """Perform approximate comparisons where the expected value is a mapping
+ with numeric values (the keys can be anything)."""
+
+ def __repr__(self) -> str:
+ return "approx({!r})".format(
+ {k: self._approx_scalar(v) for k, v in self.expected.items()}
+ )
+
+ def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
+ import math
+
+ approx_side_as_map = {
+ k: self._approx_scalar(v) for k, v in self.expected.items()
+ }
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for (approx_key, approx_value), other_value in zip(
+ approx_side_as_map.items(), other_side.values()
+ ):
+ if approx_value != other_value:
+ if approx_value.expected is not None and other_value is not None:
+ max_abs_diff = max(
+ max_abs_diff, abs(approx_value.expected - other_value)
+ )
+ if approx_value.expected == 0.0:
+ max_rel_diff = math.inf
+ else:
+ max_rel_diff = max(
+ max_rel_diff,
+ abs(
+ (approx_value.expected - other_value)
+ / approx_value.expected
+ ),
+ )
+ different_ids.append(approx_key)
+
+ message_data = [
+ (str(key), str(other_side[key]), str(approx_side_as_map[key]))
+ for key in different_ids
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ try:
+ if set(actual.keys()) != set(self.expected.keys()):
+ return False
+ except AttributeError:
+ return False
+
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ for k in self.expected.keys():
+ yield actual[k], self.expected[k]
+
+ def _check_type(self) -> None:
+ __tracebackhide__ = True
+ for key, value in self.expected.items():
+ if isinstance(value, type(self.expected)):
+ msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
+ raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
+
+
+class ApproxSequenceLike(ApproxBase):
+ """Perform approximate comparisons where the expected value is a sequence of numbers."""
+
+ def __repr__(self) -> str:
+ seq_type = type(self.expected)
+ if seq_type not in (tuple, list):
+ seq_type = list
+ return "approx({!r})".format(
+ seq_type(self._approx_scalar(x) for x in self.expected)
+ )
+
+ def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
+ import math
+
+ if len(self.expected) != len(other_side):
+ return [
+ "Impossible to compare lists with different sizes.",
+ f"Lengths: {len(self.expected)} and {len(other_side)}",
+ ]
+
+ approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected)
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for i, (approx_value, other_value) in enumerate(
+ zip(approx_side_as_map, other_side)
+ ):
+ if approx_value != other_value:
+ abs_diff = abs(approx_value.expected - other_value)
+ max_abs_diff = max(max_abs_diff, abs_diff)
+ if other_value == 0.0:
+ max_rel_diff = math.inf
+ else:
+ max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
+ different_ids.append(i)
+
+ message_data = [
+ (str(i), str(other_side[i]), str(approx_side_as_map[i]))
+ for i in different_ids
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ try:
+ if len(actual) != len(self.expected):
+ return False
+ except TypeError:
+ return False
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ return zip(actual, self.expected)
+
+ def _check_type(self) -> None:
+ __tracebackhide__ = True
+ for index, x in enumerate(self.expected):
+ if isinstance(x, type(self.expected)):
+ msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
+ raise TypeError(msg.format(x, index, pprint.pformat(self.expected)))
+
+
+class ApproxScalar(ApproxBase):
+ """Perform approximate comparisons where the expected value is a single number."""
+
+ # Using Real should be better than this Union, but not possible yet:
+ # https://github.com/python/typeshed/pull/3108
+ DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12
+ DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6
+
+ def __repr__(self) -> str:
+ """Return a string communicating both the expected value and the
+ tolerance for the comparison being made.
+
+ For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
+ """
+ # Don't show a tolerance for values that aren't compared using
+ # tolerances, i.e. non-numerics and infinities. Need to call abs to
+ # handle complex numbers, e.g. (inf + 1j).
+ if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
+ abs(self.expected) # type: ignore[arg-type]
+ ):
+ return str(self.expected)
+
+ # If a sensible tolerance can't be calculated, self.tolerance will
+ # raise a ValueError. In this case, display '???'.
+ try:
+ vetted_tolerance = f"{self.tolerance:.1e}"
+ if (
+ isinstance(self.expected, Complex)
+ and self.expected.imag
+ and not math.isinf(self.tolerance)
+ ):
+ vetted_tolerance += " ∠ ±180°"
+ except ValueError:
+ vetted_tolerance = "???"
+
+ return f"{self.expected} ± {vetted_tolerance}"
+
+ def __eq__(self, actual) -> bool:
+ """Return whether the given value is equal to the expected value
+ within the pre-specified tolerance."""
+ asarray = _as_numpy_array(actual)
+ if asarray is not None:
+ # Call ``__eq__()`` manually to prevent infinite-recursion with
+ # numpy<1.13. See #3748.
+ return all(self.__eq__(a) for a in asarray.flat)
+
+ # Short-circuit exact equality.
+ if actual == self.expected:
+ return True
+
+ # If either type is non-numeric, fall back to strict equality.
+ # NB: we need Complex, rather than just Number, to ensure that __abs__,
+ # __sub__, and __float__ are defined.
+ if not (
+ isinstance(self.expected, (Complex, Decimal))
+ and isinstance(actual, (Complex, Decimal))
+ ):
+ return False
+
+ # Allow the user to control whether NaNs are considered equal to each
+ # other or not. The abs() calls are for compatibility with complex
+ # numbers.
+ if math.isnan(abs(self.expected)): # type: ignore[arg-type]
+ return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
+
+ # Infinity shouldn't be approximately equal to anything but itself, but
+ # if there's a relative tolerance, it will be infinite and infinity
+ # will seem approximately equal to everything. The equal-to-itself
+ # case would have been short circuited above, so here we can just
+ # return false if the expected value is infinite. The abs() call is
+ # for compatibility with complex numbers.
+ if math.isinf(abs(self.expected)): # type: ignore[arg-type]
+ return False
+
+ # Return true if the two numbers are within the tolerance.
+ result: bool = abs(self.expected - actual) <= self.tolerance
+ return result
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @property
+ def tolerance(self):
+ """Return the tolerance for the comparison.
+
+ This could be either an absolute tolerance or a relative tolerance,
+ depending on what the user specified or which would be larger.
+ """
+
+ def set_default(x, default):
+ return x if x is not None else default
+
+ # Figure out what the absolute tolerance should be. ``self.abs`` is
+ # either None or a value specified by the user.
+ absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE)
+
+ if absolute_tolerance < 0:
+ raise ValueError(
+ f"absolute tolerance can't be negative: {absolute_tolerance}"
+ )
+ if math.isnan(absolute_tolerance):
+ raise ValueError("absolute tolerance can't be NaN.")
+
+ # If the user specified an absolute tolerance but not a relative one,
+ # just return the absolute tolerance.
+ if self.rel is None:
+ if self.abs is not None:
+ return absolute_tolerance
+
+ # Figure out what the relative tolerance should be. ``self.rel`` is
+ # either None or a value specified by the user. This is done after
+ # we've made sure the user didn't ask for an absolute tolerance only,
+ # because we don't want to raise errors about the relative tolerance if
+ # we aren't even going to use it.
+ relative_tolerance = set_default(
+ self.rel, self.DEFAULT_RELATIVE_TOLERANCE
+ ) * abs(self.expected)
+
+ if relative_tolerance < 0:
+ raise ValueError(
+ f"relative tolerance can't be negative: {relative_tolerance}"
+ )
+ if math.isnan(relative_tolerance):
+ raise ValueError("relative tolerance can't be NaN.")
+
+ # Return the larger of the relative and absolute tolerances.
+ return max(relative_tolerance, absolute_tolerance)
+
+
+class ApproxDecimal(ApproxScalar):
+ """Perform approximate comparisons where the expected value is a Decimal."""
+
+ DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
+ DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
+
+
+def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
+ """Assert that two numbers (or two ordered sequences of numbers) are equal to each other
+ within some tolerance.
+
+ Due to the :doc:`python:tutorial/floatingpoint`, numbers that we
+ would intuitively expect to be equal are not always so::
+
+ >>> 0.1 + 0.2 == 0.3
+ False
+
+ This problem is commonly encountered when writing tests, e.g. when making
+ sure that floating-point values are what you expect them to be. One way to
+ deal with this problem is to assert that two floating-point numbers are
+ equal to within some appropriate tolerance::
+
+ >>> abs((0.1 + 0.2) - 0.3) < 1e-6
+ True
+
+ However, comparisons like this are tedious to write and difficult to
+ understand. Furthermore, absolute comparisons like the one above are
+ usually discouraged because there's no tolerance that works well for all
+ situations. ``1e-6`` is good for numbers around ``1``, but too small for
+ very big numbers and too big for very small ones. It's better to express
+ the tolerance as a fraction of the expected value, but relative comparisons
+ like that are even more difficult to write correctly and concisely.
+
+ The ``approx`` class performs floating-point comparisons using a syntax
+ that's as intuitive as possible::
+
+ >>> from pytest import approx
+ >>> 0.1 + 0.2 == approx(0.3)
+ True
+
+ The same syntax also works for ordered sequences of numbers::
+
+ >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
+ True
+
+ ``numpy`` arrays::
+
+ >>> import numpy as np # doctest: +SKIP
+ >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP
+ True
+
+ And for a ``numpy`` array against a scalar::
+
+ >>> import numpy as np # doctest: +SKIP
+ >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
+ True
+
+ Only ordered sequences are supported, because ``approx`` needs
+ to infer the relative position of the sequences without ambiguity. This means
+ ``sets`` and other unordered sequences are not supported.
+
+ Finally, dictionary *values* can also be compared::
+
+ >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
+ True
+
+ The comparison will be true if both mappings have the same keys and their
+ respective values match the expected tolerances.
+
+ **Tolerances**
+
+ By default, ``approx`` considers numbers within a relative tolerance of
+ ``1e-6`` (i.e. one part in a million) of its expected value to be equal.
+ This treatment would lead to surprising results if the expected value was
+ ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``.
+ To handle this case less surprisingly, ``approx`` also considers numbers
+ within an absolute tolerance of ``1e-12`` of its expected value to be
+ equal. Infinity and NaN are special cases. Infinity is only considered
+ equal to itself, regardless of the relative tolerance. NaN is not
+ considered equal to anything by default, but you can make it be equal to
+ itself by setting the ``nan_ok`` argument to True. (This is meant to
+ facilitate comparing arrays that use NaN to mean "no data".)
+
+ Both the relative and absolute tolerances can be changed by passing
+ arguments to the ``approx`` constructor::
+
+ >>> 1.0001 == approx(1)
+ False
+ >>> 1.0001 == approx(1, rel=1e-3)
+ True
+ >>> 1.0001 == approx(1, abs=1e-3)
+ True
+
+ If you specify ``abs`` but not ``rel``, the comparison will not consider
+ the relative tolerance at all. In other words, two numbers that are within
+ the default relative tolerance of ``1e-6`` will still be considered unequal
+ if they exceed the specified absolute tolerance. If you specify both
+ ``abs`` and ``rel``, the numbers will be considered equal if either
+ tolerance is met::
+
+ >>> 1 + 1e-8 == approx(1)
+ True
+ >>> 1 + 1e-8 == approx(1, abs=1e-12)
+ False
+ >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
+ True
+
+ You can also use ``approx`` to compare nonnumeric types, or dicts and
+ sequences containing nonnumeric types, in which case it falls back to
+ strict equality. This can be useful for comparing dicts and sequences that
+ can contain optional values::
+
+ >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None})
+ True
+ >>> [None, 1.0000005] == approx([None,1])
+ True
+ >>> ["foo", 1.0000005] == approx([None,1])
+ False
+
+ If you're thinking about using ``approx``, then you might want to know how
+ it compares to other good ways of comparing floating-point numbers. All of
+ these algorithms are based on relative and absolute tolerances and should
+ agree for the most part, but they do have meaningful differences:
+
+ - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative
+ tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute
+ tolerance is met. Because the relative tolerance is calculated w.r.t.
+ both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
+ ``b`` is a "reference value"). You have to specify an absolute tolerance
+ if you want to compare to ``0.0`` because there is no tolerance by
+ default. More information: :py:func:`math.isclose`.
+
+ - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference
+ between ``a`` and ``b`` is less that the sum of the relative tolerance
+ w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance
+ is only calculated w.r.t. ``b``, this test is asymmetric and you can
+ think of ``b`` as the reference value. Support for comparing sequences
+ is provided by :py:func:`numpy.allclose`. More information:
+ :std:doc:`numpy:reference/generated/numpy.isclose`.
+
+ - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
+ are within an absolute tolerance of ``1e-7``. No relative tolerance is
+ considered , so this function is not appropriate for very large or very
+ small numbers. Also, it's only available in subclasses of ``unittest.TestCase``
+ and it's ugly because it doesn't follow PEP8. More information:
+ :py:meth:`unittest.TestCase.assertAlmostEqual`.
+
+ - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative
+ tolerance is met w.r.t. ``b`` or if the absolute tolerance is met.
+ Because the relative tolerance is only calculated w.r.t. ``b``, this test
+ is asymmetric and you can think of ``b`` as the reference value. In the
+ special case that you explicitly specify an absolute tolerance but not a
+ relative tolerance, only the absolute tolerance is considered.
+
+ .. note::
+
+ ``approx`` can handle numpy arrays, but we recommend the
+ specialised test helpers in :std:doc:`numpy:reference/routines.testing`
+ if you need support for comparisons, NaNs, or ULP-based tolerances.
+
+ To match strings using regex, you can use
+ `Matches `_
+ from the
+ `re_assert package `_.
+
+ .. warning::
+
+ .. versionchanged:: 3.2
+
+ In order to avoid inconsistent behavior, :py:exc:`TypeError` is
+ raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.
+ The example below illustrates the problem::
+
+ assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10)
+ assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10)
+
+ In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)``
+ to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to
+ comparison. This is because the call hierarchy of rich comparisons
+ follows a fixed behavior. More information: :py:meth:`object.__ge__`
+
+ .. versionchanged:: 3.7.1
+ ``approx`` raises ``TypeError`` when it encounters a dict value or
+ sequence element of nonnumeric type.
+
+ .. versionchanged:: 6.1.0
+ ``approx`` falls back to strict equality for nonnumeric types instead
+ of raising ``TypeError``.
+ """
+
+ # Delegate the comparison to a class that knows how to deal with the type
+ # of the expected value (e.g. int, float, list, dict, numpy.array, etc).
+ #
+ # The primary responsibility of these classes is to implement ``__eq__()``
+ # and ``__repr__()``. The former is used to actually check if some
+ # "actual" value is equivalent to the given expected value within the
+ # allowed tolerance. The latter is used to show the user the expected
+ # value and tolerance, in the case that a test failed.
+ #
+ # The actual logic for making approximate comparisons can be found in
+ # ApproxScalar, which is used to compare individual numbers. All of the
+ # other Approx classes eventually delegate to this class. The ApproxBase
+ # class provides some convenient methods and overloads, but isn't really
+ # essential.
+
+ __tracebackhide__ = True
+
+ if isinstance(expected, Decimal):
+ cls: Type[ApproxBase] = ApproxDecimal
+ elif isinstance(expected, Mapping):
+ cls = ApproxMapping
+ elif _is_numpy_array(expected):
+ expected = _as_numpy_array(expected)
+ cls = ApproxNumpy
+ elif (
+ hasattr(expected, "__getitem__")
+ and isinstance(expected, Sized)
+ # Type ignored because the error is wrong -- not unreachable.
+ and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
+ ):
+ cls = ApproxSequenceLike
+ elif (
+ isinstance(expected, Collection)
+ # Type ignored because the error is wrong -- not unreachable.
+ and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
+ ):
+ msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
+ raise TypeError(msg)
+ else:
+ cls = ApproxScalar
+
+ return cls(expected, rel, abs, nan_ok)
+
+
+def _is_numpy_array(obj: object) -> bool:
+ """
+ Return true if the given object is implicitly convertible to ndarray,
+ and numpy is already imported.
+ """
+ return _as_numpy_array(obj) is not None
+
+
+def _as_numpy_array(obj: object) -> Optional["ndarray"]:
+ """
+ Return an ndarray if the given object is implicitly convertible to ndarray,
+ and numpy is already imported, otherwise None.
+ """
+ import sys
+
+ np: Any = sys.modules.get("numpy")
+ if np is not None:
+ # avoid infinite recursion on numpy scalars, which have __array__
+ if np.isscalar(obj):
+ return None
+ elif isinstance(obj, np.ndarray):
+ return obj
+ elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"):
+ return np.asarray(obj)
+ return None
+
+
+# builtin pytest.raises helper
+
+E = TypeVar("E", bound=BaseException)
+
+
+@overload
+def raises(
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+ *,
+ match: Optional[Union[str, Pattern[str]]] = ...,
+) -> "RaisesContext[E]":
+ ...
+
+
+@overload
+def raises( # noqa: F811
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+ func: Callable[..., Any],
+ *args: Any,
+ **kwargs: Any,
+) -> _pytest._code.ExceptionInfo[E]:
+ ...
+
+
+def raises( # noqa: F811
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
+) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
+ r"""Assert that a code block/function call raises an exception.
+
+ :param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
+ The expected exception type, or a tuple if one of multiple possible
+ exception types are expected.
+ :kwparam str | typing.Pattern[str] | None match:
+ If specified, a string containing a regular expression,
+ or a regular expression object, that is tested against the string
+ representation of the exception using :func:`re.search`.
+
+ To match a literal string that may contain :ref:`special characters
+ `, the pattern can first be escaped with :func:`re.escape`.
+
+ (This is only used when :py:func:`pytest.raises` is used as a context manager,
+ and passed through to the function otherwise.
+ When using :py:func:`pytest.raises` as a function, you can use:
+ ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
+
+ .. currentmodule:: _pytest._code
+
+ Use ``pytest.raises`` as a context manager, which will capture the exception of the given
+ type::
+
+ >>> import pytest
+ >>> with pytest.raises(ZeroDivisionError):
+ ... 1/0
+
+ If the code block does not raise the expected exception (``ZeroDivisionError`` in the example
+ above), or no exception at all, the check will fail instead.
+
+ You can also use the keyword argument ``match`` to assert that the
+ exception matches a text or regex::
+
+ >>> with pytest.raises(ValueError, match='must be 0 or None'):
+ ... raise ValueError("value must be 0 or None")
+
+ >>> with pytest.raises(ValueError, match=r'must be \d+$'):
+ ... raise ValueError("value must be 42")
+
+ The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
+ details of the captured exception::
+
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... raise ValueError("value must be 42")
+ >>> assert exc_info.type is ValueError
+ >>> assert exc_info.value.args[0] == "value must be 42"
+
+ .. note::
+
+ When using ``pytest.raises`` as a context manager, it's worthwhile to
+ note that normal context manager rules apply and that the exception
+ raised *must* be the final line in the scope of the context manager.
+ Lines of code after that, within the scope of the context manager will
+ not be executed. For example::
+
+ >>> value = 15
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... if value > 10:
+ ... raise ValueError("value must be <= 10")
+ ... assert exc_info.type is ValueError # this will not execute
+
+ Instead, the following approach must be taken (note the difference in
+ scope)::
+
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... if value > 10:
+ ... raise ValueError("value must be <= 10")
+ ...
+ >>> assert exc_info.type is ValueError
+
+ **Using with** ``pytest.mark.parametrize``
+
+ When using :ref:`pytest.mark.parametrize ref`
+ it is possible to parametrize tests such that
+ some runs raise an exception and others do not.
+
+ See :ref:`parametrizing_conditional_raising` for an example.
+
+ **Legacy form**
+
+ It is possible to specify a callable by passing a to-be-called lambda::
+
+ >>> raises(ZeroDivisionError, lambda: 1/0)
+
+
+ or you can specify an arbitrary callable with arguments::
+
+ >>> def f(x): return 1/x
+ ...
+ >>> raises(ZeroDivisionError, f, 0)
+
+ >>> raises(ZeroDivisionError, f, x=0)
+
+
+ The form above is fully supported but discouraged for new code because the
+ context manager form is regarded as more readable and less error-prone.
+
+ .. note::
+ Similar to caught exception objects in Python, explicitly clearing
+ local references to returned ``ExceptionInfo`` objects can
+ help the Python interpreter speed up its garbage collection.
+
+ Clearing those references breaks a reference cycle
+ (``ExceptionInfo`` --> caught exception --> frame stack raising
+ the exception --> current frame stack --> local variables -->
+ ``ExceptionInfo``) which makes Python keep all objects referenced
+ from that cycle (including all local variables in the current
+ frame) alive until the next cyclic garbage collection run.
+ More detailed information can be found in the official Python
+ documentation for :ref:`the try statement `.
+ """
+ __tracebackhide__ = True
+
+ if not expected_exception:
+ raise ValueError(
+ f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. "
+ f"Raising exceptions is already understood as failing the test, so you don't need "
+ f"any special code to say 'this should never raise an exception'."
+ )
+ if isinstance(expected_exception, type):
+ expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
+ else:
+ expected_exceptions = expected_exception
+ for exc in expected_exceptions:
+ if not isinstance(exc, type) or not issubclass(exc, BaseException):
+ msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
+ not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
+ raise TypeError(msg.format(not_a))
+
+ message = f"DID NOT RAISE {expected_exception}"
+
+ if not args:
+ match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None)
+ if kwargs:
+ msg = "Unexpected keyword arguments passed to pytest.raises: "
+ msg += ", ".join(sorted(kwargs))
+ msg += "\nUse context-manager form instead?"
+ raise TypeError(msg)
+ return RaisesContext(expected_exception, message, match)
+ else:
+ func = args[0]
+ if not callable(func):
+ raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
+ try:
+ func(*args[1:], **kwargs)
+ except expected_exception as e:
+ return _pytest._code.ExceptionInfo.from_exception(e)
+ fail(message)
+
+
+# This doesn't work with mypy for now. Use fail.Exception instead.
+raises.Exception = fail.Exception # type: ignore
+
+
+@final
+class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
+ def __init__(
+ self,
+ expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+ message: str,
+ match_expr: Optional[Union[str, Pattern[str]]] = None,
+ ) -> None:
+ self.expected_exception = expected_exception
+ self.message = message
+ self.match_expr = match_expr
+ self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None
+
+ def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
+ self.excinfo = _pytest._code.ExceptionInfo.for_later()
+ return self.excinfo
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> bool:
+ __tracebackhide__ = True
+ if exc_type is None:
+ fail(self.message)
+ assert self.excinfo is not None
+ if not issubclass(exc_type, self.expected_exception):
+ return False
+ # Cast to narrow the exception type now that it's verified.
+ exc_info = cast(Tuple[Type[E], E, TracebackType], (exc_type, exc_val, exc_tb))
+ self.excinfo.fill_unfilled(exc_info)
+ if self.match_expr is not None:
+ self.excinfo.match(self.match_expr)
+ return True
diff --git a/venv/lib/python3.12/site-packages/_pytest/python_path.py b/venv/lib/python3.12/site-packages/_pytest/python_path.py
new file mode 100644
index 0000000..cceabbc
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/python_path.py
@@ -0,0 +1,24 @@
+import sys
+
+import pytest
+from pytest import Config
+from pytest import Parser
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini("pythonpath", type="paths", help="Add paths to sys.path", default=[])
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_load_initial_conftests(early_config: Config) -> None:
+ # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
+ for path in reversed(early_config.getini("pythonpath")):
+ sys.path.insert(0, str(path))
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_unconfigure(config: Config) -> None:
+ for path in config.getini("pythonpath"):
+ path_str = str(path)
+ if path_str in sys.path:
+ sys.path.remove(path_str)
diff --git a/venv/lib/python3.12/site-packages/_pytest/recwarn.py b/venv/lib/python3.12/site-packages/_pytest/recwarn.py
new file mode 100644
index 0000000..d76ea02
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/recwarn.py
@@ -0,0 +1,313 @@
+"""Record warnings during test function execution."""
+import re
+import warnings
+from pprint import pformat
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Pattern
+from typing import Tuple
+from typing import Type
+from typing import TypeVar
+from typing import Union
+
+from _pytest.compat import final
+from _pytest.compat import overload
+from _pytest.deprecated import check_ispytest
+from _pytest.deprecated import WARNS_NONE_ARG
+from _pytest.fixtures import fixture
+from _pytest.outcomes import fail
+
+
+T = TypeVar("T")
+
+
+@fixture
+def recwarn() -> Generator["WarningsRecorder", None, None]:
+ """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
+
+ See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
+ on warning categories.
+ """
+ wrec = WarningsRecorder(_ispytest=True)
+ with wrec:
+ warnings.simplefilter("default")
+ yield wrec
+
+
+@overload
+def deprecated_call(
+ *, match: Optional[Union[str, Pattern[str]]] = ...
+) -> "WarningsRecorder":
+ ...
+
+
+@overload
+def deprecated_call( # noqa: F811
+ func: Callable[..., T], *args: Any, **kwargs: Any
+) -> T:
+ ...
+
+
+def deprecated_call( # noqa: F811
+ func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
+) -> Union["WarningsRecorder", Any]:
+ """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning``.
+
+ This function can be used as a context manager::
+
+ >>> import warnings
+ >>> def api_call_v2():
+ ... warnings.warn('use v3 of this api', DeprecationWarning)
+ ... return 200
+
+ >>> import pytest
+ >>> with pytest.deprecated_call():
+ ... assert api_call_v2() == 200
+
+ It can also be used by passing a function and ``*args`` and ``**kwargs``,
+ in which case it will ensure calling ``func(*args, **kwargs)`` produces one of
+ the warnings types above. The return value is the return value of the function.
+
+ In the context manager form you may use the keyword argument ``match`` to assert
+ that the warning matches a text or regex.
+
+ The context manager produces a list of :class:`warnings.WarningMessage` objects,
+ one for each warning raised.
+ """
+ __tracebackhide__ = True
+ if func is not None:
+ args = (func,) + args
+ return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
+
+
+@overload
+def warns(
+ expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
+ *,
+ match: Optional[Union[str, Pattern[str]]] = ...,
+) -> "WarningsChecker":
+ ...
+
+
+@overload
+def warns( # noqa: F811
+ expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]],
+ func: Callable[..., T],
+ *args: Any,
+ **kwargs: Any,
+) -> T:
+ ...
+
+
+def warns( # noqa: F811
+ expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
+ *args: Any,
+ match: Optional[Union[str, Pattern[str]]] = None,
+ **kwargs: Any,
+) -> Union["WarningsChecker", Any]:
+ r"""Assert that code raises a particular class of warning.
+
+ Specifically, the parameter ``expected_warning`` can be a warning class or sequence
+ of warning classes, and the code inside the ``with`` block must issue at least one
+ warning of that class or classes.
+
+ This helper produces a list of :class:`warnings.WarningMessage` objects, one for
+ each warning raised (regardless of whether it is an ``expected_warning`` or not).
+
+ This function can be used as a context manager, which will capture all the raised
+ warnings inside it::
+
+ >>> import pytest
+ >>> with pytest.warns(RuntimeWarning):
+ ... warnings.warn("my warning", RuntimeWarning)
+
+ In the context manager form you may use the keyword argument ``match`` to assert
+ that the warning matches a text or regex::
+
+ >>> with pytest.warns(UserWarning, match='must be 0 or None'):
+ ... warnings.warn("value must be 0 or None", UserWarning)
+
+ >>> with pytest.warns(UserWarning, match=r'must be \d+$'):
+ ... warnings.warn("value must be 42", UserWarning)
+
+ >>> with pytest.warns(UserWarning, match=r'must be \d+$'):
+ ... warnings.warn("this is not here", UserWarning)
+ Traceback (most recent call last):
+ ...
+ Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
+
+ **Using with** ``pytest.mark.parametrize``
+
+ When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests
+ such that some runs raise a warning and others do not.
+
+ This could be achieved in the same way as with exceptions, see
+ :ref:`parametrizing_conditional_raising` for an example.
+
+ """
+ __tracebackhide__ = True
+ if not args:
+ if kwargs:
+ argnames = ", ".join(sorted(kwargs))
+ raise TypeError(
+ f"Unexpected keyword arguments passed to pytest.warns: {argnames}"
+ "\nUse context-manager form instead?"
+ )
+ return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
+ else:
+ func = args[0]
+ if not callable(func):
+ raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
+ with WarningsChecker(expected_warning, _ispytest=True):
+ return func(*args[1:], **kwargs)
+
+
+class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
+ """A context manager to record raised warnings.
+
+ Each recorded warning is an instance of :class:`warnings.WarningMessage`.
+
+ Adapted from `warnings.catch_warnings`.
+
+ .. note::
+ ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
+ differently; see :ref:`ensuring_function_triggers`.
+
+ """
+
+ def __init__(self, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ # Type ignored due to the way typeshed handles warnings.catch_warnings.
+ super().__init__(record=True) # type: ignore[call-arg]
+ self._entered = False
+ self._list: List[warnings.WarningMessage] = []
+
+ @property
+ def list(self) -> List["warnings.WarningMessage"]:
+ """The list of recorded warnings."""
+ return self._list
+
+ def __getitem__(self, i: int) -> "warnings.WarningMessage":
+ """Get a recorded warning by index."""
+ return self._list[i]
+
+ def __iter__(self) -> Iterator["warnings.WarningMessage"]:
+ """Iterate through the recorded warnings."""
+ return iter(self._list)
+
+ def __len__(self) -> int:
+ """The number of recorded warnings."""
+ return len(self._list)
+
+ def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage":
+ """Pop the first recorded warning, raise exception if not exists."""
+ for i, w in enumerate(self._list):
+ if issubclass(w.category, cls):
+ return self._list.pop(i)
+ __tracebackhide__ = True
+ raise AssertionError(f"{cls!r} not found in warning list")
+
+ def clear(self) -> None:
+ """Clear the list of recorded warnings."""
+ self._list[:] = []
+
+ # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__
+ # -- it returns a List but we only emulate one.
+ def __enter__(self) -> "WarningsRecorder": # type: ignore
+ if self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError(f"Cannot enter {self!r} twice")
+ _list = super().__enter__()
+ # record=True means it's None.
+ assert _list is not None
+ self._list = _list
+ warnings.simplefilter("always")
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ if not self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError(f"Cannot exit {self!r} without entering first")
+
+ super().__exit__(exc_type, exc_val, exc_tb)
+
+ # Built-in catch_warnings does not reset entered state so we do it
+ # manually here for this context manager to become reusable.
+ self._entered = False
+
+
+@final
+class WarningsChecker(WarningsRecorder):
+ def __init__(
+ self,
+ expected_warning: Optional[
+ Union[Type[Warning], Tuple[Type[Warning], ...]]
+ ] = Warning,
+ match_expr: Optional[Union[str, Pattern[str]]] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ super().__init__(_ispytest=True)
+
+ msg = "exceptions must be derived from Warning, not %s"
+ if expected_warning is None:
+ warnings.warn(WARNS_NONE_ARG, stacklevel=4)
+ expected_warning_tup = None
+ elif isinstance(expected_warning, tuple):
+ for exc in expected_warning:
+ if not issubclass(exc, Warning):
+ raise TypeError(msg % type(exc))
+ expected_warning_tup = expected_warning
+ elif issubclass(expected_warning, Warning):
+ expected_warning_tup = (expected_warning,)
+ else:
+ raise TypeError(msg % type(expected_warning))
+
+ self.expected_warning = expected_warning_tup
+ self.match_expr = match_expr
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ super().__exit__(exc_type, exc_val, exc_tb)
+
+ __tracebackhide__ = True
+
+ def found_str():
+ return pformat([record.message for record in self], indent=2)
+
+ # only check if we're not currently handling an exception
+ if exc_type is None and exc_val is None and exc_tb is None:
+ if self.expected_warning is not None:
+ if not any(issubclass(r.category, self.expected_warning) for r in self):
+ __tracebackhide__ = True
+ fail(
+ f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
+ f"The list of emitted warnings is: {found_str()}."
+ )
+ elif self.match_expr is not None:
+ for r in self:
+ if issubclass(r.category, self.expected_warning):
+ if re.compile(self.match_expr).search(str(r.message)):
+ break
+ else:
+ fail(
+ f"""\
+DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.
+ Regex: {self.match_expr}
+ Emitted warnings: {found_str()}"""
+ )
diff --git a/venv/lib/python3.12/site-packages/_pytest/reports.py b/venv/lib/python3.12/site-packages/_pytest/reports.py
new file mode 100644
index 0000000..74e8794
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/reports.py
@@ -0,0 +1,622 @@
+import dataclasses
+import os
+from io import StringIO
+from pprint import pprint
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import NoReturn
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ExceptionRepr
+from _pytest._code.code import ReprEntry
+from _pytest._code.code import ReprEntryNative
+from _pytest._code.code import ReprExceptionInfo
+from _pytest._code.code import ReprFileLocation
+from _pytest._code.code import ReprFuncArgs
+from _pytest._code.code import ReprLocals
+from _pytest._code.code import ReprTraceback
+from _pytest._code.code import TerminalRepr
+from _pytest._io import TerminalWriter
+from _pytest.compat import final
+from _pytest.config import Config
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import skip
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from _pytest.runner import CallInfo
+
+
+def getworkerinfoline(node):
+ try:
+ return node._workerinfocache
+ except AttributeError:
+ d = node.workerinfo
+ ver = "%s.%s.%s" % d["version_info"][:3]
+ node._workerinfocache = s = "[{}] {} -- Python {} {}".format(
+ d["id"], d["sysplatform"], ver, d["executable"]
+ )
+ return s
+
+
+_R = TypeVar("_R", bound="BaseReport")
+
+
+class BaseReport:
+ when: Optional[str]
+ location: Optional[Tuple[str, Optional[int], str]]
+ longrepr: Union[
+ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+ ]
+ sections: List[Tuple[str, str]]
+ nodeid: str
+ outcome: "Literal['passed', 'failed', 'skipped']"
+
+ def __init__(self, **kw: Any) -> None:
+ self.__dict__.update(kw)
+
+ if TYPE_CHECKING:
+ # Can have arbitrary fields given to __init__().
+ def __getattr__(self, key: str) -> Any:
+ ...
+
+ def toterminal(self, out: TerminalWriter) -> None:
+ if hasattr(self, "node"):
+ worker_info = getworkerinfoline(self.node)
+ if worker_info:
+ out.line(worker_info)
+
+ longrepr = self.longrepr
+ if longrepr is None:
+ return
+
+ if hasattr(longrepr, "toterminal"):
+ longrepr_terminal = cast(TerminalRepr, longrepr)
+ longrepr_terminal.toterminal(out)
+ else:
+ try:
+ s = str(longrepr)
+ except UnicodeEncodeError:
+ s = ""
+ out.line(s)
+
+ def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]:
+ for name, content in self.sections:
+ if name.startswith(prefix):
+ yield prefix, content
+
+ @property
+ def longreprtext(self) -> str:
+ """Read-only property that returns the full string representation of
+ ``longrepr``.
+
+ .. versionadded:: 3.0
+ """
+ file = StringIO()
+ tw = TerminalWriter(file)
+ tw.hasmarkup = False
+ self.toterminal(tw)
+ exc = file.getvalue()
+ return exc.strip()
+
+ @property
+ def caplog(self) -> str:
+ """Return captured log lines, if log capturing is enabled.
+
+ .. versionadded:: 3.5
+ """
+ return "\n".join(
+ content for (prefix, content) in self.get_sections("Captured log")
+ )
+
+ @property
+ def capstdout(self) -> str:
+ """Return captured text from stdout, if capturing is enabled.
+
+ .. versionadded:: 3.0
+ """
+ return "".join(
+ content for (prefix, content) in self.get_sections("Captured stdout")
+ )
+
+ @property
+ def capstderr(self) -> str:
+ """Return captured text from stderr, if capturing is enabled.
+
+ .. versionadded:: 3.0
+ """
+ return "".join(
+ content for (prefix, content) in self.get_sections("Captured stderr")
+ )
+
+ @property
+ def passed(self) -> bool:
+ """Whether the outcome is passed."""
+ return self.outcome == "passed"
+
+ @property
+ def failed(self) -> bool:
+ """Whether the outcome is failed."""
+ return self.outcome == "failed"
+
+ @property
+ def skipped(self) -> bool:
+ """Whether the outcome is skipped."""
+ return self.outcome == "skipped"
+
+ @property
+ def fspath(self) -> str:
+ """The path portion of the reported node, as a string."""
+ return self.nodeid.split("::")[0]
+
+ @property
+ def count_towards_summary(self) -> bool:
+ """**Experimental** Whether this report should be counted towards the
+ totals shown at the end of the test session: "1 passed, 1 failure, etc".
+
+ .. note::
+
+ This function is considered **experimental**, so beware that it is subject to changes
+ even in patch releases.
+ """
+ return True
+
+ @property
+ def head_line(self) -> Optional[str]:
+ """**Experimental** The head line shown with longrepr output for this
+ report, more commonly during traceback representation during
+ failures::
+
+ ________ Test.foo ________
+
+
+ In the example above, the head_line is "Test.foo".
+
+ .. note::
+
+ This function is considered **experimental**, so beware that it is subject to changes
+ even in patch releases.
+ """
+ if self.location is not None:
+ fspath, lineno, domain = self.location
+ return domain
+ return None
+
+ def _get_verbose_word(self, config: Config):
+ _category, _short, verbose = config.hook.pytest_report_teststatus(
+ report=self, config=config
+ )
+ return verbose
+
+ def _to_json(self) -> Dict[str, Any]:
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+
+ Experimental method.
+ """
+ return _report_to_json(self)
+
+ @classmethod
+ def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R:
+ """Create either a TestReport or CollectReport, depending on the calling class.
+
+ It is the callers responsibility to know which class to pass here.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+
+ Experimental method.
+ """
+ kwargs = _report_kwargs_from_json(reportdict)
+ return cls(**kwargs)
+
+
+def _report_unserialization_failure(
+ type_name: str, report_class: Type[BaseReport], reportdict
+) -> NoReturn:
+ url = "https://github.com/pytest-dev/pytest/issues"
+ stream = StringIO()
+ pprint("-" * 100, stream=stream)
+ pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
+ pprint("report_name: %s" % report_class, stream=stream)
+ pprint(reportdict, stream=stream)
+ pprint("Please report this bug at %s" % url, stream=stream)
+ pprint("-" * 100, stream=stream)
+ raise RuntimeError(stream.getvalue())
+
+
+@final
+class TestReport(BaseReport):
+ """Basic test report object (also used for setup and teardown calls if
+ they fail).
+
+ Reports can contain arbitrary extra attributes.
+ """
+
+ __test__ = False
+
+ def __init__(
+ self,
+ nodeid: str,
+ location: Tuple[str, Optional[int], str],
+ keywords: Mapping[str, Any],
+ outcome: "Literal['passed', 'failed', 'skipped']",
+ longrepr: Union[
+ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+ ],
+ when: "Literal['setup', 'call', 'teardown']",
+ sections: Iterable[Tuple[str, str]] = (),
+ duration: float = 0,
+ start: float = 0,
+ stop: float = 0,
+ user_properties: Optional[Iterable[Tuple[str, object]]] = None,
+ **extra,
+ ) -> None:
+ #: Normalized collection nodeid.
+ self.nodeid = nodeid
+
+ #: A (filesystempath, lineno, domaininfo) tuple indicating the
+ #: actual location of a test item - it might be different from the
+ #: collected one e.g. if a method is inherited from a different module.
+ #: The filesystempath may be relative to ``config.rootdir``.
+ #: The line number is 0-based.
+ self.location: Tuple[str, Optional[int], str] = location
+
+ #: A name -> value dictionary containing all keywords and
+ #: markers associated with a test invocation.
+ self.keywords: Mapping[str, Any] = keywords
+
+ #: Test outcome, always one of "passed", "failed", "skipped".
+ self.outcome = outcome
+
+ #: None or a failure representation.
+ self.longrepr = longrepr
+
+ #: One of 'setup', 'call', 'teardown' to indicate runtest phase.
+ self.when = when
+
+ #: User properties is a list of tuples (name, value) that holds user
+ #: defined properties of the test.
+ self.user_properties = list(user_properties or [])
+
+ #: Tuples of str ``(heading, content)`` with extra information
+ #: for the test report. Used by pytest to add text captured
+ #: from ``stdout``, ``stderr``, and intercepted logging events. May
+ #: be used by other plugins to add arbitrary information to reports.
+ self.sections = list(sections)
+
+ #: Time it took to run just the test.
+ self.duration: float = duration
+
+ #: The system time when the call started, in seconds since the epoch.
+ self.start: float = start
+ #: The system time when the call ended, in seconds since the epoch.
+ self.stop: float = stop
+
+ self.__dict__.update(extra)
+
+ def __repr__(self) -> str:
+ return "<{} {!r} when={!r} outcome={!r}>".format(
+ self.__class__.__name__, self.nodeid, self.when, self.outcome
+ )
+
+ @classmethod
+ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
+ """Create and fill a TestReport with standard item and call info.
+
+ :param item: The item.
+ :param call: The call info.
+ """
+ when = call.when
+ # Remove "collect" from the Literal type -- only for collection calls.
+ assert when != "collect"
+ duration = call.duration
+ start = call.start
+ stop = call.stop
+ keywords = {x: 1 for x in item.keywords}
+ excinfo = call.excinfo
+ sections = []
+ if not call.excinfo:
+ outcome: Literal["passed", "failed", "skipped"] = "passed"
+ longrepr: Union[
+ None,
+ ExceptionInfo[BaseException],
+ Tuple[str, int, str],
+ str,
+ TerminalRepr,
+ ] = None
+ else:
+ if not isinstance(excinfo, ExceptionInfo):
+ outcome = "failed"
+ longrepr = excinfo
+ elif isinstance(excinfo.value, skip.Exception):
+ outcome = "skipped"
+ r = excinfo._getreprcrash()
+ assert (
+ r is not None
+ ), "There should always be a traceback entry for skipping a test."
+ if excinfo.value._use_item_location:
+ path, line = item.reportinfo()[:2]
+ assert line is not None
+ longrepr = os.fspath(path), line + 1, r.message
+ else:
+ longrepr = (str(r.path), r.lineno, r.message)
+ else:
+ outcome = "failed"
+ if call.when == "call":
+ longrepr = item.repr_failure(excinfo)
+ else: # exception in setup or teardown
+ longrepr = item._repr_failure_py(
+ excinfo, style=item.config.getoption("tbstyle", "auto")
+ )
+ for rwhen, key, content in item._report_sections:
+ sections.append((f"Captured {key} {rwhen}", content))
+ return cls(
+ item.nodeid,
+ item.location,
+ keywords,
+ outcome,
+ longrepr,
+ when,
+ sections,
+ duration,
+ start,
+ stop,
+ user_properties=item.user_properties,
+ )
+
+
+@final
+class CollectReport(BaseReport):
+ """Collection report object.
+
+ Reports can contain arbitrary extra attributes.
+ """
+
+ when = "collect"
+
+ def __init__(
+ self,
+ nodeid: str,
+ outcome: "Literal['passed', 'failed', 'skipped']",
+ longrepr: Union[
+ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
+ ],
+ result: Optional[List[Union[Item, Collector]]],
+ sections: Iterable[Tuple[str, str]] = (),
+ **extra,
+ ) -> None:
+ #: Normalized collection nodeid.
+ self.nodeid = nodeid
+
+ #: Test outcome, always one of "passed", "failed", "skipped".
+ self.outcome = outcome
+
+ #: None or a failure representation.
+ self.longrepr = longrepr
+
+ #: The collected items and collection nodes.
+ self.result = result or []
+
+ #: Tuples of str ``(heading, content)`` with extra information
+ #: for the test report. Used by pytest to add text captured
+ #: from ``stdout``, ``stderr``, and intercepted logging events. May
+ #: be used by other plugins to add arbitrary information to reports.
+ self.sections = list(sections)
+
+ self.__dict__.update(extra)
+
+ @property
+ def location( # type:ignore[override]
+ self,
+ ) -> Optional[Tuple[str, Optional[int], str]]:
+ return (self.fspath, None, self.fspath)
+
+ def __repr__(self) -> str:
+ return "".format(
+ self.nodeid, len(self.result), self.outcome
+ )
+
+
+class CollectErrorRepr(TerminalRepr):
+ def __init__(self, msg: str) -> None:
+ self.longrepr = msg
+
+ def toterminal(self, out: TerminalWriter) -> None:
+ out.line(self.longrepr, red=True)
+
+
+def pytest_report_to_serializable(
+ report: Union[CollectReport, TestReport]
+) -> Optional[Dict[str, Any]]:
+ if isinstance(report, (TestReport, CollectReport)):
+ data = report._to_json()
+ data["$report_type"] = report.__class__.__name__
+ return data
+ # TODO: Check if this is actually reachable.
+ return None # type: ignore[unreachable]
+
+
+def pytest_report_from_serializable(
+ data: Dict[str, Any],
+) -> Optional[Union[CollectReport, TestReport]]:
+ if "$report_type" in data:
+ if data["$report_type"] == "TestReport":
+ return TestReport._from_json(data)
+ elif data["$report_type"] == "CollectReport":
+ return CollectReport._from_json(data)
+ assert False, "Unknown report_type unserialize data: {}".format(
+ data["$report_type"]
+ )
+ return None
+
+
+def _report_to_json(report: BaseReport) -> Dict[str, Any]:
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+ """
+
+ def serialize_repr_entry(
+ entry: Union[ReprEntry, ReprEntryNative]
+ ) -> Dict[str, Any]:
+ data = dataclasses.asdict(entry)
+ for key, value in data.items():
+ if hasattr(value, "__dict__"):
+ data[key] = dataclasses.asdict(value)
+ entry_data = {"type": type(entry).__name__, "data": data}
+ return entry_data
+
+ def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
+ result = dataclasses.asdict(reprtraceback)
+ result["reprentries"] = [
+ serialize_repr_entry(x) for x in reprtraceback.reprentries
+ ]
+ return result
+
+ def serialize_repr_crash(
+ reprcrash: Optional[ReprFileLocation],
+ ) -> Optional[Dict[str, Any]]:
+ if reprcrash is not None:
+ return dataclasses.asdict(reprcrash)
+ else:
+ return None
+
+ def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
+ assert rep.longrepr is not None
+ # TODO: Investigate whether the duck typing is really necessary here.
+ longrepr = cast(ExceptionRepr, rep.longrepr)
+ result: Dict[str, Any] = {
+ "reprcrash": serialize_repr_crash(longrepr.reprcrash),
+ "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
+ "sections": longrepr.sections,
+ }
+ if isinstance(longrepr, ExceptionChainRepr):
+ result["chain"] = []
+ for repr_traceback, repr_crash, description in longrepr.chain:
+ result["chain"].append(
+ (
+ serialize_repr_traceback(repr_traceback),
+ serialize_repr_crash(repr_crash),
+ description,
+ )
+ )
+ else:
+ result["chain"] = None
+ return result
+
+ d = report.__dict__.copy()
+ if hasattr(report.longrepr, "toterminal"):
+ if hasattr(report.longrepr, "reprtraceback") and hasattr(
+ report.longrepr, "reprcrash"
+ ):
+ d["longrepr"] = serialize_exception_longrepr(report)
+ else:
+ d["longrepr"] = str(report.longrepr)
+ else:
+ d["longrepr"] = report.longrepr
+ for name in d:
+ if isinstance(d[name], os.PathLike):
+ d[name] = os.fspath(d[name])
+ elif name == "result":
+ d[name] = None # for now
+ return d
+
+
+def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
+ """Return **kwargs that can be used to construct a TestReport or
+ CollectReport instance.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+ """
+
+ def deserialize_repr_entry(entry_data):
+ data = entry_data["data"]
+ entry_type = entry_data["type"]
+ if entry_type == "ReprEntry":
+ reprfuncargs = None
+ reprfileloc = None
+ reprlocals = None
+ if data["reprfuncargs"]:
+ reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
+ if data["reprfileloc"]:
+ reprfileloc = ReprFileLocation(**data["reprfileloc"])
+ if data["reprlocals"]:
+ reprlocals = ReprLocals(data["reprlocals"]["lines"])
+
+ reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry(
+ lines=data["lines"],
+ reprfuncargs=reprfuncargs,
+ reprlocals=reprlocals,
+ reprfileloc=reprfileloc,
+ style=data["style"],
+ )
+ elif entry_type == "ReprEntryNative":
+ reprentry = ReprEntryNative(data["lines"])
+ else:
+ _report_unserialization_failure(entry_type, TestReport, reportdict)
+ return reprentry
+
+ def deserialize_repr_traceback(repr_traceback_dict):
+ repr_traceback_dict["reprentries"] = [
+ deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"]
+ ]
+ return ReprTraceback(**repr_traceback_dict)
+
+ def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
+ if repr_crash_dict is not None:
+ return ReprFileLocation(**repr_crash_dict)
+ else:
+ return None
+
+ if (
+ reportdict["longrepr"]
+ and "reprcrash" in reportdict["longrepr"]
+ and "reprtraceback" in reportdict["longrepr"]
+ ):
+ reprtraceback = deserialize_repr_traceback(
+ reportdict["longrepr"]["reprtraceback"]
+ )
+ reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"])
+ if reportdict["longrepr"]["chain"]:
+ chain = []
+ for repr_traceback_data, repr_crash_data, description in reportdict[
+ "longrepr"
+ ]["chain"]:
+ chain.append(
+ (
+ deserialize_repr_traceback(repr_traceback_data),
+ deserialize_repr_crash(repr_crash_data),
+ description,
+ )
+ )
+ exception_info: Union[
+ ExceptionChainRepr, ReprExceptionInfo
+ ] = ExceptionChainRepr(chain)
+ else:
+ exception_info = ReprExceptionInfo(
+ reprtraceback=reprtraceback,
+ reprcrash=reprcrash,
+ )
+
+ for section in reportdict["longrepr"]["sections"]:
+ exception_info.addsection(*section)
+ reportdict["longrepr"] = exception_info
+
+ return reportdict
diff --git a/venv/lib/python3.12/site-packages/_pytest/runner.py b/venv/lib/python3.12/site-packages/_pytest/runner.py
new file mode 100644
index 0000000..f861c05
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/runner.py
@@ -0,0 +1,551 @@
+"""Basic collect and runtest protocol implementations."""
+import bdb
+import dataclasses
+import os
+import sys
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import Generic
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from .reports import BaseReport
+from .reports import CollectErrorRepr
+from .reports import CollectReport
+from .reports import TestReport
+from _pytest import timing
+from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest.compat import final
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.outcomes import Exit
+from _pytest.outcomes import OutcomeException
+from _pytest.outcomes import Skipped
+from _pytest.outcomes import TEST_OUTCOME
+
+if sys.version_info[:2] < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from _pytest.main import Session
+ from _pytest.terminal import TerminalReporter
+
+#
+# pytest plugin hooks.
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting", "Reporting", after="general")
+ group.addoption(
+ "--durations",
+ action="store",
+ type=int,
+ default=None,
+ metavar="N",
+ help="Show N slowest setup/test durations (N=0 for all)",
+ )
+ group.addoption(
+ "--durations-min",
+ action="store",
+ type=float,
+ default=0.005,
+ metavar="N",
+ help="Minimal duration in seconds for inclusion in slowest list. "
+ "Default: 0.005.",
+ )
+
+
+def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
+ durations = terminalreporter.config.option.durations
+ durations_min = terminalreporter.config.option.durations_min
+ verbose = terminalreporter.config.getvalue("verbose")
+ if durations is None:
+ return
+ tr = terminalreporter
+ dlist = []
+ for replist in tr.stats.values():
+ for rep in replist:
+ if hasattr(rep, "duration"):
+ dlist.append(rep)
+ if not dlist:
+ return
+ dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
+ if not durations:
+ tr.write_sep("=", "slowest durations")
+ else:
+ tr.write_sep("=", "slowest %s durations" % durations)
+ dlist = dlist[:durations]
+
+ for i, rep in enumerate(dlist):
+ if verbose < 2 and rep.duration < durations_min:
+ tr.write_line("")
+ tr.write_line(
+ "(%s durations < %gs hidden. Use -vv to show these durations.)"
+ % (len(dlist) - i, durations_min)
+ )
+ break
+ tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
+
+
+def pytest_sessionstart(session: "Session") -> None:
+ session._setupstate = SetupState()
+
+
+def pytest_sessionfinish(session: "Session") -> None:
+ session._setupstate.teardown_exact(None)
+
+
+def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
+ ihook = item.ihook
+ ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
+ runtestprotocol(item, nextitem=nextitem)
+ ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
+ return True
+
+
+def runtestprotocol(
+ item: Item, log: bool = True, nextitem: Optional[Item] = None
+) -> List[TestReport]:
+ hasrequest = hasattr(item, "_request")
+ if hasrequest and not item._request: # type: ignore[attr-defined]
+ # This only happens if the item is re-run, as is done by
+ # pytest-rerunfailures.
+ item._initrequest() # type: ignore[attr-defined]
+ rep = call_and_report(item, "setup", log)
+ reports = [rep]
+ if rep.passed:
+ if item.config.getoption("setupshow", False):
+ show_test_item(item)
+ if not item.config.getoption("setuponly", False):
+ reports.append(call_and_report(item, "call", log))
+ reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
+ # After all teardown hooks have been called
+ # want funcargs and request info to go away.
+ if hasrequest:
+ item._request = False # type: ignore[attr-defined]
+ item.funcargs = None # type: ignore[attr-defined]
+ return reports
+
+
+def show_test_item(item: Item) -> None:
+ """Show test function, parameters and the fixtures of the test item."""
+ tw = item.config.get_terminal_writer()
+ tw.line()
+ tw.write(" " * 8)
+ tw.write(item.nodeid)
+ used_fixtures = sorted(getattr(item, "fixturenames", []))
+ if used_fixtures:
+ tw.write(" (fixtures used: {})".format(", ".join(used_fixtures)))
+ tw.flush()
+
+
+def pytest_runtest_setup(item: Item) -> None:
+ _update_current_test_var(item, "setup")
+ item.session._setupstate.setup(item)
+
+
+def pytest_runtest_call(item: Item) -> None:
+ _update_current_test_var(item, "call")
+ try:
+ del sys.last_type
+ del sys.last_value
+ del sys.last_traceback
+ except AttributeError:
+ pass
+ try:
+ item.runtest()
+ except Exception as e:
+ # Store trace info to allow postmortem debugging
+ sys.last_type = type(e)
+ sys.last_value = e
+ assert e.__traceback__ is not None
+ # Skip *this* frame
+ sys.last_traceback = e.__traceback__.tb_next
+ raise e
+
+
+def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
+ _update_current_test_var(item, "teardown")
+ item.session._setupstate.teardown_exact(nextitem)
+ _update_current_test_var(item, None)
+
+
+def _update_current_test_var(
+ item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
+) -> None:
+ """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
+
+ If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
+ """
+ var_name = "PYTEST_CURRENT_TEST"
+ if when:
+ value = f"{item.nodeid} ({when})"
+ # don't allow null bytes on environment variables (see #2644, #2957)
+ value = value.replace("\x00", "(null)")
+ os.environ[var_name] = value
+ else:
+ os.environ.pop(var_name)
+
+
+def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
+ if report.when in ("setup", "teardown"):
+ if report.failed:
+ # category, shortletter, verbose-word
+ return "error", "E", "ERROR"
+ elif report.skipped:
+ return "skipped", "s", "SKIPPED"
+ else:
+ return "", "", ""
+ return None
+
+
+#
+# Implementation
+
+
+def call_and_report(
+ item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds
+) -> TestReport:
+ call = call_runtest_hook(item, when, **kwds)
+ hook = item.ihook
+ report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
+ if log:
+ hook.pytest_runtest_logreport(report=report)
+ if check_interactive_exception(call, report):
+ hook.pytest_exception_interact(node=item, call=call, report=report)
+ return report
+
+
+def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
+ """Check whether the call raised an exception that should be reported as
+ interactive."""
+ if call.excinfo is None:
+ # Didn't raise.
+ return False
+ if hasattr(report, "wasxfail"):
+ # Exception was expected.
+ return False
+ if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)):
+ # Special control flow exception.
+ return False
+ return True
+
+
+def call_runtest_hook(
+ item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds
+) -> "CallInfo[None]":
+ if when == "setup":
+ ihook: Callable[..., None] = item.ihook.pytest_runtest_setup
+ elif when == "call":
+ ihook = item.ihook.pytest_runtest_call
+ elif when == "teardown":
+ ihook = item.ihook.pytest_runtest_teardown
+ else:
+ assert False, f"Unhandled runtest hook case: {when}"
+ reraise: Tuple[Type[BaseException], ...] = (Exit,)
+ if not item.config.getoption("usepdb", False):
+ reraise += (KeyboardInterrupt,)
+ return CallInfo.from_call(
+ lambda: ihook(item=item, **kwds), when=when, reraise=reraise
+ )
+
+
+TResult = TypeVar("TResult", covariant=True)
+
+
+@final
+@dataclasses.dataclass
+class CallInfo(Generic[TResult]):
+ """Result/Exception info of a function invocation."""
+
+ _result: Optional[TResult]
+ #: The captured exception of the call, if it raised.
+ excinfo: Optional[ExceptionInfo[BaseException]]
+ #: The system time when the call started, in seconds since the epoch.
+ start: float
+ #: The system time when the call ended, in seconds since the epoch.
+ stop: float
+ #: The call duration, in seconds.
+ duration: float
+ #: The context of invocation: "collect", "setup", "call" or "teardown".
+ when: "Literal['collect', 'setup', 'call', 'teardown']"
+
+ def __init__(
+ self,
+ result: Optional[TResult],
+ excinfo: Optional[ExceptionInfo[BaseException]],
+ start: float,
+ stop: float,
+ duration: float,
+ when: "Literal['collect', 'setup', 'call', 'teardown']",
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._result = result
+ self.excinfo = excinfo
+ self.start = start
+ self.stop = stop
+ self.duration = duration
+ self.when = when
+
+ @property
+ def result(self) -> TResult:
+ """The return value of the call, if it didn't raise.
+
+ Can only be accessed if excinfo is None.
+ """
+ if self.excinfo is not None:
+ raise AttributeError(f"{self!r} has no valid result")
+ # The cast is safe because an exception wasn't raised, hence
+ # _result has the expected function return type (which may be
+ # None, that's why a cast and not an assert).
+ return cast(TResult, self._result)
+
+ @classmethod
+ def from_call(
+ cls,
+ func: "Callable[[], TResult]",
+ when: "Literal['collect', 'setup', 'call', 'teardown']",
+ reraise: Optional[
+ Union[Type[BaseException], Tuple[Type[BaseException], ...]]
+ ] = None,
+ ) -> "CallInfo[TResult]":
+ """Call func, wrapping the result in a CallInfo.
+
+ :param func:
+ The function to call. Called without arguments.
+ :param when:
+ The phase in which the function is called.
+ :param reraise:
+ Exception or exceptions that shall propagate if raised by the
+ function, instead of being wrapped in the CallInfo.
+ """
+ excinfo = None
+ start = timing.time()
+ precise_start = timing.perf_counter()
+ try:
+ result: Optional[TResult] = func()
+ except BaseException:
+ excinfo = ExceptionInfo.from_current()
+ if reraise is not None and isinstance(excinfo.value, reraise):
+ raise
+ result = None
+ # use the perf counter
+ precise_stop = timing.perf_counter()
+ duration = precise_stop - precise_start
+ stop = timing.time()
+ return cls(
+ start=start,
+ stop=stop,
+ duration=duration,
+ when=when,
+ result=result,
+ excinfo=excinfo,
+ _ispytest=True,
+ )
+
+ def __repr__(self) -> str:
+ if self.excinfo is None:
+ return f""
+ return f""
+
+
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
+ return TestReport.from_item_and_call(item, call)
+
+
+def pytest_make_collect_report(collector: Collector) -> CollectReport:
+ call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
+ longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
+ if not call.excinfo:
+ outcome: Literal["passed", "skipped", "failed"] = "passed"
+ else:
+ skip_exceptions = [Skipped]
+ unittest = sys.modules.get("unittest")
+ if unittest is not None:
+ # Type ignored because unittest is loaded dynamically.
+ skip_exceptions.append(unittest.SkipTest) # type: ignore
+ if isinstance(call.excinfo.value, tuple(skip_exceptions)):
+ outcome = "skipped"
+ r_ = collector._repr_failure_py(call.excinfo, "line")
+ assert isinstance(r_, ExceptionChainRepr), repr(r_)
+ r = r_.reprcrash
+ assert r
+ longrepr = (str(r.path), r.lineno, r.message)
+ else:
+ outcome = "failed"
+ errorinfo = collector.repr_failure(call.excinfo)
+ if not hasattr(errorinfo, "toterminal"):
+ assert isinstance(errorinfo, str)
+ errorinfo = CollectErrorRepr(errorinfo)
+ longrepr = errorinfo
+ result = call.result if not call.excinfo else None
+ rep = CollectReport(collector.nodeid, outcome, longrepr, result)
+ rep.call = call # type: ignore # see collect_one_node
+ return rep
+
+
+class SetupState:
+ """Shared state for setting up/tearing down test items or collectors
+ in a session.
+
+ Suppose we have a collection tree as follows:
+
+
+
+
+
+
+
+ The SetupState maintains a stack. The stack starts out empty:
+
+ []
+
+ During the setup phase of item1, setup(item1) is called. What it does
+ is:
+
+ push session to stack, run session.setup()
+ push mod1 to stack, run mod1.setup()
+ push item1 to stack, run item1.setup()
+
+ The stack is:
+
+ [session, mod1, item1]
+
+ While the stack is in this shape, it is allowed to add finalizers to
+ each of session, mod1, item1 using addfinalizer().
+
+ During the teardown phase of item1, teardown_exact(item2) is called,
+ where item2 is the next item to item1. What it does is:
+
+ pop item1 from stack, run its teardowns
+ pop mod1 from stack, run its teardowns
+
+ mod1 was popped because it ended its purpose with item1. The stack is:
+
+ [session]
+
+ During the setup phase of item2, setup(item2) is called. What it does
+ is:
+
+ push mod2 to stack, run mod2.setup()
+ push item2 to stack, run item2.setup()
+
+ Stack:
+
+ [session, mod2, item2]
+
+ During the teardown phase of item2, teardown_exact(None) is called,
+ because item2 is the last item. What it does is:
+
+ pop item2 from stack, run its teardowns
+ pop mod2 from stack, run its teardowns
+ pop session from stack, run its teardowns
+
+ Stack:
+
+ []
+
+ The end!
+ """
+
+ def __init__(self) -> None:
+ # The stack is in the dict insertion order.
+ self.stack: Dict[
+ Node,
+ Tuple[
+ # Node's finalizers.
+ List[Callable[[], object]],
+ # Node's exception, if its setup raised.
+ Optional[Union[OutcomeException, Exception]],
+ ],
+ ] = {}
+
+ def setup(self, item: Item) -> None:
+ """Setup objects along the collector chain to the item."""
+ needed_collectors = item.listchain()
+
+ # If a collector fails its setup, fail its entire subtree of items.
+ # The setup is not retried for each item - the same exception is used.
+ for col, (finalizers, exc) in self.stack.items():
+ assert col in needed_collectors, "previous item was not torn down properly"
+ if exc:
+ raise exc
+
+ for col in needed_collectors[len(self.stack) :]:
+ assert col not in self.stack
+ # Push onto the stack.
+ self.stack[col] = ([col.teardown], None)
+ try:
+ col.setup()
+ except TEST_OUTCOME as exc:
+ self.stack[col] = (self.stack[col][0], exc)
+ raise exc
+
+ def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
+ """Attach a finalizer to the given node.
+
+ The node must be currently active in the stack.
+ """
+ assert node and not isinstance(node, tuple)
+ assert callable(finalizer)
+ assert node in self.stack, (node, self.stack)
+ self.stack[node][0].append(finalizer)
+
+ def teardown_exact(self, nextitem: Optional[Item]) -> None:
+ """Teardown the current stack up until reaching nodes that nextitem
+ also descends from.
+
+ When nextitem is None (meaning we're at the last item), the entire
+ stack is torn down.
+ """
+ needed_collectors = nextitem and nextitem.listchain() or []
+ exceptions: List[BaseException] = []
+ while self.stack:
+ if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
+ break
+ node, (finalizers, _) = self.stack.popitem()
+ these_exceptions = []
+ while finalizers:
+ fin = finalizers.pop()
+ try:
+ fin()
+ except TEST_OUTCOME as e:
+ these_exceptions.append(e)
+
+ if len(these_exceptions) == 1:
+ exceptions.extend(these_exceptions)
+ elif these_exceptions:
+ msg = f"errors while tearing down {node!r}"
+ exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1]))
+
+ if len(exceptions) == 1:
+ raise exceptions[0]
+ elif exceptions:
+ raise BaseExceptionGroup("errors during test teardown", exceptions[::-1])
+ if nextitem is None:
+ assert not self.stack
+
+
+def collect_one_node(collector: Collector) -> CollectReport:
+ ihook = collector.ihook
+ ihook.pytest_collectstart(collector=collector)
+ rep: CollectReport = ihook.pytest_make_collect_report(collector=collector)
+ call = rep.__dict__.pop("call", None)
+ if call and check_interactive_exception(call, rep):
+ ihook.pytest_exception_interact(node=collector, call=call, report=rep)
+ return rep
diff --git a/venv/lib/python3.12/site-packages/_pytest/scope.py b/venv/lib/python3.12/site-packages/_pytest/scope.py
new file mode 100644
index 0000000..7a746fb
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/scope.py
@@ -0,0 +1,91 @@
+"""
+Scope definition and related utilities.
+
+Those are defined here, instead of in the 'fixtures' module because
+their use is spread across many other pytest modules, and centralizing it in 'fixtures'
+would cause circular references.
+
+Also this makes the module light to import, as it should.
+"""
+from enum import Enum
+from functools import total_ordering
+from typing import Optional
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ _ScopeName = Literal["session", "package", "module", "class", "function"]
+
+
+@total_ordering
+class Scope(Enum):
+ """
+ Represents one of the possible fixture scopes in pytest.
+
+ Scopes are ordered from lower to higher, that is:
+
+ ->>> higher ->>>
+
+ Function < Class < Module < Package < Session
+
+ <<<- lower <<<-
+ """
+
+ # Scopes need to be listed from lower to higher.
+ Function: "_ScopeName" = "function"
+ Class: "_ScopeName" = "class"
+ Module: "_ScopeName" = "module"
+ Package: "_ScopeName" = "package"
+ Session: "_ScopeName" = "session"
+
+ def next_lower(self) -> "Scope":
+ """Return the next lower scope."""
+ index = _SCOPE_INDICES[self]
+ if index == 0:
+ raise ValueError(f"{self} is the lower-most scope")
+ return _ALL_SCOPES[index - 1]
+
+ def next_higher(self) -> "Scope":
+ """Return the next higher scope."""
+ index = _SCOPE_INDICES[self]
+ if index == len(_SCOPE_INDICES) - 1:
+ raise ValueError(f"{self} is the upper-most scope")
+ return _ALL_SCOPES[index + 1]
+
+ def __lt__(self, other: "Scope") -> bool:
+ self_index = _SCOPE_INDICES[self]
+ other_index = _SCOPE_INDICES[other]
+ return self_index < other_index
+
+ @classmethod
+ def from_user(
+ cls, scope_name: "_ScopeName", descr: str, where: Optional[str] = None
+ ) -> "Scope":
+ """
+ Given a scope name from the user, return the equivalent Scope enum. Should be used
+ whenever we want to convert a user provided scope name to its enum object.
+
+ If the scope name is invalid, construct a user friendly message and call pytest.fail.
+ """
+ from _pytest.outcomes import fail
+
+ try:
+ # Holding this reference is necessary for mypy at the moment.
+ scope = Scope(scope_name)
+ except ValueError:
+ fail(
+ "{} {}got an unexpected scope value '{}'".format(
+ descr, f"from {where} " if where else "", scope_name
+ ),
+ pytrace=False,
+ )
+ return scope
+
+
+_ALL_SCOPES = list(Scope)
+_SCOPE_INDICES = {scope: index for index, scope in enumerate(_ALL_SCOPES)}
+
+
+# Ordered list of scopes which can contain many tests (in practice all except Function).
+HIGH_SCOPES = [x for x in Scope if x is not Scope.Function]
diff --git a/venv/lib/python3.12/site-packages/_pytest/setuponly.py b/venv/lib/python3.12/site-packages/_pytest/setuponly.py
new file mode 100644
index 0000000..583590d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/setuponly.py
@@ -0,0 +1,97 @@
+from typing import Generator
+from typing import Optional
+from typing import Union
+
+import pytest
+from _pytest._io.saferepr import saferepr
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureDef
+from _pytest.fixtures import SubRequest
+from _pytest.scope import Scope
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--setuponly",
+ "--setup-only",
+ action="store_true",
+ help="Only setup fixtures, do not execute tests",
+ )
+ group.addoption(
+ "--setupshow",
+ "--setup-show",
+ action="store_true",
+ help="Show setup of fixtures while executing tests",
+ )
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[object], request: SubRequest
+) -> Generator[None, None, None]:
+ yield
+ if request.config.option.setupshow:
+ if hasattr(request, "param"):
+ # Save the fixture parameter so ._show_fixture_action() can
+ # display it now and during the teardown (in .finish()).
+ if fixturedef.ids:
+ if callable(fixturedef.ids):
+ param = fixturedef.ids(request.param)
+ else:
+ param = fixturedef.ids[request.param_index]
+ else:
+ param = request.param
+ fixturedef.cached_param = param # type: ignore[attr-defined]
+ _show_fixture_action(fixturedef, "SETUP")
+
+
+def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None:
+ if fixturedef.cached_result is not None:
+ config = fixturedef._fixturemanager.config
+ if config.option.setupshow:
+ _show_fixture_action(fixturedef, "TEARDOWN")
+ if hasattr(fixturedef, "cached_param"):
+ del fixturedef.cached_param # type: ignore[attr-defined]
+
+
+def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None:
+ config = fixturedef._fixturemanager.config
+ capman = config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture()
+
+ tw = config.get_terminal_writer()
+ tw.line()
+ # Use smaller indentation the higher the scope: Session = 0, Package = 1, etc.
+ scope_indent = list(reversed(Scope)).index(fixturedef._scope)
+ tw.write(" " * 2 * scope_indent)
+ tw.write(
+ "{step} {scope} {fixture}".format(
+ step=msg.ljust(8), # align the output to TEARDOWN
+ scope=fixturedef.scope[0].upper(),
+ fixture=fixturedef.argname,
+ )
+ )
+
+ if msg == "SETUP":
+ deps = sorted(arg for arg in fixturedef.argnames if arg != "request")
+ if deps:
+ tw.write(" (fixtures used: {})".format(", ".join(deps)))
+
+ if hasattr(fixturedef, "cached_param"):
+ tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") # type: ignore[attr-defined]
+
+ tw.flush()
+
+ if capman:
+ capman.resume_global_capture()
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.setuponly:
+ config.option.setupshow = True
+ return None
diff --git a/venv/lib/python3.12/site-packages/_pytest/setupplan.py b/venv/lib/python3.12/site-packages/_pytest/setupplan.py
new file mode 100644
index 0000000..1a4ebdd
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/setupplan.py
@@ -0,0 +1,40 @@
+from typing import Optional
+from typing import Union
+
+import pytest
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureDef
+from _pytest.fixtures import SubRequest
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--setupplan",
+ "--setup-plan",
+ action="store_true",
+ help="Show what fixtures and tests would be executed but "
+ "don't execute anything",
+ )
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[object], request: SubRequest
+) -> Optional[object]:
+ # Will return a dummy fixture if the setuponly option is provided.
+ if request.config.option.setupplan:
+ my_cache_key = fixturedef.cache_key(request)
+ fixturedef.cached_result = (None, my_cache_key, None)
+ return fixturedef.cached_result
+ return None
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
+ if config.option.setupplan:
+ config.option.setuponly = True
+ config.option.setupshow = True
+ return None
diff --git a/venv/lib/python3.12/site-packages/_pytest/skipping.py b/venv/lib/python3.12/site-packages/_pytest/skipping.py
new file mode 100644
index 0000000..26ce737
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/skipping.py
@@ -0,0 +1,297 @@
+"""Support for skip/xfail functions and markers."""
+import dataclasses
+import os
+import platform
+import sys
+import traceback
+from collections.abc import Mapping
+from typing import Generator
+from typing import Optional
+from typing import Tuple
+from typing import Type
+
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.mark.structures import Mark
+from _pytest.nodes import Item
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.outcomes import xfail
+from _pytest.reports import BaseReport
+from _pytest.runner import CallInfo
+from _pytest.stash import StashKey
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--runxfail",
+ action="store_true",
+ dest="runxfail",
+ default=False,
+ help="Report the results of xfail tests as if they were not marked",
+ )
+
+ parser.addini(
+ "xfail_strict",
+ "Default for the strict parameter of xfail "
+ "markers when not given explicitly (default: False)",
+ default=False,
+ type="bool",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ if config.option.runxfail:
+ # yay a hack
+ import pytest
+
+ old = pytest.xfail
+ config.add_cleanup(lambda: setattr(pytest, "xfail", old))
+
+ def nop(*args, **kwargs):
+ pass
+
+ nop.Exception = xfail.Exception # type: ignore[attr-defined]
+ setattr(pytest, "xfail", nop)
+
+ config.addinivalue_line(
+ "markers",
+ "skip(reason=None): skip the given test function with an optional reason. "
+ 'Example: skip(reason="no way of currently testing this") skips the '
+ "test.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "skipif(condition, ..., *, reason=...): "
+ "skip the given test function if any of the conditions evaluate to True. "
+ "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. "
+ "See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif",
+ )
+ config.addinivalue_line(
+ "markers",
+ "xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): "
+ "mark the test function as an expected failure if any of the conditions "
+ "evaluate to True. Optionally specify a reason for better reporting "
+ "and run=False if you don't even want to execute the test function. "
+ "If only specific exception(s) are expected, you can list them in "
+ "raises, and if the test fails in other ways, it will be reported as "
+ "a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail",
+ )
+
+
+def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, str]:
+ """Evaluate a single skipif/xfail condition.
+
+ If an old-style string condition is given, it is eval()'d, otherwise the
+ condition is bool()'d. If this fails, an appropriately formatted pytest.fail
+ is raised.
+
+ Returns (result, reason). The reason is only relevant if the result is True.
+ """
+ # String condition.
+ if isinstance(condition, str):
+ globals_ = {
+ "os": os,
+ "sys": sys,
+ "platform": platform,
+ "config": item.config,
+ }
+ for dictionary in reversed(
+ item.ihook.pytest_markeval_namespace(config=item.config)
+ ):
+ if not isinstance(dictionary, Mapping):
+ raise ValueError(
+ "pytest_markeval_namespace() needs to return a dict, got {!r}".format(
+ dictionary
+ )
+ )
+ globals_.update(dictionary)
+ if hasattr(item, "obj"):
+ globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
+ try:
+ filename = f"<{mark.name} condition>"
+ condition_code = compile(condition, filename, "eval")
+ result = eval(condition_code, globals_)
+ except SyntaxError as exc:
+ msglines = [
+ "Error evaluating %r condition" % mark.name,
+ " " + condition,
+ " " + " " * (exc.offset or 0) + "^",
+ "SyntaxError: invalid syntax",
+ ]
+ fail("\n".join(msglines), pytrace=False)
+ except Exception as exc:
+ msglines = [
+ "Error evaluating %r condition" % mark.name,
+ " " + condition,
+ *traceback.format_exception_only(type(exc), exc),
+ ]
+ fail("\n".join(msglines), pytrace=False)
+
+ # Boolean condition.
+ else:
+ try:
+ result = bool(condition)
+ except Exception as exc:
+ msglines = [
+ "Error evaluating %r condition as a boolean" % mark.name,
+ *traceback.format_exception_only(type(exc), exc),
+ ]
+ fail("\n".join(msglines), pytrace=False)
+
+ reason = mark.kwargs.get("reason", None)
+ if reason is None:
+ if isinstance(condition, str):
+ reason = "condition: " + condition
+ else:
+ # XXX better be checked at collection time
+ msg = (
+ "Error evaluating %r: " % mark.name
+ + "you need to specify reason=STRING when using booleans as conditions."
+ )
+ fail(msg, pytrace=False)
+
+ return result, reason
+
+
+@dataclasses.dataclass(frozen=True)
+class Skip:
+ """The result of evaluate_skip_marks()."""
+
+ reason: str = "unconditional skip"
+
+
+def evaluate_skip_marks(item: Item) -> Optional[Skip]:
+ """Evaluate skip and skipif marks on item, returning Skip if triggered."""
+ for mark in item.iter_markers(name="skipif"):
+ if "condition" not in mark.kwargs:
+ conditions = mark.args
+ else:
+ conditions = (mark.kwargs["condition"],)
+
+ # Unconditional.
+ if not conditions:
+ reason = mark.kwargs.get("reason", "")
+ return Skip(reason)
+
+ # If any of the conditions are true.
+ for condition in conditions:
+ result, reason = evaluate_condition(item, mark, condition)
+ if result:
+ return Skip(reason)
+
+ for mark in item.iter_markers(name="skip"):
+ try:
+ return Skip(*mark.args, **mark.kwargs)
+ except TypeError as e:
+ raise TypeError(str(e) + " - maybe you meant pytest.mark.skipif?") from None
+
+ return None
+
+
+@dataclasses.dataclass(frozen=True)
+class Xfail:
+ """The result of evaluate_xfail_marks()."""
+
+ __slots__ = ("reason", "run", "strict", "raises")
+
+ reason: str
+ run: bool
+ strict: bool
+ raises: Optional[Tuple[Type[BaseException], ...]]
+
+
+def evaluate_xfail_marks(item: Item) -> Optional[Xfail]:
+ """Evaluate xfail marks on item, returning Xfail if triggered."""
+ for mark in item.iter_markers(name="xfail"):
+ run = mark.kwargs.get("run", True)
+ strict = mark.kwargs.get("strict", item.config.getini("xfail_strict"))
+ raises = mark.kwargs.get("raises", None)
+ if "condition" not in mark.kwargs:
+ conditions = mark.args
+ else:
+ conditions = (mark.kwargs["condition"],)
+
+ # Unconditional.
+ if not conditions:
+ reason = mark.kwargs.get("reason", "")
+ return Xfail(reason, run, strict, raises)
+
+ # If any of the conditions are true.
+ for condition in conditions:
+ result, reason = evaluate_condition(item, mark, condition)
+ if result:
+ return Xfail(reason, run, strict, raises)
+
+ return None
+
+
+# Saves the xfail mark evaluation. Can be refreshed during call if None.
+xfailed_key = StashKey[Optional[Xfail]]()
+
+
+@hookimpl(tryfirst=True)
+def pytest_runtest_setup(item: Item) -> None:
+ skipped = evaluate_skip_marks(item)
+ if skipped:
+ raise skip.Exception(skipped.reason, _use_item_location=True)
+
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+ if xfailed and not item.config.option.runxfail and not xfailed.run:
+ xfail("[NOTRUN] " + xfailed.reason)
+
+
+@hookimpl(hookwrapper=True)
+def pytest_runtest_call(item: Item) -> Generator[None, None, None]:
+ xfailed = item.stash.get(xfailed_key, None)
+ if xfailed is None:
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+
+ if xfailed and not item.config.option.runxfail and not xfailed.run:
+ xfail("[NOTRUN] " + xfailed.reason)
+
+ yield
+
+ # The test run may have added an xfail mark dynamically.
+ xfailed = item.stash.get(xfailed_key, None)
+ if xfailed is None:
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+
+
+@hookimpl(hookwrapper=True)
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
+ outcome = yield
+ rep = outcome.get_result()
+ xfailed = item.stash.get(xfailed_key, None)
+ if item.config.option.runxfail:
+ pass # don't interfere
+ elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception):
+ assert call.excinfo.value.msg is not None
+ rep.wasxfail = "reason: " + call.excinfo.value.msg
+ rep.outcome = "skipped"
+ elif not rep.skipped and xfailed:
+ if call.excinfo:
+ raises = xfailed.raises
+ if raises is not None and not isinstance(call.excinfo.value, raises):
+ rep.outcome = "failed"
+ else:
+ rep.outcome = "skipped"
+ rep.wasxfail = xfailed.reason
+ elif call.when == "call":
+ if xfailed.strict:
+ rep.outcome = "failed"
+ rep.longrepr = "[XPASS(strict)] " + xfailed.reason
+ else:
+ rep.outcome = "passed"
+ rep.wasxfail = xfailed.reason
+
+
+def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
+ if hasattr(report, "wasxfail"):
+ if report.skipped:
+ return "xfailed", "x", "XFAIL"
+ elif report.passed:
+ return "xpassed", "X", "XPASS"
+ return None
diff --git a/venv/lib/python3.12/site-packages/_pytest/stash.py b/venv/lib/python3.12/site-packages/_pytest/stash.py
new file mode 100644
index 0000000..e61d75b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/stash.py
@@ -0,0 +1,112 @@
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Generic
+from typing import TypeVar
+from typing import Union
+
+
+__all__ = ["Stash", "StashKey"]
+
+
+T = TypeVar("T")
+D = TypeVar("D")
+
+
+class StashKey(Generic[T]):
+ """``StashKey`` is an object used as a key to a :class:`Stash`.
+
+ A ``StashKey`` is associated with the type ``T`` of the value of the key.
+
+ A ``StashKey`` is unique and cannot conflict with another key.
+ """
+
+ __slots__ = ()
+
+
+class Stash:
+ r"""``Stash`` is a type-safe heterogeneous mutable mapping that
+ allows keys and value types to be defined separately from
+ where it (the ``Stash``) is created.
+
+ Usually you will be given an object which has a ``Stash``, for example
+ :class:`~pytest.Config` or a :class:`~_pytest.nodes.Node`:
+
+ .. code-block:: python
+
+ stash: Stash = some_object.stash
+
+ If a module or plugin wants to store data in this ``Stash``, it creates
+ :class:`StashKey`\s for its keys (at the module level):
+
+ .. code-block:: python
+
+ # At the top-level of the module
+ some_str_key = StashKey[str]()
+ some_bool_key = StashKey[bool]()
+
+ To store information:
+
+ .. code-block:: python
+
+ # Value type must match the key.
+ stash[some_str_key] = "value"
+ stash[some_bool_key] = True
+
+ To retrieve the information:
+
+ .. code-block:: python
+
+ # The static type of some_str is str.
+ some_str = stash[some_str_key]
+ # The static type of some_bool is bool.
+ some_bool = stash[some_bool_key]
+ """
+
+ __slots__ = ("_storage",)
+
+ def __init__(self) -> None:
+ self._storage: Dict[StashKey[Any], object] = {}
+
+ def __setitem__(self, key: StashKey[T], value: T) -> None:
+ """Set a value for key."""
+ self._storage[key] = value
+
+ def __getitem__(self, key: StashKey[T]) -> T:
+ """Get the value for key.
+
+ Raises ``KeyError`` if the key wasn't set before.
+ """
+ return cast(T, self._storage[key])
+
+ def get(self, key: StashKey[T], default: D) -> Union[T, D]:
+ """Get the value for key, or return default if the key wasn't set
+ before."""
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def setdefault(self, key: StashKey[T], default: T) -> T:
+ """Return the value of key if already set, otherwise set the value
+ of key to default and return default."""
+ try:
+ return self[key]
+ except KeyError:
+ self[key] = default
+ return default
+
+ def __delitem__(self, key: StashKey[T]) -> None:
+ """Delete the value for key.
+
+ Raises ``KeyError`` if the key wasn't set before.
+ """
+ del self._storage[key]
+
+ def __contains__(self, key: StashKey[T]) -> bool:
+ """Return whether key was set."""
+ return key in self._storage
+
+ def __len__(self) -> int:
+ """Return how many items exist in the stash."""
+ return len(self._storage)
diff --git a/venv/lib/python3.12/site-packages/_pytest/stepwise.py b/venv/lib/python3.12/site-packages/_pytest/stepwise.py
new file mode 100644
index 0000000..74ad9db
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/stepwise.py
@@ -0,0 +1,130 @@
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+
+import pytest
+from _pytest import nodes
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.main import Session
+from _pytest.reports import TestReport
+
+if TYPE_CHECKING:
+ from _pytest.cacheprovider import Cache
+
+STEPWISE_CACHE_DIR = "cache/stepwise"
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--sw",
+ "--stepwise",
+ action="store_true",
+ default=False,
+ dest="stepwise",
+ help="Exit on test failure and continue from last failing test next time",
+ )
+ group.addoption(
+ "--sw-skip",
+ "--stepwise-skip",
+ action="store_true",
+ default=False,
+ dest="stepwise_skip",
+ help="Ignore the first failing test but stop on the next failing test. "
+ "Implicitly enables --stepwise.",
+ )
+
+
+@pytest.hookimpl
+def pytest_configure(config: Config) -> None:
+ if config.option.stepwise_skip:
+ # allow --stepwise-skip to work on it's own merits.
+ config.option.stepwise = True
+ if config.getoption("stepwise"):
+ config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
+
+
+def pytest_sessionfinish(session: Session) -> None:
+ if not session.config.getoption("stepwise"):
+ assert session.config.cache is not None
+ if hasattr(session.config, "workerinput"):
+ # Do not update cache if this process is a xdist worker to prevent
+ # race conditions (#10641).
+ return
+ # Clear the list of failing tests if the plugin is not active.
+ session.config.cache.set(STEPWISE_CACHE_DIR, [])
+
+
+class StepwisePlugin:
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ self.session: Optional[Session] = None
+ self.report_status = ""
+ assert config.cache is not None
+ self.cache: Cache = config.cache
+ self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None)
+ self.skip: bool = config.getoption("stepwise_skip")
+
+ def pytest_sessionstart(self, session: Session) -> None:
+ self.session = session
+
+ def pytest_collection_modifyitems(
+ self, config: Config, items: List[nodes.Item]
+ ) -> None:
+ if not self.lastfailed:
+ self.report_status = "no previously failed tests, not skipping."
+ return
+
+ # check all item nodes until we find a match on last failed
+ failed_index = None
+ for index, item in enumerate(items):
+ if item.nodeid == self.lastfailed:
+ failed_index = index
+ break
+
+ # If the previously failed test was not found among the test items,
+ # do not skip any tests.
+ if failed_index is None:
+ self.report_status = "previously failed test not found, not skipping."
+ else:
+ self.report_status = f"skipping {failed_index} already passed items."
+ deselected = items[:failed_index]
+ del items[:failed_index]
+ config.hook.pytest_deselected(items=deselected)
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ if report.failed:
+ if self.skip:
+ # Remove test from the failed ones (if it exists) and unset the skip option
+ # to make sure the following tests will not be skipped.
+ if report.nodeid == self.lastfailed:
+ self.lastfailed = None
+
+ self.skip = False
+ else:
+ # Mark test as the last failing and interrupt the test session.
+ self.lastfailed = report.nodeid
+ assert self.session is not None
+ self.session.shouldstop = (
+ "Test failed, continuing from this test next run."
+ )
+
+ else:
+ # If the test was actually run and did pass.
+ if report.when == "call":
+ # Remove test from the failed ones, if exists.
+ if report.nodeid == self.lastfailed:
+ self.lastfailed = None
+
+ def pytest_report_collectionfinish(self) -> Optional[str]:
+ if self.config.getoption("verbose") >= 0 and self.report_status:
+ return f"stepwise: {self.report_status}"
+ return None
+
+ def pytest_sessionfinish(self) -> None:
+ if hasattr(self.config, "workerinput"):
+ # Do not update cache if this process is a xdist worker to prevent
+ # race conditions (#10641).
+ return
+ self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
diff --git a/venv/lib/python3.12/site-packages/_pytest/terminal.py b/venv/lib/python3.12/site-packages/_pytest/terminal.py
new file mode 100644
index 0000000..b0cdb58
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/terminal.py
@@ -0,0 +1,1481 @@
+"""Terminal reporting of the full testing process.
+
+This is a good source for looking at the various reporting hooks.
+"""
+import argparse
+import dataclasses
+import datetime
+import inspect
+import platform
+import sys
+import textwrap
+import warnings
+from collections import Counter
+from functools import partial
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import ClassVar
+from typing import Dict
+from typing import Generator
+from typing import List
+from typing import Mapping
+from typing import NamedTuple
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import TextIO
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+import pluggy
+
+import _pytest._version
+from _pytest import nodes
+from _pytest import timing
+from _pytest._code import ExceptionInfo
+from _pytest._code.code import ExceptionRepr
+from _pytest._io import TerminalWriter
+from _pytest._io.wcwidth import wcswidth
+from _pytest.assertion.util import running_on_ci
+from _pytest.compat import final
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.reports import BaseReport
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from _pytest.main import Session
+
+
+REPORT_COLLECTING_RESOLUTION = 0.5
+
+KNOWN_TYPES = (
+ "failed",
+ "passed",
+ "skipped",
+ "deselected",
+ "xfailed",
+ "xpassed",
+ "warnings",
+ "error",
+)
+
+_REPORTCHARS_DEFAULT = "fE"
+
+
+class MoreQuietAction(argparse.Action):
+ """A modified copy of the argparse count action which counts down and updates
+ the legacy quiet attribute at the same time.
+
+ Used to unify verbosity handling.
+ """
+
+ def __init__(
+ self,
+ option_strings: Sequence[str],
+ dest: str,
+ default: object = None,
+ required: bool = False,
+ help: Optional[str] = None,
+ ) -> None:
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=0,
+ default=default,
+ required=required,
+ help=help,
+ )
+
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: Union[str, Sequence[object], None],
+ option_string: Optional[str] = None,
+ ) -> None:
+ new_count = getattr(namespace, self.dest, 0) - 1
+ setattr(namespace, self.dest, new_count)
+ # todo Deprecate config.quiet
+ namespace.quiet = getattr(namespace, "quiet", 0) + 1
+
+
+class TestShortLogReport(NamedTuple):
+ """Used to store the test status result category, shortletter and verbose word.
+ For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
+
+ :ivar category:
+ The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string.
+
+ :ivar letter:
+ The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
+
+ :ivar word:
+ Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
+ ``"ERROR"``, or the empty string.
+ """
+
+ category: str
+ letter: str
+ word: Union[str, Tuple[str, Mapping[str, bool]]]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting", "Reporting", after="general")
+ group._addoption(
+ "-v",
+ "--verbose",
+ action="count",
+ default=0,
+ dest="verbose",
+ help="Increase verbosity",
+ )
+ group._addoption(
+ "--no-header",
+ action="store_true",
+ default=False,
+ dest="no_header",
+ help="Disable header",
+ )
+ group._addoption(
+ "--no-summary",
+ action="store_true",
+ default=False,
+ dest="no_summary",
+ help="Disable summary",
+ )
+ group._addoption(
+ "-q",
+ "--quiet",
+ action=MoreQuietAction,
+ default=0,
+ dest="verbose",
+ help="Decrease verbosity",
+ )
+ group._addoption(
+ "--verbosity",
+ dest="verbose",
+ type=int,
+ default=0,
+ help="Set verbosity. Default: 0.",
+ )
+ group._addoption(
+ "-r",
+ action="store",
+ dest="reportchars",
+ default=_REPORTCHARS_DEFAULT,
+ metavar="chars",
+ help="Show extra test summary info as specified by chars: (f)ailed, "
+ "(E)rror, (s)kipped, (x)failed, (X)passed, "
+ "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. "
+ "(w)arnings are enabled by default (see --disable-warnings), "
+ "'N' can be used to reset the list. (default: 'fE').",
+ )
+ group._addoption(
+ "--disable-warnings",
+ "--disable-pytest-warnings",
+ default=False,
+ dest="disable_warnings",
+ action="store_true",
+ help="Disable warnings summary",
+ )
+ group._addoption(
+ "-l",
+ "--showlocals",
+ action="store_true",
+ dest="showlocals",
+ default=False,
+ help="Show locals in tracebacks (disabled by default)",
+ )
+ group._addoption(
+ "--no-showlocals",
+ action="store_false",
+ dest="showlocals",
+ help="Hide locals in tracebacks (negate --showlocals passed through addopts)",
+ )
+ group._addoption(
+ "--tb",
+ metavar="style",
+ action="store",
+ dest="tbstyle",
+ default="auto",
+ choices=["auto", "long", "short", "no", "line", "native"],
+ help="Traceback print mode (auto/long/short/line/native/no)",
+ )
+ group._addoption(
+ "--show-capture",
+ action="store",
+ dest="showcapture",
+ choices=["no", "stdout", "stderr", "log", "all"],
+ default="all",
+ help="Controls how captured stdout/stderr/log is shown on failed tests. "
+ "Default: all.",
+ )
+ group._addoption(
+ "--fulltrace",
+ "--full-trace",
+ action="store_true",
+ default=False,
+ help="Don't cut any tracebacks (default is to cut)",
+ )
+ group._addoption(
+ "--color",
+ metavar="color",
+ action="store",
+ dest="color",
+ default="auto",
+ choices=["yes", "no", "auto"],
+ help="Color terminal output (yes/no/auto)",
+ )
+ group._addoption(
+ "--code-highlight",
+ default="yes",
+ choices=["yes", "no"],
+ help="Whether code should be highlighted (only if --color is also enabled). "
+ "Default: yes.",
+ )
+
+ parser.addini(
+ "console_output_style",
+ help='Console output: "classic", or with additional progress information '
+ '("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces '
+ "progress even when capture=no)",
+ default="progress",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ reporter = TerminalReporter(config, sys.stdout)
+ config.pluginmanager.register(reporter, "terminalreporter")
+ if config.option.debug or config.option.traceconfig:
+
+ def mywriter(tags, args):
+ msg = " ".join(map(str, args))
+ reporter.write_line("[traceconfig] " + msg)
+
+ config.trace.root.setprocessor("pytest:config", mywriter)
+
+
+def getreportopt(config: Config) -> str:
+ reportchars: str = config.option.reportchars
+
+ old_aliases = {"F", "S"}
+ reportopts = ""
+ for char in reportchars:
+ if char in old_aliases:
+ char = char.lower()
+ if char == "a":
+ reportopts = "sxXEf"
+ elif char == "A":
+ reportopts = "PpsxXEf"
+ elif char == "N":
+ reportopts = ""
+ elif char not in reportopts:
+ reportopts += char
+
+ if not config.option.disable_warnings and "w" not in reportopts:
+ reportopts = "w" + reportopts
+ elif config.option.disable_warnings and "w" in reportopts:
+ reportopts = reportopts.replace("w", "")
+
+ return reportopts
+
+
+@hookimpl(trylast=True) # after _pytest.runner
+def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
+ letter = "F"
+ if report.passed:
+ letter = "."
+ elif report.skipped:
+ letter = "s"
+
+ outcome: str = report.outcome
+ if report.when in ("collect", "setup", "teardown") and outcome == "failed":
+ outcome = "error"
+ letter = "E"
+
+ return outcome, letter, outcome.upper()
+
+
+@dataclasses.dataclass
+class WarningReport:
+ """Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
+
+ :ivar str message:
+ User friendly message about the warning.
+ :ivar str|None nodeid:
+ nodeid that generated the warning (see ``get_location``).
+ :ivar tuple fslocation:
+ File system location of the source of the warning (see ``get_location``).
+ """
+
+ message: str
+ nodeid: Optional[str] = None
+ fslocation: Optional[Tuple[str, int]] = None
+
+ count_towards_summary: ClassVar = True
+
+ def get_location(self, config: Config) -> Optional[str]:
+ """Return the more user-friendly information about the location of a warning, or None."""
+ if self.nodeid:
+ return self.nodeid
+ if self.fslocation:
+ filename, linenum = self.fslocation
+ relpath = bestrelpath(config.invocation_params.dir, absolutepath(filename))
+ return f"{relpath}:{linenum}"
+ return None
+
+
+@final
+class TerminalReporter:
+ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
+ import _pytest.config
+
+ self.config = config
+ self._numcollected = 0
+ self._session: Optional[Session] = None
+ self._showfspath: Optional[bool] = None
+
+ self.stats: Dict[str, List[Any]] = {}
+ self._main_color: Optional[str] = None
+ self._known_types: Optional[List[str]] = None
+ self.startpath = config.invocation_params.dir
+ if file is None:
+ file = sys.stdout
+ self._tw = _pytest.config.create_terminal_writer(config, file)
+ self._screen_width = self._tw.fullwidth
+ self.currentfspath: Union[None, Path, str, int] = None
+ self.reportchars = getreportopt(config)
+ self.hasmarkup = self._tw.hasmarkup
+ self.isatty = file.isatty()
+ self._progress_nodeids_reported: Set[str] = set()
+ self._show_progress_info = self._determine_show_progress_info()
+ self._collect_report_last_write: Optional[float] = None
+ self._already_displayed_warnings: Optional[int] = None
+ self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None
+
+ def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
+ """Return whether we should display progress information based on the current config."""
+ # do not show progress if we are not capturing output (#3038) unless explicitly
+ # overridden by progress-even-when-capture-no
+ if (
+ self.config.getoption("capture", "no") == "no"
+ and self.config.getini("console_output_style")
+ != "progress-even-when-capture-no"
+ ):
+ return False
+ # do not show progress if we are showing fixture setup/teardown
+ if self.config.getoption("setupshow", False):
+ return False
+ cfg: str = self.config.getini("console_output_style")
+ if cfg == "progress" or cfg == "progress-even-when-capture-no":
+ return "progress"
+ elif cfg == "count":
+ return "count"
+ else:
+ return False
+
+ @property
+ def verbosity(self) -> int:
+ verbosity: int = self.config.option.verbose
+ return verbosity
+
+ @property
+ def showheader(self) -> bool:
+ return self.verbosity >= 0
+
+ @property
+ def no_header(self) -> bool:
+ return bool(self.config.option.no_header)
+
+ @property
+ def no_summary(self) -> bool:
+ return bool(self.config.option.no_summary)
+
+ @property
+ def showfspath(self) -> bool:
+ if self._showfspath is None:
+ return self.verbosity >= 0
+ return self._showfspath
+
+ @showfspath.setter
+ def showfspath(self, value: Optional[bool]) -> None:
+ self._showfspath = value
+
+ @property
+ def showlongtestinfo(self) -> bool:
+ return self.verbosity > 0
+
+ def hasopt(self, char: str) -> bool:
+ char = {"xfailed": "x", "skipped": "s"}.get(char, char)
+ return char in self.reportchars
+
+ def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None:
+ fspath = self.config.rootpath / nodeid.split("::")[0]
+ if self.currentfspath is None or fspath != self.currentfspath:
+ if self.currentfspath is not None and self._show_progress_info:
+ self._write_progress_information_filling_space()
+ self.currentfspath = fspath
+ relfspath = bestrelpath(self.startpath, fspath)
+ self._tw.line()
+ self._tw.write(relfspath + " ")
+ self._tw.write(res, flush=True, **markup)
+
+ def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None:
+ if self.currentfspath != prefix:
+ self._tw.line()
+ self.currentfspath = prefix
+ self._tw.write(prefix)
+ if extra:
+ self._tw.write(extra, **kwargs)
+ self.currentfspath = -2
+
+ def ensure_newline(self) -> None:
+ if self.currentfspath:
+ self._tw.line()
+ self.currentfspath = None
+
+ def wrap_write(
+ self,
+ content: str,
+ *,
+ flush: bool = False,
+ margin: int = 8,
+ line_sep: str = "\n",
+ **markup: bool,
+ ) -> None:
+ """Wrap message with margin for progress info."""
+ width_of_current_line = self._tw.width_of_current_line
+ wrapped = line_sep.join(
+ textwrap.wrap(
+ " " * width_of_current_line + content,
+ width=self._screen_width - margin,
+ drop_whitespace=True,
+ replace_whitespace=False,
+ ),
+ )
+ wrapped = wrapped[width_of_current_line:]
+ self._tw.write(wrapped, flush=flush, **markup)
+
+ def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
+ self._tw.write(content, flush=flush, **markup)
+
+ def flush(self) -> None:
+ self._tw.flush()
+
+ def write_line(self, line: Union[str, bytes], **markup: bool) -> None:
+ if not isinstance(line, str):
+ line = str(line, errors="replace")
+ self.ensure_newline()
+ self._tw.line(line, **markup)
+
+ def rewrite(self, line: str, **markup: bool) -> None:
+ """Rewinds the terminal cursor to the beginning and writes the given line.
+
+ :param erase:
+ If True, will also add spaces until the full terminal width to ensure
+ previous lines are properly erased.
+
+ The rest of the keyword arguments are markup instructions.
+ """
+ erase = markup.pop("erase", False)
+ if erase:
+ fill_count = self._tw.fullwidth - len(line) - 1
+ fill = " " * fill_count
+ else:
+ fill = ""
+ line = str(line)
+ self._tw.write("\r" + line + fill, **markup)
+
+ def write_sep(
+ self,
+ sep: str,
+ title: Optional[str] = None,
+ fullwidth: Optional[int] = None,
+ **markup: bool,
+ ) -> None:
+ self.ensure_newline()
+ self._tw.sep(sep, title, fullwidth, **markup)
+
+ def section(self, title: str, sep: str = "=", **kw: bool) -> None:
+ self._tw.sep(sep, title, **kw)
+
+ def line(self, msg: str, **kw: bool) -> None:
+ self._tw.line(msg, **kw)
+
+ def _add_stats(self, category: str, items: Sequence[Any]) -> None:
+ set_main_color = category not in self.stats
+ self.stats.setdefault(category, []).extend(items)
+ if set_main_color:
+ self._set_main_color()
+
+ def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool:
+ for line in str(excrepr).split("\n"):
+ self.write_line("INTERNALERROR> " + line)
+ return True
+
+ def pytest_warning_recorded(
+ self,
+ warning_message: warnings.WarningMessage,
+ nodeid: str,
+ ) -> None:
+ from _pytest.warnings import warning_record_to_str
+
+ fslocation = warning_message.filename, warning_message.lineno
+ message = warning_record_to_str(warning_message)
+
+ warning_report = WarningReport(
+ fslocation=fslocation, message=message, nodeid=nodeid
+ )
+ self._add_stats("warnings", [warning_report])
+
+ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
+ if self.config.option.traceconfig:
+ msg = f"PLUGIN registered: {plugin}"
+ # XXX This event may happen during setup/teardown time
+ # which unfortunately captures our output here
+ # which garbles our output if we use self.write_line.
+ self.write_line(msg)
+
+ def pytest_deselected(self, items: Sequence[Item]) -> None:
+ self._add_stats("deselected", items)
+
+ def pytest_runtest_logstart(
+ self, nodeid: str, location: Tuple[str, Optional[int], str]
+ ) -> None:
+ # Ensure that the path is printed before the
+ # 1st test of a module starts running.
+ if self.showlongtestinfo:
+ line = self._locationline(nodeid, *location)
+ self.write_ensure_prefix(line, "")
+ self.flush()
+ elif self.showfspath:
+ self.write_fspath_result(nodeid, "")
+ self.flush()
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ self._tests_ran = True
+ rep = report
+
+ res = TestShortLogReport(
+ *self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
+ )
+ category, letter, word = res.category, res.letter, res.word
+ if not isinstance(word, tuple):
+ markup = None
+ else:
+ word, markup = word
+ self._add_stats(category, [rep])
+ if not letter and not word:
+ # Probably passed setup/teardown.
+ return
+ running_xdist = hasattr(rep, "node")
+ if markup is None:
+ was_xfail = hasattr(report, "wasxfail")
+ if rep.passed and not was_xfail:
+ markup = {"green": True}
+ elif rep.passed and was_xfail:
+ markup = {"yellow": True}
+ elif rep.failed:
+ markup = {"red": True}
+ elif rep.skipped:
+ markup = {"yellow": True}
+ else:
+ markup = {}
+ if self.verbosity <= 0:
+ self._tw.write(letter, **markup)
+ else:
+ self._progress_nodeids_reported.add(rep.nodeid)
+ line = self._locationline(rep.nodeid, *rep.location)
+ if not running_xdist:
+ self.write_ensure_prefix(line, word, **markup)
+ if rep.skipped or hasattr(report, "wasxfail"):
+ reason = _get_raw_skip_reason(rep)
+ if self.config.option.verbose < 2:
+ available_width = (
+ (self._tw.fullwidth - self._tw.width_of_current_line)
+ - len(" [100%]")
+ - 1
+ )
+ formatted_reason = _format_trimmed(
+ " ({})", reason, available_width
+ )
+ else:
+ formatted_reason = f" ({reason})"
+
+ if reason and formatted_reason is not None:
+ self.wrap_write(formatted_reason)
+ if self._show_progress_info:
+ self._write_progress_information_filling_space()
+ else:
+ self.ensure_newline()
+ self._tw.write("[%s]" % rep.node.gateway.id)
+ if self._show_progress_info:
+ self._tw.write(
+ self._get_progress_information_message() + " ", cyan=True
+ )
+ else:
+ self._tw.write(" ")
+ self._tw.write(word, **markup)
+ self._tw.write(" " + line)
+ self.currentfspath = -2
+ self.flush()
+
+ @property
+ def _is_last_item(self) -> bool:
+ assert self._session is not None
+ return len(self._progress_nodeids_reported) == self._session.testscollected
+
+ def pytest_runtest_logfinish(self, nodeid: str) -> None:
+ assert self._session
+ if self.verbosity <= 0 and self._show_progress_info:
+ if self._show_progress_info == "count":
+ num_tests = self._session.testscollected
+ progress_length = len(f" [{num_tests}/{num_tests}]")
+ else:
+ progress_length = len(" [100%]")
+
+ self._progress_nodeids_reported.add(nodeid)
+
+ if self._is_last_item:
+ self._write_progress_information_filling_space()
+ else:
+ main_color, _ = self._get_main_color()
+ w = self._width_of_current_line
+ past_edge = w + progress_length + 1 >= self._screen_width
+ if past_edge:
+ msg = self._get_progress_information_message()
+ self._tw.write(msg + "\n", **{main_color: True})
+
+ def _get_progress_information_message(self) -> str:
+ assert self._session
+ collected = self._session.testscollected
+ if self._show_progress_info == "count":
+ if collected:
+ progress = self._progress_nodeids_reported
+ counter_format = f"{{:{len(str(collected))}d}}"
+ format_string = f" [{counter_format}/{{}}]"
+ return format_string.format(len(progress), collected)
+ return f" [ {collected} / {collected} ]"
+ else:
+ if collected:
+ return " [{:3d}%]".format(
+ len(self._progress_nodeids_reported) * 100 // collected
+ )
+ return " [100%]"
+
+ def _write_progress_information_filling_space(self) -> None:
+ color, _ = self._get_main_color()
+ msg = self._get_progress_information_message()
+ w = self._width_of_current_line
+ fill = self._tw.fullwidth - w - 1
+ self.write(msg.rjust(fill), flush=True, **{color: True})
+
+ @property
+ def _width_of_current_line(self) -> int:
+ """Return the width of the current line."""
+ return self._tw.width_of_current_line
+
+ def pytest_collection(self) -> None:
+ if self.isatty:
+ if self.config.option.verbose >= 0:
+ self.write("collecting ... ", flush=True, bold=True)
+ self._collect_report_last_write = timing.time()
+ elif self.config.option.verbose >= 1:
+ self.write("collecting ... ", flush=True, bold=True)
+
+ def pytest_collectreport(self, report: CollectReport) -> None:
+ if report.failed:
+ self._add_stats("error", [report])
+ elif report.skipped:
+ self._add_stats("skipped", [report])
+ items = [x for x in report.result if isinstance(x, Item)]
+ self._numcollected += len(items)
+ if self.isatty:
+ self.report_collect()
+
+ def report_collect(self, final: bool = False) -> None:
+ if self.config.option.verbose < 0:
+ return
+
+ if not final:
+ # Only write "collecting" report every 0.5s.
+ t = timing.time()
+ if (
+ self._collect_report_last_write is not None
+ and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION
+ ):
+ return
+ self._collect_report_last_write = t
+
+ errors = len(self.stats.get("error", []))
+ skipped = len(self.stats.get("skipped", []))
+ deselected = len(self.stats.get("deselected", []))
+ selected = self._numcollected - deselected
+ line = "collected " if final else "collecting "
+ line += (
+ str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
+ )
+ if errors:
+ line += " / %d error%s" % (errors, "s" if errors != 1 else "")
+ if deselected:
+ line += " / %d deselected" % deselected
+ if skipped:
+ line += " / %d skipped" % skipped
+ if self._numcollected > selected:
+ line += " / %d selected" % selected
+ if self.isatty:
+ self.rewrite(line, bold=True, erase=True)
+ if final:
+ self.write("\n")
+ else:
+ self.write_line(line)
+
+ @hookimpl(trylast=True)
+ def pytest_sessionstart(self, session: "Session") -> None:
+ self._session = session
+ self._sessionstarttime = timing.time()
+ if not self.showheader:
+ return
+ self.write_sep("=", "test session starts", bold=True)
+ verinfo = platform.python_version()
+ if not self.no_header:
+ msg = f"platform {sys.platform} -- Python {verinfo}"
+ pypy_version_info = getattr(sys, "pypy_version_info", None)
+ if pypy_version_info:
+ verinfo = ".".join(map(str, pypy_version_info[:3]))
+ msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]"
+ msg += ", pytest-{}, pluggy-{}".format(
+ _pytest._version.version, pluggy.__version__
+ )
+ if (
+ self.verbosity > 0
+ or self.config.option.debug
+ or getattr(self.config.option, "pastebin", None)
+ ):
+ msg += " -- " + str(sys.executable)
+ self.write_line(msg)
+ lines = self.config.hook.pytest_report_header(
+ config=self.config, start_path=self.startpath
+ )
+ self._write_report_lines_from_hooks(lines)
+
+ def _write_report_lines_from_hooks(
+ self, lines: Sequence[Union[str, Sequence[str]]]
+ ) -> None:
+ for line_or_lines in reversed(lines):
+ if isinstance(line_or_lines, str):
+ self.write_line(line_or_lines)
+ else:
+ for line in line_or_lines:
+ self.write_line(line)
+
+ def pytest_report_header(self, config: Config) -> List[str]:
+ result = [f"rootdir: {config.rootpath}"]
+
+ if config.inipath:
+ result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
+
+ if config.args_source == Config.ArgsSource.TESTPATHS:
+ testpaths: List[str] = config.getini("testpaths")
+ result.append("testpaths: {}".format(", ".join(testpaths)))
+
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
+ if plugininfo:
+ result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo)))
+ return result
+
+ def pytest_collection_finish(self, session: "Session") -> None:
+ self.report_collect(True)
+
+ lines = self.config.hook.pytest_report_collectionfinish(
+ config=self.config,
+ start_path=self.startpath,
+ items=session.items,
+ )
+ self._write_report_lines_from_hooks(lines)
+
+ if self.config.getoption("collectonly"):
+ if session.items:
+ if self.config.option.verbose > -1:
+ self._tw.line("")
+ self._printcollecteditems(session.items)
+
+ failed = self.stats.get("failed")
+ if failed:
+ self._tw.sep("!", "collection failures")
+ for rep in failed:
+ rep.toterminal(self._tw)
+
+ def _printcollecteditems(self, items: Sequence[Item]) -> None:
+ if self.config.option.verbose < 0:
+ if self.config.option.verbose < -1:
+ counts = Counter(item.nodeid.split("::", 1)[0] for item in items)
+ for name, count in sorted(counts.items()):
+ self._tw.line("%s: %d" % (name, count))
+ else:
+ for item in items:
+ self._tw.line(item.nodeid)
+ return
+ stack: List[Node] = []
+ indent = ""
+ for item in items:
+ needed_collectors = item.listchain()[1:] # strip root node
+ while stack:
+ if stack == needed_collectors[: len(stack)]:
+ break
+ stack.pop()
+ for col in needed_collectors[len(stack) :]:
+ stack.append(col)
+ indent = (len(stack) - 1) * " "
+ self._tw.line(f"{indent}{col}")
+ if self.config.option.verbose >= 1:
+ obj = getattr(col, "obj", None)
+ doc = inspect.getdoc(obj) if obj else None
+ if doc:
+ for line in doc.splitlines():
+ self._tw.line("{}{}".format(indent + " ", line))
+
+ @hookimpl(hookwrapper=True)
+ def pytest_sessionfinish(
+ self, session: "Session", exitstatus: Union[int, ExitCode]
+ ):
+ outcome = yield
+ outcome.get_result()
+ self._tw.line("")
+ summary_exit_codes = (
+ ExitCode.OK,
+ ExitCode.TESTS_FAILED,
+ ExitCode.INTERRUPTED,
+ ExitCode.USAGE_ERROR,
+ ExitCode.NO_TESTS_COLLECTED,
+ )
+ if exitstatus in summary_exit_codes and not self.no_summary:
+ self.config.hook.pytest_terminal_summary(
+ terminalreporter=self, exitstatus=exitstatus, config=self.config
+ )
+ if session.shouldfail:
+ self.write_sep("!", str(session.shouldfail), red=True)
+ if exitstatus == ExitCode.INTERRUPTED:
+ self._report_keyboardinterrupt()
+ self._keyboardinterrupt_memo = None
+ elif session.shouldstop:
+ self.write_sep("!", str(session.shouldstop), red=True)
+ self.summary_stats()
+
+ @hookimpl(hookwrapper=True)
+ def pytest_terminal_summary(self) -> Generator[None, None, None]:
+ self.summary_errors()
+ self.summary_failures()
+ self.summary_warnings()
+ self.summary_passes()
+ yield
+ self.short_test_summary()
+ # Display any extra warnings from teardown here (if any).
+ self.summary_warnings()
+
+ def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None:
+ self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
+
+ def pytest_unconfigure(self) -> None:
+ if self._keyboardinterrupt_memo is not None:
+ self._report_keyboardinterrupt()
+
+ def _report_keyboardinterrupt(self) -> None:
+ excrepr = self._keyboardinterrupt_memo
+ assert excrepr is not None
+ assert excrepr.reprcrash is not None
+ msg = excrepr.reprcrash.message
+ self.write_sep("!", msg)
+ if "KeyboardInterrupt" in msg:
+ if self.config.option.fulltrace:
+ excrepr.toterminal(self._tw)
+ else:
+ excrepr.reprcrash.toterminal(self._tw)
+ self._tw.line(
+ "(to show a full traceback on KeyboardInterrupt use --full-trace)",
+ yellow=True,
+ )
+
+ def _locationline(
+ self, nodeid: str, fspath: str, lineno: Optional[int], domain: str
+ ) -> str:
+ def mkrel(nodeid: str) -> str:
+ line = self.config.cwd_relative_nodeid(nodeid)
+ if domain and line.endswith(domain):
+ line = line[: -len(domain)]
+ values = domain.split("[")
+ values[0] = values[0].replace(".", "::") # don't replace '.' in params
+ line += "[".join(values)
+ return line
+
+ # collect_fspath comes from testid which has a "/"-normalized path.
+ if fspath:
+ res = mkrel(nodeid)
+ if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
+ "\\", nodes.SEP
+ ):
+ res += " <- " + bestrelpath(self.startpath, Path(fspath))
+ else:
+ res = "[location]"
+ return res + " "
+
+ def _getfailureheadline(self, rep):
+ head_line = rep.head_line
+ if head_line:
+ return head_line
+ return "test session" # XXX?
+
+ def _getcrashline(self, rep):
+ try:
+ return str(rep.longrepr.reprcrash)
+ except AttributeError:
+ try:
+ return str(rep.longrepr)[:50]
+ except AttributeError:
+ return ""
+
+ #
+ # Summaries for sessionfinish.
+ #
+ def getreports(self, name: str):
+ return [x for x in self.stats.get(name, ()) if not hasattr(x, "_pdbshown")]
+
+ def summary_warnings(self) -> None:
+ if self.hasopt("w"):
+ all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings")
+ if not all_warnings:
+ return
+
+ final = self._already_displayed_warnings is not None
+ if final:
+ warning_reports = all_warnings[self._already_displayed_warnings :]
+ else:
+ warning_reports = all_warnings
+ self._already_displayed_warnings = len(warning_reports)
+ if not warning_reports:
+ return
+
+ reports_grouped_by_message: Dict[str, List[WarningReport]] = {}
+ for wr in warning_reports:
+ reports_grouped_by_message.setdefault(wr.message, []).append(wr)
+
+ def collapsed_location_report(reports: List[WarningReport]) -> str:
+ locations = []
+ for w in reports:
+ location = w.get_location(self.config)
+ if location:
+ locations.append(location)
+
+ if len(locations) < 10:
+ return "\n".join(map(str, locations))
+
+ counts_by_filename = Counter(
+ str(loc).split("::", 1)[0] for loc in locations
+ )
+ return "\n".join(
+ "{}: {} warning{}".format(k, v, "s" if v > 1 else "")
+ for k, v in counts_by_filename.items()
+ )
+
+ title = "warnings summary (final)" if final else "warnings summary"
+ self.write_sep("=", title, yellow=True, bold=False)
+ for message, message_reports in reports_grouped_by_message.items():
+ maybe_location = collapsed_location_report(message_reports)
+ if maybe_location:
+ self._tw.line(maybe_location)
+ lines = message.splitlines()
+ indented = "\n".join(" " + x for x in lines)
+ message = indented.rstrip()
+ else:
+ message = message.rstrip()
+ self._tw.line(message)
+ self._tw.line()
+ self._tw.line(
+ "-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html"
+ )
+
+ def summary_passes(self) -> None:
+ if self.config.option.tbstyle != "no":
+ if self.hasopt("P"):
+ reports: List[TestReport] = self.getreports("passed")
+ if not reports:
+ return
+ self.write_sep("=", "PASSES")
+ for rep in reports:
+ if rep.sections:
+ msg = self._getfailureheadline(rep)
+ self.write_sep("_", msg, green=True, bold=True)
+ self._outrep_summary(rep)
+ self._handle_teardown_sections(rep.nodeid)
+
+ def _get_teardown_reports(self, nodeid: str) -> List[TestReport]:
+ reports = self.getreports("")
+ return [
+ report
+ for report in reports
+ if report.when == "teardown" and report.nodeid == nodeid
+ ]
+
+ def _handle_teardown_sections(self, nodeid: str) -> None:
+ for report in self._get_teardown_reports(nodeid):
+ self.print_teardown_sections(report)
+
+ def print_teardown_sections(self, rep: TestReport) -> None:
+ showcapture = self.config.option.showcapture
+ if showcapture == "no":
+ return
+ for secname, content in rep.sections:
+ if showcapture != "all" and showcapture not in secname:
+ continue
+ if "teardown" in secname:
+ self._tw.sep("-", secname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ self._tw.line(content)
+
+ def summary_failures(self) -> None:
+ if self.config.option.tbstyle != "no":
+ reports: List[BaseReport] = self.getreports("failed")
+ if not reports:
+ return
+ self.write_sep("=", "FAILURES")
+ if self.config.option.tbstyle == "line":
+ for rep in reports:
+ line = self._getcrashline(rep)
+ self.write_line(line)
+ else:
+ for rep in reports:
+ msg = self._getfailureheadline(rep)
+ self.write_sep("_", msg, red=True, bold=True)
+ self._outrep_summary(rep)
+ self._handle_teardown_sections(rep.nodeid)
+
+ def summary_errors(self) -> None:
+ if self.config.option.tbstyle != "no":
+ reports: List[BaseReport] = self.getreports("error")
+ if not reports:
+ return
+ self.write_sep("=", "ERRORS")
+ for rep in self.stats["error"]:
+ msg = self._getfailureheadline(rep)
+ if rep.when == "collect":
+ msg = "ERROR collecting " + msg
+ else:
+ msg = f"ERROR at {rep.when} of {msg}"
+ self.write_sep("_", msg, red=True, bold=True)
+ self._outrep_summary(rep)
+
+ def _outrep_summary(self, rep: BaseReport) -> None:
+ rep.toterminal(self._tw)
+ showcapture = self.config.option.showcapture
+ if showcapture == "no":
+ return
+ for secname, content in rep.sections:
+ if showcapture != "all" and showcapture not in secname:
+ continue
+ self._tw.sep("-", secname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ self._tw.line(content)
+
+ def summary_stats(self) -> None:
+ if self.verbosity < -1:
+ return
+
+ session_duration = timing.time() - self._sessionstarttime
+ (parts, main_color) = self.build_summary_stats_line()
+ line_parts = []
+
+ display_sep = self.verbosity >= 0
+ if display_sep:
+ fullwidth = self._tw.fullwidth
+ for text, markup in parts:
+ with_markup = self._tw.markup(text, **markup)
+ if display_sep:
+ fullwidth += len(with_markup) - len(text)
+ line_parts.append(with_markup)
+ msg = ", ".join(line_parts)
+
+ main_markup = {main_color: True}
+ duration = f" in {format_session_duration(session_duration)}"
+ duration_with_markup = self._tw.markup(duration, **main_markup)
+ if display_sep:
+ fullwidth += len(duration_with_markup) - len(duration)
+ msg += duration_with_markup
+
+ if display_sep:
+ markup_for_end_sep = self._tw.markup("", **main_markup)
+ if markup_for_end_sep.endswith("\x1b[0m"):
+ markup_for_end_sep = markup_for_end_sep[:-4]
+ fullwidth += len(markup_for_end_sep)
+ msg += markup_for_end_sep
+
+ if display_sep:
+ self.write_sep("=", msg, fullwidth=fullwidth, **main_markup)
+ else:
+ self.write_line(msg, **main_markup)
+
+ def short_test_summary(self) -> None:
+ if not self.reportchars:
+ return
+
+ def show_simple(lines: List[str], *, stat: str) -> None:
+ failed = self.stats.get(stat, [])
+ if not failed:
+ return
+ config = self.config
+ for rep in failed:
+ color = _color_for_type.get(stat, _color_for_type_default)
+ line = _get_line_with_reprcrash_message(
+ config, rep, self._tw, {color: True}
+ )
+ lines.append(line)
+
+ def show_xfailed(lines: List[str]) -> None:
+ xfailed = self.stats.get("xfailed", [])
+ for rep in xfailed:
+ verbose_word = rep._get_verbose_word(self.config)
+ markup_word = self._tw.markup(
+ verbose_word, **{_color_for_type["warnings"]: True}
+ )
+ nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
+ line = f"{markup_word} {nodeid}"
+ reason = rep.wasxfail
+ if reason:
+ line += " - " + str(reason)
+
+ lines.append(line)
+
+ def show_xpassed(lines: List[str]) -> None:
+ xpassed = self.stats.get("xpassed", [])
+ for rep in xpassed:
+ verbose_word = rep._get_verbose_word(self.config)
+ markup_word = self._tw.markup(
+ verbose_word, **{_color_for_type["warnings"]: True}
+ )
+ nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
+ reason = rep.wasxfail
+ lines.append(f"{markup_word} {nodeid} {reason}")
+
+ def show_skipped(lines: List[str]) -> None:
+ skipped: List[CollectReport] = self.stats.get("skipped", [])
+ fskips = _folded_skips(self.startpath, skipped) if skipped else []
+ if not fskips:
+ return
+ verbose_word = skipped[0]._get_verbose_word(self.config)
+ markup_word = self._tw.markup(
+ verbose_word, **{_color_for_type["warnings"]: True}
+ )
+ prefix = "Skipped: "
+ for num, fspath, lineno, reason in fskips:
+ if reason.startswith(prefix):
+ reason = reason[len(prefix) :]
+ if lineno is not None:
+ lines.append(
+ "%s [%d] %s:%d: %s" % (markup_word, num, fspath, lineno, reason)
+ )
+ else:
+ lines.append("%s [%d] %s: %s" % (markup_word, num, fspath, reason))
+
+ REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = {
+ "x": show_xfailed,
+ "X": show_xpassed,
+ "f": partial(show_simple, stat="failed"),
+ "s": show_skipped,
+ "p": partial(show_simple, stat="passed"),
+ "E": partial(show_simple, stat="error"),
+ }
+
+ lines: List[str] = []
+ for char in self.reportchars:
+ action = REPORTCHAR_ACTIONS.get(char)
+ if action: # skipping e.g. "P" (passed with output) here.
+ action(lines)
+
+ if lines:
+ self.write_sep("=", "short test summary info", cyan=True, bold=True)
+ for line in lines:
+ self.write_line(line)
+
+ def _get_main_color(self) -> Tuple[str, List[str]]:
+ if self._main_color is None or self._known_types is None or self._is_last_item:
+ self._set_main_color()
+ assert self._main_color
+ assert self._known_types
+ return self._main_color, self._known_types
+
+ def _determine_main_color(self, unknown_type_seen: bool) -> str:
+ stats = self.stats
+ if "failed" in stats or "error" in stats:
+ main_color = "red"
+ elif "warnings" in stats or "xpassed" in stats or unknown_type_seen:
+ main_color = "yellow"
+ elif "passed" in stats or not self._is_last_item:
+ main_color = "green"
+ else:
+ main_color = "yellow"
+ return main_color
+
+ def _set_main_color(self) -> None:
+ unknown_types: List[str] = []
+ for found_type in self.stats.keys():
+ if found_type: # setup/teardown reports have an empty key, ignore them
+ if found_type not in KNOWN_TYPES and found_type not in unknown_types:
+ unknown_types.append(found_type)
+ self._known_types = list(KNOWN_TYPES) + unknown_types
+ self._main_color = self._determine_main_color(bool(unknown_types))
+
+ def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
+ """
+ Build the parts used in the last summary stats line.
+
+ The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===".
+
+ This function builds a list of the "parts" that make up for the text in that line, in
+ the example above it would be:
+
+ [
+ ("12 passed", {"green": True}),
+ ("2 errors", {"red": True}
+ ]
+
+ That last dict for each line is a "markup dictionary", used by TerminalWriter to
+ color output.
+
+ The final color of the line is also determined by this function, and is the second
+ element of the returned tuple.
+ """
+ if self.config.getoption("collectonly"):
+ return self._build_collect_only_summary_stats_line()
+ else:
+ return self._build_normal_summary_stats_line()
+
+ def _get_reports_to_display(self, key: str) -> List[Any]:
+ """Get test/collection reports for the given status key, such as `passed` or `error`."""
+ reports = self.stats.get(key, [])
+ return [x for x in reports if getattr(x, "count_towards_summary", True)]
+
+ def _build_normal_summary_stats_line(
+ self,
+ ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
+ main_color, known_types = self._get_main_color()
+ parts = []
+
+ for key in known_types:
+ reports = self._get_reports_to_display(key)
+ if reports:
+ count = len(reports)
+ color = _color_for_type.get(key, _color_for_type_default)
+ markup = {color: True, "bold": color == main_color}
+ parts.append(("%d %s" % pluralize(count, key), markup))
+
+ if not parts:
+ parts = [("no tests ran", {_color_for_type_default: True})]
+
+ return parts, main_color
+
+ def _build_collect_only_summary_stats_line(
+ self,
+ ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
+ deselected = len(self._get_reports_to_display("deselected"))
+ errors = len(self._get_reports_to_display("error"))
+
+ if self._numcollected == 0:
+ parts = [("no tests collected", {"yellow": True})]
+ main_color = "yellow"
+
+ elif deselected == 0:
+ main_color = "green"
+ collected_output = "%d %s collected" % pluralize(self._numcollected, "test")
+ parts = [(collected_output, {main_color: True})]
+ else:
+ all_tests_were_deselected = self._numcollected == deselected
+ if all_tests_were_deselected:
+ main_color = "yellow"
+ collected_output = f"no tests collected ({deselected} deselected)"
+ else:
+ main_color = "green"
+ selected = self._numcollected - deselected
+ collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)"
+
+ parts = [(collected_output, {main_color: True})]
+
+ if errors:
+ main_color = _color_for_type["error"]
+ parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})]
+
+ return parts, main_color
+
+
+def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport):
+ nodeid = config.cwd_relative_nodeid(rep.nodeid)
+ path, *parts = nodeid.split("::")
+ if parts:
+ parts_markup = tw.markup("::".join(parts), bold=True)
+ return path + "::" + parts_markup
+ else:
+ return path
+
+
+def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]:
+ """Format msg into format, ellipsizing it if doesn't fit in available_width.
+
+ Returns None if even the ellipsis can't fit.
+ """
+ # Only use the first line.
+ i = msg.find("\n")
+ if i != -1:
+ msg = msg[:i]
+
+ ellipsis = "..."
+ format_width = wcswidth(format.format(""))
+ if format_width + len(ellipsis) > available_width:
+ return None
+
+ if format_width + wcswidth(msg) > available_width:
+ available_width -= len(ellipsis)
+ msg = msg[:available_width]
+ while format_width + wcswidth(msg) > available_width:
+ msg = msg[:-1]
+ msg += ellipsis
+
+ return format.format(msg)
+
+
+def _get_line_with_reprcrash_message(
+ config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: Dict[str, bool]
+) -> str:
+ """Get summary line for a report, trying to add reprcrash message."""
+ verbose_word = rep._get_verbose_word(config)
+ word = tw.markup(verbose_word, **word_markup)
+ node = _get_node_id_with_markup(tw, config, rep)
+
+ line = f"{word} {node}"
+ line_width = wcswidth(line)
+
+ try:
+ # Type ignored intentionally -- possible AttributeError expected.
+ msg = rep.longrepr.reprcrash.message # type: ignore[union-attr]
+ except AttributeError:
+ pass
+ else:
+ if not running_on_ci():
+ available_width = tw.fullwidth - line_width
+ msg = _format_trimmed(" - {}", msg, available_width)
+ else:
+ msg = f" - {msg}"
+ if msg is not None:
+ line += msg
+
+ return line
+
+
+def _folded_skips(
+ startpath: Path,
+ skipped: Sequence[CollectReport],
+) -> List[Tuple[int, str, Optional[int], str]]:
+ d: Dict[Tuple[str, Optional[int], str], List[CollectReport]] = {}
+ for event in skipped:
+ assert event.longrepr is not None
+ assert isinstance(event.longrepr, tuple), (event, event.longrepr)
+ assert len(event.longrepr) == 3, (event, event.longrepr)
+ fspath, lineno, reason = event.longrepr
+ # For consistency, report all fspaths in relative form.
+ fspath = bestrelpath(startpath, Path(fspath))
+ keywords = getattr(event, "keywords", {})
+ # Folding reports with global pytestmark variable.
+ # This is a workaround, because for now we cannot identify the scope of a skip marker
+ # TODO: Revisit after marks scope would be fixed.
+ if (
+ event.when == "setup"
+ and "skip" in keywords
+ and "pytestmark" not in keywords
+ ):
+ key: Tuple[str, Optional[int], str] = (fspath, None, reason)
+ else:
+ key = (fspath, lineno, reason)
+ d.setdefault(key, []).append(event)
+ values: List[Tuple[int, str, Optional[int], str]] = []
+ for key, events in d.items():
+ values.append((len(events), *key))
+ return values
+
+
+_color_for_type = {
+ "failed": "red",
+ "error": "red",
+ "warnings": "yellow",
+ "passed": "green",
+}
+_color_for_type_default = "yellow"
+
+
+def pluralize(count: int, noun: str) -> Tuple[int, str]:
+ # No need to pluralize words such as `failed` or `passed`.
+ if noun not in ["error", "warnings", "test"]:
+ return count, noun
+
+ # The `warnings` key is plural. To avoid API breakage, we keep it that way but
+ # set it to singular here so we can determine plurality in the same way as we do
+ # for `error`.
+ noun = noun.replace("warnings", "warning")
+
+ return count, noun + "s" if count != 1 else noun
+
+
+def _plugin_nameversions(plugininfo) -> List[str]:
+ values: List[str] = []
+ for plugin, dist in plugininfo:
+ # Gets us name and version!
+ name = "{dist.project_name}-{dist.version}".format(dist=dist)
+ # Questionable convenience, but it keeps things short.
+ if name.startswith("pytest-"):
+ name = name[7:]
+ # We decided to print python package names they can have more than one plugin.
+ if name not in values:
+ values.append(name)
+ return values
+
+
+def format_session_duration(seconds: float) -> str:
+ """Format the given seconds in a human readable manner to show in the final summary."""
+ if seconds < 60:
+ return f"{seconds:.2f}s"
+ else:
+ dt = datetime.timedelta(seconds=int(seconds))
+ return f"{seconds:.2f}s ({dt})"
+
+
+def _get_raw_skip_reason(report: TestReport) -> str:
+ """Get the reason string of a skip/xfail/xpass test report.
+
+ The string is just the part given by the user.
+ """
+ if hasattr(report, "wasxfail"):
+ reason = cast(str, report.wasxfail)
+ if reason.startswith("reason: "):
+ reason = reason[len("reason: ") :]
+ return reason
+ else:
+ assert report.skipped
+ assert isinstance(report.longrepr, tuple)
+ _, _, reason = report.longrepr
+ if reason.startswith("Skipped: "):
+ reason = reason[len("Skipped: ") :]
+ elif reason == "Skipped":
+ reason = ""
+ return reason
diff --git a/venv/lib/python3.12/site-packages/_pytest/threadexception.py b/venv/lib/python3.12/site-packages/_pytest/threadexception.py
new file mode 100644
index 0000000..43341e7
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/threadexception.py
@@ -0,0 +1,88 @@
+import threading
+import traceback
+import warnings
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import Optional
+from typing import Type
+
+import pytest
+
+
+# Copied from cpython/Lib/test/support/threading_helper.py, with modifications.
+class catch_threading_exception:
+ """Context manager catching threading.Thread exception using
+ threading.excepthook.
+
+ Storing exc_value using a custom hook can create a reference cycle. The
+ reference cycle is broken explicitly when the context manager exits.
+
+ Storing thread using a custom hook can resurrect it if it is set to an
+ object which is being finalized. Exiting the context manager clears the
+ stored object.
+
+ Usage:
+ with threading_helper.catch_threading_exception() as cm:
+ # code spawning a thread which raises an exception
+ ...
+ # check the thread exception: use cm.args
+ ...
+ # cm.args attribute no longer exists at this point
+ # (to break a reference cycle)
+ """
+
+ def __init__(self) -> None:
+ self.args: Optional["threading.ExceptHookArgs"] = None
+ self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None
+
+ def _hook(self, args: "threading.ExceptHookArgs") -> None:
+ self.args = args
+
+ def __enter__(self) -> "catch_threading_exception":
+ self._old_hook = threading.excepthook
+ threading.excepthook = self._hook
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ assert self._old_hook is not None
+ threading.excepthook = self._old_hook
+ self._old_hook = None
+ del self.args
+
+
+def thread_exception_runtest_hook() -> Generator[None, None, None]:
+ with catch_threading_exception() as cm:
+ yield
+ if cm.args:
+ thread_name = "" if cm.args.thread is None else cm.args.thread.name
+ msg = f"Exception in thread {thread_name}\n\n"
+ msg += "".join(
+ traceback.format_exception(
+ cm.args.exc_type,
+ cm.args.exc_value,
+ cm.args.exc_traceback,
+ )
+ )
+ warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))
+
+
+@pytest.hookimpl(hookwrapper=True, trylast=True)
+def pytest_runtest_setup() -> Generator[None, None, None]:
+ yield from thread_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_call() -> Generator[None, None, None]:
+ yield from thread_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_teardown() -> Generator[None, None, None]:
+ yield from thread_exception_runtest_hook()
diff --git a/venv/lib/python3.12/site-packages/_pytest/timing.py b/venv/lib/python3.12/site-packages/_pytest/timing.py
new file mode 100644
index 0000000..925163a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/timing.py
@@ -0,0 +1,12 @@
+"""Indirection for time functions.
+
+We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
+pytest runtime information (issue #185).
+
+Fixture "mock_timing" also interacts with this module for pytest's own tests.
+"""
+from time import perf_counter
+from time import sleep
+from time import time
+
+__all__ = ["perf_counter", "sleep", "time"]
diff --git a/venv/lib/python3.12/site-packages/_pytest/tmpdir.py b/venv/lib/python3.12/site-packages/_pytest/tmpdir.py
new file mode 100644
index 0000000..3cc2bac
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/tmpdir.py
@@ -0,0 +1,324 @@
+"""Support for providing temporary directories to test functions."""
+import dataclasses
+import os
+import re
+import tempfile
+from pathlib import Path
+from shutil import rmtree
+from typing import Any
+from typing import Dict
+from typing import Generator
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from _pytest.nodes import Item
+from _pytest.reports import CollectReport
+from _pytest.stash import StashKey
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ RetentionType = Literal["all", "failed", "none"]
+
+
+from _pytest.config.argparsing import Parser
+
+from .pathlib import LOCK_TIMEOUT
+from .pathlib import make_numbered_dir
+from .pathlib import make_numbered_dir_with_cleanup
+from .pathlib import rm_rf
+from .pathlib import cleanup_dead_symlinks
+from _pytest.compat import final, get_user_id
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.monkeypatch import MonkeyPatch
+
+tmppath_result_key = StashKey[Dict[str, bool]]()
+
+
+@final
+@dataclasses.dataclass
+class TempPathFactory:
+ """Factory for temporary directories under the common base temp directory.
+
+ The base directory can be configured using the ``--basetemp`` option.
+ """
+
+ _given_basetemp: Optional[Path]
+ # pluggy TagTracerSub, not currently exposed, so Any.
+ _trace: Any
+ _basetemp: Optional[Path]
+ _retention_count: int
+ _retention_policy: "RetentionType"
+
+ def __init__(
+ self,
+ given_basetemp: Optional[Path],
+ retention_count: int,
+ retention_policy: "RetentionType",
+ trace,
+ basetemp: Optional[Path] = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ if given_basetemp is None:
+ self._given_basetemp = None
+ else:
+ # Use os.path.abspath() to get absolute path instead of resolve() as it
+ # does not work the same in all platforms (see #4427).
+ # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
+ self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
+ self._trace = trace
+ self._retention_count = retention_count
+ self._retention_policy = retention_policy
+ self._basetemp = basetemp
+
+ @classmethod
+ def from_config(
+ cls,
+ config: Config,
+ *,
+ _ispytest: bool = False,
+ ) -> "TempPathFactory":
+ """Create a factory according to pytest configuration.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ count = int(config.getini("tmp_path_retention_count"))
+ if count < 0:
+ raise ValueError(
+ f"tmp_path_retention_count must be >= 0. Current input: {count}."
+ )
+
+ policy = config.getini("tmp_path_retention_policy")
+ if policy not in ("all", "failed", "none"):
+ raise ValueError(
+ f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
+ )
+
+ return cls(
+ given_basetemp=config.option.basetemp,
+ trace=config.trace.get("tmpdir"),
+ retention_count=count,
+ retention_policy=policy,
+ _ispytest=True,
+ )
+
+ def _ensure_relative_to_basetemp(self, basename: str) -> str:
+ basename = os.path.normpath(basename)
+ if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
+ raise ValueError(f"{basename} is not a normalized and relative path")
+ return basename
+
+ def mktemp(self, basename: str, numbered: bool = True) -> Path:
+ """Create a new temporary directory managed by the factory.
+
+ :param basename:
+ Directory base name, must be a relative path.
+
+ :param numbered:
+ If ``True``, ensure the directory is unique by adding a numbered
+ suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
+ means that this function will create directories named ``"foo-0"``,
+ ``"foo-1"``, ``"foo-2"`` and so on.
+
+ :returns:
+ The path to the new directory.
+ """
+ basename = self._ensure_relative_to_basetemp(basename)
+ if not numbered:
+ p = self.getbasetemp().joinpath(basename)
+ p.mkdir(mode=0o700)
+ else:
+ p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
+ self._trace("mktemp", p)
+ return p
+
+ def getbasetemp(self) -> Path:
+ """Return the base temporary directory, creating it if needed.
+
+ :returns:
+ The base temporary directory.
+ """
+ if self._basetemp is not None:
+ return self._basetemp
+
+ if self._given_basetemp is not None:
+ basetemp = self._given_basetemp
+ if basetemp.exists():
+ rm_rf(basetemp)
+ basetemp.mkdir(mode=0o700)
+ basetemp = basetemp.resolve()
+ else:
+ from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
+ temproot = Path(from_env or tempfile.gettempdir()).resolve()
+ user = get_user() or "unknown"
+ # use a sub-directory in the temproot to speed-up
+ # make_numbered_dir() call
+ rootdir = temproot.joinpath(f"pytest-of-{user}")
+ try:
+ rootdir.mkdir(mode=0o700, exist_ok=True)
+ except OSError:
+ # getuser() likely returned illegal characters for the platform, use unknown back off mechanism
+ rootdir = temproot.joinpath("pytest-of-unknown")
+ rootdir.mkdir(mode=0o700, exist_ok=True)
+ # Because we use exist_ok=True with a predictable name, make sure
+ # we are the owners, to prevent any funny business (on unix, where
+ # temproot is usually shared).
+ # Also, to keep things private, fixup any world-readable temp
+ # rootdir's permissions. Historically 0o755 was used, so we can't
+ # just error out on this, at least for a while.
+ uid = get_user_id()
+ if uid is not None:
+ rootdir_stat = rootdir.stat()
+ if rootdir_stat.st_uid != uid:
+ raise OSError(
+ f"The temporary directory {rootdir} is not owned by the current user. "
+ "Fix this and try again."
+ )
+ if (rootdir_stat.st_mode & 0o077) != 0:
+ os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
+ keep = self._retention_count
+ if self._retention_policy == "none":
+ keep = 0
+ basetemp = make_numbered_dir_with_cleanup(
+ prefix="pytest-",
+ root=rootdir,
+ keep=keep,
+ lock_timeout=LOCK_TIMEOUT,
+ mode=0o700,
+ )
+ assert basetemp is not None, basetemp
+ self._basetemp = basetemp
+ self._trace("new basetemp", basetemp)
+ return basetemp
+
+
+def get_user() -> Optional[str]:
+ """Return the current user name, or None if getuser() does not work
+ in the current environment (see #1010)."""
+ try:
+ # In some exotic environments, getpass may not be importable.
+ import getpass
+
+ return getpass.getuser()
+ except (ImportError, KeyError):
+ return None
+
+
+def pytest_configure(config: Config) -> None:
+ """Create a TempPathFactory and attach it to the config object.
+
+ This is to comply with existing plugins which expect the handler to be
+ available at pytest_configure time, but ideally should be moved entirely
+ to the tmp_path_factory session fixture.
+ """
+ mp = MonkeyPatch()
+ config.add_cleanup(mp.undo)
+ _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
+ mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "tmp_path_retention_count",
+ help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.",
+ default=3,
+ )
+
+ parser.addini(
+ "tmp_path_retention_policy",
+ help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
+ "(all/failed/none)",
+ default="all",
+ )
+
+
+@fixture(scope="session")
+def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
+ """Return a :class:`pytest.TempPathFactory` instance for the test session."""
+ # Set dynamically by pytest_configure() above.
+ return request.config._tmp_path_factory # type: ignore
+
+
+def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
+ name = request.node.name
+ name = re.sub(r"[\W]", "_", name)
+ MAXVAL = 30
+ name = name[:MAXVAL]
+ return factory.mktemp(name, numbered=True)
+
+
+@fixture
+def tmp_path(
+ request: FixtureRequest, tmp_path_factory: TempPathFactory
+) -> Generator[Path, None, None]:
+ """Return a temporary directory path object which is unique to each test
+ function invocation, created as a sub directory of the base temporary
+ directory.
+
+ By default, a new base temporary directory is created each test session,
+ and old bases are removed after 3 sessions, to aid in debugging.
+ This behavior can be configured with :confval:`tmp_path_retention_count` and
+ :confval:`tmp_path_retention_policy`.
+ If ``--basetemp`` is used then it is cleared each session. See :ref:`base
+ temporary directory`.
+
+ The returned object is a :class:`pathlib.Path` object.
+ """
+
+ path = _mk_tmp(request, tmp_path_factory)
+ yield path
+
+ # Remove the tmpdir if the policy is "failed" and the test passed.
+ tmp_path_factory: TempPathFactory = request.session.config._tmp_path_factory # type: ignore
+ policy = tmp_path_factory._retention_policy
+ result_dict = request.node.stash[tmppath_result_key]
+
+ if policy == "failed" and result_dict.get("call", True):
+ # We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
+ # permissions, etc, in which case we ignore it.
+ rmtree(path, ignore_errors=True)
+
+ del request.node.stash[tmppath_result_key]
+
+
+def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
+ """After each session, remove base directory if all the tests passed,
+ the policy is "failed", and the basetemp is not specified by a user.
+ """
+ tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
+ basetemp = tmp_path_factory._basetemp
+ if basetemp is None:
+ return
+
+ policy = tmp_path_factory._retention_policy
+ if (
+ exitstatus == 0
+ and policy == "failed"
+ and tmp_path_factory._given_basetemp is None
+ ):
+ if basetemp.is_dir():
+ # We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
+ # permissions, etc, in which case we ignore it.
+ rmtree(basetemp, ignore_errors=True)
+
+ # Remove dead symlinks.
+ if basetemp.is_dir():
+ cleanup_dead_symlinks(basetemp)
+
+
+@hookimpl(tryfirst=True, hookwrapper=True)
+def pytest_runtest_makereport(item: Item, call):
+ outcome = yield
+ result: CollectReport = outcome.get_result()
+
+ empty: Dict[str, bool] = {}
+ item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed
diff --git a/venv/lib/python3.12/site-packages/_pytest/unittest.py b/venv/lib/python3.12/site-packages/_pytest/unittest.py
new file mode 100644
index 0000000..d42a12a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/unittest.py
@@ -0,0 +1,421 @@
+"""Discover and run std-library "unittest" style tests."""
+import sys
+import traceback
+import types
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import Iterable
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+import _pytest._code
+import pytest
+from _pytest.compat import getimfunc
+from _pytest.compat import is_async_function
+from _pytest.config import hookimpl
+from _pytest.fixtures import FixtureRequest
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import exit
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.outcomes import xfail
+from _pytest.python import Class
+from _pytest.python import Function
+from _pytest.python import Module
+from _pytest.runner import CallInfo
+from _pytest.scope import Scope
+
+if TYPE_CHECKING:
+ import unittest
+ import twisted.trial.unittest
+
+ _SysExcInfoType = Union[
+ Tuple[Type[BaseException], BaseException, types.TracebackType],
+ Tuple[None, None, None],
+ ]
+
+
+def pytest_pycollect_makeitem(
+ collector: Union[Module, Class], name: str, obj: object
+) -> Optional["UnitTestCase"]:
+ # Has unittest been imported and is obj a subclass of its TestCase?
+ try:
+ ut = sys.modules["unittest"]
+ # Type ignored because `ut` is an opaque module.
+ if not issubclass(obj, ut.TestCase): # type: ignore
+ return None
+ except Exception:
+ return None
+ # Yes, so let's collect it.
+ item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj)
+ return item
+
+
+class UnitTestCase(Class):
+ # Marker for fixturemanger.getfixtureinfo()
+ # to declare that our children do not support funcargs.
+ nofuncargs = True
+
+ def collect(self) -> Iterable[Union[Item, Collector]]:
+ from unittest import TestLoader
+
+ cls = self.obj
+ if not getattr(cls, "__test__", True):
+ return
+
+ skipped = _is_skipped(cls)
+ if not skipped:
+ self._inject_setup_teardown_fixtures(cls)
+ self._inject_setup_class_fixture()
+
+ self.session._fixturemanager.parsefactories(self, unittest=True)
+ loader = TestLoader()
+ foundsomething = False
+ for name in loader.getTestCaseNames(self.obj):
+ x = getattr(self.obj, name)
+ if not getattr(x, "__test__", True):
+ continue
+ funcobj = getimfunc(x)
+ yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj)
+ foundsomething = True
+
+ if not foundsomething:
+ runtest = getattr(self.obj, "runTest", None)
+ if runtest is not None:
+ ut = sys.modules.get("twisted.trial.unittest", None)
+ # Type ignored because `ut` is an opaque module.
+ if ut is None or runtest != ut.TestCase.runTest: # type: ignore
+ yield TestCaseFunction.from_parent(self, name="runTest")
+
+ def _inject_setup_teardown_fixtures(self, cls: type) -> None:
+ """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
+ teardown functions (#517)."""
+ class_fixture = _make_xunit_fixture(
+ cls,
+ "setUpClass",
+ "tearDownClass",
+ "doClassCleanups",
+ scope=Scope.Class,
+ pass_self=False,
+ )
+ if class_fixture:
+ cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined]
+
+ method_fixture = _make_xunit_fixture(
+ cls,
+ "setup_method",
+ "teardown_method",
+ None,
+ scope=Scope.Function,
+ pass_self=True,
+ )
+ if method_fixture:
+ cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined]
+
+
+def _make_xunit_fixture(
+ obj: type,
+ setup_name: str,
+ teardown_name: str,
+ cleanup_name: Optional[str],
+ scope: Scope,
+ pass_self: bool,
+):
+ setup = getattr(obj, setup_name, None)
+ teardown = getattr(obj, teardown_name, None)
+ if setup is None and teardown is None:
+ return None
+
+ if cleanup_name:
+ cleanup = getattr(obj, cleanup_name, lambda *args: None)
+ else:
+
+ def cleanup(*args):
+ pass
+
+ @pytest.fixture(
+ scope=scope.value,
+ autouse=True,
+ # Use a unique name to speed up lookup.
+ name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}",
+ )
+ def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
+ if _is_skipped(self):
+ reason = self.__unittest_skip_why__
+ raise pytest.skip.Exception(reason, _use_item_location=True)
+ if setup is not None:
+ try:
+ if pass_self:
+ setup(self, request.function)
+ else:
+ setup()
+ # unittest does not call the cleanup function for every BaseException, so we
+ # follow this here.
+ except Exception:
+ if pass_self:
+ cleanup(self)
+ else:
+ cleanup()
+
+ raise
+ yield
+ try:
+ if teardown is not None:
+ if pass_self:
+ teardown(self, request.function)
+ else:
+ teardown()
+ finally:
+ if pass_self:
+ cleanup(self)
+ else:
+ cleanup()
+
+ return fixture
+
+
+class TestCaseFunction(Function):
+ nofuncargs = True
+ _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
+ _testcase: Optional["unittest.TestCase"] = None
+
+ def _getobj(self):
+ assert self.parent is not None
+ # Unlike a regular Function in a Class, where `item.obj` returns
+ # a *bound* method (attached to an instance), TestCaseFunction's
+ # `obj` returns an *unbound* method (not attached to an instance).
+ # This inconsistency is probably not desirable, but needs some
+ # consideration before changing.
+ return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
+
+ def setup(self) -> None:
+ # A bound method to be called during teardown() if set (see 'runtest()').
+ self._explicit_tearDown: Optional[Callable[[], None]] = None
+ assert self.parent is not None
+ self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
+ self._obj = getattr(self._testcase, self.name)
+ if hasattr(self, "_request"):
+ self._request._fillfixtures()
+
+ def teardown(self) -> None:
+ if self._explicit_tearDown is not None:
+ self._explicit_tearDown()
+ self._explicit_tearDown = None
+ self._testcase = None
+ self._obj = None
+
+ def startTest(self, testcase: "unittest.TestCase") -> None:
+ pass
+
+ def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
+ # Unwrap potential exception info (see twisted trial support below).
+ rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
+ try:
+ excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(rawexcinfo) # type: ignore[arg-type]
+ # Invoke the attributes to trigger storing the traceback
+ # trial causes some issue there.
+ excinfo.value
+ excinfo.traceback
+ except TypeError:
+ try:
+ try:
+ values = traceback.format_exception(*rawexcinfo)
+ values.insert(
+ 0,
+ "NOTE: Incompatible Exception Representation, "
+ "displaying natively:\n\n",
+ )
+ fail("".join(values), pytrace=False)
+ except (fail.Exception, KeyboardInterrupt):
+ raise
+ except BaseException:
+ fail(
+ "ERROR: Unknown Incompatible Exception "
+ "representation:\n%r" % (rawexcinfo,),
+ pytrace=False,
+ )
+ except KeyboardInterrupt:
+ raise
+ except fail.Exception:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ self.__dict__.setdefault("_excinfo", []).append(excinfo)
+
+ def addError(
+ self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
+ ) -> None:
+ try:
+ if isinstance(rawexcinfo[1], exit.Exception):
+ exit(rawexcinfo[1].msg)
+ except TypeError:
+ pass
+ self._addexcinfo(rawexcinfo)
+
+ def addFailure(
+ self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType"
+ ) -> None:
+ self._addexcinfo(rawexcinfo)
+
+ def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None:
+ try:
+ raise pytest.skip.Exception(reason, _use_item_location=True)
+ except skip.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addExpectedFailure(
+ self,
+ testcase: "unittest.TestCase",
+ rawexcinfo: "_SysExcInfoType",
+ reason: str = "",
+ ) -> None:
+ try:
+ xfail(str(reason))
+ except xfail.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addUnexpectedSuccess(
+ self,
+ testcase: "unittest.TestCase",
+ reason: Optional["twisted.trial.unittest.Todo"] = None,
+ ) -> None:
+ msg = "Unexpected success"
+ if reason:
+ msg += f": {reason.reason}"
+ # Preserve unittest behaviour - fail the test. Explicitly not an XPASS.
+ try:
+ fail(msg, pytrace=False)
+ except fail.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addSuccess(self, testcase: "unittest.TestCase") -> None:
+ pass
+
+ def stopTest(self, testcase: "unittest.TestCase") -> None:
+ pass
+
+ def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
+ pass
+
+ def runtest(self) -> None:
+ from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
+
+ assert self._testcase is not None
+
+ maybe_wrap_pytest_function_for_tracing(self)
+
+ # Let the unittest framework handle async functions.
+ if is_async_function(self.obj):
+ # Type ignored because self acts as the TestResult, but is not actually one.
+ self._testcase(result=self) # type: ignore[arg-type]
+ else:
+ # When --pdb is given, we want to postpone calling tearDown() otherwise
+ # when entering the pdb prompt, tearDown() would have probably cleaned up
+ # instance variables, which makes it difficult to debug.
+ # Arguably we could always postpone tearDown(), but this changes the moment where the
+ # TestCase instance interacts with the results object, so better to only do it
+ # when absolutely needed.
+ # We need to consider if the test itself is skipped, or the whole class.
+ assert isinstance(self.parent, UnitTestCase)
+ skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj)
+ if self.config.getoption("usepdb") and not skipped:
+ self._explicit_tearDown = self._testcase.tearDown
+ setattr(self._testcase, "tearDown", lambda *args: None)
+
+ # We need to update the actual bound method with self.obj, because
+ # wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
+ setattr(self._testcase, self.name, self.obj)
+ try:
+ self._testcase(result=self) # type: ignore[arg-type]
+ finally:
+ delattr(self._testcase, self.name)
+
+ def _traceback_filter(
+ self, excinfo: _pytest._code.ExceptionInfo[BaseException]
+ ) -> _pytest._code.Traceback:
+ traceback = super()._traceback_filter(excinfo)
+ ntraceback = traceback.filter(
+ lambda x: not x.frame.f_globals.get("__unittest"),
+ )
+ if not ntraceback:
+ ntraceback = traceback
+ return ntraceback
+
+
+@hookimpl(tryfirst=True)
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
+ if isinstance(item, TestCaseFunction):
+ if item._excinfo:
+ call.excinfo = item._excinfo.pop(0)
+ try:
+ del call.result
+ except AttributeError:
+ pass
+
+ # Convert unittest.SkipTest to pytest.skip.
+ # This is actually only needed for nose, which reuses unittest.SkipTest for
+ # its own nose.SkipTest. For unittest TestCases, SkipTest is already
+ # handled internally, and doesn't reach here.
+ unittest = sys.modules.get("unittest")
+ if (
+ unittest
+ and call.excinfo
+ and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
+ ):
+ excinfo = call.excinfo
+ call2 = CallInfo[None].from_call(
+ lambda: pytest.skip(str(excinfo.value)), call.when
+ )
+ call.excinfo = call2.excinfo
+
+
+# Twisted trial support.
+
+
+@hookimpl(hookwrapper=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
+ if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
+ ut: Any = sys.modules["twisted.python.failure"]
+ Failure__init__ = ut.Failure.__init__
+ check_testcase_implements_trial_reporter()
+
+ def excstore(
+ self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
+ ):
+ if exc_value is None:
+ self._rawexcinfo = sys.exc_info()
+ else:
+ if exc_type is None:
+ exc_type = type(exc_value)
+ self._rawexcinfo = (exc_type, exc_value, exc_tb)
+ try:
+ Failure__init__(
+ self, exc_value, exc_type, exc_tb, captureVars=captureVars
+ )
+ except TypeError:
+ Failure__init__(self, exc_value, exc_type, exc_tb)
+
+ ut.Failure.__init__ = excstore
+ yield
+ ut.Failure.__init__ = Failure__init__
+ else:
+ yield
+
+
+def check_testcase_implements_trial_reporter(done: List[int] = []) -> None:
+ if done:
+ return
+ from zope.interface import classImplements
+ from twisted.trial.itrial import IReporter
+
+ classImplements(TestCaseFunction, IReporter)
+ done.append(1)
+
+
+def _is_skipped(obj) -> bool:
+ """Return True if the given object has been marked with @unittest.skip."""
+ return bool(getattr(obj, "__unittest_skip__", False))
diff --git a/venv/lib/python3.12/site-packages/_pytest/unraisableexception.py b/venv/lib/python3.12/site-packages/_pytest/unraisableexception.py
new file mode 100644
index 0000000..fcb5d82
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/unraisableexception.py
@@ -0,0 +1,93 @@
+import sys
+import traceback
+import warnings
+from types import TracebackType
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import Optional
+from typing import Type
+
+import pytest
+
+
+# Copied from cpython/Lib/test/support/__init__.py, with modifications.
+class catch_unraisable_exception:
+ """Context manager catching unraisable exception using sys.unraisablehook.
+
+ Storing the exception value (cm.unraisable.exc_value) creates a reference
+ cycle. The reference cycle is broken explicitly when the context manager
+ exits.
+
+ Storing the object (cm.unraisable.object) can resurrect it if it is set to
+ an object which is being finalized. Exiting the context manager clears the
+ stored object.
+
+ Usage:
+ with catch_unraisable_exception() as cm:
+ # code creating an "unraisable exception"
+ ...
+ # check the unraisable exception: use cm.unraisable
+ ...
+ # cm.unraisable attribute no longer exists at this point
+ # (to break a reference cycle)
+ """
+
+ def __init__(self) -> None:
+ self.unraisable: Optional["sys.UnraisableHookArgs"] = None
+ self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None
+
+ def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
+ # Storing unraisable.object can resurrect an object which is being
+ # finalized. Storing unraisable.exc_value creates a reference cycle.
+ self.unraisable = unraisable
+
+ def __enter__(self) -> "catch_unraisable_exception":
+ self._old_hook = sys.unraisablehook
+ sys.unraisablehook = self._hook
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ assert self._old_hook is not None
+ sys.unraisablehook = self._old_hook
+ self._old_hook = None
+ del self.unraisable
+
+
+def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
+ with catch_unraisable_exception() as cm:
+ yield
+ if cm.unraisable:
+ if cm.unraisable.err_msg is not None:
+ err_msg = cm.unraisable.err_msg
+ else:
+ err_msg = "Exception ignored in"
+ msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
+ msg += "".join(
+ traceback.format_exception(
+ cm.unraisable.exc_type,
+ cm.unraisable.exc_value,
+ cm.unraisable.exc_traceback,
+ )
+ )
+ warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_setup() -> Generator[None, None, None]:
+ yield from unraisable_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_call() -> Generator[None, None, None]:
+ yield from unraisable_exception_runtest_hook()
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_teardown() -> Generator[None, None, None]:
+ yield from unraisable_exception_runtest_hook()
diff --git a/venv/lib/python3.12/site-packages/_pytest/warning_types.py b/venv/lib/python3.12/site-packages/_pytest/warning_types.py
new file mode 100644
index 0000000..bd5f418
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/warning_types.py
@@ -0,0 +1,170 @@
+import dataclasses
+import inspect
+import warnings
+from types import FunctionType
+from typing import Any
+from typing import Generic
+from typing import Type
+from typing import TypeVar
+
+from _pytest.compat import final
+
+
+class PytestWarning(UserWarning):
+ """Base class for all warnings emitted by pytest."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestAssertRewriteWarning(PytestWarning):
+ """Warning emitted by the pytest assert rewrite module."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestCacheWarning(PytestWarning):
+ """Warning emitted by the cache plugin in various situations."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestConfigWarning(PytestWarning):
+ """Warning emitted for configuration issues."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestCollectionWarning(PytestWarning):
+ """Warning emitted when pytest is not able to collect a file or symbol in a module."""
+
+ __module__ = "pytest"
+
+
+class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
+ """Warning class for features that will be removed in a future version."""
+
+ __module__ = "pytest"
+
+
+class PytestRemovedIn8Warning(PytestDeprecationWarning):
+ """Warning class for features that will be removed in pytest 8."""
+
+ __module__ = "pytest"
+
+
+class PytestReturnNotNoneWarning(PytestRemovedIn8Warning):
+ """Warning emitted when a test function is returning value other than None."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
+ """Warning category used to denote experiments in pytest.
+
+ Use sparingly as the API might change or even be removed completely in a
+ future version.
+ """
+
+ __module__ = "pytest"
+
+ @classmethod
+ def simple(cls, apiname: str) -> "PytestExperimentalApiWarning":
+ return cls(
+ "{apiname} is an experimental api that may change over time".format(
+ apiname=apiname
+ )
+ )
+
+
+@final
+class PytestUnhandledCoroutineWarning(PytestReturnNotNoneWarning):
+ """Warning emitted for an unhandled coroutine.
+
+ A coroutine was encountered when collecting test functions, but was not
+ handled by any async-aware plugin.
+ Coroutine test functions are not natively supported.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnknownMarkWarning(PytestWarning):
+ """Warning emitted on use of unknown markers.
+
+ See :ref:`mark` for details.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnraisableExceptionWarning(PytestWarning):
+ """An unraisable exception was reported.
+
+ Unraisable exceptions are exceptions raised in :meth:`__del__ `
+ implementations and similar situations when the exception cannot be raised
+ as normal.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnhandledThreadExceptionWarning(PytestWarning):
+ """An unhandled exception occurred in a :class:`~threading.Thread`.
+
+ Such exceptions don't propagate normally.
+ """
+
+ __module__ = "pytest"
+
+
+_W = TypeVar("_W", bound=PytestWarning)
+
+
+@final
+@dataclasses.dataclass
+class UnformattedWarning(Generic[_W]):
+ """A warning meant to be formatted during runtime.
+
+ This is used to hold warnings that need to format their message at runtime,
+ as opposed to a direct message.
+ """
+
+ category: Type["_W"]
+ template: str
+
+ def format(self, **kwargs: Any) -> _W:
+ """Return an instance of the warning category, formatted with given kwargs."""
+ return self.category(self.template.format(**kwargs))
+
+
+def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
+ """
+ Issue the warning :param:`message` for the definition of the given :param:`method`
+
+ this helps to log warnings for functions defined prior to finding an issue with them
+ (like hook wrappers being marked in a legacy mechanism)
+ """
+ lineno = method.__code__.co_firstlineno
+ filename = inspect.getfile(method)
+ module = method.__module__
+ mod_globals = method.__globals__
+ try:
+ warnings.warn_explicit(
+ message,
+ type(message),
+ filename=filename,
+ module=module,
+ registry=mod_globals.setdefault("__warningregistry__", {}),
+ lineno=lineno,
+ )
+ except Warning as w:
+ # If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message.
+ raise type(w)(f"{w}\n at {filename}:{lineno}") from None
diff --git a/venv/lib/python3.12/site-packages/_pytest/warnings.py b/venv/lib/python3.12/site-packages/_pytest/warnings.py
new file mode 100644
index 0000000..4aaa944
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_pytest/warnings.py
@@ -0,0 +1,148 @@
+import sys
+import warnings
+from contextlib import contextmanager
+from typing import Generator
+from typing import Optional
+from typing import TYPE_CHECKING
+
+import pytest
+from _pytest.config import apply_warning_filters
+from _pytest.config import Config
+from _pytest.config import parse_warning_filter
+from _pytest.main import Session
+from _pytest.nodes import Item
+from _pytest.terminal import TerminalReporter
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+
+def pytest_configure(config: Config) -> None:
+ config.addinivalue_line(
+ "markers",
+ "filterwarnings(warning): add a warning filter to the given test. "
+ "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ",
+ )
+
+
+@contextmanager
+def catch_warnings_for_item(
+ config: Config,
+ ihook,
+ when: "Literal['config', 'collect', 'runtest']",
+ item: Optional[Item],
+) -> Generator[None, None, None]:
+ """Context manager that catches warnings generated in the contained execution block.
+
+ ``item`` can be None if we are not in the context of an item execution.
+
+ Each warning captured triggers the ``pytest_warning_recorded`` hook.
+ """
+ config_filters = config.getini("filterwarnings")
+ cmdline_filters = config.known_args_namespace.pythonwarnings or []
+ with warnings.catch_warnings(record=True) as log:
+ # mypy can't infer that record=True means log is not None; help it.
+ assert log is not None
+
+ if not sys.warnoptions:
+ # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
+ warnings.filterwarnings("always", category=DeprecationWarning)
+ warnings.filterwarnings("always", category=PendingDeprecationWarning)
+
+ apply_warning_filters(config_filters, cmdline_filters)
+
+ # apply filters from "filterwarnings" marks
+ nodeid = "" if item is None else item.nodeid
+ if item is not None:
+ for mark in item.iter_markers(name="filterwarnings"):
+ for arg in mark.args:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
+
+ yield
+
+ for warning_message in log:
+ ihook.pytest_warning_recorded.call_historic(
+ kwargs=dict(
+ warning_message=warning_message,
+ nodeid=nodeid,
+ when=when,
+ location=None,
+ )
+ )
+
+
+def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
+ """Convert a warnings.WarningMessage to a string."""
+ warn_msg = warning_message.message
+ msg = warnings.formatwarning(
+ str(warn_msg),
+ warning_message.category,
+ warning_message.filename,
+ warning_message.lineno,
+ warning_message.line,
+ )
+ if warning_message.source is not None:
+ try:
+ import tracemalloc
+ except ImportError:
+ pass
+ else:
+ tb = tracemalloc.get_object_traceback(warning_message.source)
+ if tb is not None:
+ formatted_tb = "\n".join(tb.format())
+ # Use a leading new line to better separate the (large) output
+ # from the traceback to the previous warning text.
+ msg += f"\nObject allocated at:\n{formatted_tb}"
+ else:
+ # No need for a leading new line.
+ url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
+ msg += "Enable tracemalloc to get traceback where the object was allocated.\n"
+ msg += f"See {url} for more info."
+ return msg
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
+ with catch_warnings_for_item(
+ config=item.config, ihook=item.ihook, when="runtest", item=item
+ ):
+ yield
+
+
+@pytest.hookimpl(hookwrapper=True, tryfirst=True)
+def pytest_collection(session: Session) -> Generator[None, None, None]:
+ config = session.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="collect", item=None
+ ):
+ yield
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_terminal_summary(
+ terminalreporter: TerminalReporter,
+) -> Generator[None, None, None]:
+ config = terminalreporter.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="config", item=None
+ ):
+ yield
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_sessionfinish(session: Session) -> Generator[None, None, None]:
+ config = session.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="config", item=None
+ ):
+ yield
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_load_initial_conftests(
+ early_config: "Config",
+) -> Generator[None, None, None]:
+ with catch_warnings_for_item(
+ config=early_config, ihook=early_config.hook, when="config", item=None
+ ):
+ yield
diff --git a/venv/lib/python3.12/site-packages/_yaml/__init__.py b/venv/lib/python3.12/site-packages/_yaml/__init__.py
new file mode 100644
index 0000000..7baa8c4
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/_yaml/__init__.py
@@ -0,0 +1,33 @@
+# This is a stub package designed to roughly emulate the _yaml
+# extension module, which previously existed as a standalone module
+# and has been moved into the `yaml` package namespace.
+# It does not perfectly mimic its old counterpart, but should get
+# close enough for anyone who's relying on it even when they shouldn't.
+import yaml
+
+# in some circumstances, the yaml module we imoprted may be from a different version, so we need
+# to tread carefully when poking at it here (it may not have the attributes we expect)
+if not getattr(yaml, '__with_libyaml__', False):
+ from sys import version_info
+
+ exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
+ raise exc("No module named '_yaml'")
+else:
+ from yaml._yaml import *
+ import warnings
+ warnings.warn(
+ 'The _yaml extension module is now located at yaml._yaml'
+ ' and its location is subject to change. To use the'
+ ' LibYAML-based parser and emitter, import from `yaml`:'
+ ' `from yaml import CLoader as Loader, CDumper as Dumper`.',
+ DeprecationWarning
+ )
+ del warnings
+ # Don't `del yaml` here because yaml is actually an existing
+ # namespace member of _yaml.
+
+__name__ = '_yaml'
+# If the module is top-level (i.e. not a part of any specific package)
+# then the attribute should be set to ''.
+# https://docs.python.org/3.8/library/types.html
+__package__ = ''
diff --git a/venv/lib/python3.12/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..20d913a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/_yaml/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/LICENSE b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/LICENSE
new file mode 100644
index 0000000..74b9ce3
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2009-2023 Michael Bayer.
+
+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.
\ No newline at end of file
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/METADATA b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/METADATA
new file mode 100644
index 0000000..7a6884d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/METADATA
@@ -0,0 +1,142 @@
+Metadata-Version: 2.1
+Name: alembic
+Version: 1.13.1
+Summary: A database migration tool for SQLAlchemy.
+Home-page: https://alembic.sqlalchemy.org
+Author: Mike Bayer
+Author-email: mike_mp@zzzcomputing.com
+License: MIT
+Project-URL: Documentation, https://alembic.sqlalchemy.org/en/latest/
+Project-URL: Changelog, https://alembic.sqlalchemy.org/en/latest/changelog.html
+Project-URL: Source, https://github.com/sqlalchemy/alembic/
+Project-URL: Issue Tracker, https://github.com/sqlalchemy/alembic/issues/
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Environment :: Console
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+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 :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Database :: Front-Ends
+Requires-Python: >=3.8
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Requires-Dist: SQLAlchemy >=1.3.0
+Requires-Dist: Mako
+Requires-Dist: typing-extensions >=4
+Requires-Dist: importlib-metadata ; python_version < "3.9"
+Requires-Dist: importlib-resources ; python_version < "3.9"
+Provides-Extra: tz
+Requires-Dist: backports.zoneinfo ; (python_version < "3.9") and extra == 'tz'
+
+Alembic is a database migrations tool written by the author
+of `SQLAlchemy `_. A migrations tool
+offers the following functionality:
+
+* Can emit ALTER statements to a database in order to change
+ the structure of tables and other constructs
+* Provides a system whereby "migration scripts" may be constructed;
+ each script indicates a particular series of steps that can "upgrade" a
+ target database to a new version, and optionally a series of steps that can
+ "downgrade" similarly, doing the same steps in reverse.
+* Allows the scripts to execute in some sequential manner.
+
+The goals of Alembic are:
+
+* Very open ended and transparent configuration and operation. A new
+ Alembic environment is generated from a set of templates which is selected
+ among a set of options when setup first occurs. The templates then deposit a
+ series of scripts that define fully how database connectivity is established
+ and how migration scripts are invoked; the migration scripts themselves are
+ generated from a template within that series of scripts. The scripts can
+ then be further customized to define exactly how databases will be
+ interacted with and what structure new migration files should take.
+* Full support for transactional DDL. The default scripts ensure that all
+ migrations occur within a transaction - for those databases which support
+ this (Postgresql, Microsoft SQL Server), migrations can be tested with no
+ need to manually undo changes upon failure.
+* Minimalist script construction. Basic operations like renaming
+ tables/columns, adding/removing columns, changing column attributes can be
+ performed through one line commands like alter_column(), rename_table(),
+ add_constraint(). There is no need to recreate full SQLAlchemy Table
+ structures for simple operations like these - the functions themselves
+ generate minimalist schema structures behind the scenes to achieve the given
+ DDL sequence.
+* "auto generation" of migrations. While real world migrations are far more
+ complex than what can be automatically determined, Alembic can still
+ eliminate the initial grunt work in generating new migration directives
+ from an altered schema. The ``--autogenerate`` feature will inspect the
+ current status of a database using SQLAlchemy's schema inspection
+ capabilities, compare it to the current state of the database model as
+ specified in Python, and generate a series of "candidate" migrations,
+ rendering them into a new migration script as Python directives. The
+ developer then edits the new file, adding additional directives and data
+ migrations as needed, to produce a finished migration. Table and column
+ level changes can be detected, with constraints and indexes to follow as
+ well.
+* Full support for migrations generated as SQL scripts. Those of us who
+ work in corporate environments know that direct access to DDL commands on a
+ production database is a rare privilege, and DBAs want textual SQL scripts.
+ Alembic's usage model and commands are oriented towards being able to run a
+ series of migrations into a textual output file as easily as it runs them
+ directly to a database. Care must be taken in this mode to not invoke other
+ operations that rely upon in-memory SELECTs of rows - Alembic tries to
+ provide helper constructs like bulk_insert() to help with data-oriented
+ operations that are compatible with script-based DDL.
+* Non-linear, dependency-graph versioning. Scripts are given UUID
+ identifiers similarly to a DVCS, and the linkage of one script to the next
+ is achieved via human-editable markers within the scripts themselves.
+ The structure of a set of migration files is considered as a
+ directed-acyclic graph, meaning any migration file can be dependent
+ on any other arbitrary set of migration files, or none at
+ all. Through this open-ended system, migration files can be organized
+ into branches, multiple roots, and mergepoints, without restriction.
+ Commands are provided to produce new branches, roots, and merges of
+ branches automatically.
+* Provide a library of ALTER constructs that can be used by any SQLAlchemy
+ application. The DDL constructs build upon SQLAlchemy's own DDLElement base
+ and can be used standalone by any application or script.
+* At long last, bring SQLite and its inability to ALTER things into the fold,
+ but in such a way that SQLite's very special workflow needs are accommodated
+ in an explicit way that makes the most of a bad situation, through the
+ concept of a "batch" migration, where multiple changes to a table can
+ be batched together to form a series of instructions for a single, subsequent
+ "move-and-copy" workflow. You can even use "move-and-copy" workflow for
+ other databases, if you want to recreate a table in the background
+ on a busy system.
+
+Documentation and status of Alembic is at https://alembic.sqlalchemy.org/
+
+The SQLAlchemy Project
+======================
+
+Alembic is part of the `SQLAlchemy Project `_ and
+adheres to the same standards and conventions as the core project.
+
+Development / Bug reporting / Pull requests
+___________________________________________
+
+Please refer to the
+`SQLAlchemy Community Guide `_ for
+guidelines on coding and participating in this project.
+
+Code of Conduct
+_______________
+
+Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
+constructive communication between users and developers.
+Please see our current Code of Conduct at
+`Code of Conduct `_.
+
+License
+=======
+
+Alembic is distributed under the `MIT license
+`_.
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/RECORD b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/RECORD
new file mode 100644
index 0000000..e955509
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/RECORD
@@ -0,0 +1,151 @@
+../../../bin/alembic,sha256=YTIGTZJw6SsoGZiT_995SnIeawYoyskHou-iO3DGg9w,289
+alembic-1.13.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+alembic-1.13.1.dist-info/LICENSE,sha256=soUmiob0QW6vTQWyrjiAwVb3xZqPk1pAK8BW6vszrwg,1058
+alembic-1.13.1.dist-info/METADATA,sha256=W1F2NBRkhqW55HiGmEIpdmiRt2skU5wwJd21UHFbSdQ,7390
+alembic-1.13.1.dist-info/RECORD,,
+alembic-1.13.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic-1.13.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
+alembic-1.13.1.dist-info/entry_points.txt,sha256=aykM30soxwGN0pB7etLc1q0cHJbL9dy46RnK9VX4LLw,48
+alembic-1.13.1.dist-info/top_level.txt,sha256=FwKWd5VsPFC8iQjpu1u9Cn-JnK3-V1RhUCmWqz1cl-s,8
+alembic/__init__.py,sha256=PMiQT_1tHyC_Od3GDBArBk7r14v8F62_xkzliPq9tLU,63
+alembic/__main__.py,sha256=373m7-TBh72JqrSMYviGrxCHZo-cnweM8AGF8A22PmY,78
+alembic/__pycache__/__init__.cpython-312.pyc,,
+alembic/__pycache__/__main__.cpython-312.pyc,,
+alembic/__pycache__/command.cpython-312.pyc,,
+alembic/__pycache__/config.cpython-312.pyc,,
+alembic/__pycache__/context.cpython-312.pyc,,
+alembic/__pycache__/environment.cpython-312.pyc,,
+alembic/__pycache__/migration.cpython-312.pyc,,
+alembic/__pycache__/op.cpython-312.pyc,,
+alembic/autogenerate/__init__.py,sha256=ntmUTXhjLm4_zmqIwyVaECdpPDn6_u1yM9vYk6-553E,543
+alembic/autogenerate/__pycache__/__init__.cpython-312.pyc,,
+alembic/autogenerate/__pycache__/api.cpython-312.pyc,,
+alembic/autogenerate/__pycache__/compare.cpython-312.pyc,,
+alembic/autogenerate/__pycache__/render.cpython-312.pyc,,
+alembic/autogenerate/__pycache__/rewriter.cpython-312.pyc,,
+alembic/autogenerate/api.py,sha256=Oc7MRtDhkSICsQ82fYP9bBMYaAjzzW2X_izM3AQU-OY,22171
+alembic/autogenerate/compare.py,sha256=3QLK2yCDW37bXbAIXcHGz4YOFlOW8bSfLHJ8bmzgG1M,44938
+alembic/autogenerate/render.py,sha256=uSbCpkh72mo00xGZ8CJa3teM_gqulCoNtxH0Ey8Ny1k,34939
+alembic/autogenerate/rewriter.py,sha256=uZWRkTYJoncoEJ5WY1QBRiozjyChqZDJPy4LtcRibjM,7846
+alembic/command.py,sha256=jWFNS-wPWA-Klfm0GsPfWh_8sPj4n7rKROJ0zrwhoR0,21712
+alembic/config.py,sha256=I12lm4V-AXSt-7nvub-Vtx5Zfa68pYP5xSrFQQd45rQ,22256
+alembic/context.py,sha256=hK1AJOQXJ29Bhn276GYcosxeG7pC5aZRT5E8c4bMJ4Q,195
+alembic/context.pyi,sha256=hUHbSnbSeEEMVkk0gDSXOq4_9edSjYzsjmmf-mL9Iao,31737
+alembic/ddl/__init__.py,sha256=Df8fy4Vn_abP8B7q3x8gyFwEwnLw6hs2Ljt_bV3EZWE,152
+alembic/ddl/__pycache__/__init__.cpython-312.pyc,,
+alembic/ddl/__pycache__/_autogen.cpython-312.pyc,,
+alembic/ddl/__pycache__/base.cpython-312.pyc,,
+alembic/ddl/__pycache__/impl.cpython-312.pyc,,
+alembic/ddl/__pycache__/mssql.cpython-312.pyc,,
+alembic/ddl/__pycache__/mysql.cpython-312.pyc,,
+alembic/ddl/__pycache__/oracle.cpython-312.pyc,,
+alembic/ddl/__pycache__/postgresql.cpython-312.pyc,,
+alembic/ddl/__pycache__/sqlite.cpython-312.pyc,,
+alembic/ddl/_autogen.py,sha256=0no9ywWP8gjvO57Ozc2naab4qNusVNn2fiJekjc275g,9179
+alembic/ddl/base.py,sha256=Jd7oPoAOGjOMcdMUIzSKnTjd8NKnTd7IjBXXyVpDCkU,9955
+alembic/ddl/impl.py,sha256=vkhkXFpLPJBG9jW2kv_sR5CC5czNd1ERLjLtfLuOFP0,28778
+alembic/ddl/mssql.py,sha256=ydvgBSaftKYjaBaMyqius66Ta4CICQSj79Og3Ed2atY,14219
+alembic/ddl/mysql.py,sha256=am221U_UK3wX33tNcXNiOObZV-Pa4CXuv7vN-epF9IU,16788
+alembic/ddl/oracle.py,sha256=TmoCq_FlbfyWAAk3e_q6mMQU0YmlfIcgKHpRfNMmgr0,6211
+alembic/ddl/postgresql.py,sha256=dcWLdDSqivzizVCce_H6RnOVAayPXDFfns-NC4-UaA8,29842
+alembic/ddl/sqlite.py,sha256=wLXhb8bJWRspKQTb-iVfepR4LXYgOuEbUWKX5qwDhIQ,7570
+alembic/environment.py,sha256=MM5lPayGT04H3aeng1H7GQ8HEAs3VGX5yy6mDLCPLT4,43
+alembic/migration.py,sha256=MV6Fju6rZtn2fTREKzXrCZM6aIBGII4OMZFix0X-GLs,41
+alembic/op.py,sha256=flHtcsVqOD-ZgZKK2pv-CJ5Cwh-KJ7puMUNXzishxLw,167
+alembic/op.pyi,sha256=8R6SJr1dQharU0blmMJJj23XFO_hk9ZmAQBJBQOeXRU,49816
+alembic/operations/__init__.py,sha256=e0KQSZAgLpTWvyvreB7DWg7RJV_MWSOPVDgCqsd2FzY,318
+alembic/operations/__pycache__/__init__.cpython-312.pyc,,
+alembic/operations/__pycache__/base.cpython-312.pyc,,
+alembic/operations/__pycache__/batch.cpython-312.pyc,,
+alembic/operations/__pycache__/ops.cpython-312.pyc,,
+alembic/operations/__pycache__/schemaobj.cpython-312.pyc,,
+alembic/operations/__pycache__/toimpl.cpython-312.pyc,,
+alembic/operations/base.py,sha256=LCx4NH5NA2NLWQFaZTqE_p2KgLtqJ76oVcp1Grj-zFM,74004
+alembic/operations/batch.py,sha256=YqtD4hJ3_RkFxvI7zbmBwxcLEyLHYyWQpsz4l5L85yI,26943
+alembic/operations/ops.py,sha256=2vtYFhYFDEVq158HwORfGTsobDM7c-t0lewuR7JKw7g,94353
+alembic/operations/schemaobj.py,sha256=vjoD57QvjbzzA-jyPIXulbOmb5_bGPtxoq58KsKI4Y0,9424
+alembic/operations/toimpl.py,sha256=SoNY2_gZX2baXTD-pNjpCWnON8D2QBSYQBIjo13-WKA,7115
+alembic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic/runtime/__pycache__/__init__.cpython-312.pyc,,
+alembic/runtime/__pycache__/environment.cpython-312.pyc,,
+alembic/runtime/__pycache__/migration.cpython-312.pyc,,
+alembic/runtime/environment.py,sha256=9wSJaePNAXBXvirif_85ql7dSq4bXM1E6pSb2k-6uGI,41508
+alembic/runtime/migration.py,sha256=Yfv2fa11wiQ0WgoZaFldlWxCPq4dVDOCEOxST_-1VB0,50066
+alembic/script/__init__.py,sha256=lSj06O391Iy5avWAiq8SPs6N8RBgxkSPjP8wpXcNDGg,100
+alembic/script/__pycache__/__init__.cpython-312.pyc,,
+alembic/script/__pycache__/base.cpython-312.pyc,,
+alembic/script/__pycache__/revision.cpython-312.pyc,,
+alembic/script/__pycache__/write_hooks.cpython-312.pyc,,
+alembic/script/base.py,sha256=4gkppn2FKCYDoBgzGkTslcwdyxSabmHvGq0uGKulwbI,37586
+alembic/script/revision.py,sha256=sfnXQw2UwiXs0E6gEPHBKWuSsB5KyuxZPTrFn__hIEk,62060
+alembic/script/write_hooks.py,sha256=NGB6NGgfdf7HK6XNNpSKqUCfzxazj-NRUePgFx7MJSM,5036
+alembic/templates/async/README,sha256=ISVtAOvqvKk_5ThM5ioJE-lMkvf9IbknFUFVU_vPma4,58
+alembic/templates/async/__pycache__/env.cpython-312.pyc,,
+alembic/templates/async/alembic.ini.mako,sha256=uuhJETLWQuiYcs_jAOXHEjshEJ7VslEc1q4RRj0HWbE,3525
+alembic/templates/async/env.py,sha256=zbOCf3Y7w2lg92hxSwmG1MM_7y56i_oRH4AKp0pQBYo,2389
+alembic/templates/async/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
+alembic/templates/generic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
+alembic/templates/generic/__pycache__/env.cpython-312.pyc,,
+alembic/templates/generic/alembic.ini.mako,sha256=sT7F852yN3c8X1-GKFlhuWExXxw9hY1eb1ZZ9flFSzc,3634
+alembic/templates/generic/env.py,sha256=TLRWOVW3Xpt_Tpf8JFzlnoPn_qoUu8UV77Y4o9XD6yI,2103
+alembic/templates/generic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
+alembic/templates/multidb/README,sha256=dWLDhnBgphA4Nzb7sNlMfCS3_06YqVbHhz-9O5JNqyI,606
+alembic/templates/multidb/__pycache__/env.cpython-312.pyc,,
+alembic/templates/multidb/alembic.ini.mako,sha256=mPh8JFJfWiGs6tMtL8_HAQ-Dz1QOoJgE5Vm76nIMqgU,3728
+alembic/templates/multidb/env.py,sha256=6zNjnW8mXGUk7erTsAvrfhvqoczJ-gagjVq1Ypg2YIQ,4230
+alembic/templates/multidb/script.py.mako,sha256=N06nMtNSwHkgl0EBXDyMt8njp9tlOesR583gfq21nbY,1090
+alembic/testing/__init__.py,sha256=kOxOh5nwmui9d-_CCq9WA4Udwy7ITjm453w74CTLZDo,1159
+alembic/testing/__pycache__/__init__.cpython-312.pyc,,
+alembic/testing/__pycache__/assertions.cpython-312.pyc,,
+alembic/testing/__pycache__/env.cpython-312.pyc,,
+alembic/testing/__pycache__/fixtures.cpython-312.pyc,,
+alembic/testing/__pycache__/requirements.cpython-312.pyc,,
+alembic/testing/__pycache__/schemacompare.cpython-312.pyc,,
+alembic/testing/__pycache__/util.cpython-312.pyc,,
+alembic/testing/__pycache__/warnings.cpython-312.pyc,,
+alembic/testing/assertions.py,sha256=1CbJk8c8-WO9eJ0XJ0jJvMsNRLUrXV41NOeIJUAlOBk,5015
+alembic/testing/env.py,sha256=zJacVb_z6uLs2U1TtkmnFH9P3_F-3IfYbVv4UEPOvfo,10754
+alembic/testing/fixtures.py,sha256=NyP4wE_dFN9ZzSGiBagRu1cdzkka03nwJYJYHYrrkSY,9112
+alembic/testing/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic/testing/plugin/__pycache__/__init__.cpython-312.pyc,,
+alembic/testing/plugin/__pycache__/bootstrap.cpython-312.pyc,,
+alembic/testing/plugin/bootstrap.py,sha256=9C6wtjGrIVztZ928w27hsQE0KcjDLIUtUN3dvZKsMVk,50
+alembic/testing/requirements.py,sha256=dKeAO1l5TwBqXarJN-IPORlCqCJv-41Dj6oXoEikxHQ,5133
+alembic/testing/schemacompare.py,sha256=N5UqSNCOJetIKC4vKhpYzQEpj08XkdgIoqBmEPQ3tlc,4838
+alembic/testing/suite/__init__.py,sha256=MvE7-hwbaVN1q3NM-ztGxORU9dnIelUCINKqNxewn7Y,288
+alembic/testing/suite/__pycache__/__init__.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/_autogen_fixtures.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/test_autogen_comments.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/test_autogen_computed.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/test_autogen_diffs.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/test_autogen_fks.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/test_autogen_identity.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/test_environment.cpython-312.pyc,,
+alembic/testing/suite/__pycache__/test_op.cpython-312.pyc,,
+alembic/testing/suite/_autogen_fixtures.py,sha256=cDq1pmzHe15S6dZPGNC6sqFaCQ3hLT_oPV2IDigUGQ0,9880
+alembic/testing/suite/test_autogen_comments.py,sha256=aEGqKUDw4kHjnDk298aoGcQvXJWmZXcIX_2FxH4cJK8,6283
+alembic/testing/suite/test_autogen_computed.py,sha256=qJeBpc8urnwTFvbwWrSTIbHVkRUuCXP-dKaNbUK2U2U,6077
+alembic/testing/suite/test_autogen_diffs.py,sha256=T4SR1n_kmcOKYhR4W1-dA0e5sddJ69DSVL2HW96kAkE,8394
+alembic/testing/suite/test_autogen_fks.py,sha256=AqFmb26Buex167HYa9dZWOk8x-JlB1OK3bwcvvjDFaU,32927
+alembic/testing/suite/test_autogen_identity.py,sha256=kcuqngG7qXAKPJDX4U8sRzPKHEJECHuZ0DtuaS6tVkk,5824
+alembic/testing/suite/test_environment.py,sha256=w9F0xnLEbALeR8k6_-Tz6JHvy91IqiTSypNasVzXfZQ,11877
+alembic/testing/suite/test_op.py,sha256=2XQCdm_NmnPxHGuGj7hmxMzIhKxXNotUsKdACXzE1mM,1343
+alembic/testing/util.py,sha256=CQrcQDA8fs_7ME85z5ydb-Bt70soIIID-qNY1vbR2dg,3350
+alembic/testing/warnings.py,sha256=RxA7x_8GseANgw07Us8JN_1iGbANxaw6_VitX2ZGQH4,1078
+alembic/util/__init__.py,sha256=KSZ7UT2YzH6CietgUtljVoE3QnGjoFKOi7RL5sgUxrk,1688
+alembic/util/__pycache__/__init__.cpython-312.pyc,,
+alembic/util/__pycache__/compat.cpython-312.pyc,,
+alembic/util/__pycache__/editor.cpython-312.pyc,,
+alembic/util/__pycache__/exc.cpython-312.pyc,,
+alembic/util/__pycache__/langhelpers.cpython-312.pyc,,
+alembic/util/__pycache__/messaging.cpython-312.pyc,,
+alembic/util/__pycache__/pyfiles.cpython-312.pyc,,
+alembic/util/__pycache__/sqla_compat.cpython-312.pyc,,
+alembic/util/compat.py,sha256=RjHdQa1NomU3Zlvgfvza0OMiSRQSLRL3xVl3OdUy2UE,2594
+alembic/util/editor.py,sha256=JIz6_BdgV8_oKtnheR6DZoB7qnrHrlRgWjx09AsTsUw,2546
+alembic/util/exc.py,sha256=KQTru4zcgAmN4IxLMwLFS56XToUewaXB7oOLcPNjPwg,98
+alembic/util/langhelpers.py,sha256=KYyOjFjJ26evPmrwhdTvLQNXN0bK7AIy5sRdKD91Fvg,10038
+alembic/util/messaging.py,sha256=BM5OCZ6qmLftFRw5yPSxj539_QmfVwNYoU8qYsDqoJY,3132
+alembic/util/pyfiles.py,sha256=zltVdcwEJJCPS2gHsQvkHkQakuF6wXiZ6zfwHbGNT0g,3489
+alembic/util/sqla_compat.py,sha256=toD1S63PgZ6iEteP9bwIf5E7DIUdQPo0UQ_Fn18qWnI,19536
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/REQUESTED b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/WHEEL b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/WHEEL
new file mode 100644
index 0000000..98c0d20
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.42.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/entry_points.txt b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/entry_points.txt
new file mode 100644
index 0000000..5945268
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[console_scripts]
+alembic = alembic.config:main
diff --git a/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/top_level.txt
new file mode 100644
index 0000000..b5bd98d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic-1.13.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+alembic
diff --git a/venv/lib/python3.12/site-packages/alembic/__init__.py b/venv/lib/python3.12/site-packages/alembic/__init__.py
new file mode 100644
index 0000000..c153c8a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/__init__.py
@@ -0,0 +1,4 @@
+from . import context
+from . import op
+
+__version__ = "1.13.1"
diff --git a/venv/lib/python3.12/site-packages/alembic/__main__.py b/venv/lib/python3.12/site-packages/alembic/__main__.py
new file mode 100644
index 0000000..af1b8e8
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/__main__.py
@@ -0,0 +1,4 @@
+from .config import main
+
+if __name__ == "__main__":
+ main(prog="alembic")
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..1988237
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/__main__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/__main__.cpython-312.pyc
new file mode 100644
index 0000000..1582fe6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/__main__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/command.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/command.cpython-312.pyc
new file mode 100644
index 0000000..a6aee58
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/command.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/config.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/config.cpython-312.pyc
new file mode 100644
index 0000000..99435d2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/config.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/context.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/context.cpython-312.pyc
new file mode 100644
index 0000000..32dfc5c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/context.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/environment.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/environment.cpython-312.pyc
new file mode 100644
index 0000000..1916f15
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/environment.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/migration.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/migration.cpython-312.pyc
new file mode 100644
index 0000000..2fd1b4e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/migration.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/__pycache__/op.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/__pycache__/op.cpython-312.pyc
new file mode 100644
index 0000000..429fa2e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/__pycache__/op.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/__init__.py b/venv/lib/python3.12/site-packages/alembic/autogenerate/__init__.py
new file mode 100644
index 0000000..445ddb2
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/autogenerate/__init__.py
@@ -0,0 +1,10 @@
+from .api import _render_migration_diffs as _render_migration_diffs
+from .api import compare_metadata as compare_metadata
+from .api import produce_migrations as produce_migrations
+from .api import render_python_code as render_python_code
+from .api import RevisionContext as RevisionContext
+from .compare import _produce_net_changes as _produce_net_changes
+from .compare import comparators as comparators
+from .render import render_op_text as render_op_text
+from .render import renderers as renderers
+from .rewriter import Rewriter as Rewriter
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..8cc6e71
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/api.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/api.cpython-312.pyc
new file mode 100644
index 0000000..033a2f1
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/api.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/compare.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/compare.cpython-312.pyc
new file mode 100644
index 0000000..7a9c1ba
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/compare.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/render.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/render.cpython-312.pyc
new file mode 100644
index 0000000..1333e1b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/render.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/rewriter.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/rewriter.cpython-312.pyc
new file mode 100644
index 0000000..c8040bd
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/autogenerate/__pycache__/rewriter.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/api.py b/venv/lib/python3.12/site-packages/alembic/autogenerate/api.py
new file mode 100644
index 0000000..aa8f32f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/autogenerate/api.py
@@ -0,0 +1,650 @@
+from __future__ import annotations
+
+import contextlib
+from typing import Any
+from typing import Dict
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import inspect
+
+from . import compare
+from . import render
+from .. import util
+from ..operations import ops
+from ..util import sqla_compat
+
+"""Provide the 'autogenerate' feature which can produce migration operations
+automatically."""
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.engine import Dialect
+ from sqlalchemy.engine import Inspector
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.schema import Table
+
+ from ..config import Config
+ from ..operations.ops import DowngradeOps
+ from ..operations.ops import MigrationScript
+ from ..operations.ops import UpgradeOps
+ from ..runtime.environment import NameFilterParentNames
+ from ..runtime.environment import NameFilterType
+ from ..runtime.environment import ProcessRevisionDirectiveFn
+ from ..runtime.environment import RenderItemFn
+ from ..runtime.migration import MigrationContext
+ from ..script.base import Script
+ from ..script.base import ScriptDirectory
+ from ..script.revision import _GetRevArg
+
+
+def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any:
+ """Compare a database schema to that given in a
+ :class:`~sqlalchemy.schema.MetaData` instance.
+
+ The database connection is presented in the context
+ of a :class:`.MigrationContext` object, which
+ provides database connectivity as well as optional
+ comparison functions to use for datatypes and
+ server defaults - see the "autogenerate" arguments
+ at :meth:`.EnvironmentContext.configure`
+ for details on these.
+
+ The return format is a list of "diff" directives,
+ each representing individual differences::
+
+ from alembic.migration import MigrationContext
+ from alembic.autogenerate import compare_metadata
+ from sqlalchemy import (
+ create_engine,
+ MetaData,
+ Column,
+ Integer,
+ String,
+ Table,
+ text,
+ )
+ import pprint
+
+ engine = create_engine("sqlite://")
+
+ with engine.begin() as conn:
+ conn.execute(
+ text(
+ '''
+ create table foo (
+ id integer not null primary key,
+ old_data varchar,
+ x integer
+ )
+ '''
+ )
+ )
+ conn.execute(text("create table bar (data varchar)"))
+
+ metadata = MetaData()
+ Table(
+ "foo",
+ metadata,
+ Column("id", Integer, primary_key=True),
+ Column("data", Integer),
+ Column("x", Integer, nullable=False),
+ )
+ Table("bat", metadata, Column("info", String))
+
+ mc = MigrationContext.configure(engine.connect())
+
+ diff = compare_metadata(mc, metadata)
+ pprint.pprint(diff, indent=2, width=20)
+
+ Output::
+
+ [
+ (
+ "add_table",
+ Table(
+ "bat",
+ MetaData(),
+ Column("info", String(), table=),
+ schema=None,
+ ),
+ ),
+ (
+ "remove_table",
+ Table(
+ "bar",
+ MetaData(),
+ Column("data", VARCHAR(), table=),
+ schema=None,
+ ),
+ ),
+ (
+ "add_column",
+ None,
+ "foo",
+ Column("data", Integer(), table=),
+ ),
+ [
+ (
+ "modify_nullable",
+ None,
+ "foo",
+ "x",
+ {
+ "existing_comment": None,
+ "existing_server_default": False,
+ "existing_type": INTEGER(),
+ },
+ True,
+ False,
+ )
+ ],
+ (
+ "remove_column",
+ None,
+ "foo",
+ Column("old_data", VARCHAR(), table=),
+ ),
+ ]
+
+ :param context: a :class:`.MigrationContext`
+ instance.
+ :param metadata: a :class:`~sqlalchemy.schema.MetaData`
+ instance.
+
+ .. seealso::
+
+ :func:`.produce_migrations` - produces a :class:`.MigrationScript`
+ structure based on metadata comparison.
+
+ """
+
+ migration_script = produce_migrations(context, metadata)
+ assert migration_script.upgrade_ops is not None
+ return migration_script.upgrade_ops.as_diffs()
+
+
+def produce_migrations(
+ context: MigrationContext, metadata: MetaData
+) -> MigrationScript:
+ """Produce a :class:`.MigrationScript` structure based on schema
+ comparison.
+
+ This function does essentially what :func:`.compare_metadata` does,
+ but then runs the resulting list of diffs to produce the full
+ :class:`.MigrationScript` object. For an example of what this looks like,
+ see the example in :ref:`customizing_revision`.
+
+ .. seealso::
+
+ :func:`.compare_metadata` - returns more fundamental "diff"
+ data from comparing a schema.
+
+ """
+
+ autogen_context = AutogenContext(context, metadata=metadata)
+
+ migration_script = ops.MigrationScript(
+ rev_id=None,
+ upgrade_ops=ops.UpgradeOps([]),
+ downgrade_ops=ops.DowngradeOps([]),
+ )
+
+ compare._populate_migration_script(autogen_context, migration_script)
+
+ return migration_script
+
+
+def render_python_code(
+ up_or_down_op: Union[UpgradeOps, DowngradeOps],
+ sqlalchemy_module_prefix: str = "sa.",
+ alembic_module_prefix: str = "op.",
+ render_as_batch: bool = False,
+ imports: Sequence[str] = (),
+ render_item: Optional[RenderItemFn] = None,
+ migration_context: Optional[MigrationContext] = None,
+ user_module_prefix: Optional[str] = None,
+) -> str:
+ """Render Python code given an :class:`.UpgradeOps` or
+ :class:`.DowngradeOps` object.
+
+ This is a convenience function that can be used to test the
+ autogenerate output of a user-defined :class:`.MigrationScript` structure.
+
+ :param up_or_down_op: :class:`.UpgradeOps` or :class:`.DowngradeOps` object
+ :param sqlalchemy_module_prefix: module prefix for SQLAlchemy objects
+ :param alembic_module_prefix: module prefix for Alembic constructs
+ :param render_as_batch: use "batch operations" style for rendering
+ :param imports: sequence of import symbols to add
+ :param render_item: callable to render items
+ :param migration_context: optional :class:`.MigrationContext`
+ :param user_module_prefix: optional string prefix for user-defined types
+
+ .. versionadded:: 1.11.0
+
+ """
+ opts = {
+ "sqlalchemy_module_prefix": sqlalchemy_module_prefix,
+ "alembic_module_prefix": alembic_module_prefix,
+ "render_item": render_item,
+ "render_as_batch": render_as_batch,
+ "user_module_prefix": user_module_prefix,
+ }
+
+ if migration_context is None:
+ from ..runtime.migration import MigrationContext
+ from sqlalchemy.engine.default import DefaultDialect
+
+ migration_context = MigrationContext.configure(
+ dialect=DefaultDialect()
+ )
+
+ autogen_context = AutogenContext(migration_context, opts=opts)
+ autogen_context.imports = set(imports)
+ return render._indent(
+ render._render_cmd_body(up_or_down_op, autogen_context)
+ )
+
+
+def _render_migration_diffs(
+ context: MigrationContext, template_args: Dict[Any, Any]
+) -> None:
+ """legacy, used by test_autogen_composition at the moment"""
+
+ autogen_context = AutogenContext(context)
+
+ upgrade_ops = ops.UpgradeOps([])
+ compare._produce_net_changes(autogen_context, upgrade_ops)
+
+ migration_script = ops.MigrationScript(
+ rev_id=None,
+ upgrade_ops=upgrade_ops,
+ downgrade_ops=upgrade_ops.reverse(),
+ )
+
+ render._render_python_into_templatevars(
+ autogen_context, migration_script, template_args
+ )
+
+
+class AutogenContext:
+ """Maintains configuration and state that's specific to an
+ autogenerate operation."""
+
+ metadata: Optional[MetaData] = None
+ """The :class:`~sqlalchemy.schema.MetaData` object
+ representing the destination.
+
+ This object is the one that is passed within ``env.py``
+ to the :paramref:`.EnvironmentContext.configure.target_metadata`
+ parameter. It represents the structure of :class:`.Table` and other
+ objects as stated in the current database model, and represents the
+ destination structure for the database being examined.
+
+ While the :class:`~sqlalchemy.schema.MetaData` object is primarily
+ known as a collection of :class:`~sqlalchemy.schema.Table` objects,
+ it also has an :attr:`~sqlalchemy.schema.MetaData.info` dictionary
+ that may be used by end-user schemes to store additional schema-level
+ objects that are to be compared in custom autogeneration schemes.
+
+ """
+
+ connection: Optional[Connection] = None
+ """The :class:`~sqlalchemy.engine.base.Connection` object currently
+ connected to the database backend being compared.
+
+ This is obtained from the :attr:`.MigrationContext.bind` and is
+ ultimately set up in the ``env.py`` script.
+
+ """
+
+ dialect: Optional[Dialect] = None
+ """The :class:`~sqlalchemy.engine.Dialect` object currently in use.
+
+ This is normally obtained from the
+ :attr:`~sqlalchemy.engine.base.Connection.dialect` attribute.
+
+ """
+
+ imports: Set[str] = None # type: ignore[assignment]
+ """A ``set()`` which contains string Python import directives.
+
+ The directives are to be rendered into the ``${imports}`` section
+ of a script template. The set is normally empty and can be modified
+ within hooks such as the
+ :paramref:`.EnvironmentContext.configure.render_item` hook.
+
+ .. seealso::
+
+ :ref:`autogen_render_types`
+
+ """
+
+ migration_context: MigrationContext = None # type: ignore[assignment]
+ """The :class:`.MigrationContext` established by the ``env.py`` script."""
+
+ def __init__(
+ self,
+ migration_context: MigrationContext,
+ metadata: Optional[MetaData] = None,
+ opts: Optional[Dict[str, Any]] = None,
+ autogenerate: bool = True,
+ ) -> None:
+ if (
+ autogenerate
+ and migration_context is not None
+ and migration_context.as_sql
+ ):
+ raise util.CommandError(
+ "autogenerate can't use as_sql=True as it prevents querying "
+ "the database for schema information"
+ )
+
+ if opts is None:
+ opts = migration_context.opts
+
+ self.metadata = metadata = (
+ opts.get("target_metadata", None) if metadata is None else metadata
+ )
+
+ if (
+ autogenerate
+ and metadata is None
+ and migration_context is not None
+ and migration_context.script is not None
+ ):
+ raise util.CommandError(
+ "Can't proceed with --autogenerate option; environment "
+ "script %s does not provide "
+ "a MetaData object or sequence of objects to the context."
+ % (migration_context.script.env_py_location)
+ )
+
+ include_object = opts.get("include_object", None)
+ include_name = opts.get("include_name", None)
+
+ object_filters = []
+ name_filters = []
+ if include_object:
+ object_filters.append(include_object)
+ if include_name:
+ name_filters.append(include_name)
+
+ self._object_filters = object_filters
+ self._name_filters = name_filters
+
+ self.migration_context = migration_context
+ if self.migration_context is not None:
+ self.connection = self.migration_context.bind
+ self.dialect = self.migration_context.dialect
+
+ self.imports = set()
+ self.opts: Dict[str, Any] = opts
+ self._has_batch: bool = False
+
+ @util.memoized_property
+ def inspector(self) -> Inspector:
+ if self.connection is None:
+ raise TypeError(
+ "can't return inspector as this "
+ "AutogenContext has no database connection"
+ )
+ return inspect(self.connection)
+
+ @contextlib.contextmanager
+ def _within_batch(self) -> Iterator[None]:
+ self._has_batch = True
+ yield
+ self._has_batch = False
+
+ def run_name_filters(
+ self,
+ name: Optional[str],
+ type_: NameFilterType,
+ parent_names: NameFilterParentNames,
+ ) -> bool:
+ """Run the context's name filters and return True if the targets
+ should be part of the autogenerate operation.
+
+ This method should be run for every kind of name encountered within the
+ reflection side of an autogenerate operation, giving the environment
+ the chance to filter what names should be reflected as database
+ objects. The filters here are produced directly via the
+ :paramref:`.EnvironmentContext.configure.include_name` parameter.
+
+ """
+ if "schema_name" in parent_names:
+ if type_ == "table":
+ table_name = name
+ else:
+ table_name = parent_names.get("table_name", None)
+ if table_name:
+ schema_name = parent_names["schema_name"]
+ if schema_name:
+ parent_names["schema_qualified_table_name"] = "%s.%s" % (
+ schema_name,
+ table_name,
+ )
+ else:
+ parent_names["schema_qualified_table_name"] = table_name
+
+ for fn in self._name_filters:
+ if not fn(name, type_, parent_names):
+ return False
+ else:
+ return True
+
+ def run_object_filters(
+ self,
+ object_: SchemaItem,
+ name: sqla_compat._ConstraintName,
+ type_: NameFilterType,
+ reflected: bool,
+ compare_to: Optional[SchemaItem],
+ ) -> bool:
+ """Run the context's object filters and return True if the targets
+ should be part of the autogenerate operation.
+
+ This method should be run for every kind of object encountered within
+ an autogenerate operation, giving the environment the chance
+ to filter what objects should be included in the comparison.
+ The filters here are produced directly via the
+ :paramref:`.EnvironmentContext.configure.include_object` parameter.
+
+ """
+ for fn in self._object_filters:
+ if not fn(object_, name, type_, reflected, compare_to):
+ return False
+ else:
+ return True
+
+ run_filters = run_object_filters
+
+ @util.memoized_property
+ def sorted_tables(self) -> List[Table]:
+ """Return an aggregate of the :attr:`.MetaData.sorted_tables`
+ collection(s).
+
+ For a sequence of :class:`.MetaData` objects, this
+ concatenates the :attr:`.MetaData.sorted_tables` collection
+ for each individual :class:`.MetaData` in the order of the
+ sequence. It does **not** collate the sorted tables collections.
+
+ """
+ result = []
+ for m in util.to_list(self.metadata):
+ result.extend(m.sorted_tables)
+ return result
+
+ @util.memoized_property
+ def table_key_to_table(self) -> Dict[str, Table]:
+ """Return an aggregate of the :attr:`.MetaData.tables` dictionaries.
+
+ The :attr:`.MetaData.tables` collection is a dictionary of table key
+ to :class:`.Table`; this method aggregates the dictionary across
+ multiple :class:`.MetaData` objects into one dictionary.
+
+ Duplicate table keys are **not** supported; if two :class:`.MetaData`
+ objects contain the same table key, an exception is raised.
+
+ """
+ result: Dict[str, Table] = {}
+ for m in util.to_list(self.metadata):
+ intersect = set(result).intersection(set(m.tables))
+ if intersect:
+ raise ValueError(
+ "Duplicate table keys across multiple "
+ "MetaData objects: %s"
+ % (", ".join('"%s"' % key for key in sorted(intersect)))
+ )
+
+ result.update(m.tables)
+ return result
+
+
+class RevisionContext:
+ """Maintains configuration and state that's specific to a revision
+ file generation operation."""
+
+ generated_revisions: List[MigrationScript]
+ process_revision_directives: Optional[ProcessRevisionDirectiveFn]
+
+ def __init__(
+ self,
+ config: Config,
+ script_directory: ScriptDirectory,
+ command_args: Dict[str, Any],
+ process_revision_directives: Optional[
+ ProcessRevisionDirectiveFn
+ ] = None,
+ ) -> None:
+ self.config = config
+ self.script_directory = script_directory
+ self.command_args = command_args
+ self.process_revision_directives = process_revision_directives
+ self.template_args = {
+ "config": config # Let templates use config for
+ # e.g. multiple databases
+ }
+ self.generated_revisions = [self._default_revision()]
+
+ def _to_script(
+ self, migration_script: MigrationScript
+ ) -> Optional[Script]:
+ template_args: Dict[str, Any] = self.template_args.copy()
+
+ if getattr(migration_script, "_needs_render", False):
+ autogen_context = self._last_autogen_context
+
+ # clear out existing imports if we are doing multiple
+ # renders
+ autogen_context.imports = set()
+ if migration_script.imports:
+ autogen_context.imports.update(migration_script.imports)
+ render._render_python_into_templatevars(
+ autogen_context, migration_script, template_args
+ )
+
+ assert migration_script.rev_id is not None
+ return self.script_directory.generate_revision(
+ migration_script.rev_id,
+ migration_script.message,
+ refresh=True,
+ head=migration_script.head,
+ splice=migration_script.splice,
+ branch_labels=migration_script.branch_label,
+ version_path=migration_script.version_path,
+ depends_on=migration_script.depends_on,
+ **template_args,
+ )
+
+ def run_autogenerate(
+ self, rev: _GetRevArg, migration_context: MigrationContext
+ ) -> None:
+ self._run_environment(rev, migration_context, True)
+
+ def run_no_autogenerate(
+ self, rev: _GetRevArg, migration_context: MigrationContext
+ ) -> None:
+ self._run_environment(rev, migration_context, False)
+
+ def _run_environment(
+ self,
+ rev: _GetRevArg,
+ migration_context: MigrationContext,
+ autogenerate: bool,
+ ) -> None:
+ if autogenerate:
+ if self.command_args["sql"]:
+ raise util.CommandError(
+ "Using --sql with --autogenerate does not make any sense"
+ )
+ if set(self.script_directory.get_revisions(rev)) != set(
+ self.script_directory.get_revisions("heads")
+ ):
+ raise util.CommandError("Target database is not up to date.")
+
+ upgrade_token = migration_context.opts["upgrade_token"]
+ downgrade_token = migration_context.opts["downgrade_token"]
+
+ migration_script = self.generated_revisions[-1]
+ if not getattr(migration_script, "_needs_render", False):
+ migration_script.upgrade_ops_list[-1].upgrade_token = upgrade_token
+ migration_script.downgrade_ops_list[
+ -1
+ ].downgrade_token = downgrade_token
+ migration_script._needs_render = True
+ else:
+ migration_script._upgrade_ops.append(
+ ops.UpgradeOps([], upgrade_token=upgrade_token)
+ )
+ migration_script._downgrade_ops.append(
+ ops.DowngradeOps([], downgrade_token=downgrade_token)
+ )
+
+ autogen_context = AutogenContext(
+ migration_context, autogenerate=autogenerate
+ )
+ self._last_autogen_context: AutogenContext = autogen_context
+
+ if autogenerate:
+ compare._populate_migration_script(
+ autogen_context, migration_script
+ )
+
+ if self.process_revision_directives:
+ self.process_revision_directives(
+ migration_context, rev, self.generated_revisions
+ )
+
+ hook = migration_context.opts["process_revision_directives"]
+ if hook:
+ hook(migration_context, rev, self.generated_revisions)
+
+ for migration_script in self.generated_revisions:
+ migration_script._needs_render = True
+
+ def _default_revision(self) -> MigrationScript:
+ command_args: Dict[str, Any] = self.command_args
+ op = ops.MigrationScript(
+ rev_id=command_args["rev_id"] or util.rev_id(),
+ message=command_args["message"],
+ upgrade_ops=ops.UpgradeOps([]),
+ downgrade_ops=ops.DowngradeOps([]),
+ head=command_args["head"],
+ splice=command_args["splice"],
+ branch_label=command_args["branch_label"],
+ version_path=command_args["version_path"],
+ depends_on=command_args["depends_on"],
+ )
+ return op
+
+ def generate_scripts(self) -> Iterator[Optional[Script]]:
+ for generated_revision in self.generated_revisions:
+ yield self._to_script(generated_revision)
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/compare.py b/venv/lib/python3.12/site-packages/alembic/autogenerate/compare.py
new file mode 100644
index 0000000..fcef531
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/autogenerate/compare.py
@@ -0,0 +1,1329 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import contextlib
+import logging
+import re
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterator
+from typing import Mapping
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy import event
+from sqlalchemy import inspect
+from sqlalchemy import schema as sa_schema
+from sqlalchemy import text
+from sqlalchemy import types as sqltypes
+from sqlalchemy.sql import expression
+from sqlalchemy.sql.schema import ForeignKeyConstraint
+from sqlalchemy.sql.schema import Index
+from sqlalchemy.sql.schema import UniqueConstraint
+from sqlalchemy.util import OrderedSet
+
+from .. import util
+from ..ddl._autogen import is_index_sig
+from ..ddl._autogen import is_uq_sig
+from ..operations import ops
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.engine.reflection import Inspector
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Table
+
+ from alembic.autogenerate.api import AutogenContext
+ from alembic.ddl.impl import DefaultImpl
+ from alembic.operations.ops import AlterColumnOp
+ from alembic.operations.ops import MigrationScript
+ from alembic.operations.ops import ModifyTableOps
+ from alembic.operations.ops import UpgradeOps
+ from ..ddl._autogen import _constraint_sig
+
+
+log = logging.getLogger(__name__)
+
+
+def _populate_migration_script(
+ autogen_context: AutogenContext, migration_script: MigrationScript
+) -> None:
+ upgrade_ops = migration_script.upgrade_ops_list[-1]
+ downgrade_ops = migration_script.downgrade_ops_list[-1]
+
+ _produce_net_changes(autogen_context, upgrade_ops)
+ upgrade_ops.reverse_into(downgrade_ops)
+
+
+comparators = util.Dispatcher(uselist=True)
+
+
+def _produce_net_changes(
+ autogen_context: AutogenContext, upgrade_ops: UpgradeOps
+) -> None:
+ connection = autogen_context.connection
+ assert connection is not None
+ include_schemas = autogen_context.opts.get("include_schemas", False)
+
+ inspector: Inspector = inspect(connection)
+
+ default_schema = connection.dialect.default_schema_name
+ schemas: Set[Optional[str]]
+ if include_schemas:
+ schemas = set(inspector.get_schema_names())
+ # replace default schema name with None
+ schemas.discard("information_schema")
+ # replace the "default" schema with None
+ schemas.discard(default_schema)
+ schemas.add(None)
+ else:
+ schemas = {None}
+
+ schemas = {
+ s for s in schemas if autogen_context.run_name_filters(s, "schema", {})
+ }
+
+ assert autogen_context.dialect is not None
+ comparators.dispatch("schema", autogen_context.dialect.name)(
+ autogen_context, upgrade_ops, schemas
+ )
+
+
+@comparators.dispatch_for("schema")
+def _autogen_for_tables(
+ autogen_context: AutogenContext,
+ upgrade_ops: UpgradeOps,
+ schemas: Union[Set[None], Set[Optional[str]]],
+) -> None:
+ inspector = autogen_context.inspector
+
+ conn_table_names: Set[Tuple[Optional[str], str]] = set()
+
+ version_table_schema = (
+ autogen_context.migration_context.version_table_schema
+ )
+ version_table = autogen_context.migration_context.version_table
+
+ for schema_name in schemas:
+ tables = set(inspector.get_table_names(schema=schema_name))
+ if schema_name == version_table_schema:
+ tables = tables.difference(
+ [autogen_context.migration_context.version_table]
+ )
+
+ conn_table_names.update(
+ (schema_name, tname)
+ for tname in tables
+ if autogen_context.run_name_filters(
+ tname, "table", {"schema_name": schema_name}
+ )
+ )
+
+ metadata_table_names = OrderedSet(
+ [(table.schema, table.name) for table in autogen_context.sorted_tables]
+ ).difference([(version_table_schema, version_table)])
+
+ _compare_tables(
+ conn_table_names,
+ metadata_table_names,
+ inspector,
+ upgrade_ops,
+ autogen_context,
+ )
+
+
+def _compare_tables(
+ conn_table_names: set,
+ metadata_table_names: set,
+ inspector: Inspector,
+ upgrade_ops: UpgradeOps,
+ autogen_context: AutogenContext,
+) -> None:
+ default_schema = inspector.bind.dialect.default_schema_name
+
+ # tables coming from the connection will not have "schema"
+ # set if it matches default_schema_name; so we need a list
+ # of table names from local metadata that also have "None" if schema
+ # == default_schema_name. Most setups will be like this anyway but
+ # some are not (see #170)
+ metadata_table_names_no_dflt_schema = OrderedSet(
+ [
+ (schema if schema != default_schema else None, tname)
+ for schema, tname in metadata_table_names
+ ]
+ )
+
+ # to adjust for the MetaData collection storing the tables either
+ # as "schemaname.tablename" or just "tablename", create a new lookup
+ # which will match the "non-default-schema" keys to the Table object.
+ tname_to_table = {
+ no_dflt_schema: autogen_context.table_key_to_table[
+ sa_schema._get_table_key(tname, schema)
+ ]
+ for no_dflt_schema, (schema, tname) in zip(
+ metadata_table_names_no_dflt_schema, metadata_table_names
+ )
+ }
+ metadata_table_names = metadata_table_names_no_dflt_schema
+
+ for s, tname in metadata_table_names.difference(conn_table_names):
+ name = "%s.%s" % (s, tname) if s else tname
+ metadata_table = tname_to_table[(s, tname)]
+ if autogen_context.run_object_filters(
+ metadata_table, tname, "table", False, None
+ ):
+ upgrade_ops.ops.append(
+ ops.CreateTableOp.from_table(metadata_table)
+ )
+ log.info("Detected added table %r", name)
+ modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
+
+ comparators.dispatch("table")(
+ autogen_context,
+ modify_table_ops,
+ s,
+ tname,
+ None,
+ metadata_table,
+ )
+ if not modify_table_ops.is_empty():
+ upgrade_ops.ops.append(modify_table_ops)
+
+ removal_metadata = sa_schema.MetaData()
+ for s, tname in conn_table_names.difference(metadata_table_names):
+ name = sa_schema._get_table_key(tname, s)
+ exists = name in removal_metadata.tables
+ t = sa_schema.Table(tname, removal_metadata, schema=s)
+
+ if not exists:
+ event.listen(
+ t,
+ "column_reflect",
+ # fmt: off
+ autogen_context.migration_context.impl.
+ _compat_autogen_column_reflect
+ (inspector),
+ # fmt: on
+ )
+ sqla_compat._reflect_table(inspector, t)
+ if autogen_context.run_object_filters(t, tname, "table", True, None):
+ modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
+
+ comparators.dispatch("table")(
+ autogen_context, modify_table_ops, s, tname, t, None
+ )
+ if not modify_table_ops.is_empty():
+ upgrade_ops.ops.append(modify_table_ops)
+
+ upgrade_ops.ops.append(ops.DropTableOp.from_table(t))
+ log.info("Detected removed table %r", name)
+
+ existing_tables = conn_table_names.intersection(metadata_table_names)
+
+ existing_metadata = sa_schema.MetaData()
+ conn_column_info = {}
+ for s, tname in existing_tables:
+ name = sa_schema._get_table_key(tname, s)
+ exists = name in existing_metadata.tables
+ t = sa_schema.Table(tname, existing_metadata, schema=s)
+ if not exists:
+ event.listen(
+ t,
+ "column_reflect",
+ # fmt: off
+ autogen_context.migration_context.impl.
+ _compat_autogen_column_reflect(inspector),
+ # fmt: on
+ )
+ sqla_compat._reflect_table(inspector, t)
+ conn_column_info[(s, tname)] = t
+
+ for s, tname in sorted(existing_tables, key=lambda x: (x[0] or "", x[1])):
+ s = s or None
+ name = "%s.%s" % (s, tname) if s else tname
+ metadata_table = tname_to_table[(s, tname)]
+ conn_table = existing_metadata.tables[name]
+
+ if autogen_context.run_object_filters(
+ metadata_table, tname, "table", False, conn_table
+ ):
+ modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
+ with _compare_columns(
+ s,
+ tname,
+ conn_table,
+ metadata_table,
+ modify_table_ops,
+ autogen_context,
+ inspector,
+ ):
+ comparators.dispatch("table")(
+ autogen_context,
+ modify_table_ops,
+ s,
+ tname,
+ conn_table,
+ metadata_table,
+ )
+
+ if not modify_table_ops.is_empty():
+ upgrade_ops.ops.append(modify_table_ops)
+
+
+_IndexColumnSortingOps: Mapping[str, Any] = util.immutabledict(
+ {
+ "asc": expression.asc,
+ "desc": expression.desc,
+ "nulls_first": expression.nullsfirst,
+ "nulls_last": expression.nullslast,
+ "nullsfirst": expression.nullsfirst, # 1_3 name
+ "nullslast": expression.nullslast, # 1_3 name
+ }
+)
+
+
+def _make_index(
+ impl: DefaultImpl, params: Dict[str, Any], conn_table: Table
+) -> Optional[Index]:
+ exprs: list[Union[Column[Any], TextClause]] = []
+ sorting = params.get("column_sorting")
+
+ for num, col_name in enumerate(params["column_names"]):
+ item: Union[Column[Any], TextClause]
+ if col_name is None:
+ assert "expressions" in params
+ name = params["expressions"][num]
+ item = text(name)
+ else:
+ name = col_name
+ item = conn_table.c[col_name]
+ if sorting and name in sorting:
+ for operator in sorting[name]:
+ if operator in _IndexColumnSortingOps:
+ item = _IndexColumnSortingOps[operator](item)
+ exprs.append(item)
+ ix = sa_schema.Index(
+ params["name"],
+ *exprs,
+ unique=params["unique"],
+ _table=conn_table,
+ **impl.adjust_reflected_dialect_options(params, "index"),
+ )
+ if "duplicates_constraint" in params:
+ ix.info["duplicates_constraint"] = params["duplicates_constraint"]
+ return ix
+
+
+def _make_unique_constraint(
+ impl: DefaultImpl, params: Dict[str, Any], conn_table: Table
+) -> UniqueConstraint:
+ uq = sa_schema.UniqueConstraint(
+ *[conn_table.c[cname] for cname in params["column_names"]],
+ name=params["name"],
+ **impl.adjust_reflected_dialect_options(params, "unique_constraint"),
+ )
+ if "duplicates_index" in params:
+ uq.info["duplicates_index"] = params["duplicates_index"]
+
+ return uq
+
+
+def _make_foreign_key(
+ params: Dict[str, Any], conn_table: Table
+) -> ForeignKeyConstraint:
+ tname = params["referred_table"]
+ if params["referred_schema"]:
+ tname = "%s.%s" % (params["referred_schema"], tname)
+
+ options = params.get("options", {})
+
+ const = sa_schema.ForeignKeyConstraint(
+ [conn_table.c[cname] for cname in params["constrained_columns"]],
+ ["%s.%s" % (tname, n) for n in params["referred_columns"]],
+ onupdate=options.get("onupdate"),
+ ondelete=options.get("ondelete"),
+ deferrable=options.get("deferrable"),
+ initially=options.get("initially"),
+ name=params["name"],
+ )
+ # needed by 0.7
+ conn_table.append_constraint(const)
+ return const
+
+
+@contextlib.contextmanager
+def _compare_columns(
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Table,
+ metadata_table: Table,
+ modify_table_ops: ModifyTableOps,
+ autogen_context: AutogenContext,
+ inspector: Inspector,
+) -> Iterator[None]:
+ name = "%s.%s" % (schema, tname) if schema else tname
+ metadata_col_names = OrderedSet(
+ c.name for c in metadata_table.c if not c.system
+ )
+ metadata_cols_by_name = {
+ c.name: c for c in metadata_table.c if not c.system
+ }
+
+ conn_col_names = {
+ c.name: c
+ for c in conn_table.c
+ if autogen_context.run_name_filters(
+ c.name, "column", {"table_name": tname, "schema_name": schema}
+ )
+ }
+
+ for cname in metadata_col_names.difference(conn_col_names):
+ if autogen_context.run_object_filters(
+ metadata_cols_by_name[cname], cname, "column", False, None
+ ):
+ modify_table_ops.ops.append(
+ ops.AddColumnOp.from_column_and_tablename(
+ schema, tname, metadata_cols_by_name[cname]
+ )
+ )
+ log.info("Detected added column '%s.%s'", name, cname)
+
+ for colname in metadata_col_names.intersection(conn_col_names):
+ metadata_col = metadata_cols_by_name[colname]
+ conn_col = conn_table.c[colname]
+ if not autogen_context.run_object_filters(
+ metadata_col, colname, "column", False, conn_col
+ ):
+ continue
+ alter_column_op = ops.AlterColumnOp(tname, colname, schema=schema)
+
+ comparators.dispatch("column")(
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ colname,
+ conn_col,
+ metadata_col,
+ )
+
+ if alter_column_op.has_changes():
+ modify_table_ops.ops.append(alter_column_op)
+
+ yield
+
+ for cname in set(conn_col_names).difference(metadata_col_names):
+ if autogen_context.run_object_filters(
+ conn_table.c[cname], cname, "column", True, None
+ ):
+ modify_table_ops.ops.append(
+ ops.DropColumnOp.from_column_and_tablename(
+ schema, tname, conn_table.c[cname]
+ )
+ )
+ log.info("Detected removed column '%s.%s'", name, cname)
+
+
+_C = TypeVar("_C", bound=Union[UniqueConstraint, ForeignKeyConstraint, Index])
+
+
+@comparators.dispatch_for("table")
+def _compare_indexes_and_uniques(
+ autogen_context: AutogenContext,
+ modify_ops: ModifyTableOps,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Optional[Table],
+ metadata_table: Optional[Table],
+) -> None:
+ inspector = autogen_context.inspector
+ is_create_table = conn_table is None
+ is_drop_table = metadata_table is None
+ impl = autogen_context.migration_context.impl
+
+ # 1a. get raw indexes and unique constraints from metadata ...
+ if metadata_table is not None:
+ metadata_unique_constraints = {
+ uq
+ for uq in metadata_table.constraints
+ if isinstance(uq, sa_schema.UniqueConstraint)
+ }
+ metadata_indexes = set(metadata_table.indexes)
+ else:
+ metadata_unique_constraints = set()
+ metadata_indexes = set()
+
+ conn_uniques = conn_indexes = frozenset() # type:ignore[var-annotated]
+
+ supports_unique_constraints = False
+
+ unique_constraints_duplicate_unique_indexes = False
+
+ if conn_table is not None:
+ # 1b. ... and from connection, if the table exists
+ try:
+ conn_uniques = inspector.get_unique_constraints( # type:ignore[assignment] # noqa
+ tname, schema=schema
+ )
+ supports_unique_constraints = True
+ except NotImplementedError:
+ pass
+ except TypeError:
+ # number of arguments is off for the base
+ # method in SQLAlchemy due to the cache decorator
+ # not being present
+ pass
+ else:
+ conn_uniques = [ # type:ignore[assignment]
+ uq
+ for uq in conn_uniques
+ if autogen_context.run_name_filters(
+ uq["name"],
+ "unique_constraint",
+ {"table_name": tname, "schema_name": schema},
+ )
+ ]
+ for uq in conn_uniques:
+ if uq.get("duplicates_index"):
+ unique_constraints_duplicate_unique_indexes = True
+ try:
+ conn_indexes = inspector.get_indexes( # type:ignore[assignment]
+ tname, schema=schema
+ )
+ except NotImplementedError:
+ pass
+ else:
+ conn_indexes = [ # type:ignore[assignment]
+ ix
+ for ix in conn_indexes
+ if autogen_context.run_name_filters(
+ ix["name"],
+ "index",
+ {"table_name": tname, "schema_name": schema},
+ )
+ ]
+
+ # 2. convert conn-level objects from raw inspector records
+ # into schema objects
+ if is_drop_table:
+ # for DROP TABLE uniques are inline, don't need them
+ conn_uniques = set() # type:ignore[assignment]
+ else:
+ conn_uniques = { # type:ignore[assignment]
+ _make_unique_constraint(impl, uq_def, conn_table)
+ for uq_def in conn_uniques
+ }
+
+ conn_indexes = { # type:ignore[assignment]
+ index
+ for index in (
+ _make_index(impl, ix, conn_table) for ix in conn_indexes
+ )
+ if index is not None
+ }
+
+ # 2a. if the dialect dupes unique indexes as unique constraints
+ # (mysql and oracle), correct for that
+
+ if unique_constraints_duplicate_unique_indexes:
+ _correct_for_uq_duplicates_uix(
+ conn_uniques,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ autogen_context.dialect,
+ impl,
+ )
+
+ # 3. give the dialect a chance to omit indexes and constraints that
+ # we know are either added implicitly by the DB or that the DB
+ # can't accurately report on
+ impl.correct_for_autogen_constraints(
+ conn_uniques, # type: ignore[arg-type]
+ conn_indexes, # type: ignore[arg-type]
+ metadata_unique_constraints,
+ metadata_indexes,
+ )
+
+ # 4. organize the constraints into "signature" collections, the
+ # _constraint_sig() objects provide a consistent facade over both
+ # Index and UniqueConstraint so we can easily work with them
+ # interchangeably
+ metadata_unique_constraints_sig = {
+ impl._create_metadata_constraint_sig(uq)
+ for uq in metadata_unique_constraints
+ }
+
+ metadata_indexes_sig = {
+ impl._create_metadata_constraint_sig(ix) for ix in metadata_indexes
+ }
+
+ conn_unique_constraints = {
+ impl._create_reflected_constraint_sig(uq) for uq in conn_uniques
+ }
+
+ conn_indexes_sig = {
+ impl._create_reflected_constraint_sig(ix) for ix in conn_indexes
+ }
+
+ # 5. index things by name, for those objects that have names
+ metadata_names = {
+ cast(str, c.md_name_to_sql_name(autogen_context)): c
+ for c in metadata_unique_constraints_sig.union(metadata_indexes_sig)
+ if c.is_named
+ }
+
+ conn_uniques_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig]
+ conn_indexes_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig]
+
+ conn_uniques_by_name = {c.name: c for c in conn_unique_constraints}
+ conn_indexes_by_name = {c.name: c for c in conn_indexes_sig}
+ conn_names = {
+ c.name: c
+ for c in conn_unique_constraints.union(conn_indexes_sig)
+ if sqla_compat.constraint_name_string(c.name)
+ }
+
+ doubled_constraints = {
+ name: (conn_uniques_by_name[name], conn_indexes_by_name[name])
+ for name in set(conn_uniques_by_name).intersection(
+ conn_indexes_by_name
+ )
+ }
+
+ # 6. index things by "column signature", to help with unnamed unique
+ # constraints.
+ conn_uniques_by_sig = {uq.unnamed: uq for uq in conn_unique_constraints}
+ metadata_uniques_by_sig = {
+ uq.unnamed: uq for uq in metadata_unique_constraints_sig
+ }
+ unnamed_metadata_uniques = {
+ uq.unnamed: uq
+ for uq in metadata_unique_constraints_sig
+ if not sqla_compat._constraint_is_named(
+ uq.const, autogen_context.dialect
+ )
+ }
+
+ # assumptions:
+ # 1. a unique constraint or an index from the connection *always*
+ # has a name.
+ # 2. an index on the metadata side *always* has a name.
+ # 3. a unique constraint on the metadata side *might* have a name.
+ # 4. The backend may double up indexes as unique constraints and
+ # vice versa (e.g. MySQL, Postgresql)
+
+ def obj_added(obj: _constraint_sig):
+ if is_index_sig(obj):
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "index", False, None
+ ):
+ modify_ops.ops.append(ops.CreateIndexOp.from_index(obj.const))
+ log.info(
+ "Detected added index '%r' on '%s'",
+ obj.name,
+ obj.column_names,
+ )
+ elif is_uq_sig(obj):
+ if not supports_unique_constraints:
+ # can't report unique indexes as added if we don't
+ # detect them
+ return
+ if is_create_table or is_drop_table:
+ # unique constraints are created inline with table defs
+ return
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "unique_constraint", False, None
+ ):
+ modify_ops.ops.append(
+ ops.AddConstraintOp.from_constraint(obj.const)
+ )
+ log.info(
+ "Detected added unique constraint %r on '%s'",
+ obj.name,
+ obj.column_names,
+ )
+ else:
+ assert False
+
+ def obj_removed(obj: _constraint_sig):
+ if is_index_sig(obj):
+ if obj.is_unique and not supports_unique_constraints:
+ # many databases double up unique constraints
+ # as unique indexes. without that list we can't
+ # be sure what we're doing here
+ return
+
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "index", True, None
+ ):
+ modify_ops.ops.append(ops.DropIndexOp.from_index(obj.const))
+ log.info("Detected removed index %r on %r", obj.name, tname)
+ elif is_uq_sig(obj):
+ if is_create_table or is_drop_table:
+ # if the whole table is being dropped, we don't need to
+ # consider unique constraint separately
+ return
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "unique_constraint", True, None
+ ):
+ modify_ops.ops.append(
+ ops.DropConstraintOp.from_constraint(obj.const)
+ )
+ log.info(
+ "Detected removed unique constraint %r on %r",
+ obj.name,
+ tname,
+ )
+ else:
+ assert False
+
+ def obj_changed(
+ old: _constraint_sig,
+ new: _constraint_sig,
+ msg: str,
+ ):
+ if is_index_sig(old):
+ assert is_index_sig(new)
+
+ if autogen_context.run_object_filters(
+ new.const, new.name, "index", False, old.const
+ ):
+ log.info(
+ "Detected changed index %r on %r: %s", old.name, tname, msg
+ )
+ modify_ops.ops.append(ops.DropIndexOp.from_index(old.const))
+ modify_ops.ops.append(ops.CreateIndexOp.from_index(new.const))
+ elif is_uq_sig(old):
+ assert is_uq_sig(new)
+
+ if autogen_context.run_object_filters(
+ new.const, new.name, "unique_constraint", False, old.const
+ ):
+ log.info(
+ "Detected changed unique constraint %r on %r: %s",
+ old.name,
+ tname,
+ msg,
+ )
+ modify_ops.ops.append(
+ ops.DropConstraintOp.from_constraint(old.const)
+ )
+ modify_ops.ops.append(
+ ops.AddConstraintOp.from_constraint(new.const)
+ )
+ else:
+ assert False
+
+ for removed_name in sorted(set(conn_names).difference(metadata_names)):
+ conn_obj = conn_names[removed_name]
+ if (
+ is_uq_sig(conn_obj)
+ and conn_obj.unnamed in unnamed_metadata_uniques
+ ):
+ continue
+ elif removed_name in doubled_constraints:
+ conn_uq, conn_idx = doubled_constraints[removed_name]
+ if (
+ all(
+ conn_idx.unnamed != meta_idx.unnamed
+ for meta_idx in metadata_indexes_sig
+ )
+ and conn_uq.unnamed not in metadata_uniques_by_sig
+ ):
+ obj_removed(conn_uq)
+ obj_removed(conn_idx)
+ else:
+ obj_removed(conn_obj)
+
+ for existing_name in sorted(set(metadata_names).intersection(conn_names)):
+ metadata_obj = metadata_names[existing_name]
+
+ if existing_name in doubled_constraints:
+ conn_uq, conn_idx = doubled_constraints[existing_name]
+ if is_index_sig(metadata_obj):
+ conn_obj = conn_idx
+ else:
+ conn_obj = conn_uq
+ else:
+ conn_obj = conn_names[existing_name]
+
+ if type(conn_obj) != type(metadata_obj):
+ obj_removed(conn_obj)
+ obj_added(metadata_obj)
+ else:
+ comparison = metadata_obj.compare_to_reflected(conn_obj)
+
+ if comparison.is_different:
+ # constraint are different
+ obj_changed(conn_obj, metadata_obj, comparison.message)
+ elif comparison.is_skip:
+ # constraint cannot be compared, skip them
+ thing = (
+ "index" if is_index_sig(conn_obj) else "unique constraint"
+ )
+ log.info(
+ "Cannot compare %s %r, assuming equal and skipping. %s",
+ thing,
+ conn_obj.name,
+ comparison.message,
+ )
+ else:
+ # constraint are equal
+ assert comparison.is_equal
+
+ for added_name in sorted(set(metadata_names).difference(conn_names)):
+ obj = metadata_names[added_name]
+ obj_added(obj)
+
+ for uq_sig in unnamed_metadata_uniques:
+ if uq_sig not in conn_uniques_by_sig:
+ obj_added(unnamed_metadata_uniques[uq_sig])
+
+
+def _correct_for_uq_duplicates_uix(
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ dialect,
+ impl,
+):
+ # dedupe unique indexes vs. constraints, since MySQL / Oracle
+ # doesn't really have unique constraints as a separate construct.
+ # but look in the metadata and try to maintain constructs
+ # that already seem to be defined one way or the other
+ # on that side. This logic was formerly local to MySQL dialect,
+ # generalized to Oracle and others. See #276
+
+ # resolve final rendered name for unique constraints defined in the
+ # metadata. this includes truncation of long names. naming convention
+ # names currently should already be set as cons.name, however leave this
+ # to the sqla_compat to decide.
+ metadata_cons_names = [
+ (sqla_compat._get_constraint_final_name(cons, dialect), cons)
+ for cons in metadata_unique_constraints
+ ]
+
+ metadata_uq_names = {
+ name for name, cons in metadata_cons_names if name is not None
+ }
+
+ unnamed_metadata_uqs = {
+ impl._create_metadata_constraint_sig(cons).unnamed
+ for name, cons in metadata_cons_names
+ if name is None
+ }
+
+ metadata_ix_names = {
+ sqla_compat._get_constraint_final_name(cons, dialect)
+ for cons in metadata_indexes
+ if cons.unique
+ }
+
+ # for reflection side, names are in their final database form
+ # already since they're from the database
+ conn_ix_names = {cons.name: cons for cons in conn_indexes if cons.unique}
+
+ uqs_dupe_indexes = {
+ cons.name: cons
+ for cons in conn_unique_constraints
+ if cons.info["duplicates_index"]
+ }
+
+ for overlap in uqs_dupe_indexes:
+ if overlap not in metadata_uq_names:
+ if (
+ impl._create_reflected_constraint_sig(
+ uqs_dupe_indexes[overlap]
+ ).unnamed
+ not in unnamed_metadata_uqs
+ ):
+ conn_unique_constraints.discard(uqs_dupe_indexes[overlap])
+ elif overlap not in metadata_ix_names:
+ conn_indexes.discard(conn_ix_names[overlap])
+
+
+@comparators.dispatch_for("column")
+def _compare_nullable(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: Union[quoted_name, str],
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ metadata_col_nullable = metadata_col.nullable
+ conn_col_nullable = conn_col.nullable
+ alter_column_op.existing_nullable = conn_col_nullable
+
+ if conn_col_nullable is not metadata_col_nullable:
+ if (
+ sqla_compat._server_default_is_computed(
+ metadata_col.server_default, conn_col.server_default
+ )
+ and sqla_compat._nullability_might_be_unset(metadata_col)
+ or (
+ sqla_compat._server_default_is_identity(
+ metadata_col.server_default, conn_col.server_default
+ )
+ )
+ ):
+ log.info(
+ "Ignoring nullable change on identity column '%s.%s'",
+ tname,
+ cname,
+ )
+ else:
+ alter_column_op.modify_nullable = metadata_col_nullable
+ log.info(
+ "Detected %s on column '%s.%s'",
+ "NULL" if metadata_col_nullable else "NOT NULL",
+ tname,
+ cname,
+ )
+
+
+@comparators.dispatch_for("column")
+def _setup_autoincrement(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: quoted_name,
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ if metadata_col.table._autoincrement_column is metadata_col:
+ alter_column_op.kw["autoincrement"] = True
+ elif metadata_col.autoincrement is True:
+ alter_column_op.kw["autoincrement"] = True
+ elif metadata_col.autoincrement is False:
+ alter_column_op.kw["autoincrement"] = False
+
+
+@comparators.dispatch_for("column")
+def _compare_type(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: Union[quoted_name, str],
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ conn_type = conn_col.type
+ alter_column_op.existing_type = conn_type
+ metadata_type = metadata_col.type
+ if conn_type._type_affinity is sqltypes.NullType:
+ log.info(
+ "Couldn't determine database type " "for column '%s.%s'",
+ tname,
+ cname,
+ )
+ return
+ if metadata_type._type_affinity is sqltypes.NullType:
+ log.info(
+ "Column '%s.%s' has no type within " "the model; can't compare",
+ tname,
+ cname,
+ )
+ return
+
+ isdiff = autogen_context.migration_context._compare_type(
+ conn_col, metadata_col
+ )
+
+ if isdiff:
+ alter_column_op.modify_type = metadata_type
+ log.info(
+ "Detected type change from %r to %r on '%s.%s'",
+ conn_type,
+ metadata_type,
+ tname,
+ cname,
+ )
+
+
+def _render_server_default_for_compare(
+ metadata_default: Optional[Any], autogen_context: AutogenContext
+) -> Optional[str]:
+ if isinstance(metadata_default, sa_schema.DefaultClause):
+ if isinstance(metadata_default.arg, str):
+ metadata_default = metadata_default.arg
+ else:
+ metadata_default = str(
+ metadata_default.arg.compile(
+ dialect=autogen_context.dialect,
+ compile_kwargs={"literal_binds": True},
+ )
+ )
+ if isinstance(metadata_default, str):
+ return metadata_default
+ else:
+ return None
+
+
+def _normalize_computed_default(sqltext: str) -> str:
+ """we want to warn if a computed sql expression has changed. however
+ we don't want false positives and the warning is not that critical.
+ so filter out most forms of variability from the SQL text.
+
+ """
+
+ return re.sub(r"[ \(\)'\"`\[\]]", "", sqltext).lower()
+
+
+def _compare_computed_default(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: str,
+ cname: str,
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ rendered_metadata_default = str(
+ cast(sa_schema.Computed, metadata_col.server_default).sqltext.compile(
+ dialect=autogen_context.dialect,
+ compile_kwargs={"literal_binds": True},
+ )
+ )
+
+ # since we cannot change computed columns, we do only a crude comparison
+ # here where we try to eliminate syntactical differences in order to
+ # get a minimal comparison just to emit a warning.
+
+ rendered_metadata_default = _normalize_computed_default(
+ rendered_metadata_default
+ )
+
+ if isinstance(conn_col.server_default, sa_schema.Computed):
+ rendered_conn_default = str(
+ conn_col.server_default.sqltext.compile(
+ dialect=autogen_context.dialect,
+ compile_kwargs={"literal_binds": True},
+ )
+ )
+ if rendered_conn_default is None:
+ rendered_conn_default = ""
+ else:
+ rendered_conn_default = _normalize_computed_default(
+ rendered_conn_default
+ )
+ else:
+ rendered_conn_default = ""
+
+ if rendered_metadata_default != rendered_conn_default:
+ _warn_computed_not_supported(tname, cname)
+
+
+def _warn_computed_not_supported(tname: str, cname: str) -> None:
+ util.warn("Computed default on %s.%s cannot be modified" % (tname, cname))
+
+
+def _compare_identity_default(
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ cname,
+ conn_col,
+ metadata_col,
+):
+ impl = autogen_context.migration_context.impl
+ diff, ignored_attr, is_alter = impl._compare_identity_default(
+ metadata_col.server_default, conn_col.server_default
+ )
+
+ return diff, is_alter
+
+
+@comparators.dispatch_for("column")
+def _compare_server_default(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: Union[quoted_name, str],
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> Optional[bool]:
+ metadata_default = metadata_col.server_default
+ conn_col_default = conn_col.server_default
+ if conn_col_default is None and metadata_default is None:
+ return False
+
+ if sqla_compat._server_default_is_computed(metadata_default):
+ # return False in case of a computed column as the server
+ # default. Note that DDL for adding or removing "GENERATED AS" from
+ # an existing column is not currently known for any backend.
+ # Once SQLAlchemy can reflect "GENERATED" as the "computed" element,
+ # we would also want to ignore and/or warn for changes vs. the
+ # metadata (or support backend specific DDL if applicable).
+ if not sqla_compat.has_computed_reflection:
+ return False
+
+ else:
+ return (
+ _compare_computed_default( # type:ignore[func-returns-value]
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ cname,
+ conn_col,
+ metadata_col,
+ )
+ )
+ if sqla_compat._server_default_is_computed(conn_col_default):
+ _warn_computed_not_supported(tname, cname)
+ return False
+
+ if sqla_compat._server_default_is_identity(
+ metadata_default, conn_col_default
+ ):
+ alter_column_op.existing_server_default = conn_col_default
+ diff, is_alter = _compare_identity_default(
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ cname,
+ conn_col,
+ metadata_col,
+ )
+ if is_alter:
+ alter_column_op.modify_server_default = metadata_default
+ if diff:
+ log.info(
+ "Detected server default on column '%s.%s': "
+ "identity options attributes %s",
+ tname,
+ cname,
+ sorted(diff),
+ )
+ else:
+ rendered_metadata_default = _render_server_default_for_compare(
+ metadata_default, autogen_context
+ )
+
+ rendered_conn_default = (
+ cast(Any, conn_col_default).arg.text if conn_col_default else None
+ )
+
+ alter_column_op.existing_server_default = conn_col_default
+
+ is_diff = autogen_context.migration_context._compare_server_default(
+ conn_col,
+ metadata_col,
+ rendered_metadata_default,
+ rendered_conn_default,
+ )
+ if is_diff:
+ alter_column_op.modify_server_default = metadata_default
+ log.info("Detected server default on column '%s.%s'", tname, cname)
+
+ return None
+
+
+@comparators.dispatch_for("column")
+def _compare_column_comment(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: quoted_name,
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> Optional[Literal[False]]:
+ assert autogen_context.dialect is not None
+ if not autogen_context.dialect.supports_comments:
+ return None
+
+ metadata_comment = metadata_col.comment
+ conn_col_comment = conn_col.comment
+ if conn_col_comment is None and metadata_comment is None:
+ return False
+
+ alter_column_op.existing_comment = conn_col_comment
+
+ if conn_col_comment != metadata_comment:
+ alter_column_op.modify_comment = metadata_comment
+ log.info("Detected column comment '%s.%s'", tname, cname)
+
+ return None
+
+
+@comparators.dispatch_for("table")
+def _compare_foreign_keys(
+ autogen_context: AutogenContext,
+ modify_table_ops: ModifyTableOps,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Table,
+ metadata_table: Table,
+) -> None:
+ # if we're doing CREATE TABLE, all FKs are created
+ # inline within the table def
+ if conn_table is None or metadata_table is None:
+ return
+
+ inspector = autogen_context.inspector
+ metadata_fks = {
+ fk
+ for fk in metadata_table.constraints
+ if isinstance(fk, sa_schema.ForeignKeyConstraint)
+ }
+
+ conn_fks_list = [
+ fk
+ for fk in inspector.get_foreign_keys(tname, schema=schema)
+ if autogen_context.run_name_filters(
+ fk["name"],
+ "foreign_key_constraint",
+ {"table_name": tname, "schema_name": schema},
+ )
+ ]
+
+ conn_fks = {
+ _make_foreign_key(const, conn_table) # type: ignore[arg-type]
+ for const in conn_fks_list
+ }
+
+ impl = autogen_context.migration_context.impl
+
+ # give the dialect a chance to correct the FKs to match more
+ # closely
+ autogen_context.migration_context.impl.correct_for_autogen_foreignkeys(
+ conn_fks, metadata_fks
+ )
+
+ metadata_fks_sig = {
+ impl._create_metadata_constraint_sig(fk) for fk in metadata_fks
+ }
+
+ conn_fks_sig = {
+ impl._create_reflected_constraint_sig(fk) for fk in conn_fks
+ }
+
+ # check if reflected FKs include options, indicating the backend
+ # can reflect FK options
+ if conn_fks_list and "options" in conn_fks_list[0]:
+ conn_fks_by_sig = {c.unnamed: c for c in conn_fks_sig}
+ metadata_fks_by_sig = {c.unnamed: c for c in metadata_fks_sig}
+ else:
+ # otherwise compare by sig without options added
+ conn_fks_by_sig = {c.unnamed_no_options: c for c in conn_fks_sig}
+ metadata_fks_by_sig = {
+ c.unnamed_no_options: c for c in metadata_fks_sig
+ }
+
+ metadata_fks_by_name = {
+ c.name: c for c in metadata_fks_sig if c.name is not None
+ }
+ conn_fks_by_name = {c.name: c for c in conn_fks_sig if c.name is not None}
+
+ def _add_fk(obj, compare_to):
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "foreign_key_constraint", False, compare_to
+ ):
+ modify_table_ops.ops.append(
+ ops.CreateForeignKeyOp.from_constraint(const.const) # type: ignore[has-type] # noqa: E501
+ )
+
+ log.info(
+ "Detected added foreign key (%s)(%s) on table %s%s",
+ ", ".join(obj.source_columns),
+ ", ".join(obj.target_columns),
+ "%s." % obj.source_schema if obj.source_schema else "",
+ obj.source_table,
+ )
+
+ def _remove_fk(obj, compare_to):
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "foreign_key_constraint", True, compare_to
+ ):
+ modify_table_ops.ops.append(
+ ops.DropConstraintOp.from_constraint(obj.const)
+ )
+ log.info(
+ "Detected removed foreign key (%s)(%s) on table %s%s",
+ ", ".join(obj.source_columns),
+ ", ".join(obj.target_columns),
+ "%s." % obj.source_schema if obj.source_schema else "",
+ obj.source_table,
+ )
+
+ # so far it appears we don't need to do this by name at all.
+ # SQLite doesn't preserve constraint names anyway
+
+ for removed_sig in set(conn_fks_by_sig).difference(metadata_fks_by_sig):
+ const = conn_fks_by_sig[removed_sig]
+ if removed_sig not in metadata_fks_by_sig:
+ compare_to = (
+ metadata_fks_by_name[const.name].const
+ if const.name in metadata_fks_by_name
+ else None
+ )
+ _remove_fk(const, compare_to)
+
+ for added_sig in set(metadata_fks_by_sig).difference(conn_fks_by_sig):
+ const = metadata_fks_by_sig[added_sig]
+ if added_sig not in conn_fks_by_sig:
+ compare_to = (
+ conn_fks_by_name[const.name].const
+ if const.name in conn_fks_by_name
+ else None
+ )
+ _add_fk(const, compare_to)
+
+
+@comparators.dispatch_for("table")
+def _compare_table_comment(
+ autogen_context: AutogenContext,
+ modify_table_ops: ModifyTableOps,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Optional[Table],
+ metadata_table: Optional[Table],
+) -> None:
+ assert autogen_context.dialect is not None
+ if not autogen_context.dialect.supports_comments:
+ return
+
+ # if we're doing CREATE TABLE, comments will be created inline
+ # with the create_table op.
+ if conn_table is None or metadata_table is None:
+ return
+
+ if conn_table.comment is None and metadata_table.comment is None:
+ return
+
+ if metadata_table.comment is None and conn_table.comment is not None:
+ modify_table_ops.ops.append(
+ ops.DropTableCommentOp(
+ tname, existing_comment=conn_table.comment, schema=schema
+ )
+ )
+ elif metadata_table.comment != conn_table.comment:
+ modify_table_ops.ops.append(
+ ops.CreateTableCommentOp(
+ tname,
+ metadata_table.comment,
+ existing_comment=conn_table.comment,
+ schema=schema,
+ )
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/render.py b/venv/lib/python3.12/site-packages/alembic/autogenerate/render.py
new file mode 100644
index 0000000..317a6db
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/autogenerate/render.py
@@ -0,0 +1,1097 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+from io import StringIO
+import re
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from mako.pygen import PythonPrinter
+from sqlalchemy import schema as sa_schema
+from sqlalchemy import sql
+from sqlalchemy import types as sqltypes
+from sqlalchemy.sql.elements import conv
+from sqlalchemy.sql.elements import quoted_name
+
+from .. import util
+from ..operations import ops
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.sql.base import DialectKWArgs
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.schema import CheckConstraint
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import FetchedValue
+ from sqlalchemy.sql.schema import ForeignKey
+ from sqlalchemy.sql.schema import ForeignKeyConstraint
+ from sqlalchemy.sql.schema import Index
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import PrimaryKeyConstraint
+ from sqlalchemy.sql.schema import UniqueConstraint
+ from sqlalchemy.sql.sqltypes import ARRAY
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from alembic.autogenerate.api import AutogenContext
+ from alembic.config import Config
+ from alembic.operations.ops import MigrationScript
+ from alembic.operations.ops import ModifyTableOps
+ from alembic.util.sqla_compat import Computed
+ from alembic.util.sqla_compat import Identity
+
+
+MAX_PYTHON_ARGS = 255
+
+
+def _render_gen_name(
+ autogen_context: AutogenContext,
+ name: sqla_compat._ConstraintName,
+) -> Optional[Union[quoted_name, str, _f_name]]:
+ if isinstance(name, conv):
+ return _f_name(_alembic_autogenerate_prefix(autogen_context), name)
+ else:
+ return sqla_compat.constraint_name_or_none(name)
+
+
+def _indent(text: str) -> str:
+ text = re.compile(r"^", re.M).sub(" ", text).strip()
+ text = re.compile(r" +$", re.M).sub("", text)
+ return text
+
+
+def _render_python_into_templatevars(
+ autogen_context: AutogenContext,
+ migration_script: MigrationScript,
+ template_args: Dict[str, Union[str, Config]],
+) -> None:
+ imports = autogen_context.imports
+
+ for upgrade_ops, downgrade_ops in zip(
+ migration_script.upgrade_ops_list, migration_script.downgrade_ops_list
+ ):
+ template_args[upgrade_ops.upgrade_token] = _indent(
+ _render_cmd_body(upgrade_ops, autogen_context)
+ )
+ template_args[downgrade_ops.downgrade_token] = _indent(
+ _render_cmd_body(downgrade_ops, autogen_context)
+ )
+ template_args["imports"] = "\n".join(sorted(imports))
+
+
+default_renderers = renderers = util.Dispatcher()
+
+
+def _render_cmd_body(
+ op_container: ops.OpContainer,
+ autogen_context: AutogenContext,
+) -> str:
+ buf = StringIO()
+ printer = PythonPrinter(buf)
+
+ printer.writeline(
+ "# ### commands auto generated by Alembic - please adjust! ###"
+ )
+
+ has_lines = False
+ for op in op_container.ops:
+ lines = render_op(autogen_context, op)
+ has_lines = has_lines or bool(lines)
+
+ for line in lines:
+ printer.writeline(line)
+
+ if not has_lines:
+ printer.writeline("pass")
+
+ printer.writeline("# ### end Alembic commands ###")
+
+ return buf.getvalue()
+
+
+def render_op(
+ autogen_context: AutogenContext, op: ops.MigrateOperation
+) -> List[str]:
+ renderer = renderers.dispatch(op)
+ lines = util.to_list(renderer(autogen_context, op))
+ return lines
+
+
+def render_op_text(
+ autogen_context: AutogenContext, op: ops.MigrateOperation
+) -> str:
+ return "\n".join(render_op(autogen_context, op))
+
+
+@renderers.dispatch_for(ops.ModifyTableOps)
+def _render_modify_table(
+ autogen_context: AutogenContext, op: ModifyTableOps
+) -> List[str]:
+ opts = autogen_context.opts
+ render_as_batch = opts.get("render_as_batch", False)
+
+ if op.ops:
+ lines = []
+ if render_as_batch:
+ with autogen_context._within_batch():
+ lines.append(
+ "with op.batch_alter_table(%r, schema=%r) as batch_op:"
+ % (op.table_name, op.schema)
+ )
+ for t_op in op.ops:
+ t_lines = render_op(autogen_context, t_op)
+ lines.extend(t_lines)
+ lines.append("")
+ else:
+ for t_op in op.ops:
+ t_lines = render_op(autogen_context, t_op)
+ lines.extend(t_lines)
+
+ return lines
+ else:
+ return []
+
+
+@renderers.dispatch_for(ops.CreateTableCommentOp)
+def _render_create_table_comment(
+ autogen_context: AutogenContext, op: ops.CreateTableCommentOp
+) -> str:
+ if autogen_context._has_batch:
+ templ = (
+ "{prefix}create_table_comment(\n"
+ "{indent}{comment},\n"
+ "{indent}existing_comment={existing}\n"
+ ")"
+ )
+ else:
+ templ = (
+ "{prefix}create_table_comment(\n"
+ "{indent}'{tname}',\n"
+ "{indent}{comment},\n"
+ "{indent}existing_comment={existing},\n"
+ "{indent}schema={schema}\n"
+ ")"
+ )
+ return templ.format(
+ prefix=_alembic_autogenerate_prefix(autogen_context),
+ tname=op.table_name,
+ comment="%r" % op.comment if op.comment is not None else None,
+ existing="%r" % op.existing_comment
+ if op.existing_comment is not None
+ else None,
+ schema="'%s'" % op.schema if op.schema is not None else None,
+ indent=" ",
+ )
+
+
+@renderers.dispatch_for(ops.DropTableCommentOp)
+def _render_drop_table_comment(
+ autogen_context: AutogenContext, op: ops.DropTableCommentOp
+) -> str:
+ if autogen_context._has_batch:
+ templ = (
+ "{prefix}drop_table_comment(\n"
+ "{indent}existing_comment={existing}\n"
+ ")"
+ )
+ else:
+ templ = (
+ "{prefix}drop_table_comment(\n"
+ "{indent}'{tname}',\n"
+ "{indent}existing_comment={existing},\n"
+ "{indent}schema={schema}\n"
+ ")"
+ )
+ return templ.format(
+ prefix=_alembic_autogenerate_prefix(autogen_context),
+ tname=op.table_name,
+ existing="%r" % op.existing_comment
+ if op.existing_comment is not None
+ else None,
+ schema="'%s'" % op.schema if op.schema is not None else None,
+ indent=" ",
+ )
+
+
+@renderers.dispatch_for(ops.CreateTableOp)
+def _add_table(autogen_context: AutogenContext, op: ops.CreateTableOp) -> str:
+ table = op.to_table()
+
+ args = [
+ col
+ for col in [
+ _render_column(col, autogen_context) for col in table.columns
+ ]
+ if col
+ ] + sorted(
+ [
+ rcons
+ for rcons in [
+ _render_constraint(
+ cons, autogen_context, op._namespace_metadata
+ )
+ for cons in table.constraints
+ ]
+ if rcons is not None
+ ]
+ )
+
+ if len(args) > MAX_PYTHON_ARGS:
+ args_str = "*[" + ",\n".join(args) + "]"
+ else:
+ args_str = ",\n".join(args)
+
+ text = "%(prefix)screate_table(%(tablename)r,\n%(args)s" % {
+ "tablename": _ident(op.table_name),
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "args": args_str,
+ }
+ if op.schema:
+ text += ",\nschema=%r" % _ident(op.schema)
+
+ comment = table.comment
+ if comment:
+ text += ",\ncomment=%r" % _ident(comment)
+
+ info = table.info
+ if info:
+ text += f",\ninfo={info!r}"
+
+ for k in sorted(op.kw):
+ text += ",\n%s=%r" % (k.replace(" ", "_"), op.kw[k])
+
+ if table._prefixes:
+ prefixes = ", ".join("'%s'" % p for p in table._prefixes)
+ text += ",\nprefixes=[%s]" % prefixes
+
+ text += "\n)"
+ return text
+
+
+@renderers.dispatch_for(ops.DropTableOp)
+def _drop_table(autogen_context: AutogenContext, op: ops.DropTableOp) -> str:
+ text = "%(prefix)sdrop_table(%(tname)r" % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": _ident(op.table_name),
+ }
+ if op.schema:
+ text += ", schema=%r" % _ident(op.schema)
+ text += ")"
+ return text
+
+
+def _render_dialect_kwargs_items(
+ autogen_context: AutogenContext, item: DialectKWArgs
+) -> list[str]:
+ return [
+ f"{key}={_render_potential_expr(val, autogen_context)}"
+ for key, val in item.dialect_kwargs.items()
+ ]
+
+
+@renderers.dispatch_for(ops.CreateIndexOp)
+def _add_index(autogen_context: AutogenContext, op: ops.CreateIndexOp) -> str:
+ index = op.to_index()
+
+ has_batch = autogen_context._has_batch
+
+ if has_batch:
+ tmpl = (
+ "%(prefix)screate_index(%(name)r, [%(columns)s], "
+ "unique=%(unique)r%(kwargs)s)"
+ )
+ else:
+ tmpl = (
+ "%(prefix)screate_index(%(name)r, %(table)r, [%(columns)s], "
+ "unique=%(unique)r%(schema)s%(kwargs)s)"
+ )
+
+ assert index.table is not None
+
+ opts = _render_dialect_kwargs_items(autogen_context, index)
+ text = tmpl % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "name": _render_gen_name(autogen_context, index.name),
+ "table": _ident(index.table.name),
+ "columns": ", ".join(
+ _get_index_rendered_expressions(index, autogen_context)
+ ),
+ "unique": index.unique or False,
+ "schema": (", schema=%r" % _ident(index.table.schema))
+ if index.table.schema
+ else "",
+ "kwargs": ", " + ", ".join(opts) if opts else "",
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.DropIndexOp)
+def _drop_index(autogen_context: AutogenContext, op: ops.DropIndexOp) -> str:
+ index = op.to_index()
+
+ has_batch = autogen_context._has_batch
+
+ if has_batch:
+ tmpl = "%(prefix)sdrop_index(%(name)r%(kwargs)s)"
+ else:
+ tmpl = (
+ "%(prefix)sdrop_index(%(name)r, "
+ "table_name=%(table_name)r%(schema)s%(kwargs)s)"
+ )
+ opts = _render_dialect_kwargs_items(autogen_context, index)
+ text = tmpl % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "name": _render_gen_name(autogen_context, op.index_name),
+ "table_name": _ident(op.table_name),
+ "schema": ((", schema=%r" % _ident(op.schema)) if op.schema else ""),
+ "kwargs": ", " + ", ".join(opts) if opts else "",
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.CreateUniqueConstraintOp)
+def _add_unique_constraint(
+ autogen_context: AutogenContext, op: ops.CreateUniqueConstraintOp
+) -> List[str]:
+ return [_uq_constraint(op.to_constraint(), autogen_context, True)]
+
+
+@renderers.dispatch_for(ops.CreateForeignKeyOp)
+def _add_fk_constraint(
+ autogen_context: AutogenContext, op: ops.CreateForeignKeyOp
+) -> str:
+ args = [repr(_render_gen_name(autogen_context, op.constraint_name))]
+ if not autogen_context._has_batch:
+ args.append(repr(_ident(op.source_table)))
+
+ args.extend(
+ [
+ repr(_ident(op.referent_table)),
+ repr([_ident(col) for col in op.local_cols]),
+ repr([_ident(col) for col in op.remote_cols]),
+ ]
+ )
+ kwargs = [
+ "referent_schema",
+ "onupdate",
+ "ondelete",
+ "initially",
+ "deferrable",
+ "use_alter",
+ "match",
+ ]
+ if not autogen_context._has_batch:
+ kwargs.insert(0, "source_schema")
+
+ for k in kwargs:
+ if k in op.kw:
+ value = op.kw[k]
+ if value is not None:
+ args.append("%s=%r" % (k, value))
+
+ return "%(prefix)screate_foreign_key(%(args)s)" % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+
+
+@renderers.dispatch_for(ops.CreatePrimaryKeyOp)
+def _add_pk_constraint(constraint, autogen_context):
+ raise NotImplementedError()
+
+
+@renderers.dispatch_for(ops.CreateCheckConstraintOp)
+def _add_check_constraint(constraint, autogen_context):
+ raise NotImplementedError()
+
+
+@renderers.dispatch_for(ops.DropConstraintOp)
+def _drop_constraint(
+ autogen_context: AutogenContext, op: ops.DropConstraintOp
+) -> str:
+ prefix = _alembic_autogenerate_prefix(autogen_context)
+ name = _render_gen_name(autogen_context, op.constraint_name)
+ schema = _ident(op.schema) if op.schema else None
+ type_ = _ident(op.constraint_type) if op.constraint_type else None
+
+ params_strs = []
+ params_strs.append(repr(name))
+ if not autogen_context._has_batch:
+ params_strs.append(repr(_ident(op.table_name)))
+ if schema is not None:
+ params_strs.append(f"schema={schema!r}")
+ if type_ is not None:
+ params_strs.append(f"type_={type_!r}")
+
+ return f"{prefix}drop_constraint({', '.join(params_strs)})"
+
+
+@renderers.dispatch_for(ops.AddColumnOp)
+def _add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str:
+ schema, tname, column = op.schema, op.table_name, op.column
+ if autogen_context._has_batch:
+ template = "%(prefix)sadd_column(%(column)s)"
+ else:
+ template = "%(prefix)sadd_column(%(tname)r, %(column)s"
+ if schema:
+ template += ", schema=%(schema)r"
+ template += ")"
+ text = template % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": tname,
+ "column": _render_column(column, autogen_context),
+ "schema": schema,
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.DropColumnOp)
+def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
+ schema, tname, column_name = op.schema, op.table_name, op.column_name
+
+ if autogen_context._has_batch:
+ template = "%(prefix)sdrop_column(%(cname)r)"
+ else:
+ template = "%(prefix)sdrop_column(%(tname)r, %(cname)r"
+ if schema:
+ template += ", schema=%(schema)r"
+ template += ")"
+
+ text = template % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": _ident(tname),
+ "cname": _ident(column_name),
+ "schema": _ident(schema),
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.AlterColumnOp)
+def _alter_column(
+ autogen_context: AutogenContext, op: ops.AlterColumnOp
+) -> str:
+ tname = op.table_name
+ cname = op.column_name
+ server_default = op.modify_server_default
+ type_ = op.modify_type
+ nullable = op.modify_nullable
+ comment = op.modify_comment
+ autoincrement = op.kw.get("autoincrement", None)
+ existing_type = op.existing_type
+ existing_nullable = op.existing_nullable
+ existing_comment = op.existing_comment
+ existing_server_default = op.existing_server_default
+ schema = op.schema
+
+ indent = " " * 11
+
+ if autogen_context._has_batch:
+ template = "%(prefix)salter_column(%(cname)r"
+ else:
+ template = "%(prefix)salter_column(%(tname)r, %(cname)r"
+
+ text = template % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": tname,
+ "cname": cname,
+ }
+ if existing_type is not None:
+ text += ",\n%sexisting_type=%s" % (
+ indent,
+ _repr_type(existing_type, autogen_context),
+ )
+ if server_default is not False:
+ rendered = _render_server_default(server_default, autogen_context)
+ text += ",\n%sserver_default=%s" % (indent, rendered)
+
+ if type_ is not None:
+ text += ",\n%stype_=%s" % (indent, _repr_type(type_, autogen_context))
+ if nullable is not None:
+ text += ",\n%snullable=%r" % (indent, nullable)
+ if comment is not False:
+ text += ",\n%scomment=%r" % (indent, comment)
+ if existing_comment is not None:
+ text += ",\n%sexisting_comment=%r" % (indent, existing_comment)
+ if nullable is None and existing_nullable is not None:
+ text += ",\n%sexisting_nullable=%r" % (indent, existing_nullable)
+ if autoincrement is not None:
+ text += ",\n%sautoincrement=%r" % (indent, autoincrement)
+ if server_default is False and existing_server_default:
+ rendered = _render_server_default(
+ existing_server_default, autogen_context
+ )
+ text += ",\n%sexisting_server_default=%s" % (indent, rendered)
+ if schema and not autogen_context._has_batch:
+ text += ",\n%sschema=%r" % (indent, schema)
+ text += ")"
+ return text
+
+
+class _f_name:
+ def __init__(self, prefix: str, name: conv) -> None:
+ self.prefix = prefix
+ self.name = name
+
+ def __repr__(self) -> str:
+ return "%sf(%r)" % (self.prefix, _ident(self.name))
+
+
+def _ident(name: Optional[Union[quoted_name, str]]) -> Optional[str]:
+ """produce a __repr__() object for a string identifier that may
+ use quoted_name() in SQLAlchemy 0.9 and greater.
+
+ The issue worked around here is that quoted_name() doesn't have
+ very good repr() behavior by itself when unicode is involved.
+
+ """
+ if name is None:
+ return name
+ elif isinstance(name, quoted_name):
+ return str(name)
+ elif isinstance(name, str):
+ return name
+
+
+def _render_potential_expr(
+ value: Any,
+ autogen_context: AutogenContext,
+ *,
+ wrap_in_text: bool = True,
+ is_server_default: bool = False,
+ is_index: bool = False,
+) -> str:
+ if isinstance(value, sql.ClauseElement):
+ if wrap_in_text:
+ template = "%(prefix)stext(%(sql)r)"
+ else:
+ template = "%(sql)r"
+
+ return template % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "sql": autogen_context.migration_context.impl.render_ddl_sql_expr(
+ value, is_server_default=is_server_default, is_index=is_index
+ ),
+ }
+
+ else:
+ return repr(value)
+
+
+def _get_index_rendered_expressions(
+ idx: Index, autogen_context: AutogenContext
+) -> List[str]:
+ return [
+ repr(_ident(getattr(exp, "name", None)))
+ if isinstance(exp, sa_schema.Column)
+ else _render_potential_expr(exp, autogen_context, is_index=True)
+ for exp in idx.expressions
+ ]
+
+
+def _uq_constraint(
+ constraint: UniqueConstraint,
+ autogen_context: AutogenContext,
+ alter: bool,
+) -> str:
+ opts: List[Tuple[str, Any]] = []
+
+ has_batch = autogen_context._has_batch
+
+ if constraint.deferrable:
+ opts.append(("deferrable", str(constraint.deferrable)))
+ if constraint.initially:
+ opts.append(("initially", str(constraint.initially)))
+ if not has_batch and alter and constraint.table.schema:
+ opts.append(("schema", _ident(constraint.table.schema)))
+ if not alter and constraint.name:
+ opts.append(
+ ("name", _render_gen_name(autogen_context, constraint.name))
+ )
+ dialect_options = _render_dialect_kwargs_items(autogen_context, constraint)
+
+ if alter:
+ args = [repr(_render_gen_name(autogen_context, constraint.name))]
+ if not has_batch:
+ args += [repr(_ident(constraint.table.name))]
+ args.append(repr([_ident(col.name) for col in constraint.columns]))
+ args.extend(["%s=%r" % (k, v) for k, v in opts])
+ args.extend(dialect_options)
+ return "%(prefix)screate_unique_constraint(%(args)s)" % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+ else:
+ args = [repr(_ident(col.name)) for col in constraint.columns]
+ args.extend(["%s=%r" % (k, v) for k, v in opts])
+ args.extend(dialect_options)
+ return "%(prefix)sUniqueConstraint(%(args)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+
+
+def _user_autogenerate_prefix(autogen_context, target):
+ prefix = autogen_context.opts["user_module_prefix"]
+ if prefix is None:
+ return "%s." % target.__module__
+ else:
+ return prefix
+
+
+def _sqlalchemy_autogenerate_prefix(autogen_context: AutogenContext) -> str:
+ return autogen_context.opts["sqlalchemy_module_prefix"] or ""
+
+
+def _alembic_autogenerate_prefix(autogen_context: AutogenContext) -> str:
+ if autogen_context._has_batch:
+ return "batch_op."
+ else:
+ return autogen_context.opts["alembic_module_prefix"] or ""
+
+
+def _user_defined_render(
+ type_: str, object_: Any, autogen_context: AutogenContext
+) -> Union[str, Literal[False]]:
+ if "render_item" in autogen_context.opts:
+ render = autogen_context.opts["render_item"]
+ if render:
+ rendered = render(type_, object_, autogen_context)
+ if rendered is not False:
+ return rendered
+ return False
+
+
+def _render_column(
+ column: Column[Any], autogen_context: AutogenContext
+) -> str:
+ rendered = _user_defined_render("column", column, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ args: List[str] = []
+ opts: List[Tuple[str, Any]] = []
+
+ if column.server_default:
+ rendered = _render_server_default( # type:ignore[assignment]
+ column.server_default, autogen_context
+ )
+ if rendered:
+ if _should_render_server_default_positionally(
+ column.server_default
+ ):
+ args.append(rendered)
+ else:
+ opts.append(("server_default", rendered))
+
+ if (
+ column.autoincrement is not None
+ and column.autoincrement != sqla_compat.AUTOINCREMENT_DEFAULT
+ ):
+ opts.append(("autoincrement", column.autoincrement))
+
+ if column.nullable is not None:
+ opts.append(("nullable", column.nullable))
+
+ if column.system:
+ opts.append(("system", column.system))
+
+ comment = column.comment
+ if comment:
+ opts.append(("comment", "%r" % comment))
+
+ # TODO: for non-ascii colname, assign a "key"
+ return "%(prefix)sColumn(%(name)r, %(type)s, %(args)s%(kwargs)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "name": _ident(column.name),
+ "type": _repr_type(column.type, autogen_context),
+ "args": ", ".join([str(arg) for arg in args]) + ", " if args else "",
+ "kwargs": (
+ ", ".join(
+ ["%s=%s" % (kwname, val) for kwname, val in opts]
+ + [
+ "%s=%s"
+ % (key, _render_potential_expr(val, autogen_context))
+ for key, val in sqla_compat._column_kwargs(column).items()
+ ]
+ )
+ ),
+ }
+
+
+def _should_render_server_default_positionally(server_default: Any) -> bool:
+ return sqla_compat._server_default_is_computed(
+ server_default
+ ) or sqla_compat._server_default_is_identity(server_default)
+
+
+def _render_server_default(
+ default: Optional[
+ Union[FetchedValue, str, TextClause, ColumnElement[Any]]
+ ],
+ autogen_context: AutogenContext,
+ repr_: bool = True,
+) -> Optional[str]:
+ rendered = _user_defined_render("server_default", default, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ if sqla_compat._server_default_is_computed(default):
+ return _render_computed(cast("Computed", default), autogen_context)
+ elif sqla_compat._server_default_is_identity(default):
+ return _render_identity(cast("Identity", default), autogen_context)
+ elif isinstance(default, sa_schema.DefaultClause):
+ if isinstance(default.arg, str):
+ default = default.arg
+ else:
+ return _render_potential_expr(
+ default.arg, autogen_context, is_server_default=True
+ )
+
+ if isinstance(default, str) and repr_:
+ default = repr(re.sub(r"^'|'$", "", default))
+
+ return cast(str, default)
+
+
+def _render_computed(
+ computed: Computed, autogen_context: AutogenContext
+) -> str:
+ text = _render_potential_expr(
+ computed.sqltext, autogen_context, wrap_in_text=False
+ )
+
+ kwargs = {}
+ if computed.persisted is not None:
+ kwargs["persisted"] = computed.persisted
+ return "%(prefix)sComputed(%(text)s, %(kwargs)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "text": text,
+ "kwargs": (", ".join("%s=%s" % pair for pair in kwargs.items())),
+ }
+
+
+def _render_identity(
+ identity: Identity, autogen_context: AutogenContext
+) -> str:
+ kwargs = sqla_compat._get_identity_options_dict(
+ identity, dialect_kwargs=True
+ )
+
+ return "%(prefix)sIdentity(%(kwargs)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "kwargs": (", ".join("%s=%s" % pair for pair in kwargs.items())),
+ }
+
+
+def _repr_type(
+ type_: TypeEngine,
+ autogen_context: AutogenContext,
+ _skip_variants: bool = False,
+) -> str:
+ rendered = _user_defined_render("type", type_, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ if hasattr(autogen_context.migration_context, "impl"):
+ impl_rt = autogen_context.migration_context.impl.render_type(
+ type_, autogen_context
+ )
+ else:
+ impl_rt = None
+
+ mod = type(type_).__module__
+ imports = autogen_context.imports
+ if mod.startswith("sqlalchemy.dialects"):
+ match = re.match(r"sqlalchemy\.dialects\.(\w+)", mod)
+ assert match is not None
+ dname = match.group(1)
+ if imports is not None:
+ imports.add("from sqlalchemy.dialects import %s" % dname)
+ if impl_rt:
+ return impl_rt
+ else:
+ return "%s.%r" % (dname, type_)
+ elif impl_rt:
+ return impl_rt
+ elif not _skip_variants and sqla_compat._type_has_variants(type_):
+ return _render_Variant_type(type_, autogen_context)
+ elif mod.startswith("sqlalchemy."):
+ if "_render_%s_type" % type_.__visit_name__ in globals():
+ fn = globals()["_render_%s_type" % type_.__visit_name__]
+ return fn(type_, autogen_context)
+ else:
+ prefix = _sqlalchemy_autogenerate_prefix(autogen_context)
+ return "%s%r" % (prefix, type_)
+ else:
+ prefix = _user_autogenerate_prefix(autogen_context, type_)
+ return "%s%r" % (prefix, type_)
+
+
+def _render_ARRAY_type(type_: ARRAY, autogen_context: AutogenContext) -> str:
+ return cast(
+ str,
+ _render_type_w_subtype(
+ type_, autogen_context, "item_type", r"(.+?\()"
+ ),
+ )
+
+
+def _render_Variant_type(
+ type_: TypeEngine, autogen_context: AutogenContext
+) -> str:
+ base_type, variant_mapping = sqla_compat._get_variant_mapping(type_)
+ base = _repr_type(base_type, autogen_context, _skip_variants=True)
+ assert base is not None and base is not False # type: ignore[comparison-overlap] # noqa:E501
+ for dialect in sorted(variant_mapping):
+ typ = variant_mapping[dialect]
+ base += ".with_variant(%s, %r)" % (
+ _repr_type(typ, autogen_context, _skip_variants=True),
+ dialect,
+ )
+ return base
+
+
+def _render_type_w_subtype(
+ type_: TypeEngine,
+ autogen_context: AutogenContext,
+ attrname: str,
+ regexp: str,
+ prefix: Optional[str] = None,
+) -> Union[Optional[str], Literal[False]]:
+ outer_repr = repr(type_)
+ inner_type = getattr(type_, attrname, None)
+ if inner_type is None:
+ return False
+
+ inner_repr = repr(inner_type)
+
+ inner_repr = re.sub(r"([\(\)])", r"\\\1", inner_repr)
+ sub_type = _repr_type(getattr(type_, attrname), autogen_context)
+ outer_type = re.sub(regexp + inner_repr, r"\1%s" % sub_type, outer_repr)
+
+ if prefix:
+ return "%s%s" % (prefix, outer_type)
+
+ mod = type(type_).__module__
+ if mod.startswith("sqlalchemy.dialects"):
+ match = re.match(r"sqlalchemy\.dialects\.(\w+)", mod)
+ assert match is not None
+ dname = match.group(1)
+ return "%s.%s" % (dname, outer_type)
+ elif mod.startswith("sqlalchemy"):
+ prefix = _sqlalchemy_autogenerate_prefix(autogen_context)
+ return "%s%s" % (prefix, outer_type)
+ else:
+ return None
+
+
+_constraint_renderers = util.Dispatcher()
+
+
+def _render_constraint(
+ constraint: Constraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> Optional[str]:
+ try:
+ renderer = _constraint_renderers.dispatch(constraint)
+ except ValueError:
+ util.warn("No renderer is established for object %r" % constraint)
+ return "[Unknown Python object %r]" % constraint
+ else:
+ return renderer(constraint, autogen_context, namespace_metadata)
+
+
+@_constraint_renderers.dispatch_for(sa_schema.PrimaryKeyConstraint)
+def _render_primary_key(
+ constraint: PrimaryKeyConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> Optional[str]:
+ rendered = _user_defined_render("primary_key", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ if not constraint.columns:
+ return None
+
+ opts = []
+ if constraint.name:
+ opts.append(
+ ("name", repr(_render_gen_name(autogen_context, constraint.name)))
+ )
+ return "%(prefix)sPrimaryKeyConstraint(%(args)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "args": ", ".join(
+ [repr(c.name) for c in constraint.columns]
+ + ["%s=%s" % (kwname, val) for kwname, val in opts]
+ ),
+ }
+
+
+def _fk_colspec(
+ fk: ForeignKey,
+ metadata_schema: Optional[str],
+ namespace_metadata: MetaData,
+) -> str:
+ """Implement a 'safe' version of ForeignKey._get_colspec() that
+ won't fail if the remote table can't be resolved.
+
+ """
+ colspec = fk._get_colspec()
+ tokens = colspec.split(".")
+ tname, colname = tokens[-2:]
+
+ if metadata_schema is not None and len(tokens) == 2:
+ table_fullname = "%s.%s" % (metadata_schema, tname)
+ else:
+ table_fullname = ".".join(tokens[0:-1])
+
+ if (
+ not fk.link_to_name
+ and fk.parent is not None
+ and fk.parent.table is not None
+ ):
+ # try to resolve the remote table in order to adjust for column.key.
+ # the FK constraint needs to be rendered in terms of the column
+ # name.
+
+ if table_fullname in namespace_metadata.tables:
+ col = namespace_metadata.tables[table_fullname].c.get(colname)
+ if col is not None:
+ colname = _ident(col.name) # type: ignore[assignment]
+
+ colspec = "%s.%s" % (table_fullname, colname)
+
+ return colspec
+
+
+def _populate_render_fk_opts(
+ constraint: ForeignKeyConstraint, opts: List[Tuple[str, str]]
+) -> None:
+ if constraint.onupdate:
+ opts.append(("onupdate", repr(constraint.onupdate)))
+ if constraint.ondelete:
+ opts.append(("ondelete", repr(constraint.ondelete)))
+ if constraint.initially:
+ opts.append(("initially", repr(constraint.initially)))
+ if constraint.deferrable:
+ opts.append(("deferrable", repr(constraint.deferrable)))
+ if constraint.use_alter:
+ opts.append(("use_alter", repr(constraint.use_alter)))
+ if constraint.match:
+ opts.append(("match", repr(constraint.match)))
+
+
+@_constraint_renderers.dispatch_for(sa_schema.ForeignKeyConstraint)
+def _render_foreign_key(
+ constraint: ForeignKeyConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: MetaData,
+) -> Optional[str]:
+ rendered = _user_defined_render("foreign_key", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ opts = []
+ if constraint.name:
+ opts.append(
+ ("name", repr(_render_gen_name(autogen_context, constraint.name)))
+ )
+
+ _populate_render_fk_opts(constraint, opts)
+
+ apply_metadata_schema = namespace_metadata.schema
+ return (
+ "%(prefix)sForeignKeyConstraint([%(cols)s], "
+ "[%(refcols)s], %(args)s)"
+ % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "cols": ", ".join(
+ repr(_ident(f.parent.name)) for f in constraint.elements
+ ),
+ "refcols": ", ".join(
+ repr(_fk_colspec(f, apply_metadata_schema, namespace_metadata))
+ for f in constraint.elements
+ ),
+ "args": ", ".join(
+ ["%s=%s" % (kwname, val) for kwname, val in opts]
+ ),
+ }
+ )
+
+
+@_constraint_renderers.dispatch_for(sa_schema.UniqueConstraint)
+def _render_unique_constraint(
+ constraint: UniqueConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> str:
+ rendered = _user_defined_render("unique", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ return _uq_constraint(constraint, autogen_context, False)
+
+
+@_constraint_renderers.dispatch_for(sa_schema.CheckConstraint)
+def _render_check_constraint(
+ constraint: CheckConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> Optional[str]:
+ rendered = _user_defined_render("check", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ # detect the constraint being part of
+ # a parent type which is probably in the Table already.
+ # ideally SQLAlchemy would give us more of a first class
+ # way to detect this.
+ if (
+ constraint._create_rule
+ and hasattr(constraint._create_rule, "target")
+ and isinstance(
+ constraint._create_rule.target,
+ sqltypes.TypeEngine,
+ )
+ ):
+ return None
+ opts = []
+ if constraint.name:
+ opts.append(
+ ("name", repr(_render_gen_name(autogen_context, constraint.name)))
+ )
+ return "%(prefix)sCheckConstraint(%(sqltext)s%(opts)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "opts": ", " + (", ".join("%s=%s" % (k, v) for k, v in opts))
+ if opts
+ else "",
+ "sqltext": _render_potential_expr(
+ constraint.sqltext, autogen_context, wrap_in_text=False
+ ),
+ }
+
+
+@renderers.dispatch_for(ops.ExecuteSQLOp)
+def _execute_sql(autogen_context: AutogenContext, op: ops.ExecuteSQLOp) -> str:
+ if not isinstance(op.sqltext, str):
+ raise NotImplementedError(
+ "Autogenerate rendering of SQL Expression language constructs "
+ "not supported here; please use a plain SQL string"
+ )
+ return "op.execute(%r)" % op.sqltext
+
+
+renderers = default_renderers.branch()
diff --git a/venv/lib/python3.12/site-packages/alembic/autogenerate/rewriter.py b/venv/lib/python3.12/site-packages/alembic/autogenerate/rewriter.py
new file mode 100644
index 0000000..8994dcf
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/autogenerate/rewriter.py
@@ -0,0 +1,240 @@
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Iterator
+from typing import List
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from .. import util
+from ..operations import ops
+
+if TYPE_CHECKING:
+ from ..operations.ops import AddColumnOp
+ from ..operations.ops import AlterColumnOp
+ from ..operations.ops import CreateTableOp
+ from ..operations.ops import DowngradeOps
+ from ..operations.ops import MigrateOperation
+ from ..operations.ops import MigrationScript
+ from ..operations.ops import ModifyTableOps
+ from ..operations.ops import OpContainer
+ from ..operations.ops import UpgradeOps
+ from ..runtime.migration import MigrationContext
+ from ..script.revision import _GetRevArg
+
+ProcessRevisionDirectiveFn = Callable[
+ ["MigrationContext", "_GetRevArg", List["MigrationScript"]], None
+]
+
+
+class Rewriter:
+ """A helper object that allows easy 'rewriting' of ops streams.
+
+ The :class:`.Rewriter` object is intended to be passed along
+ to the
+ :paramref:`.EnvironmentContext.configure.process_revision_directives`
+ parameter in an ``env.py`` script. Once constructed, any number
+ of "rewrites" functions can be associated with it, which will be given
+ the opportunity to modify the structure without having to have explicit
+ knowledge of the overall structure.
+
+ The function is passed the :class:`.MigrationContext` object and
+ ``revision`` tuple that are passed to the :paramref:`.Environment
+ Context.configure.process_revision_directives` function normally,
+ and the third argument is an individual directive of the type
+ noted in the decorator. The function has the choice of returning
+ a single op directive, which normally can be the directive that
+ was actually passed, or a new directive to replace it, or a list
+ of zero or more directives to replace it.
+
+ .. seealso::
+
+ :ref:`autogen_rewriter` - usage example
+
+ """
+
+ _traverse = util.Dispatcher()
+
+ _chained: Tuple[Union[ProcessRevisionDirectiveFn, Rewriter], ...] = ()
+
+ def __init__(self) -> None:
+ self.dispatch = util.Dispatcher()
+
+ def chain(
+ self,
+ other: Union[
+ ProcessRevisionDirectiveFn,
+ Rewriter,
+ ],
+ ) -> Rewriter:
+ """Produce a "chain" of this :class:`.Rewriter` to another.
+
+ This allows two or more rewriters to operate serially on a stream,
+ e.g.::
+
+ writer1 = autogenerate.Rewriter()
+ writer2 = autogenerate.Rewriter()
+
+
+ @writer1.rewrites(ops.AddColumnOp)
+ def add_column_nullable(context, revision, op):
+ op.column.nullable = True
+ return op
+
+
+ @writer2.rewrites(ops.AddColumnOp)
+ def add_column_idx(context, revision, op):
+ idx_op = ops.CreateIndexOp(
+ "ixc", op.table_name, [op.column.name]
+ )
+ return [op, idx_op]
+
+ writer = writer1.chain(writer2)
+
+ :param other: a :class:`.Rewriter` instance
+ :return: a new :class:`.Rewriter` that will run the operations
+ of this writer, then the "other" writer, in succession.
+
+ """
+ wr = self.__class__.__new__(self.__class__)
+ wr.__dict__.update(self.__dict__)
+ wr._chained += (other,)
+ return wr
+
+ def rewrites(
+ self,
+ operator: Union[
+ Type[AddColumnOp],
+ Type[MigrateOperation],
+ Type[AlterColumnOp],
+ Type[CreateTableOp],
+ Type[ModifyTableOps],
+ ],
+ ) -> Callable[..., Any]:
+ """Register a function as rewriter for a given type.
+
+ The function should receive three arguments, which are
+ the :class:`.MigrationContext`, a ``revision`` tuple, and
+ an op directive of the type indicated. E.g.::
+
+ @writer1.rewrites(ops.AddColumnOp)
+ def add_column_nullable(context, revision, op):
+ op.column.nullable = True
+ return op
+
+ """
+ return self.dispatch.dispatch_for(operator)
+
+ def _rewrite(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrateOperation,
+ ) -> Iterator[MigrateOperation]:
+ try:
+ _rewriter = self.dispatch.dispatch(directive)
+ except ValueError:
+ _rewriter = None
+ yield directive
+ else:
+ if self in directive._mutations:
+ yield directive
+ else:
+ for r_directive in util.to_list(
+ _rewriter(context, revision, directive), []
+ ):
+ r_directive._mutations = r_directive._mutations.union(
+ [self]
+ )
+ yield r_directive
+
+ def __call__(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directives: List[MigrationScript],
+ ) -> None:
+ self.process_revision_directives(context, revision, directives)
+ for process_revision_directives in self._chained:
+ process_revision_directives(context, revision, directives)
+
+ @_traverse.dispatch_for(ops.MigrationScript)
+ def _traverse_script(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrationScript,
+ ) -> None:
+ upgrade_ops_list: List[UpgradeOps] = []
+ for upgrade_ops in directive.upgrade_ops_list:
+ ret = self._traverse_for(context, revision, upgrade_ops)
+ if len(ret) != 1:
+ raise ValueError(
+ "Can only return single object for UpgradeOps traverse"
+ )
+ upgrade_ops_list.append(ret[0])
+
+ directive.upgrade_ops = upgrade_ops_list # type: ignore
+
+ downgrade_ops_list: List[DowngradeOps] = []
+ for downgrade_ops in directive.downgrade_ops_list:
+ ret = self._traverse_for(context, revision, downgrade_ops)
+ if len(ret) != 1:
+ raise ValueError(
+ "Can only return single object for DowngradeOps traverse"
+ )
+ downgrade_ops_list.append(ret[0])
+ directive.downgrade_ops = downgrade_ops_list # type: ignore
+
+ @_traverse.dispatch_for(ops.OpContainer)
+ def _traverse_op_container(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: OpContainer,
+ ) -> None:
+ self._traverse_list(context, revision, directive.ops)
+
+ @_traverse.dispatch_for(ops.MigrateOperation)
+ def _traverse_any_directive(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrateOperation,
+ ) -> None:
+ pass
+
+ def _traverse_for(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrateOperation,
+ ) -> Any:
+ directives = list(self._rewrite(context, revision, directive))
+ for directive in directives:
+ traverser = self._traverse.dispatch(directive)
+ traverser(self, context, revision, directive)
+ return directives
+
+ def _traverse_list(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directives: Any,
+ ) -> None:
+ dest = []
+ for directive in directives:
+ dest.extend(self._traverse_for(context, revision, directive))
+
+ directives[:] = dest
+
+ def process_revision_directives(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directives: List[MigrationScript],
+ ) -> None:
+ self._traverse_list(context, revision, directives)
diff --git a/venv/lib/python3.12/site-packages/alembic/command.py b/venv/lib/python3.12/site-packages/alembic/command.py
new file mode 100644
index 0000000..37aa6e6
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/command.py
@@ -0,0 +1,749 @@
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+from __future__ import annotations
+
+import os
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from . import autogenerate as autogen
+from . import util
+from .runtime.environment import EnvironmentContext
+from .script import ScriptDirectory
+
+if TYPE_CHECKING:
+ from alembic.config import Config
+ from alembic.script.base import Script
+ from alembic.script.revision import _RevIdType
+ from .runtime.environment import ProcessRevisionDirectiveFn
+
+
+def list_templates(config: Config) -> None:
+ """List available templates.
+
+ :param config: a :class:`.Config` object.
+
+ """
+
+ config.print_stdout("Available templates:\n")
+ for tempname in os.listdir(config.get_template_directory()):
+ with open(
+ os.path.join(config.get_template_directory(), tempname, "README")
+ ) as readme:
+ synopsis = next(readme).rstrip()
+ config.print_stdout("%s - %s", tempname, synopsis)
+
+ config.print_stdout("\nTemplates are used via the 'init' command, e.g.:")
+ config.print_stdout("\n alembic init --template generic ./scripts")
+
+
+def init(
+ config: Config,
+ directory: str,
+ template: str = "generic",
+ package: bool = False,
+) -> None:
+ """Initialize a new scripts directory.
+
+ :param config: a :class:`.Config` object.
+
+ :param directory: string path of the target directory
+
+ :param template: string name of the migration environment template to
+ use.
+
+ :param package: when True, write ``__init__.py`` files into the
+ environment location as well as the versions/ location.
+
+ """
+
+ if os.access(directory, os.F_OK) and os.listdir(directory):
+ raise util.CommandError(
+ "Directory %s already exists and is not empty" % directory
+ )
+
+ template_dir = os.path.join(config.get_template_directory(), template)
+ if not os.access(template_dir, os.F_OK):
+ raise util.CommandError("No such template %r" % template)
+
+ if not os.access(directory, os.F_OK):
+ with util.status(
+ f"Creating directory {os.path.abspath(directory)!r}",
+ **config.messaging_opts,
+ ):
+ os.makedirs(directory)
+
+ versions = os.path.join(directory, "versions")
+ with util.status(
+ f"Creating directory {os.path.abspath(versions)!r}",
+ **config.messaging_opts,
+ ):
+ os.makedirs(versions)
+
+ script = ScriptDirectory(directory)
+
+ config_file: str | None = None
+ for file_ in os.listdir(template_dir):
+ file_path = os.path.join(template_dir, file_)
+ if file_ == "alembic.ini.mako":
+ assert config.config_file_name is not None
+ config_file = os.path.abspath(config.config_file_name)
+ if os.access(config_file, os.F_OK):
+ util.msg(
+ f"File {config_file!r} already exists, skipping",
+ **config.messaging_opts,
+ )
+ else:
+ script._generate_template(
+ file_path, config_file, script_location=directory
+ )
+ elif os.path.isfile(file_path):
+ output_file = os.path.join(directory, file_)
+ script._copy_file(file_path, output_file)
+
+ if package:
+ for path in [
+ os.path.join(os.path.abspath(directory), "__init__.py"),
+ os.path.join(os.path.abspath(versions), "__init__.py"),
+ ]:
+ with util.status(f"Adding {path!r}", **config.messaging_opts):
+ with open(path, "w"):
+ pass
+
+ assert config_file is not None
+ util.msg(
+ "Please edit configuration/connection/logging "
+ f"settings in {config_file!r} before proceeding.",
+ **config.messaging_opts,
+ )
+
+
+def revision(
+ config: Config,
+ message: Optional[str] = None,
+ autogenerate: bool = False,
+ sql: bool = False,
+ head: str = "head",
+ splice: bool = False,
+ branch_label: Optional[_RevIdType] = None,
+ version_path: Optional[str] = None,
+ rev_id: Optional[str] = None,
+ depends_on: Optional[str] = None,
+ process_revision_directives: Optional[ProcessRevisionDirectiveFn] = None,
+) -> Union[Optional[Script], List[Optional[Script]]]:
+ """Create a new revision file.
+
+ :param config: a :class:`.Config` object.
+
+ :param message: string message to apply to the revision; this is the
+ ``-m`` option to ``alembic revision``.
+
+ :param autogenerate: whether or not to autogenerate the script from
+ the database; this is the ``--autogenerate`` option to
+ ``alembic revision``.
+
+ :param sql: whether to dump the script out as a SQL string; when specified,
+ the script is dumped to stdout. This is the ``--sql`` option to
+ ``alembic revision``.
+
+ :param head: head revision to build the new revision upon as a parent;
+ this is the ``--head`` option to ``alembic revision``.
+
+ :param splice: whether or not the new revision should be made into a
+ new head of its own; is required when the given ``head`` is not itself
+ a head. This is the ``--splice`` option to ``alembic revision``.
+
+ :param branch_label: string label to apply to the branch; this is the
+ ``--branch-label`` option to ``alembic revision``.
+
+ :param version_path: string symbol identifying a specific version path
+ from the configuration; this is the ``--version-path`` option to
+ ``alembic revision``.
+
+ :param rev_id: optional revision identifier to use instead of having
+ one generated; this is the ``--rev-id`` option to ``alembic revision``.
+
+ :param depends_on: optional list of "depends on" identifiers; this is the
+ ``--depends-on`` option to ``alembic revision``.
+
+ :param process_revision_directives: this is a callable that takes the
+ same form as the callable described at
+ :paramref:`.EnvironmentContext.configure.process_revision_directives`;
+ will be applied to the structure generated by the revision process
+ where it can be altered programmatically. Note that unlike all
+ the other parameters, this option is only available via programmatic
+ use of :func:`.command.revision`
+
+ """
+
+ script_directory = ScriptDirectory.from_config(config)
+
+ command_args = dict(
+ message=message,
+ autogenerate=autogenerate,
+ sql=sql,
+ head=head,
+ splice=splice,
+ branch_label=branch_label,
+ version_path=version_path,
+ rev_id=rev_id,
+ depends_on=depends_on,
+ )
+ revision_context = autogen.RevisionContext(
+ config,
+ script_directory,
+ command_args,
+ process_revision_directives=process_revision_directives,
+ )
+
+ environment = util.asbool(config.get_main_option("revision_environment"))
+
+ if autogenerate:
+ environment = True
+
+ if sql:
+ raise util.CommandError(
+ "Using --sql with --autogenerate does not make any sense"
+ )
+
+ def retrieve_migrations(rev, context):
+ revision_context.run_autogenerate(rev, context)
+ return []
+
+ elif environment:
+
+ def retrieve_migrations(rev, context):
+ revision_context.run_no_autogenerate(rev, context)
+ return []
+
+ elif sql:
+ raise util.CommandError(
+ "Using --sql with the revision command when "
+ "revision_environment is not configured does not make any sense"
+ )
+
+ if environment:
+ with EnvironmentContext(
+ config,
+ script_directory,
+ fn=retrieve_migrations,
+ as_sql=sql,
+ template_args=revision_context.template_args,
+ revision_context=revision_context,
+ ):
+ script_directory.run_env()
+
+ # the revision_context now has MigrationScript structure(s) present.
+ # these could theoretically be further processed / rewritten *here*,
+ # in addition to the hooks present within each run_migrations() call,
+ # or at the end of env.py run_migrations_online().
+
+ scripts = [script for script in revision_context.generate_scripts()]
+ if len(scripts) == 1:
+ return scripts[0]
+ else:
+ return scripts
+
+
+def check(config: "Config") -> None:
+ """Check if revision command with autogenerate has pending upgrade ops.
+
+ :param config: a :class:`.Config` object.
+
+ .. versionadded:: 1.9.0
+
+ """
+
+ script_directory = ScriptDirectory.from_config(config)
+
+ command_args = dict(
+ message=None,
+ autogenerate=True,
+ sql=False,
+ head="head",
+ splice=False,
+ branch_label=None,
+ version_path=None,
+ rev_id=None,
+ depends_on=None,
+ )
+ revision_context = autogen.RevisionContext(
+ config,
+ script_directory,
+ command_args,
+ )
+
+ def retrieve_migrations(rev, context):
+ revision_context.run_autogenerate(rev, context)
+ return []
+
+ with EnvironmentContext(
+ config,
+ script_directory,
+ fn=retrieve_migrations,
+ as_sql=False,
+ template_args=revision_context.template_args,
+ revision_context=revision_context,
+ ):
+ script_directory.run_env()
+
+ # the revision_context now has MigrationScript structure(s) present.
+
+ migration_script = revision_context.generated_revisions[-1]
+ diffs = []
+ for upgrade_ops in migration_script.upgrade_ops_list:
+ diffs.extend(upgrade_ops.as_diffs())
+
+ if diffs:
+ raise util.AutogenerateDiffsDetected(
+ f"New upgrade operations detected: {diffs}"
+ )
+ else:
+ config.print_stdout("No new upgrade operations detected.")
+
+
+def merge(
+ config: Config,
+ revisions: _RevIdType,
+ message: Optional[str] = None,
+ branch_label: Optional[_RevIdType] = None,
+ rev_id: Optional[str] = None,
+) -> Optional[Script]:
+ """Merge two revisions together. Creates a new migration file.
+
+ :param config: a :class:`.Config` instance
+
+ :param message: string message to apply to the revision
+
+ :param branch_label: string label name to apply to the new revision
+
+ :param rev_id: hardcoded revision identifier instead of generating a new
+ one.
+
+ .. seealso::
+
+ :ref:`branches`
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+ template_args = {
+ "config": config # Let templates use config for
+ # e.g. multiple databases
+ }
+
+ environment = util.asbool(config.get_main_option("revision_environment"))
+
+ if environment:
+
+ def nothing(rev, context):
+ return []
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=nothing,
+ as_sql=False,
+ template_args=template_args,
+ ):
+ script.run_env()
+
+ return script.generate_revision(
+ rev_id or util.rev_id(),
+ message,
+ refresh=True,
+ head=revisions,
+ branch_labels=branch_label,
+ **template_args, # type:ignore[arg-type]
+ )
+
+
+def upgrade(
+ config: Config,
+ revision: str,
+ sql: bool = False,
+ tag: Optional[str] = None,
+) -> None:
+ """Upgrade to a later version.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: string revision target or range for --sql mode
+
+ :param sql: if True, use ``--sql`` mode
+
+ :param tag: an arbitrary "tag" that can be intercepted by custom
+ ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
+ method.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ starting_rev = None
+ if ":" in revision:
+ if not sql:
+ raise util.CommandError("Range revision not allowed")
+ starting_rev, revision = revision.split(":", 2)
+
+ def upgrade(rev, context):
+ return script._upgrade_revs(revision, rev)
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=upgrade,
+ as_sql=sql,
+ starting_rev=starting_rev,
+ destination_rev=revision,
+ tag=tag,
+ ):
+ script.run_env()
+
+
+def downgrade(
+ config: Config,
+ revision: str,
+ sql: bool = False,
+ tag: Optional[str] = None,
+) -> None:
+ """Revert to a previous version.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: string revision target or range for --sql mode
+
+ :param sql: if True, use ``--sql`` mode
+
+ :param tag: an arbitrary "tag" that can be intercepted by custom
+ ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
+ method.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+ starting_rev = None
+ if ":" in revision:
+ if not sql:
+ raise util.CommandError("Range revision not allowed")
+ starting_rev, revision = revision.split(":", 2)
+ elif sql:
+ raise util.CommandError(
+ "downgrade with --sql requires :"
+ )
+
+ def downgrade(rev, context):
+ return script._downgrade_revs(revision, rev)
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=downgrade,
+ as_sql=sql,
+ starting_rev=starting_rev,
+ destination_rev=revision,
+ tag=tag,
+ ):
+ script.run_env()
+
+
+def show(config, rev):
+ """Show the revision(s) denoted by the given symbol.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: string revision target
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ if rev == "current":
+
+ def show_current(rev, context):
+ for sc in script.get_revisions(rev):
+ config.print_stdout(sc.log_entry)
+ return []
+
+ with EnvironmentContext(config, script, fn=show_current):
+ script.run_env()
+ else:
+ for sc in script.get_revisions(rev):
+ config.print_stdout(sc.log_entry)
+
+
+def history(
+ config: Config,
+ rev_range: Optional[str] = None,
+ verbose: bool = False,
+ indicate_current: bool = False,
+) -> None:
+ """List changeset scripts in chronological order.
+
+ :param config: a :class:`.Config` instance.
+
+ :param rev_range: string revision range
+
+ :param verbose: output in verbose mode.
+
+ :param indicate_current: indicate current revision.
+
+ """
+ base: Optional[str]
+ head: Optional[str]
+ script = ScriptDirectory.from_config(config)
+ if rev_range is not None:
+ if ":" not in rev_range:
+ raise util.CommandError(
+ "History range requires [start]:[end], " "[start]:, or :[end]"
+ )
+ base, head = rev_range.strip().split(":")
+ else:
+ base = head = None
+
+ environment = (
+ util.asbool(config.get_main_option("revision_environment"))
+ or indicate_current
+ )
+
+ def _display_history(config, script, base, head, currents=()):
+ for sc in script.walk_revisions(
+ base=base or "base", head=head or "heads"
+ ):
+ if indicate_current:
+ sc._db_current_indicator = sc.revision in currents
+
+ config.print_stdout(
+ sc.cmd_format(
+ verbose=verbose,
+ include_branches=True,
+ include_doc=True,
+ include_parents=True,
+ )
+ )
+
+ def _display_history_w_current(config, script, base, head):
+ def _display_current_history(rev, context):
+ if head == "current":
+ _display_history(config, script, base, rev, rev)
+ elif base == "current":
+ _display_history(config, script, rev, head, rev)
+ else:
+ _display_history(config, script, base, head, rev)
+ return []
+
+ with EnvironmentContext(config, script, fn=_display_current_history):
+ script.run_env()
+
+ if base == "current" or head == "current" or environment:
+ _display_history_w_current(config, script, base, head)
+ else:
+ _display_history(config, script, base, head)
+
+
+def heads(config, verbose=False, resolve_dependencies=False):
+ """Show current available heads in the script directory.
+
+ :param config: a :class:`.Config` instance.
+
+ :param verbose: output in verbose mode.
+
+ :param resolve_dependencies: treat dependency version as down revisions.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+ if resolve_dependencies:
+ heads = script.get_revisions("heads")
+ else:
+ heads = script.get_revisions(script.get_heads())
+
+ for rev in heads:
+ config.print_stdout(
+ rev.cmd_format(
+ verbose, include_branches=True, tree_indicators=False
+ )
+ )
+
+
+def branches(config, verbose=False):
+ """Show current branch points.
+
+ :param config: a :class:`.Config` instance.
+
+ :param verbose: output in verbose mode.
+
+ """
+ script = ScriptDirectory.from_config(config)
+ for sc in script.walk_revisions():
+ if sc.is_branch_point:
+ config.print_stdout(
+ "%s\n%s\n",
+ sc.cmd_format(verbose, include_branches=True),
+ "\n".join(
+ "%s -> %s"
+ % (
+ " " * len(str(sc.revision)),
+ rev_obj.cmd_format(
+ False, include_branches=True, include_doc=verbose
+ ),
+ )
+ for rev_obj in (
+ script.get_revision(rev) for rev in sc.nextrev
+ )
+ ),
+ )
+
+
+def current(config: Config, verbose: bool = False) -> None:
+ """Display the current revision for a database.
+
+ :param config: a :class:`.Config` instance.
+
+ :param verbose: output in verbose mode.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ def display_version(rev, context):
+ if verbose:
+ config.print_stdout(
+ "Current revision(s) for %s:",
+ util.obfuscate_url_pw(context.connection.engine.url),
+ )
+ for rev in script.get_all_current(rev):
+ config.print_stdout(rev.cmd_format(verbose))
+
+ return []
+
+ with EnvironmentContext(
+ config, script, fn=display_version, dont_mutate=True
+ ):
+ script.run_env()
+
+
+def stamp(
+ config: Config,
+ revision: _RevIdType,
+ sql: bool = False,
+ tag: Optional[str] = None,
+ purge: bool = False,
+) -> None:
+ """'stamp' the revision table with the given revision; don't
+ run any migrations.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: target revision or list of revisions. May be a list
+ to indicate stamping of multiple branch heads.
+
+ .. note:: this parameter is called "revisions" in the command line
+ interface.
+
+ :param sql: use ``--sql`` mode
+
+ :param tag: an arbitrary "tag" that can be intercepted by custom
+ ``env.py`` scripts via the :class:`.EnvironmentContext.get_tag_argument`
+ method.
+
+ :param purge: delete all entries in the version table before stamping.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ if sql:
+ destination_revs = []
+ starting_rev = None
+ for _revision in util.to_list(revision):
+ if ":" in _revision:
+ srev, _revision = _revision.split(":", 2)
+
+ if starting_rev != srev:
+ if starting_rev is None:
+ starting_rev = srev
+ else:
+ raise util.CommandError(
+ "Stamp operation with --sql only supports a "
+ "single starting revision at a time"
+ )
+ destination_revs.append(_revision)
+ else:
+ destination_revs = util.to_list(revision)
+
+ def do_stamp(rev, context):
+ return script._stamp_revs(util.to_tuple(destination_revs), rev)
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=do_stamp,
+ as_sql=sql,
+ starting_rev=starting_rev if sql else None,
+ destination_rev=util.to_tuple(destination_revs),
+ tag=tag,
+ purge=purge,
+ ):
+ script.run_env()
+
+
+def edit(config: Config, rev: str) -> None:
+ """Edit revision script(s) using $EDITOR.
+
+ :param config: a :class:`.Config` instance.
+
+ :param rev: target revision.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ if rev == "current":
+
+ def edit_current(rev, context):
+ if not rev:
+ raise util.CommandError("No current revisions")
+ for sc in script.get_revisions(rev):
+ util.open_in_editor(sc.path)
+ return []
+
+ with EnvironmentContext(config, script, fn=edit_current):
+ script.run_env()
+ else:
+ revs = script.get_revisions(rev)
+ if not revs:
+ raise util.CommandError(
+ "No revision files indicated by symbol '%s'" % rev
+ )
+ for sc in revs:
+ assert sc
+ util.open_in_editor(sc.path)
+
+
+def ensure_version(config: Config, sql: bool = False) -> None:
+ """Create the alembic version table if it doesn't exist already .
+
+ :param config: a :class:`.Config` instance.
+
+ :param sql: use ``--sql`` mode
+
+ .. versionadded:: 1.7.6
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ def do_ensure_version(rev, context):
+ context._ensure_version_table()
+ return []
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=do_ensure_version,
+ as_sql=sql,
+ ):
+ script.run_env()
diff --git a/venv/lib/python3.12/site-packages/alembic/config.py b/venv/lib/python3.12/site-packages/alembic/config.py
new file mode 100644
index 0000000..4b2263f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/config.py
@@ -0,0 +1,645 @@
+from __future__ import annotations
+
+from argparse import ArgumentParser
+from argparse import Namespace
+from configparser import ConfigParser
+import inspect
+import os
+import sys
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import TextIO
+from typing import Union
+
+from typing_extensions import TypedDict
+
+from . import __version__
+from . import command
+from . import util
+from .util import compat
+
+
+class Config:
+ r"""Represent an Alembic configuration.
+
+ Within an ``env.py`` script, this is available
+ via the :attr:`.EnvironmentContext.config` attribute,
+ which in turn is available at ``alembic.context``::
+
+ from alembic import context
+
+ some_param = context.config.get_main_option("my option")
+
+ When invoking Alembic programmatically, a new
+ :class:`.Config` can be created by passing
+ the name of an .ini file to the constructor::
+
+ from alembic.config import Config
+ alembic_cfg = Config("/path/to/yourapp/alembic.ini")
+
+ With a :class:`.Config` object, you can then
+ run Alembic commands programmatically using the directives
+ in :mod:`alembic.command`.
+
+ The :class:`.Config` object can also be constructed without
+ a filename. Values can be set programmatically, and
+ new sections will be created as needed::
+
+ from alembic.config import Config
+ alembic_cfg = Config()
+ alembic_cfg.set_main_option("script_location", "myapp:migrations")
+ alembic_cfg.set_main_option("sqlalchemy.url", "postgresql://foo/bar")
+ alembic_cfg.set_section_option("mysection", "foo", "bar")
+
+ .. warning::
+
+ When using programmatic configuration, make sure the
+ ``env.py`` file in use is compatible with the target configuration;
+ including that the call to Python ``logging.fileConfig()`` is
+ omitted if the programmatic configuration doesn't actually include
+ logging directives.
+
+ For passing non-string values to environments, such as connections and
+ engines, use the :attr:`.Config.attributes` dictionary::
+
+ with engine.begin() as connection:
+ alembic_cfg.attributes['connection'] = connection
+ command.upgrade(alembic_cfg, "head")
+
+ :param file\_: name of the .ini file to open.
+ :param ini_section: name of the main Alembic section within the
+ .ini file
+ :param output_buffer: optional file-like input buffer which
+ will be passed to the :class:`.MigrationContext` - used to redirect
+ the output of "offline generation" when using Alembic programmatically.
+ :param stdout: buffer where the "print" output of commands will be sent.
+ Defaults to ``sys.stdout``.
+
+ :param config_args: A dictionary of keys and values that will be used
+ for substitution in the alembic config file. The dictionary as given
+ is **copied** to a new one, stored locally as the attribute
+ ``.config_args``. When the :attr:`.Config.file_config` attribute is
+ first invoked, the replacement variable ``here`` will be added to this
+ dictionary before the dictionary is passed to ``ConfigParser()``
+ to parse the .ini file.
+
+ :param attributes: optional dictionary of arbitrary Python keys/values,
+ which will be populated into the :attr:`.Config.attributes` dictionary.
+
+ .. seealso::
+
+ :ref:`connection_sharing`
+
+ """
+
+ def __init__(
+ self,
+ file_: Union[str, os.PathLike[str], None] = None,
+ ini_section: str = "alembic",
+ output_buffer: Optional[TextIO] = None,
+ stdout: TextIO = sys.stdout,
+ cmd_opts: Optional[Namespace] = None,
+ config_args: Mapping[str, Any] = util.immutabledict(),
+ attributes: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """Construct a new :class:`.Config`"""
+ self.config_file_name = file_
+ self.config_ini_section = ini_section
+ self.output_buffer = output_buffer
+ self.stdout = stdout
+ self.cmd_opts = cmd_opts
+ self.config_args = dict(config_args)
+ if attributes:
+ self.attributes.update(attributes)
+
+ cmd_opts: Optional[Namespace] = None
+ """The command-line options passed to the ``alembic`` script.
+
+ Within an ``env.py`` script this can be accessed via the
+ :attr:`.EnvironmentContext.config` attribute.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_x_argument`
+
+ """
+
+ config_file_name: Union[str, os.PathLike[str], None] = None
+ """Filesystem path to the .ini file in use."""
+
+ config_ini_section: str = None # type:ignore[assignment]
+ """Name of the config file section to read basic configuration
+ from. Defaults to ``alembic``, that is the ``[alembic]`` section
+ of the .ini file. This value is modified using the ``-n/--name``
+ option to the Alembic runner.
+
+ """
+
+ @util.memoized_property
+ def attributes(self) -> Dict[str, Any]:
+ """A Python dictionary for storage of additional state.
+
+
+ This is a utility dictionary which can include not just strings but
+ engines, connections, schema objects, or anything else.
+ Use this to pass objects into an env.py script, such as passing
+ a :class:`sqlalchemy.engine.base.Connection` when calling
+ commands from :mod:`alembic.command` programmatically.
+
+ .. seealso::
+
+ :ref:`connection_sharing`
+
+ :paramref:`.Config.attributes`
+
+ """
+ return {}
+
+ def print_stdout(self, text: str, *arg: Any) -> None:
+ """Render a message to standard out.
+
+ When :meth:`.Config.print_stdout` is called with additional args
+ those arguments will formatted against the provided text,
+ otherwise we simply output the provided text verbatim.
+
+ This is a no-op when the``quiet`` messaging option is enabled.
+
+ e.g.::
+
+ >>> config.print_stdout('Some text %s', 'arg')
+ Some Text arg
+
+ """
+
+ if arg:
+ output = str(text) % arg
+ else:
+ output = str(text)
+
+ util.write_outstream(self.stdout, output, "\n", **self.messaging_opts)
+
+ @util.memoized_property
+ def file_config(self) -> ConfigParser:
+ """Return the underlying ``ConfigParser`` object.
+
+ Direct access to the .ini file is available here,
+ though the :meth:`.Config.get_section` and
+ :meth:`.Config.get_main_option`
+ methods provide a possibly simpler interface.
+
+ """
+
+ if self.config_file_name:
+ here = os.path.abspath(os.path.dirname(self.config_file_name))
+ else:
+ here = ""
+ self.config_args["here"] = here
+ file_config = ConfigParser(self.config_args)
+ if self.config_file_name:
+ compat.read_config_parser(file_config, [self.config_file_name])
+ else:
+ file_config.add_section(self.config_ini_section)
+ return file_config
+
+ def get_template_directory(self) -> str:
+ """Return the directory where Alembic setup templates are found.
+
+ This method is used by the alembic ``init`` and ``list_templates``
+ commands.
+
+ """
+ import alembic
+
+ package_dir = os.path.abspath(os.path.dirname(alembic.__file__))
+ return os.path.join(package_dir, "templates")
+
+ @overload
+ def get_section(
+ self, name: str, default: None = ...
+ ) -> Optional[Dict[str, str]]:
+ ...
+
+ # "default" here could also be a TypeVar
+ # _MT = TypeVar("_MT", bound=Mapping[str, str]),
+ # however mypy wasn't handling that correctly (pyright was)
+ @overload
+ def get_section(
+ self, name: str, default: Dict[str, str]
+ ) -> Dict[str, str]:
+ ...
+
+ @overload
+ def get_section(
+ self, name: str, default: Mapping[str, str]
+ ) -> Union[Dict[str, str], Mapping[str, str]]:
+ ...
+
+ def get_section(
+ self, name: str, default: Optional[Mapping[str, str]] = None
+ ) -> Optional[Mapping[str, str]]:
+ """Return all the configuration options from a given .ini file section
+ as a dictionary.
+
+ If the given section does not exist, the value of ``default``
+ is returned, which is expected to be a dictionary or other mapping.
+
+ """
+ if not self.file_config.has_section(name):
+ return default
+
+ return dict(self.file_config.items(name))
+
+ def set_main_option(self, name: str, value: str) -> None:
+ """Set an option programmatically within the 'main' section.
+
+ This overrides whatever was in the .ini file.
+
+ :param name: name of the value
+
+ :param value: the value. Note that this value is passed to
+ ``ConfigParser.set``, which supports variable interpolation using
+ pyformat (e.g. ``%(some_value)s``). A raw percent sign not part of
+ an interpolation symbol must therefore be escaped, e.g. ``%%``.
+ The given value may refer to another value already in the file
+ using the interpolation format.
+
+ """
+ self.set_section_option(self.config_ini_section, name, value)
+
+ def remove_main_option(self, name: str) -> None:
+ self.file_config.remove_option(self.config_ini_section, name)
+
+ def set_section_option(self, section: str, name: str, value: str) -> None:
+ """Set an option programmatically within the given section.
+
+ The section is created if it doesn't exist already.
+ The value here will override whatever was in the .ini
+ file.
+
+ :param section: name of the section
+
+ :param name: name of the value
+
+ :param value: the value. Note that this value is passed to
+ ``ConfigParser.set``, which supports variable interpolation using
+ pyformat (e.g. ``%(some_value)s``). A raw percent sign not part of
+ an interpolation symbol must therefore be escaped, e.g. ``%%``.
+ The given value may refer to another value already in the file
+ using the interpolation format.
+
+ """
+
+ if not self.file_config.has_section(section):
+ self.file_config.add_section(section)
+ self.file_config.set(section, name, value)
+
+ def get_section_option(
+ self, section: str, name: str, default: Optional[str] = None
+ ) -> Optional[str]:
+ """Return an option from the given section of the .ini file."""
+ if not self.file_config.has_section(section):
+ raise util.CommandError(
+ "No config file %r found, or file has no "
+ "'[%s]' section" % (self.config_file_name, section)
+ )
+ if self.file_config.has_option(section, name):
+ return self.file_config.get(section, name)
+ else:
+ return default
+
+ @overload
+ def get_main_option(self, name: str, default: str) -> str:
+ ...
+
+ @overload
+ def get_main_option(
+ self, name: str, default: Optional[str] = None
+ ) -> Optional[str]:
+ ...
+
+ def get_main_option(
+ self, name: str, default: Optional[str] = None
+ ) -> Optional[str]:
+ """Return an option from the 'main' section of the .ini file.
+
+ This defaults to being a key from the ``[alembic]``
+ section, unless the ``-n/--name`` flag were used to
+ indicate a different section.
+
+ """
+ return self.get_section_option(self.config_ini_section, name, default)
+
+ @util.memoized_property
+ def messaging_opts(self) -> MessagingOptions:
+ """The messaging options."""
+ return cast(
+ MessagingOptions,
+ util.immutabledict(
+ {"quiet": getattr(self.cmd_opts, "quiet", False)}
+ ),
+ )
+
+
+class MessagingOptions(TypedDict, total=False):
+ quiet: bool
+
+
+class CommandLine:
+ def __init__(self, prog: Optional[str] = None) -> None:
+ self._generate_args(prog)
+
+ def _generate_args(self, prog: Optional[str]) -> None:
+ def add_options(
+ fn: Any, parser: Any, positional: Any, kwargs: Any
+ ) -> None:
+ kwargs_opts = {
+ "template": (
+ "-t",
+ "--template",
+ dict(
+ default="generic",
+ type=str,
+ help="Setup template for use with 'init'",
+ ),
+ ),
+ "message": (
+ "-m",
+ "--message",
+ dict(
+ type=str, help="Message string to use with 'revision'"
+ ),
+ ),
+ "sql": (
+ "--sql",
+ dict(
+ action="store_true",
+ help="Don't emit SQL to database - dump to "
+ "standard output/file instead. See docs on "
+ "offline mode.",
+ ),
+ ),
+ "tag": (
+ "--tag",
+ dict(
+ type=str,
+ help="Arbitrary 'tag' name - can be used by "
+ "custom env.py scripts.",
+ ),
+ ),
+ "head": (
+ "--head",
+ dict(
+ type=str,
+ help="Specify head revision or @head "
+ "to base new revision on.",
+ ),
+ ),
+ "splice": (
+ "--splice",
+ dict(
+ action="store_true",
+ help="Allow a non-head revision as the "
+ "'head' to splice onto",
+ ),
+ ),
+ "depends_on": (
+ "--depends-on",
+ dict(
+ action="append",
+ help="Specify one or more revision identifiers "
+ "which this revision should depend on.",
+ ),
+ ),
+ "rev_id": (
+ "--rev-id",
+ dict(
+ type=str,
+ help="Specify a hardcoded revision id instead of "
+ "generating one",
+ ),
+ ),
+ "version_path": (
+ "--version-path",
+ dict(
+ type=str,
+ help="Specify specific path from config for "
+ "version file",
+ ),
+ ),
+ "branch_label": (
+ "--branch-label",
+ dict(
+ type=str,
+ help="Specify a branch label to apply to the "
+ "new revision",
+ ),
+ ),
+ "verbose": (
+ "-v",
+ "--verbose",
+ dict(action="store_true", help="Use more verbose output"),
+ ),
+ "resolve_dependencies": (
+ "--resolve-dependencies",
+ dict(
+ action="store_true",
+ help="Treat dependency versions as down revisions",
+ ),
+ ),
+ "autogenerate": (
+ "--autogenerate",
+ dict(
+ action="store_true",
+ help="Populate revision script with candidate "
+ "migration operations, based on comparison "
+ "of database to model.",
+ ),
+ ),
+ "rev_range": (
+ "-r",
+ "--rev-range",
+ dict(
+ action="store",
+ help="Specify a revision range; "
+ "format is [start]:[end]",
+ ),
+ ),
+ "indicate_current": (
+ "-i",
+ "--indicate-current",
+ dict(
+ action="store_true",
+ help="Indicate the current revision",
+ ),
+ ),
+ "purge": (
+ "--purge",
+ dict(
+ action="store_true",
+ help="Unconditionally erase the version table "
+ "before stamping",
+ ),
+ ),
+ "package": (
+ "--package",
+ dict(
+ action="store_true",
+ help="Write empty __init__.py files to the "
+ "environment and version locations",
+ ),
+ ),
+ }
+ positional_help = {
+ "directory": "location of scripts directory",
+ "revision": "revision identifier",
+ "revisions": "one or more revisions, or 'heads' for all heads",
+ }
+ for arg in kwargs:
+ if arg in kwargs_opts:
+ args = kwargs_opts[arg]
+ args, kw = args[0:-1], args[-1]
+ parser.add_argument(*args, **kw)
+
+ for arg in positional:
+ if (
+ arg == "revisions"
+ or fn in positional_translations
+ and positional_translations[fn][arg] == "revisions"
+ ):
+ subparser.add_argument(
+ "revisions",
+ nargs="+",
+ help=positional_help.get("revisions"),
+ )
+ else:
+ subparser.add_argument(arg, help=positional_help.get(arg))
+
+ parser = ArgumentParser(prog=prog)
+
+ parser.add_argument(
+ "--version", action="version", version="%%(prog)s %s" % __version__
+ )
+ parser.add_argument(
+ "-c",
+ "--config",
+ type=str,
+ default=os.environ.get("ALEMBIC_CONFIG", "alembic.ini"),
+ help="Alternate config file; defaults to value of "
+ 'ALEMBIC_CONFIG environment variable, or "alembic.ini"',
+ )
+ parser.add_argument(
+ "-n",
+ "--name",
+ type=str,
+ default="alembic",
+ help="Name of section in .ini file to " "use for Alembic config",
+ )
+ parser.add_argument(
+ "-x",
+ action="append",
+ help="Additional arguments consumed by "
+ "custom env.py scripts, e.g. -x "
+ "setting1=somesetting -x setting2=somesetting",
+ )
+ parser.add_argument(
+ "--raiseerr",
+ action="store_true",
+ help="Raise a full stack trace on error",
+ )
+ parser.add_argument(
+ "-q",
+ "--quiet",
+ action="store_true",
+ help="Do not log to std output.",
+ )
+ subparsers = parser.add_subparsers()
+
+ positional_translations: Dict[Any, Any] = {
+ command.stamp: {"revision": "revisions"}
+ }
+
+ for fn in [getattr(command, n) for n in dir(command)]:
+ if (
+ inspect.isfunction(fn)
+ and fn.__name__[0] != "_"
+ and fn.__module__ == "alembic.command"
+ ):
+ spec = compat.inspect_getfullargspec(fn)
+ if spec[3] is not None:
+ positional = spec[0][1 : -len(spec[3])]
+ kwarg = spec[0][-len(spec[3]) :]
+ else:
+ positional = spec[0][1:]
+ kwarg = []
+
+ if fn in positional_translations:
+ positional = [
+ positional_translations[fn].get(name, name)
+ for name in positional
+ ]
+
+ # parse first line(s) of helptext without a line break
+ help_ = fn.__doc__
+ if help_:
+ help_text = []
+ for line in help_.split("\n"):
+ if not line.strip():
+ break
+ else:
+ help_text.append(line.strip())
+ else:
+ help_text = []
+ subparser = subparsers.add_parser(
+ fn.__name__, help=" ".join(help_text)
+ )
+ add_options(fn, subparser, positional, kwarg)
+ subparser.set_defaults(cmd=(fn, positional, kwarg))
+ self.parser = parser
+
+ def run_cmd(self, config: Config, options: Namespace) -> None:
+ fn, positional, kwarg = options.cmd
+
+ try:
+ fn(
+ config,
+ *[getattr(options, k, None) for k in positional],
+ **{k: getattr(options, k, None) for k in kwarg},
+ )
+ except util.CommandError as e:
+ if options.raiseerr:
+ raise
+ else:
+ util.err(str(e), **config.messaging_opts)
+
+ def main(self, argv: Optional[Sequence[str]] = None) -> None:
+ options = self.parser.parse_args(argv)
+ if not hasattr(options, "cmd"):
+ # see http://bugs.python.org/issue9253, argparse
+ # behavior changed incompatibly in py3.3
+ self.parser.error("too few arguments")
+ else:
+ cfg = Config(
+ file_=options.config,
+ ini_section=options.name,
+ cmd_opts=options,
+ )
+ self.run_cmd(cfg, options)
+
+
+def main(
+ argv: Optional[Sequence[str]] = None,
+ prog: Optional[str] = None,
+ **kwargs: Any,
+) -> None:
+ """The console runner function for Alembic."""
+
+ CommandLine(prog=prog).main(argv=argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/venv/lib/python3.12/site-packages/alembic/context.py b/venv/lib/python3.12/site-packages/alembic/context.py
new file mode 100644
index 0000000..758fca8
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/context.py
@@ -0,0 +1,5 @@
+from .runtime.environment import EnvironmentContext
+
+# create proxy functions for
+# each method on the EnvironmentContext class.
+EnvironmentContext.create_module_class_proxy(globals(), locals())
diff --git a/venv/lib/python3.12/site-packages/alembic/context.pyi b/venv/lib/python3.12/site-packages/alembic/context.pyi
new file mode 100644
index 0000000..80619fb
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/context.pyi
@@ -0,0 +1,853 @@
+# ### this file stubs are generated by tools/write_pyi.py - do not edit ###
+# ### imports are manually managed
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Collection
+from typing import ContextManager
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Literal
+from typing import Mapping
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import TextIO
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine.base import Connection
+ from sqlalchemy.engine.url import URL
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import FetchedValue
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .autogenerate.api import AutogenContext
+ from .config import Config
+ from .operations.ops import MigrationScript
+ from .runtime.migration import _ProxyTransaction
+ from .runtime.migration import MigrationContext
+ from .runtime.migration import MigrationInfo
+ from .script import ScriptDirectory
+
+### end imports ###
+
+def begin_transaction() -> Union[_ProxyTransaction, ContextManager[None]]:
+ """Return a context manager that will
+ enclose an operation within a "transaction",
+ as defined by the environment's offline
+ and transactional DDL settings.
+
+ e.g.::
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+ :meth:`.begin_transaction` is intended to
+ "do the right thing" regardless of
+ calling context:
+
+ * If :meth:`.is_transactional_ddl` is ``False``,
+ returns a "do nothing" context manager
+ which otherwise produces no transactional
+ state or directives.
+ * If :meth:`.is_offline_mode` is ``True``,
+ returns a context manager that will
+ invoke the :meth:`.DefaultImpl.emit_begin`
+ and :meth:`.DefaultImpl.emit_commit`
+ methods, which will produce the string
+ directives ``BEGIN`` and ``COMMIT`` on
+ the output stream, as rendered by the
+ target backend (e.g. SQL Server would
+ emit ``BEGIN TRANSACTION``).
+ * Otherwise, calls :meth:`sqlalchemy.engine.Connection.begin`
+ on the current online connection, which
+ returns a :class:`sqlalchemy.engine.Transaction`
+ object. This object demarcates a real
+ transaction and is itself a context manager,
+ which will roll back if an exception
+ is raised.
+
+ Note that a custom ``env.py`` script which
+ has more specific transactional needs can of course
+ manipulate the :class:`~sqlalchemy.engine.Connection`
+ directly to produce transactional state in "online"
+ mode.
+
+ """
+
+config: Config
+
+def configure(
+ connection: Optional[Connection] = None,
+ url: Union[str, URL, None] = None,
+ dialect_name: Optional[str] = None,
+ dialect_opts: Optional[Dict[str, Any]] = None,
+ transactional_ddl: Optional[bool] = None,
+ transaction_per_migration: bool = False,
+ output_buffer: Optional[TextIO] = None,
+ starting_rev: Optional[str] = None,
+ tag: Optional[str] = None,
+ template_args: Optional[Dict[str, Any]] = None,
+ render_as_batch: bool = False,
+ target_metadata: Union[MetaData, Sequence[MetaData], None] = None,
+ include_name: Optional[
+ Callable[
+ [
+ Optional[str],
+ Literal[
+ "schema",
+ "table",
+ "column",
+ "index",
+ "unique_constraint",
+ "foreign_key_constraint",
+ ],
+ MutableMapping[
+ Literal[
+ "schema_name",
+ "table_name",
+ "schema_qualified_table_name",
+ ],
+ Optional[str],
+ ],
+ ],
+ bool,
+ ]
+ ] = None,
+ include_object: Optional[
+ Callable[
+ [
+ SchemaItem,
+ Optional[str],
+ Literal[
+ "schema",
+ "table",
+ "column",
+ "index",
+ "unique_constraint",
+ "foreign_key_constraint",
+ ],
+ bool,
+ Optional[SchemaItem],
+ ],
+ bool,
+ ]
+ ] = None,
+ include_schemas: bool = False,
+ process_revision_directives: Optional[
+ Callable[
+ [
+ MigrationContext,
+ Union[str, Iterable[Optional[str]], Iterable[str]],
+ List[MigrationScript],
+ ],
+ None,
+ ]
+ ] = None,
+ compare_type: Union[
+ bool,
+ Callable[
+ [
+ MigrationContext,
+ Column[Any],
+ Column[Any],
+ TypeEngine[Any],
+ TypeEngine[Any],
+ ],
+ Optional[bool],
+ ],
+ ] = True,
+ compare_server_default: Union[
+ bool,
+ Callable[
+ [
+ MigrationContext,
+ Column[Any],
+ Column[Any],
+ Optional[str],
+ Optional[FetchedValue],
+ Optional[str],
+ ],
+ Optional[bool],
+ ],
+ ] = False,
+ render_item: Optional[
+ Callable[[str, Any, AutogenContext], Union[str, Literal[False]]]
+ ] = None,
+ literal_binds: bool = False,
+ upgrade_token: str = "upgrades",
+ downgrade_token: str = "downgrades",
+ alembic_module_prefix: str = "op.",
+ sqlalchemy_module_prefix: str = "sa.",
+ user_module_prefix: Optional[str] = None,
+ on_version_apply: Optional[
+ Callable[
+ [
+ MigrationContext,
+ MigrationInfo,
+ Collection[Any],
+ Mapping[str, Any],
+ ],
+ None,
+ ]
+ ] = None,
+ **kw: Any,
+) -> None:
+ """Configure a :class:`.MigrationContext` within this
+ :class:`.EnvironmentContext` which will provide database
+ connectivity and other configuration to a series of
+ migration scripts.
+
+ Many methods on :class:`.EnvironmentContext` require that
+ this method has been called in order to function, as they
+ ultimately need to have database access or at least access
+ to the dialect in use. Those which do are documented as such.
+
+ The important thing needed by :meth:`.configure` is a
+ means to determine what kind of database dialect is in use.
+ An actual connection to that database is needed only if
+ the :class:`.MigrationContext` is to be used in
+ "online" mode.
+
+ If the :meth:`.is_offline_mode` function returns ``True``,
+ then no connection is needed here. Otherwise, the
+ ``connection`` parameter should be present as an
+ instance of :class:`sqlalchemy.engine.Connection`.
+
+ This function is typically called from the ``env.py``
+ script within a migration environment. It can be called
+ multiple times for an invocation. The most recent
+ :class:`~sqlalchemy.engine.Connection`
+ for which it was called is the one that will be operated upon
+ by the next call to :meth:`.run_migrations`.
+
+ General parameters:
+
+ :param connection: a :class:`~sqlalchemy.engine.Connection`
+ to use
+ for SQL execution in "online" mode. When present, is also
+ used to determine the type of dialect in use.
+ :param url: a string database url, or a
+ :class:`sqlalchemy.engine.url.URL` object.
+ The type of dialect to be used will be derived from this if
+ ``connection`` is not passed.
+ :param dialect_name: string name of a dialect, such as
+ "postgresql", "mssql", etc.
+ The type of dialect to be used will be derived from this if
+ ``connection`` and ``url`` are not passed.
+ :param dialect_opts: dictionary of options to be passed to dialect
+ constructor.
+ :param transactional_ddl: Force the usage of "transactional"
+ DDL on or off;
+ this otherwise defaults to whether or not the dialect in
+ use supports it.
+ :param transaction_per_migration: if True, nest each migration script
+ in a transaction rather than the full series of migrations to
+ run.
+ :param output_buffer: a file-like object that will be used
+ for textual output
+ when the ``--sql`` option is used to generate SQL scripts.
+ Defaults to
+ ``sys.stdout`` if not passed here and also not present on
+ the :class:`.Config`
+ object. The value here overrides that of the :class:`.Config`
+ object.
+ :param output_encoding: when using ``--sql`` to generate SQL
+ scripts, apply this encoding to the string output.
+ :param literal_binds: when using ``--sql`` to generate SQL
+ scripts, pass through the ``literal_binds`` flag to the compiler
+ so that any literal values that would ordinarily be bound
+ parameters are converted to plain strings.
+
+ .. warning:: Dialects can typically only handle simple datatypes
+ like strings and numbers for auto-literal generation. Datatypes
+ like dates, intervals, and others may still require manual
+ formatting, typically using :meth:`.Operations.inline_literal`.
+
+ .. note:: the ``literal_binds`` flag is ignored on SQLAlchemy
+ versions prior to 0.8 where this feature is not supported.
+
+ .. seealso::
+
+ :meth:`.Operations.inline_literal`
+
+ :param starting_rev: Override the "starting revision" argument
+ when using ``--sql`` mode.
+ :param tag: a string tag for usage by custom ``env.py`` scripts.
+ Set via the ``--tag`` option, can be overridden here.
+ :param template_args: dictionary of template arguments which
+ will be added to the template argument environment when
+ running the "revision" command. Note that the script environment
+ is only run within the "revision" command if the --autogenerate
+ option is used, or if the option "revision_environment=true"
+ is present in the alembic.ini file.
+
+ :param version_table: The name of the Alembic version table.
+ The default is ``'alembic_version'``.
+ :param version_table_schema: Optional schema to place version
+ table within.
+ :param version_table_pk: boolean, whether the Alembic version table
+ should use a primary key constraint for the "value" column; this
+ only takes effect when the table is first created.
+ Defaults to True; setting to False should not be necessary and is
+ here for backwards compatibility reasons.
+ :param on_version_apply: a callable or collection of callables to be
+ run for each migration step.
+ The callables will be run in the order they are given, once for
+ each migration step, after the respective operation has been
+ applied but before its transaction is finalized.
+ Each callable accepts no positional arguments and the following
+ keyword arguments:
+
+ * ``ctx``: the :class:`.MigrationContext` running the migration,
+ * ``step``: a :class:`.MigrationInfo` representing the
+ step currently being applied,
+ * ``heads``: a collection of version strings representing the
+ current heads,
+ * ``run_args``: the ``**kwargs`` passed to :meth:`.run_migrations`.
+
+ Parameters specific to the autogenerate feature, when
+ ``alembic revision`` is run with the ``--autogenerate`` feature:
+
+ :param target_metadata: a :class:`sqlalchemy.schema.MetaData`
+ object, or a sequence of :class:`~sqlalchemy.schema.MetaData`
+ objects, that will be consulted during autogeneration.
+ The tables present in each :class:`~sqlalchemy.schema.MetaData`
+ will be compared against
+ what is locally available on the target
+ :class:`~sqlalchemy.engine.Connection`
+ to produce candidate upgrade/downgrade operations.
+ :param compare_type: Indicates type comparison behavior during
+ an autogenerate
+ operation. Defaults to ``True`` turning on type comparison, which
+ has good accuracy on most backends. See :ref:`compare_types`
+ for an example as well as information on other type
+ comparison options. Set to ``False`` which disables type
+ comparison. A callable can also be passed to provide custom type
+ comparison, see :ref:`compare_types` for additional details.
+
+ .. versionchanged:: 1.12.0 The default value of
+ :paramref:`.EnvironmentContext.configure.compare_type` has been
+ changed to ``True``.
+
+ .. seealso::
+
+ :ref:`compare_types`
+
+ :paramref:`.EnvironmentContext.configure.compare_server_default`
+
+ :param compare_server_default: Indicates server default comparison
+ behavior during
+ an autogenerate operation. Defaults to ``False`` which disables
+ server default
+ comparison. Set to ``True`` to turn on server default comparison,
+ which has
+ varied accuracy depending on backend.
+
+ To customize server default comparison behavior, a callable may
+ be specified
+ which can filter server default comparisons during an
+ autogenerate operation.
+ defaults during an autogenerate operation. The format of this
+ callable is::
+
+ def my_compare_server_default(context, inspected_column,
+ metadata_column, inspected_default, metadata_default,
+ rendered_metadata_default):
+ # return True if the defaults are different,
+ # False if not, or None to allow the default implementation
+ # to compare these defaults
+ return None
+
+ context.configure(
+ # ...
+ compare_server_default = my_compare_server_default
+ )
+
+ ``inspected_column`` is a dictionary structure as returned by
+ :meth:`sqlalchemy.engine.reflection.Inspector.get_columns`, whereas
+ ``metadata_column`` is a :class:`sqlalchemy.schema.Column` from
+ the local model environment.
+
+ A return value of ``None`` indicates to allow default server default
+ comparison
+ to proceed. Note that some backends such as Postgresql actually
+ execute
+ the two defaults on the database side to compare for equivalence.
+
+ .. seealso::
+
+ :paramref:`.EnvironmentContext.configure.compare_type`
+
+ :param include_name: A callable function which is given
+ the chance to return ``True`` or ``False`` for any database reflected
+ object based on its name, including database schema names when
+ the :paramref:`.EnvironmentContext.configure.include_schemas` flag
+ is set to ``True``.
+
+ The function accepts the following positional arguments:
+
+ * ``name``: the name of the object, such as schema name or table name.
+ Will be ``None`` when indicating the default schema name of the
+ database connection.
+ * ``type``: a string describing the type of object; currently
+ ``"schema"``, ``"table"``, ``"column"``, ``"index"``,
+ ``"unique_constraint"``, or ``"foreign_key_constraint"``
+ * ``parent_names``: a dictionary of "parent" object names, that are
+ relative to the name being given. Keys in this dictionary may
+ include: ``"schema_name"``, ``"table_name"`` or
+ ``"schema_qualified_table_name"``.
+
+ E.g.::
+
+ def include_name(name, type_, parent_names):
+ if type_ == "schema":
+ return name in ["schema_one", "schema_two"]
+ else:
+ return True
+
+ context.configure(
+ # ...
+ include_schemas = True,
+ include_name = include_name
+ )
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_object`
+
+ :paramref:`.EnvironmentContext.configure.include_schemas`
+
+
+ :param include_object: A callable function which is given
+ the chance to return ``True`` or ``False`` for any object,
+ indicating if the given object should be considered in the
+ autogenerate sweep.
+
+ The function accepts the following positional arguments:
+
+ * ``object``: a :class:`~sqlalchemy.schema.SchemaItem` object such
+ as a :class:`~sqlalchemy.schema.Table`,
+ :class:`~sqlalchemy.schema.Column`,
+ :class:`~sqlalchemy.schema.Index`
+ :class:`~sqlalchemy.schema.UniqueConstraint`,
+ or :class:`~sqlalchemy.schema.ForeignKeyConstraint` object
+ * ``name``: the name of the object. This is typically available
+ via ``object.name``.
+ * ``type``: a string describing the type of object; currently
+ ``"table"``, ``"column"``, ``"index"``, ``"unique_constraint"``,
+ or ``"foreign_key_constraint"``
+ * ``reflected``: ``True`` if the given object was produced based on
+ table reflection, ``False`` if it's from a local :class:`.MetaData`
+ object.
+ * ``compare_to``: the object being compared against, if available,
+ else ``None``.
+
+ E.g.::
+
+ def include_object(object, name, type_, reflected, compare_to):
+ if (type_ == "column" and
+ not reflected and
+ object.info.get("skip_autogenerate", False)):
+ return False
+ else:
+ return True
+
+ context.configure(
+ # ...
+ include_object = include_object
+ )
+
+ For the use case of omitting specific schemas from a target database
+ when :paramref:`.EnvironmentContext.configure.include_schemas` is
+ set to ``True``, the :attr:`~sqlalchemy.schema.Table.schema`
+ attribute can be checked for each :class:`~sqlalchemy.schema.Table`
+ object passed to the hook, however it is much more efficient
+ to filter on schemas before reflection of objects takes place
+ using the :paramref:`.EnvironmentContext.configure.include_name`
+ hook.
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_name`
+
+ :paramref:`.EnvironmentContext.configure.include_schemas`
+
+ :param render_as_batch: if True, commands which alter elements
+ within a table will be placed under a ``with batch_alter_table():``
+ directive, so that batch migrations will take place.
+
+ .. seealso::
+
+ :ref:`batch_migrations`
+
+ :param include_schemas: If True, autogenerate will scan across
+ all schemas located by the SQLAlchemy
+ :meth:`~sqlalchemy.engine.reflection.Inspector.get_schema_names`
+ method, and include all differences in tables found across all
+ those schemas. When using this option, you may want to also
+ use the :paramref:`.EnvironmentContext.configure.include_name`
+ parameter to specify a callable which
+ can filter the tables/schemas that get included.
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_name`
+
+ :paramref:`.EnvironmentContext.configure.include_object`
+
+ :param render_item: Callable that can be used to override how
+ any schema item, i.e. column, constraint, type,
+ etc., is rendered for autogenerate. The callable receives a
+ string describing the type of object, the object, and
+ the autogen context. If it returns False, the
+ default rendering method will be used. If it returns None,
+ the item will not be rendered in the context of a Table
+ construct, that is, can be used to skip columns or constraints
+ within op.create_table()::
+
+ def my_render_column(type_, col, autogen_context):
+ if type_ == "column" and isinstance(col, MySpecialCol):
+ return repr(col)
+ else:
+ return False
+
+ context.configure(
+ # ...
+ render_item = my_render_column
+ )
+
+ Available values for the type string include: ``"column"``,
+ ``"primary_key"``, ``"foreign_key"``, ``"unique"``, ``"check"``,
+ ``"type"``, ``"server_default"``.
+
+ .. seealso::
+
+ :ref:`autogen_render_types`
+
+ :param upgrade_token: When autogenerate completes, the text of the
+ candidate upgrade operations will be present in this template
+ variable when ``script.py.mako`` is rendered. Defaults to
+ ``upgrades``.
+ :param downgrade_token: When autogenerate completes, the text of the
+ candidate downgrade operations will be present in this
+ template variable when ``script.py.mako`` is rendered. Defaults to
+ ``downgrades``.
+
+ :param alembic_module_prefix: When autogenerate refers to Alembic
+ :mod:`alembic.operations` constructs, this prefix will be used
+ (i.e. ``op.create_table``) Defaults to "``op.``".
+ Can be ``None`` to indicate no prefix.
+
+ :param sqlalchemy_module_prefix: When autogenerate refers to
+ SQLAlchemy
+ :class:`~sqlalchemy.schema.Column` or type classes, this prefix
+ will be used
+ (i.e. ``sa.Column("somename", sa.Integer)``) Defaults to "``sa.``".
+ Can be ``None`` to indicate no prefix.
+ Note that when dialect-specific types are rendered, autogenerate
+ will render them using the dialect module name, i.e. ``mssql.BIT()``,
+ ``postgresql.UUID()``.
+
+ :param user_module_prefix: When autogenerate refers to a SQLAlchemy
+ type (e.g. :class:`.TypeEngine`) where the module name is not
+ under the ``sqlalchemy`` namespace, this prefix will be used
+ within autogenerate. If left at its default of
+ ``None``, the ``__module__`` attribute of the type is used to
+ render the import module. It's a good practice to set this
+ and to have all custom types be available from a fixed module space,
+ in order to future-proof migration files against reorganizations
+ in modules.
+
+ .. seealso::
+
+ :ref:`autogen_module_prefix`
+
+ :param process_revision_directives: a callable function that will
+ be passed a structure representing the end result of an autogenerate
+ or plain "revision" operation, which can be manipulated to affect
+ how the ``alembic revision`` command ultimately outputs new
+ revision scripts. The structure of the callable is::
+
+ def process_revision_directives(context, revision, directives):
+ pass
+
+ The ``directives`` parameter is a Python list containing
+ a single :class:`.MigrationScript` directive, which represents
+ the revision file to be generated. This list as well as its
+ contents may be freely modified to produce any set of commands.
+ The section :ref:`customizing_revision` shows an example of
+ doing this. The ``context`` parameter is the
+ :class:`.MigrationContext` in use,
+ and ``revision`` is a tuple of revision identifiers representing the
+ current revision of the database.
+
+ The callable is invoked at all times when the ``--autogenerate``
+ option is passed to ``alembic revision``. If ``--autogenerate``
+ is not passed, the callable is invoked only if the
+ ``revision_environment`` variable is set to True in the Alembic
+ configuration, in which case the given ``directives`` collection
+ will contain empty :class:`.UpgradeOps` and :class:`.DowngradeOps`
+ collections for ``.upgrade_ops`` and ``.downgrade_ops``. The
+ ``--autogenerate`` option itself can be inferred by inspecting
+ ``context.config.cmd_opts.autogenerate``.
+
+ The callable function may optionally be an instance of
+ a :class:`.Rewriter` object. This is a helper object that
+ assists in the production of autogenerate-stream rewriter functions.
+
+ .. seealso::
+
+ :ref:`customizing_revision`
+
+ :ref:`autogen_rewriter`
+
+ :paramref:`.command.revision.process_revision_directives`
+
+ Parameters specific to individual backends:
+
+ :param mssql_batch_separator: The "batch separator" which will
+ be placed between each statement when generating offline SQL Server
+ migrations. Defaults to ``GO``. Note this is in addition to the
+ customary semicolon ``;`` at the end of each statement; SQL Server
+ considers the "batch separator" to denote the end of an
+ individual statement execution, and cannot group certain
+ dependent operations in one step.
+ :param oracle_batch_separator: The "batch separator" which will
+ be placed between each statement when generating offline
+ Oracle migrations. Defaults to ``/``. Oracle doesn't add a
+ semicolon between statements like most other backends.
+
+ """
+
+def execute(
+ sql: Union[Executable, str],
+ execution_options: Optional[Dict[str, Any]] = None,
+) -> None:
+ """Execute the given SQL using the current change context.
+
+ The behavior of :meth:`.execute` is the same
+ as that of :meth:`.Operations.execute`. Please see that
+ function's documentation for full detail including
+ caveats and limitations.
+
+ This function requires that a :class:`.MigrationContext` has
+ first been made available via :meth:`.configure`.
+
+ """
+
+def get_bind() -> Connection:
+ """Return the current 'bind'.
+
+ In "online" mode, this is the
+ :class:`sqlalchemy.engine.Connection` currently being used
+ to emit SQL to the database.
+
+ This function requires that a :class:`.MigrationContext`
+ has first been made available via :meth:`.configure`.
+
+ """
+
+def get_context() -> MigrationContext:
+ """Return the current :class:`.MigrationContext` object.
+
+ If :meth:`.EnvironmentContext.configure` has not been
+ called yet, raises an exception.
+
+ """
+
+def get_head_revision() -> Union[str, Tuple[str, ...], None]:
+ """Return the hex identifier of the 'head' script revision.
+
+ If the script directory has multiple heads, this
+ method raises a :class:`.CommandError`;
+ :meth:`.EnvironmentContext.get_head_revisions` should be preferred.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso:: :meth:`.EnvironmentContext.get_head_revisions`
+
+ """
+
+def get_head_revisions() -> Union[str, Tuple[str, ...], None]:
+ """Return the hex identifier of the 'heads' script revision(s).
+
+ This returns a tuple containing the version number of all
+ heads in the script directory.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def get_revision_argument() -> Union[str, Tuple[str, ...], None]:
+ """Get the 'destination' revision argument.
+
+ This is typically the argument passed to the
+ ``upgrade`` or ``downgrade`` command.
+
+ If it was specified as ``head``, the actual
+ version number is returned; if specified
+ as ``base``, ``None`` is returned.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def get_starting_revision_argument() -> Union[str, Tuple[str, ...], None]:
+ """Return the 'starting revision' argument,
+ if the revision was passed using ``start:end``.
+
+ This is only meaningful in "offline" mode.
+ Returns ``None`` if no value is available
+ or was configured.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def get_tag_argument() -> Optional[str]:
+ """Return the value passed for the ``--tag`` argument, if any.
+
+ The ``--tag`` argument is not used directly by Alembic,
+ but is available for custom ``env.py`` configurations that
+ wish to use it; particularly for offline generation scripts
+ that wish to generate tagged filenames.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_x_argument` - a newer and more
+ open ended system of extending ``env.py`` scripts via the command
+ line.
+
+ """
+
+@overload
+def get_x_argument(as_dictionary: Literal[False]) -> List[str]: ...
+@overload
+def get_x_argument(as_dictionary: Literal[True]) -> Dict[str, str]: ...
+@overload
+def get_x_argument(
+ as_dictionary: bool = ...,
+) -> Union[List[str], Dict[str, str]]:
+ """Return the value(s) passed for the ``-x`` argument, if any.
+
+ The ``-x`` argument is an open ended flag that allows any user-defined
+ value or values to be passed on the command line, then available
+ here for consumption by a custom ``env.py`` script.
+
+ The return value is a list, returned directly from the ``argparse``
+ structure. If ``as_dictionary=True`` is passed, the ``x`` arguments
+ are parsed using ``key=value`` format into a dictionary that is
+ then returned. If there is no ``=`` in the argument, value is an empty
+ string.
+
+ .. versionchanged:: 1.13.1 Support ``as_dictionary=True`` when
+ arguments are passed without the ``=`` symbol.
+
+ For example, to support passing a database URL on the command line,
+ the standard ``env.py`` script can be modified like this::
+
+ cmd_line_url = context.get_x_argument(
+ as_dictionary=True).get('dbname')
+ if cmd_line_url:
+ engine = create_engine(cmd_line_url)
+ else:
+ engine = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix='sqlalchemy.',
+ poolclass=pool.NullPool)
+
+ This then takes effect by running the ``alembic`` script as::
+
+ alembic -x dbname=postgresql://user:pass@host/dbname upgrade head
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_tag_argument`
+
+ :attr:`.Config.cmd_opts`
+
+ """
+
+def is_offline_mode() -> bool:
+ """Return True if the current migrations environment
+ is running in "offline mode".
+
+ This is ``True`` or ``False`` depending
+ on the ``--sql`` flag passed.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def is_transactional_ddl() -> bool:
+ """Return True if the context is configured to expect a
+ transactional DDL capable backend.
+
+ This defaults to the type of database in use, and
+ can be overridden by the ``transactional_ddl`` argument
+ to :meth:`.configure`
+
+ This function requires that a :class:`.MigrationContext`
+ has first been made available via :meth:`.configure`.
+
+ """
+
+def run_migrations(**kw: Any) -> None:
+ """Run migrations as determined by the current command line
+ configuration
+ as well as versioning information present (or not) in the current
+ database connection (if one is present).
+
+ The function accepts optional ``**kw`` arguments. If these are
+ passed, they are sent directly to the ``upgrade()`` and
+ ``downgrade()``
+ functions within each target revision file. By modifying the
+ ``script.py.mako`` file so that the ``upgrade()`` and ``downgrade()``
+ functions accept arguments, parameters can be passed here so that
+ contextual information, usually information to identify a particular
+ database in use, can be passed from a custom ``env.py`` script
+ to the migration functions.
+
+ This function requires that a :class:`.MigrationContext` has
+ first been made available via :meth:`.configure`.
+
+ """
+
+script: ScriptDirectory
+
+def static_output(text: str) -> None:
+ """Emit text directly to the "offline" SQL stream.
+
+ Typically this is for emitting comments that
+ start with --. The statement is not treated
+ as a SQL execution, no ; or batch separator
+ is added, etc.
+
+ """
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__init__.py b/venv/lib/python3.12/site-packages/alembic/ddl/__init__.py
new file mode 100644
index 0000000..f2f72b3
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/__init__.py
@@ -0,0 +1,6 @@
+from . import mssql
+from . import mysql
+from . import oracle
+from . import postgresql
+from . import sqlite
+from .impl import DefaultImpl as DefaultImpl
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..ea983de
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/_autogen.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/_autogen.cpython-312.pyc
new file mode 100644
index 0000000..36b6294
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/_autogen.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/base.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..c38e6dd
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/base.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/impl.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/impl.cpython-312.pyc
new file mode 100644
index 0000000..e07f972
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/impl.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/mssql.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/mssql.cpython-312.pyc
new file mode 100644
index 0000000..40f6835
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/mssql.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/mysql.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/mysql.cpython-312.pyc
new file mode 100644
index 0000000..3f385c5
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/mysql.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/oracle.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/oracle.cpython-312.pyc
new file mode 100644
index 0000000..99361f8
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/oracle.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/postgresql.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/postgresql.cpython-312.pyc
new file mode 100644
index 0000000..0bb38f9
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/postgresql.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/sqlite.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/sqlite.cpython-312.pyc
new file mode 100644
index 0000000..e6c0189
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/ddl/__pycache__/sqlite.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/_autogen.py b/venv/lib/python3.12/site-packages/alembic/ddl/_autogen.py
new file mode 100644
index 0000000..e22153c
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/_autogen.py
@@ -0,0 +1,325 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+from typing import Any
+from typing import ClassVar
+from typing import Dict
+from typing import Generic
+from typing import NamedTuple
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy.sql.schema import Constraint
+from sqlalchemy.sql.schema import ForeignKeyConstraint
+from sqlalchemy.sql.schema import Index
+from sqlalchemy.sql.schema import UniqueConstraint
+from typing_extensions import TypeGuard
+
+from .. import util
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from alembic.autogenerate.api import AutogenContext
+ from alembic.ddl.impl import DefaultImpl
+
+CompareConstraintType = Union[Constraint, Index]
+
+_C = TypeVar("_C", bound=CompareConstraintType)
+
+_clsreg: Dict[str, Type[_constraint_sig]] = {}
+
+
+class ComparisonResult(NamedTuple):
+ status: Literal["equal", "different", "skip"]
+ message: str
+
+ @property
+ def is_equal(self) -> bool:
+ return self.status == "equal"
+
+ @property
+ def is_different(self) -> bool:
+ return self.status == "different"
+
+ @property
+ def is_skip(self) -> bool:
+ return self.status == "skip"
+
+ @classmethod
+ def Equal(cls) -> ComparisonResult:
+ """the constraints are equal."""
+ return cls("equal", "The two constraints are equal")
+
+ @classmethod
+ def Different(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
+ """the constraints are different for the provided reason(s)."""
+ return cls("different", ", ".join(util.to_list(reason)))
+
+ @classmethod
+ def Skip(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
+ """the constraint cannot be compared for the provided reason(s).
+
+ The message is logged, but the constraints will be otherwise
+ considered equal, meaning that no migration command will be
+ generated.
+ """
+ return cls("skip", ", ".join(util.to_list(reason)))
+
+
+class _constraint_sig(Generic[_C]):
+ const: _C
+
+ _sig: Tuple[Any, ...]
+ name: Optional[sqla_compat._ConstraintNameDefined]
+
+ impl: DefaultImpl
+
+ _is_index: ClassVar[bool] = False
+ _is_fk: ClassVar[bool] = False
+ _is_uq: ClassVar[bool] = False
+
+ _is_metadata: bool
+
+ def __init_subclass__(cls) -> None:
+ cls._register()
+
+ @classmethod
+ def _register(cls):
+ raise NotImplementedError()
+
+ def __init__(
+ self, is_metadata: bool, impl: DefaultImpl, const: _C
+ ) -> None:
+ raise NotImplementedError()
+
+ def compare_to_reflected(
+ self, other: _constraint_sig[Any]
+ ) -> ComparisonResult:
+ assert self.impl is other.impl
+ assert self._is_metadata
+ assert not other._is_metadata
+
+ return self._compare_to_reflected(other)
+
+ def _compare_to_reflected(
+ self, other: _constraint_sig[_C]
+ ) -> ComparisonResult:
+ raise NotImplementedError()
+
+ @classmethod
+ def from_constraint(
+ cls, is_metadata: bool, impl: DefaultImpl, constraint: _C
+ ) -> _constraint_sig[_C]:
+ # these could be cached by constraint/impl, however, if the
+ # constraint is modified in place, then the sig is wrong. the mysql
+ # impl currently does this, and if we fixed that we can't be sure
+ # someone else might do it too, so play it safe.
+ sig = _clsreg[constraint.__visit_name__](is_metadata, impl, constraint)
+ return sig
+
+ def md_name_to_sql_name(self, context: AutogenContext) -> Optional[str]:
+ return sqla_compat._get_constraint_final_name(
+ self.const, context.dialect
+ )
+
+ @util.memoized_property
+ def is_named(self):
+ return sqla_compat._constraint_is_named(self.const, self.impl.dialect)
+
+ @util.memoized_property
+ def unnamed(self) -> Tuple[Any, ...]:
+ return self._sig
+
+ @util.memoized_property
+ def unnamed_no_options(self) -> Tuple[Any, ...]:
+ raise NotImplementedError()
+
+ @util.memoized_property
+ def _full_sig(self) -> Tuple[Any, ...]:
+ return (self.name,) + self.unnamed
+
+ def __eq__(self, other) -> bool:
+ return self._full_sig == other._full_sig
+
+ def __ne__(self, other) -> bool:
+ return self._full_sig != other._full_sig
+
+ def __hash__(self) -> int:
+ return hash(self._full_sig)
+
+
+class _uq_constraint_sig(_constraint_sig[UniqueConstraint]):
+ _is_uq = True
+
+ @classmethod
+ def _register(cls) -> None:
+ _clsreg["unique_constraint"] = cls
+
+ is_unique = True
+
+ def __init__(
+ self,
+ is_metadata: bool,
+ impl: DefaultImpl,
+ const: UniqueConstraint,
+ ) -> None:
+ self.impl = impl
+ self.const = const
+ self.name = sqla_compat.constraint_name_or_none(const.name)
+ self._sig = tuple(sorted([col.name for col in const.columns]))
+ self._is_metadata = is_metadata
+
+ @property
+ def column_names(self) -> Tuple[str, ...]:
+ return tuple([col.name for col in self.const.columns])
+
+ def _compare_to_reflected(
+ self, other: _constraint_sig[_C]
+ ) -> ComparisonResult:
+ assert self._is_metadata
+ metadata_obj = self
+ conn_obj = other
+
+ assert is_uq_sig(conn_obj)
+ return self.impl.compare_unique_constraint(
+ metadata_obj.const, conn_obj.const
+ )
+
+
+class _ix_constraint_sig(_constraint_sig[Index]):
+ _is_index = True
+
+ name: sqla_compat._ConstraintName
+
+ @classmethod
+ def _register(cls) -> None:
+ _clsreg["index"] = cls
+
+ def __init__(
+ self, is_metadata: bool, impl: DefaultImpl, const: Index
+ ) -> None:
+ self.impl = impl
+ self.const = const
+ self.name = const.name
+ self.is_unique = bool(const.unique)
+ self._is_metadata = is_metadata
+
+ def _compare_to_reflected(
+ self, other: _constraint_sig[_C]
+ ) -> ComparisonResult:
+ assert self._is_metadata
+ metadata_obj = self
+ conn_obj = other
+
+ assert is_index_sig(conn_obj)
+ return self.impl.compare_indexes(metadata_obj.const, conn_obj.const)
+
+ @util.memoized_property
+ def has_expressions(self):
+ return sqla_compat.is_expression_index(self.const)
+
+ @util.memoized_property
+ def column_names(self) -> Tuple[str, ...]:
+ return tuple([col.name for col in self.const.columns])
+
+ @util.memoized_property
+ def column_names_optional(self) -> Tuple[Optional[str], ...]:
+ return tuple(
+ [getattr(col, "name", None) for col in self.const.expressions]
+ )
+
+ @util.memoized_property
+ def is_named(self):
+ return True
+
+ @util.memoized_property
+ def unnamed(self):
+ return (self.is_unique,) + self.column_names_optional
+
+
+class _fk_constraint_sig(_constraint_sig[ForeignKeyConstraint]):
+ _is_fk = True
+
+ @classmethod
+ def _register(cls) -> None:
+ _clsreg["foreign_key_constraint"] = cls
+
+ def __init__(
+ self,
+ is_metadata: bool,
+ impl: DefaultImpl,
+ const: ForeignKeyConstraint,
+ ) -> None:
+ self._is_metadata = is_metadata
+
+ self.impl = impl
+ self.const = const
+
+ self.name = sqla_compat.constraint_name_or_none(const.name)
+
+ (
+ self.source_schema,
+ self.source_table,
+ self.source_columns,
+ self.target_schema,
+ self.target_table,
+ self.target_columns,
+ onupdate,
+ ondelete,
+ deferrable,
+ initially,
+ ) = sqla_compat._fk_spec(const)
+
+ self._sig: Tuple[Any, ...] = (
+ self.source_schema,
+ self.source_table,
+ tuple(self.source_columns),
+ self.target_schema,
+ self.target_table,
+ tuple(self.target_columns),
+ ) + (
+ (None if onupdate.lower() == "no action" else onupdate.lower())
+ if onupdate
+ else None,
+ (None if ondelete.lower() == "no action" else ondelete.lower())
+ if ondelete
+ else None,
+ # convert initially + deferrable into one three-state value
+ "initially_deferrable"
+ if initially and initially.lower() == "deferred"
+ else "deferrable"
+ if deferrable
+ else "not deferrable",
+ )
+
+ @util.memoized_property
+ def unnamed_no_options(self):
+ return (
+ self.source_schema,
+ self.source_table,
+ tuple(self.source_columns),
+ self.target_schema,
+ self.target_table,
+ tuple(self.target_columns),
+ )
+
+
+def is_index_sig(sig: _constraint_sig) -> TypeGuard[_ix_constraint_sig]:
+ return sig._is_index
+
+
+def is_uq_sig(sig: _constraint_sig) -> TypeGuard[_uq_constraint_sig]:
+ return sig._is_uq
+
+
+def is_fk_sig(sig: _constraint_sig) -> TypeGuard[_fk_constraint_sig]:
+ return sig._is_fk
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/base.py b/venv/lib/python3.12/site-packages/alembic/ddl/base.py
new file mode 100644
index 0000000..7a85a5c
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/base.py
@@ -0,0 +1,335 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import functools
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import exc
+from sqlalchemy import Integer
+from sqlalchemy import types as sqltypes
+from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.schema import Column
+from sqlalchemy.schema import DDLElement
+from sqlalchemy.sql.elements import quoted_name
+
+from ..util.sqla_compat import _columns_for_constraint # noqa
+from ..util.sqla_compat import _find_columns # noqa
+from ..util.sqla_compat import _fk_spec # noqa
+from ..util.sqla_compat import _is_type_bound # noqa
+from ..util.sqla_compat import _table_for_constraint # noqa
+
+if TYPE_CHECKING:
+ from typing import Any
+
+ from sqlalchemy.sql.compiler import Compiled
+ from sqlalchemy.sql.compiler import DDLCompiler
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import FetchedValue
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .impl import DefaultImpl
+ from ..util.sqla_compat import Computed
+ from ..util.sqla_compat import Identity
+
+_ServerDefault = Union["TextClause", "FetchedValue", "Function[Any]", str]
+
+
+class AlterTable(DDLElement):
+
+ """Represent an ALTER TABLE statement.
+
+ Only the string name and optional schema name of the table
+ is required, not a full Table object.
+
+ """
+
+ def __init__(
+ self,
+ table_name: str,
+ schema: Optional[Union[quoted_name, str]] = None,
+ ) -> None:
+ self.table_name = table_name
+ self.schema = schema
+
+
+class RenameTable(AlterTable):
+ def __init__(
+ self,
+ old_table_name: str,
+ new_table_name: Union[quoted_name, str],
+ schema: Optional[Union[quoted_name, str]] = None,
+ ) -> None:
+ super().__init__(old_table_name, schema=schema)
+ self.new_table_name = new_table_name
+
+
+class AlterColumn(AlterTable):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ schema: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_nullable: Optional[bool] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_comment: Optional[str] = None,
+ ) -> None:
+ super().__init__(name, schema=schema)
+ self.column_name = column_name
+ self.existing_type = (
+ sqltypes.to_instance(existing_type)
+ if existing_type is not None
+ else None
+ )
+ self.existing_nullable = existing_nullable
+ self.existing_server_default = existing_server_default
+ self.existing_comment = existing_comment
+
+
+class ColumnNullable(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, nullable: bool, **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.nullable = nullable
+
+
+class ColumnType(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, type_: TypeEngine, **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.type_ = sqltypes.to_instance(type_)
+
+
+class ColumnName(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, newname: str, **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.newname = newname
+
+
+class ColumnDefault(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ default: Optional[_ServerDefault],
+ **kw,
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.default = default
+
+
+class ComputedColumnDefault(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, default: Optional[Computed], **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.default = default
+
+
+class IdentityColumnDefault(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ default: Optional[Identity],
+ impl: DefaultImpl,
+ **kw,
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.default = default
+ self.impl = impl
+
+
+class AddColumn(AlterTable):
+ def __init__(
+ self,
+ name: str,
+ column: Column[Any],
+ schema: Optional[Union[quoted_name, str]] = None,
+ ) -> None:
+ super().__init__(name, schema=schema)
+ self.column = column
+
+
+class DropColumn(AlterTable):
+ def __init__(
+ self, name: str, column: Column[Any], schema: Optional[str] = None
+ ) -> None:
+ super().__init__(name, schema=schema)
+ self.column = column
+
+
+class ColumnComment(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, comment: Optional[str], **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.comment = comment
+
+
+@compiles(RenameTable) # type: ignore[misc]
+def visit_rename_table(
+ element: RenameTable, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, element.schema),
+ )
+
+
+@compiles(AddColumn) # type: ignore[misc]
+def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ add_column(compiler, element.column, **kw),
+ )
+
+
+@compiles(DropColumn) # type: ignore[misc]
+def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ drop_column(compiler, element.column.name, **kw),
+ )
+
+
+@compiles(ColumnNullable) # type: ignore[misc]
+def visit_column_nullable(
+ element: ColumnNullable, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "DROP NOT NULL" if element.nullable else "SET NOT NULL",
+ )
+
+
+@compiles(ColumnType) # type: ignore[misc]
+def visit_column_type(element: ColumnType, compiler: DDLCompiler, **kw) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "TYPE %s" % format_type(compiler, element.type_),
+ )
+
+
+@compiles(ColumnName) # type: ignore[misc]
+def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str:
+ return "%s RENAME %s TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ )
+
+
+@compiles(ColumnDefault) # type: ignore[misc]
+def visit_column_default(
+ element: ColumnDefault, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "SET DEFAULT %s" % format_server_default(compiler, element.default)
+ if element.default is not None
+ else "DROP DEFAULT",
+ )
+
+
+@compiles(ComputedColumnDefault) # type: ignore[misc]
+def visit_computed_column(
+ element: ComputedColumnDefault, compiler: DDLCompiler, **kw
+):
+ raise exc.CompileError(
+ 'Adding or removing a "computed" construct, e.g. GENERATED '
+ "ALWAYS AS, to or from an existing column is not supported."
+ )
+
+
+@compiles(IdentityColumnDefault) # type: ignore[misc]
+def visit_identity_column(
+ element: IdentityColumnDefault, compiler: DDLCompiler, **kw
+):
+ raise exc.CompileError(
+ 'Adding, removing or modifying an "identity" construct, '
+ "e.g. GENERATED AS IDENTITY, to or from an existing "
+ "column is not supported in this dialect."
+ )
+
+
+def quote_dotted(
+ name: Union[quoted_name, str], quote: functools.partial
+) -> Union[quoted_name, str]:
+ """quote the elements of a dotted name"""
+
+ if isinstance(name, quoted_name):
+ return quote(name)
+ result = ".".join([quote(x) for x in name.split(".")])
+ return result
+
+
+def format_table_name(
+ compiler: Compiled,
+ name: Union[quoted_name, str],
+ schema: Optional[Union[quoted_name, str]],
+) -> Union[quoted_name, str]:
+ quote = functools.partial(compiler.preparer.quote)
+ if schema:
+ return quote_dotted(schema, quote) + "." + quote(name)
+ else:
+ return quote(name)
+
+
+def format_column_name(
+ compiler: DDLCompiler, name: Optional[Union[quoted_name, str]]
+) -> Union[quoted_name, str]:
+ return compiler.preparer.quote(name) # type: ignore[arg-type]
+
+
+def format_server_default(
+ compiler: DDLCompiler,
+ default: Optional[_ServerDefault],
+) -> str:
+ return compiler.get_column_default_string(
+ Column("x", Integer, server_default=default)
+ )
+
+
+def format_type(compiler: DDLCompiler, type_: TypeEngine) -> str:
+ return compiler.dialect.type_compiler.process(type_)
+
+
+def alter_table(
+ compiler: DDLCompiler,
+ name: str,
+ schema: Optional[str],
+) -> str:
+ return "ALTER TABLE %s" % format_table_name(compiler, name, schema)
+
+
+def drop_column(compiler: DDLCompiler, name: str, **kw) -> str:
+ return "DROP COLUMN %s" % format_column_name(compiler, name)
+
+
+def alter_column(compiler: DDLCompiler, name: str) -> str:
+ return "ALTER COLUMN %s" % format_column_name(compiler, name)
+
+
+def add_column(compiler: DDLCompiler, column: Column[Any], **kw) -> str:
+ text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
+
+ const = " ".join(
+ compiler.process(constraint) for constraint in column.constraints
+ )
+ if const:
+ text += " " + const
+
+ return text
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/impl.py b/venv/lib/python3.12/site-packages/alembic/ddl/impl.py
new file mode 100644
index 0000000..2e4f1ae
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/impl.py
@@ -0,0 +1,844 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import logging
+import re
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import NamedTuple
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import cast
+from sqlalchemy import schema
+from sqlalchemy import text
+
+from . import _autogen
+from . import base
+from ._autogen import _constraint_sig as _constraint_sig
+from ._autogen import ComparisonResult as ComparisonResult
+from .. import util
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+ from typing import TextIO
+
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.engine import Dialect
+ from sqlalchemy.engine.cursor import CursorResult
+ from sqlalchemy.engine.reflection import Inspector
+ from sqlalchemy.sql import ClauseElement
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import ForeignKeyConstraint
+ from sqlalchemy.sql.schema import Index
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.schema import UniqueConstraint
+ from sqlalchemy.sql.selectable import TableClause
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+ from ..autogenerate.api import AutogenContext
+ from ..operations.batch import ApplyBatchImpl
+ from ..operations.batch import BatchOperationsImpl
+
+log = logging.getLogger(__name__)
+
+
+class ImplMeta(type):
+ def __init__(
+ cls,
+ classname: str,
+ bases: Tuple[Type[DefaultImpl]],
+ dict_: Dict[str, Any],
+ ):
+ newtype = type.__init__(cls, classname, bases, dict_)
+ if "__dialect__" in dict_:
+ _impls[dict_["__dialect__"]] = cls # type: ignore[assignment]
+ return newtype
+
+
+_impls: Dict[str, Type[DefaultImpl]] = {}
+
+
+class DefaultImpl(metaclass=ImplMeta):
+
+ """Provide the entrypoint for major migration operations,
+ including database-specific behavioral variances.
+
+ While individual SQL/DDL constructs already provide
+ for database-specific implementations, variances here
+ allow for entirely different sequences of operations
+ to take place for a particular migration, such as
+ SQL Server's special 'IDENTITY INSERT' step for
+ bulk inserts.
+
+ """
+
+ __dialect__ = "default"
+
+ transactional_ddl = False
+ command_terminator = ";"
+ type_synonyms: Tuple[Set[str], ...] = ({"NUMERIC", "DECIMAL"},)
+ type_arg_extract: Sequence[str] = ()
+ # These attributes are deprecated in SQLAlchemy via #10247. They need to
+ # be ignored to support older version that did not use dialect kwargs.
+ # They only apply to Oracle and are replaced by oracle_order,
+ # oracle_on_null
+ identity_attrs_ignore: Tuple[str, ...] = ("order", "on_null")
+
+ def __init__(
+ self,
+ dialect: Dialect,
+ connection: Optional[Connection],
+ as_sql: bool,
+ transactional_ddl: Optional[bool],
+ output_buffer: Optional[TextIO],
+ context_opts: Dict[str, Any],
+ ) -> None:
+ self.dialect = dialect
+ self.connection = connection
+ self.as_sql = as_sql
+ self.literal_binds = context_opts.get("literal_binds", False)
+
+ self.output_buffer = output_buffer
+ self.memo: dict = {}
+ self.context_opts = context_opts
+ if transactional_ddl is not None:
+ self.transactional_ddl = transactional_ddl
+
+ if self.literal_binds:
+ if not self.as_sql:
+ raise util.CommandError(
+ "Can't use literal_binds setting without as_sql mode"
+ )
+
+ @classmethod
+ def get_by_dialect(cls, dialect: Dialect) -> Type[DefaultImpl]:
+ return _impls[dialect.name]
+
+ def static_output(self, text: str) -> None:
+ assert self.output_buffer is not None
+ self.output_buffer.write(text + "\n\n")
+ self.output_buffer.flush()
+
+ def requires_recreate_in_batch(
+ self, batch_op: BatchOperationsImpl
+ ) -> bool:
+ """Return True if the given :class:`.BatchOperationsImpl`
+ would need the table to be recreated and copied in order to
+ proceed.
+
+ Normally, only returns True on SQLite when operations other
+ than add_column are present.
+
+ """
+ return False
+
+ def prep_table_for_batch(
+ self, batch_impl: ApplyBatchImpl, table: Table
+ ) -> None:
+ """perform any operations needed on a table before a new
+ one is created to replace it in batch mode.
+
+ the PG dialect uses this to drop constraints on the table
+ before the new one uses those same names.
+
+ """
+
+ @property
+ def bind(self) -> Optional[Connection]:
+ return self.connection
+
+ def _exec(
+ self,
+ construct: Union[Executable, str],
+ execution_options: Optional[dict[str, Any]] = None,
+ multiparams: Sequence[dict] = (),
+ params: Dict[str, Any] = util.immutabledict(),
+ ) -> Optional[CursorResult]:
+ if isinstance(construct, str):
+ construct = text(construct)
+ if self.as_sql:
+ if multiparams or params:
+ # TODO: coverage
+ raise Exception("Execution arguments not allowed with as_sql")
+
+ compile_kw: dict[str, Any]
+ if self.literal_binds and not isinstance(
+ construct, schema.DDLElement
+ ):
+ compile_kw = dict(compile_kwargs={"literal_binds": True})
+ else:
+ compile_kw = {}
+
+ if TYPE_CHECKING:
+ assert isinstance(construct, ClauseElement)
+ compiled = construct.compile(dialect=self.dialect, **compile_kw)
+ self.static_output(
+ str(compiled).replace("\t", " ").strip()
+ + self.command_terminator
+ )
+ return None
+ else:
+ conn = self.connection
+ assert conn is not None
+ if execution_options:
+ conn = conn.execution_options(**execution_options)
+ if params:
+ assert isinstance(multiparams, tuple)
+ multiparams += (params,)
+
+ return conn.execute(construct, multiparams)
+
+ def execute(
+ self,
+ sql: Union[Executable, str],
+ execution_options: Optional[dict[str, Any]] = None,
+ ) -> None:
+ self._exec(sql, execution_options)
+
+ def alter_column(
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Union[_ServerDefault, Literal[False]] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ autoincrement: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ existing_comment: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ existing_autoincrement: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ if autoincrement is not None or existing_autoincrement is not None:
+ util.warn(
+ "autoincrement and existing_autoincrement "
+ "only make sense for MySQL",
+ stacklevel=3,
+ )
+ if nullable is not None:
+ self._exec(
+ base.ColumnNullable(
+ table_name,
+ column_name,
+ nullable,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ )
+ )
+ if server_default is not False:
+ kw = {}
+ cls_: Type[
+ Union[
+ base.ComputedColumnDefault,
+ base.IdentityColumnDefault,
+ base.ColumnDefault,
+ ]
+ ]
+ if sqla_compat._server_default_is_computed(
+ server_default, existing_server_default
+ ):
+ cls_ = base.ComputedColumnDefault
+ elif sqla_compat._server_default_is_identity(
+ server_default, existing_server_default
+ ):
+ cls_ = base.IdentityColumnDefault
+ kw["impl"] = self
+ else:
+ cls_ = base.ColumnDefault
+ self._exec(
+ cls_(
+ table_name,
+ column_name,
+ server_default, # type:ignore[arg-type]
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ **kw,
+ )
+ )
+ if type_ is not None:
+ self._exec(
+ base.ColumnType(
+ table_name,
+ column_name,
+ type_,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ )
+ )
+
+ if comment is not False:
+ self._exec(
+ base.ColumnComment(
+ table_name,
+ column_name,
+ comment,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ )
+ )
+
+ # do the new name last ;)
+ if name is not None:
+ self._exec(
+ base.ColumnName(
+ table_name,
+ column_name,
+ name,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ )
+ )
+
+ def add_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ schema: Optional[Union[str, quoted_name]] = None,
+ ) -> None:
+ self._exec(base.AddColumn(table_name, column, schema=schema))
+
+ def drop_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> None:
+ self._exec(base.DropColumn(table_name, column, schema=schema))
+
+ def add_constraint(self, const: Any) -> None:
+ if const._create_rule is None or const._create_rule(self):
+ self._exec(schema.AddConstraint(const))
+
+ def drop_constraint(self, const: Constraint) -> None:
+ self._exec(schema.DropConstraint(const))
+
+ def rename_table(
+ self,
+ old_table_name: str,
+ new_table_name: Union[str, quoted_name],
+ schema: Optional[Union[str, quoted_name]] = None,
+ ) -> None:
+ self._exec(
+ base.RenameTable(old_table_name, new_table_name, schema=schema)
+ )
+
+ def create_table(self, table: Table) -> None:
+ table.dispatch.before_create(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+ self._exec(schema.CreateTable(table))
+ table.dispatch.after_create(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+ for index in table.indexes:
+ self._exec(schema.CreateIndex(index))
+
+ with_comment = (
+ self.dialect.supports_comments and not self.dialect.inline_comments
+ )
+ comment = table.comment
+ if comment and with_comment:
+ self.create_table_comment(table)
+
+ for column in table.columns:
+ comment = column.comment
+ if comment and with_comment:
+ self.create_column_comment(column)
+
+ def drop_table(self, table: Table) -> None:
+ table.dispatch.before_drop(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+ self._exec(schema.DropTable(table))
+ table.dispatch.after_drop(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+
+ def create_index(self, index: Index, **kw: Any) -> None:
+ self._exec(schema.CreateIndex(index, **kw))
+
+ def create_table_comment(self, table: Table) -> None:
+ self._exec(schema.SetTableComment(table))
+
+ def drop_table_comment(self, table: Table) -> None:
+ self._exec(schema.DropTableComment(table))
+
+ def create_column_comment(self, column: ColumnElement[Any]) -> None:
+ self._exec(schema.SetColumnComment(column))
+
+ def drop_index(self, index: Index, **kw: Any) -> None:
+ self._exec(schema.DropIndex(index, **kw))
+
+ def bulk_insert(
+ self,
+ table: Union[TableClause, Table],
+ rows: List[dict],
+ multiinsert: bool = True,
+ ) -> None:
+ if not isinstance(rows, list):
+ raise TypeError("List expected")
+ elif rows and not isinstance(rows[0], dict):
+ raise TypeError("List of dictionaries expected")
+ if self.as_sql:
+ for row in rows:
+ self._exec(
+ sqla_compat._insert_inline(table).values(
+ **{
+ k: sqla_compat._literal_bindparam(
+ k, v, type_=table.c[k].type
+ )
+ if not isinstance(
+ v, sqla_compat._literal_bindparam
+ )
+ else v
+ for k, v in row.items()
+ }
+ )
+ )
+ else:
+ if rows:
+ if multiinsert:
+ self._exec(
+ sqla_compat._insert_inline(table), multiparams=rows
+ )
+ else:
+ for row in rows:
+ self._exec(
+ sqla_compat._insert_inline(table).values(**row)
+ )
+
+ def _tokenize_column_type(self, column: Column) -> Params:
+ definition: str
+ definition = self.dialect.type_compiler.process(column.type).lower()
+
+ # tokenize the SQLAlchemy-generated version of a type, so that
+ # the two can be compared.
+ #
+ # examples:
+ # NUMERIC(10, 5)
+ # TIMESTAMP WITH TIMEZONE
+ # INTEGER UNSIGNED
+ # INTEGER (10) UNSIGNED
+ # INTEGER(10) UNSIGNED
+ # varchar character set utf8
+ #
+
+ tokens: List[str] = re.findall(r"[\w\-_]+|\(.+?\)", definition)
+
+ term_tokens: List[str] = []
+ paren_term = None
+
+ for token in tokens:
+ if re.match(r"^\(.*\)$", token):
+ paren_term = token
+ else:
+ term_tokens.append(token)
+
+ params = Params(term_tokens[0], term_tokens[1:], [], {})
+
+ if paren_term:
+ term: str
+ for term in re.findall("[^(),]+", paren_term):
+ if "=" in term:
+ key, val = term.split("=")
+ params.kwargs[key.strip()] = val.strip()
+ else:
+ params.args.append(term.strip())
+
+ return params
+
+ def _column_types_match(
+ self, inspector_params: Params, metadata_params: Params
+ ) -> bool:
+ if inspector_params.token0 == metadata_params.token0:
+ return True
+
+ synonyms = [{t.lower() for t in batch} for batch in self.type_synonyms]
+ inspector_all_terms = " ".join(
+ [inspector_params.token0] + inspector_params.tokens
+ )
+ metadata_all_terms = " ".join(
+ [metadata_params.token0] + metadata_params.tokens
+ )
+
+ for batch in synonyms:
+ if {inspector_all_terms, metadata_all_terms}.issubset(batch) or {
+ inspector_params.token0,
+ metadata_params.token0,
+ }.issubset(batch):
+ return True
+ return False
+
+ def _column_args_match(
+ self, inspected_params: Params, meta_params: Params
+ ) -> bool:
+ """We want to compare column parameters. However, we only want
+ to compare parameters that are set. If they both have `collation`,
+ we want to make sure they are the same. However, if only one
+ specifies it, dont flag it for being less specific
+ """
+
+ if (
+ len(meta_params.tokens) == len(inspected_params.tokens)
+ and meta_params.tokens != inspected_params.tokens
+ ):
+ return False
+
+ if (
+ len(meta_params.args) == len(inspected_params.args)
+ and meta_params.args != inspected_params.args
+ ):
+ return False
+
+ insp = " ".join(inspected_params.tokens).lower()
+ meta = " ".join(meta_params.tokens).lower()
+
+ for reg in self.type_arg_extract:
+ mi = re.search(reg, insp)
+ mm = re.search(reg, meta)
+
+ if mi and mm and mi.group(1) != mm.group(1):
+ return False
+
+ return True
+
+ def compare_type(
+ self, inspector_column: Column[Any], metadata_column: Column
+ ) -> bool:
+ """Returns True if there ARE differences between the types of the two
+ columns. Takes impl.type_synonyms into account between retrospected
+ and metadata types
+ """
+ inspector_params = self._tokenize_column_type(inspector_column)
+ metadata_params = self._tokenize_column_type(metadata_column)
+
+ if not self._column_types_match(inspector_params, metadata_params):
+ return True
+ if not self._column_args_match(inspector_params, metadata_params):
+ return True
+ return False
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ return rendered_inspector_default != rendered_metadata_default
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_uniques: Set[UniqueConstraint],
+ conn_indexes: Set[Index],
+ metadata_unique_constraints: Set[UniqueConstraint],
+ metadata_indexes: Set[Index],
+ ) -> None:
+ pass
+
+ def cast_for_batch_migrate(self, existing, existing_transfer, new_type):
+ if existing.type._type_affinity is not new_type._type_affinity:
+ existing_transfer["expr"] = cast(
+ existing_transfer["expr"], new_type
+ )
+
+ def render_ddl_sql_expr(
+ self, expr: ClauseElement, is_server_default: bool = False, **kw: Any
+ ) -> str:
+ """Render a SQL expression that is typically a server default,
+ index expression, etc.
+
+ """
+
+ compile_kw = {"literal_binds": True, "include_table": False}
+
+ return str(
+ expr.compile(dialect=self.dialect, compile_kwargs=compile_kw)
+ )
+
+ def _compat_autogen_column_reflect(self, inspector: Inspector) -> Callable:
+ return self.autogen_column_reflect
+
+ def correct_for_autogen_foreignkeys(
+ self,
+ conn_fks: Set[ForeignKeyConstraint],
+ metadata_fks: Set[ForeignKeyConstraint],
+ ) -> None:
+ pass
+
+ def autogen_column_reflect(self, inspector, table, column_info):
+ """A hook that is attached to the 'column_reflect' event for when
+ a Table is reflected from the database during the autogenerate
+ process.
+
+ Dialects can elect to modify the information gathered here.
+
+ """
+
+ def start_migrations(self) -> None:
+ """A hook called when :meth:`.EnvironmentContext.run_migrations`
+ is called.
+
+ Implementations can set up per-migration-run state here.
+
+ """
+
+ def emit_begin(self) -> None:
+ """Emit the string ``BEGIN``, or the backend-specific
+ equivalent, on the current connection context.
+
+ This is used in offline mode and typically
+ via :meth:`.EnvironmentContext.begin_transaction`.
+
+ """
+ self.static_output("BEGIN" + self.command_terminator)
+
+ def emit_commit(self) -> None:
+ """Emit the string ``COMMIT``, or the backend-specific
+ equivalent, on the current connection context.
+
+ This is used in offline mode and typically
+ via :meth:`.EnvironmentContext.begin_transaction`.
+
+ """
+ self.static_output("COMMIT" + self.command_terminator)
+
+ def render_type(
+ self, type_obj: TypeEngine, autogen_context: AutogenContext
+ ) -> Union[str, Literal[False]]:
+ return False
+
+ def _compare_identity_default(self, metadata_identity, inspector_identity):
+ # ignored contains the attributes that were not considered
+ # because assumed to their default values in the db.
+ diff, ignored = _compare_identity_options(
+ metadata_identity,
+ inspector_identity,
+ sqla_compat.Identity(),
+ skip={"always"},
+ )
+
+ meta_always = getattr(metadata_identity, "always", None)
+ inspector_always = getattr(inspector_identity, "always", None)
+ # None and False are the same in this comparison
+ if bool(meta_always) != bool(inspector_always):
+ diff.add("always")
+
+ diff.difference_update(self.identity_attrs_ignore)
+
+ # returns 3 values:
+ return (
+ # different identity attributes
+ diff,
+ # ignored identity attributes
+ ignored,
+ # if the two identity should be considered different
+ bool(diff) or bool(metadata_identity) != bool(inspector_identity),
+ )
+
+ def _compare_index_unique(
+ self, metadata_index: Index, reflected_index: Index
+ ) -> Optional[str]:
+ conn_unique = bool(reflected_index.unique)
+ meta_unique = bool(metadata_index.unique)
+ if conn_unique != meta_unique:
+ return f"unique={conn_unique} to unique={meta_unique}"
+ else:
+ return None
+
+ def _create_metadata_constraint_sig(
+ self, constraint: _autogen._C, **opts: Any
+ ) -> _constraint_sig[_autogen._C]:
+ return _constraint_sig.from_constraint(True, self, constraint, **opts)
+
+ def _create_reflected_constraint_sig(
+ self, constraint: _autogen._C, **opts: Any
+ ) -> _constraint_sig[_autogen._C]:
+ return _constraint_sig.from_constraint(False, self, constraint, **opts)
+
+ def compare_indexes(
+ self,
+ metadata_index: Index,
+ reflected_index: Index,
+ ) -> ComparisonResult:
+ """Compare two indexes by comparing the signature generated by
+ ``create_index_sig``.
+
+ This method returns a ``ComparisonResult``.
+ """
+ msg: List[str] = []
+ unique_msg = self._compare_index_unique(
+ metadata_index, reflected_index
+ )
+ if unique_msg:
+ msg.append(unique_msg)
+ m_sig = self._create_metadata_constraint_sig(metadata_index)
+ r_sig = self._create_reflected_constraint_sig(reflected_index)
+
+ assert _autogen.is_index_sig(m_sig)
+ assert _autogen.is_index_sig(r_sig)
+
+ # The assumption is that the index have no expression
+ for sig in m_sig, r_sig:
+ if sig.has_expressions:
+ log.warning(
+ "Generating approximate signature for index %s. "
+ "The dialect "
+ "implementation should either skip expression indexes "
+ "or provide a custom implementation.",
+ sig.const,
+ )
+
+ if m_sig.column_names != r_sig.column_names:
+ msg.append(
+ f"expression {r_sig.column_names} to {m_sig.column_names}"
+ )
+
+ if msg:
+ return ComparisonResult.Different(msg)
+ else:
+ return ComparisonResult.Equal()
+
+ def compare_unique_constraint(
+ self,
+ metadata_constraint: UniqueConstraint,
+ reflected_constraint: UniqueConstraint,
+ ) -> ComparisonResult:
+ """Compare two unique constraints by comparing the two signatures.
+
+ The arguments are two tuples that contain the unique constraint and
+ the signatures generated by ``create_unique_constraint_sig``.
+
+ This method returns a ``ComparisonResult``.
+ """
+ metadata_tup = self._create_metadata_constraint_sig(
+ metadata_constraint
+ )
+ reflected_tup = self._create_reflected_constraint_sig(
+ reflected_constraint
+ )
+
+ meta_sig = metadata_tup.unnamed
+ conn_sig = reflected_tup.unnamed
+ if conn_sig != meta_sig:
+ return ComparisonResult.Different(
+ f"expression {conn_sig} to {meta_sig}"
+ )
+ else:
+ return ComparisonResult.Equal()
+
+ def _skip_functional_indexes(self, metadata_indexes, conn_indexes):
+ conn_indexes_by_name = {c.name: c for c in conn_indexes}
+
+ for idx in list(metadata_indexes):
+ if idx.name in conn_indexes_by_name:
+ continue
+ iex = sqla_compat.is_expression_index(idx)
+ if iex:
+ util.warn(
+ "autogenerate skipping metadata-specified "
+ "expression-based index "
+ f"{idx.name!r}; dialect {self.__dialect__!r} under "
+ f"SQLAlchemy {sqla_compat.sqlalchemy_version} can't "
+ "reflect these indexes so they can't be compared"
+ )
+ metadata_indexes.discard(idx)
+
+ def adjust_reflected_dialect_options(
+ self, reflected_object: Dict[str, Any], kind: str
+ ) -> Dict[str, Any]:
+ return reflected_object.get("dialect_options", {})
+
+
+class Params(NamedTuple):
+ token0: str
+ tokens: List[str]
+ args: List[str]
+ kwargs: Dict[str, str]
+
+
+def _compare_identity_options(
+ metadata_io: Union[schema.Identity, schema.Sequence, None],
+ inspector_io: Union[schema.Identity, schema.Sequence, None],
+ default_io: Union[schema.Identity, schema.Sequence],
+ skip: Set[str],
+):
+ # this can be used for identity or sequence compare.
+ # default_io is an instance of IdentityOption with all attributes to the
+ # default value.
+ meta_d = sqla_compat._get_identity_options_dict(metadata_io)
+ insp_d = sqla_compat._get_identity_options_dict(inspector_io)
+
+ diff = set()
+ ignored_attr = set()
+
+ def check_dicts(
+ meta_dict: Mapping[str, Any],
+ insp_dict: Mapping[str, Any],
+ default_dict: Mapping[str, Any],
+ attrs: Iterable[str],
+ ):
+ for attr in set(attrs).difference(skip):
+ meta_value = meta_dict.get(attr)
+ insp_value = insp_dict.get(attr)
+ if insp_value != meta_value:
+ default_value = default_dict.get(attr)
+ if meta_value == default_value:
+ ignored_attr.add(attr)
+ else:
+ diff.add(attr)
+
+ check_dicts(
+ meta_d,
+ insp_d,
+ sqla_compat._get_identity_options_dict(default_io),
+ set(meta_d).union(insp_d),
+ )
+ if sqla_compat.identity_has_dialect_kwargs:
+ # use only the dialect kwargs in inspector_io since metadata_io
+ # can have options for many backends
+ check_dicts(
+ getattr(metadata_io, "dialect_kwargs", {}),
+ getattr(inspector_io, "dialect_kwargs", {}),
+ default_io.dialect_kwargs, # type: ignore[union-attr]
+ getattr(inspector_io, "dialect_kwargs", {}),
+ )
+
+ return diff, ignored_attr
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/mssql.py b/venv/lib/python3.12/site-packages/alembic/ddl/mssql.py
new file mode 100644
index 0000000..baa43d5
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/mssql.py
@@ -0,0 +1,419 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import types as sqltypes
+from sqlalchemy.schema import Column
+from sqlalchemy.schema import CreateIndex
+from sqlalchemy.sql.base import Executable
+from sqlalchemy.sql.elements import ClauseElement
+
+from .base import AddColumn
+from .base import alter_column
+from .base import alter_table
+from .base import ColumnDefault
+from .base import ColumnName
+from .base import ColumnNullable
+from .base import ColumnType
+from .base import format_column_name
+from .base import format_server_default
+from .base import format_table_name
+from .base import format_type
+from .base import RenameTable
+from .impl import DefaultImpl
+from .. import util
+from ..util import sqla_compat
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.dialects.mssql.base import MSDDLCompiler
+ from sqlalchemy.dialects.mssql.base import MSSQLCompiler
+ from sqlalchemy.engine.cursor import CursorResult
+ from sqlalchemy.sql.schema import Index
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.selectable import TableClause
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+
+
+class MSSQLImpl(DefaultImpl):
+ __dialect__ = "mssql"
+ transactional_ddl = True
+ batch_separator = "GO"
+
+ type_synonyms = DefaultImpl.type_synonyms + ({"VARCHAR", "NVARCHAR"},)
+ identity_attrs_ignore = DefaultImpl.identity_attrs_ignore + (
+ "minvalue",
+ "maxvalue",
+ "nominvalue",
+ "nomaxvalue",
+ "cycle",
+ "cache",
+ )
+
+ def __init__(self, *arg, **kw) -> None:
+ super().__init__(*arg, **kw)
+ self.batch_separator = self.context_opts.get(
+ "mssql_batch_separator", self.batch_separator
+ )
+
+ def _exec(self, construct: Any, *args, **kw) -> Optional[CursorResult]:
+ result = super()._exec(construct, *args, **kw)
+ if self.as_sql and self.batch_separator:
+ self.static_output(self.batch_separator)
+ return result
+
+ def emit_begin(self) -> None:
+ self.static_output("BEGIN TRANSACTION" + self.command_terminator)
+
+ def emit_commit(self) -> None:
+ super().emit_commit()
+ if self.as_sql and self.batch_separator:
+ self.static_output(self.batch_separator)
+
+ def alter_column( # type:ignore[override]
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Optional[
+ Union[_ServerDefault, Literal[False]]
+ ] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ if nullable is not None:
+ if type_ is not None:
+ # the NULL/NOT NULL alter will handle
+ # the type alteration
+ existing_type = type_
+ type_ = None
+ elif existing_type is None:
+ raise util.CommandError(
+ "MS-SQL ALTER COLUMN operations "
+ "with NULL or NOT NULL require the "
+ "existing_type or a new type_ be passed."
+ )
+ elif existing_nullable is not None and type_ is not None:
+ nullable = existing_nullable
+
+ # the NULL/NOT NULL alter will handle
+ # the type alteration
+ existing_type = type_
+ type_ = None
+
+ elif type_ is not None:
+ util.warn(
+ "MS-SQL ALTER COLUMN operations that specify type_= "
+ "should also specify a nullable= or "
+ "existing_nullable= argument to avoid implicit conversion "
+ "of NOT NULL columns to NULL."
+ )
+
+ used_default = False
+ if sqla_compat._server_default_is_identity(
+ server_default, existing_server_default
+ ) or sqla_compat._server_default_is_computed(
+ server_default, existing_server_default
+ ):
+ used_default = True
+ kw["server_default"] = server_default
+ kw["existing_server_default"] = existing_server_default
+
+ super().alter_column(
+ table_name,
+ column_name,
+ nullable=nullable,
+ type_=type_,
+ schema=schema,
+ existing_type=existing_type,
+ existing_nullable=existing_nullable,
+ **kw,
+ )
+
+ if server_default is not False and used_default is False:
+ if existing_server_default is not False or server_default is None:
+ self._exec(
+ _ExecDropConstraint(
+ table_name,
+ column_name,
+ "sys.default_constraints",
+ schema,
+ )
+ )
+ if server_default is not None:
+ super().alter_column(
+ table_name,
+ column_name,
+ schema=schema,
+ server_default=server_default,
+ )
+
+ if name is not None:
+ super().alter_column(
+ table_name, column_name, schema=schema, name=name
+ )
+
+ def create_index(self, index: Index, **kw: Any) -> None:
+ # this likely defaults to None if not present, so get()
+ # should normally not return the default value. being
+ # defensive in any case
+ mssql_include = index.kwargs.get("mssql_include", None) or ()
+ assert index.table is not None
+ for col in mssql_include:
+ if col not in index.table.c:
+ index.table.append_column(Column(col, sqltypes.NullType))
+ self._exec(CreateIndex(index, **kw))
+
+ def bulk_insert( # type:ignore[override]
+ self, table: Union[TableClause, Table], rows: List[dict], **kw: Any
+ ) -> None:
+ if self.as_sql:
+ self._exec(
+ "SET IDENTITY_INSERT %s ON"
+ % self.dialect.identifier_preparer.format_table(table)
+ )
+ super().bulk_insert(table, rows, **kw)
+ self._exec(
+ "SET IDENTITY_INSERT %s OFF"
+ % self.dialect.identifier_preparer.format_table(table)
+ )
+ else:
+ super().bulk_insert(table, rows, **kw)
+
+ def drop_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> None:
+ drop_default = kw.pop("mssql_drop_default", False)
+ if drop_default:
+ self._exec(
+ _ExecDropConstraint(
+ table_name, column, "sys.default_constraints", schema
+ )
+ )
+ drop_check = kw.pop("mssql_drop_check", False)
+ if drop_check:
+ self._exec(
+ _ExecDropConstraint(
+ table_name, column, "sys.check_constraints", schema
+ )
+ )
+ drop_fks = kw.pop("mssql_drop_foreign_key", False)
+ if drop_fks:
+ self._exec(_ExecDropFKConstraint(table_name, column, schema))
+ super().drop_column(table_name, column, schema=schema, **kw)
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ if rendered_metadata_default is not None:
+ rendered_metadata_default = re.sub(
+ r"[\(\) \"\']", "", rendered_metadata_default
+ )
+
+ if rendered_inspector_default is not None:
+ # SQL Server collapses whitespace and adds arbitrary parenthesis
+ # within expressions. our only option is collapse all of it
+
+ rendered_inspector_default = re.sub(
+ r"[\(\) \"\']", "", rendered_inspector_default
+ )
+
+ return rendered_inspector_default != rendered_metadata_default
+
+ def _compare_identity_default(self, metadata_identity, inspector_identity):
+ diff, ignored, is_alter = super()._compare_identity_default(
+ metadata_identity, inspector_identity
+ )
+
+ if (
+ metadata_identity is None
+ and inspector_identity is not None
+ and not diff
+ and inspector_identity.column is not None
+ and inspector_identity.column.primary_key
+ ):
+ # mssql reflect primary keys with autoincrement as identity
+ # columns. if no different attributes are present ignore them
+ is_alter = False
+
+ return diff, ignored, is_alter
+
+ def adjust_reflected_dialect_options(
+ self, reflected_object: Dict[str, Any], kind: str
+ ) -> Dict[str, Any]:
+ options: Dict[str, Any]
+ options = reflected_object.get("dialect_options", {}).copy()
+ if not options.get("mssql_include"):
+ options.pop("mssql_include", None)
+ if not options.get("mssql_clustered"):
+ options.pop("mssql_clustered", None)
+ return options
+
+
+class _ExecDropConstraint(Executable, ClauseElement):
+ inherit_cache = False
+
+ def __init__(
+ self,
+ tname: str,
+ colname: Union[Column[Any], str],
+ type_: str,
+ schema: Optional[str],
+ ) -> None:
+ self.tname = tname
+ self.colname = colname
+ self.type_ = type_
+ self.schema = schema
+
+
+class _ExecDropFKConstraint(Executable, ClauseElement):
+ inherit_cache = False
+
+ def __init__(
+ self, tname: str, colname: Column[Any], schema: Optional[str]
+ ) -> None:
+ self.tname = tname
+ self.colname = colname
+ self.schema = schema
+
+
+@compiles(_ExecDropConstraint, "mssql")
+def _exec_drop_col_constraint(
+ element: _ExecDropConstraint, compiler: MSSQLCompiler, **kw
+) -> str:
+ schema, tname, colname, type_ = (
+ element.schema,
+ element.tname,
+ element.colname,
+ element.type_,
+ )
+ # from http://www.mssqltips.com/sqlservertip/1425/\
+ # working-with-default-constraints-in-sql-server/
+ return """declare @const_name varchar(256)
+select @const_name = QUOTENAME([name]) from %(type)s
+where parent_object_id = object_id('%(schema_dot)s%(tname)s')
+and col_name(parent_object_id, parent_column_id) = '%(colname)s'
+exec('alter table %(tname_quoted)s drop constraint ' + @const_name)""" % {
+ "type": type_,
+ "tname": tname,
+ "colname": colname,
+ "tname_quoted": format_table_name(compiler, tname, schema),
+ "schema_dot": schema + "." if schema else "",
+ }
+
+
+@compiles(_ExecDropFKConstraint, "mssql")
+def _exec_drop_col_fk_constraint(
+ element: _ExecDropFKConstraint, compiler: MSSQLCompiler, **kw
+) -> str:
+ schema, tname, colname = element.schema, element.tname, element.colname
+
+ return """declare @const_name varchar(256)
+select @const_name = QUOTENAME([name]) from
+sys.foreign_keys fk join sys.foreign_key_columns fkc
+on fk.object_id=fkc.constraint_object_id
+where fkc.parent_object_id = object_id('%(schema_dot)s%(tname)s')
+and col_name(fkc.parent_object_id, fkc.parent_column_id) = '%(colname)s'
+exec('alter table %(tname_quoted)s drop constraint ' + @const_name)""" % {
+ "tname": tname,
+ "colname": colname,
+ "tname_quoted": format_table_name(compiler, tname, schema),
+ "schema_dot": schema + "." if schema else "",
+ }
+
+
+@compiles(AddColumn, "mssql")
+def visit_add_column(element: AddColumn, compiler: MSDDLCompiler, **kw) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ mssql_add_column(compiler, element.column, **kw),
+ )
+
+
+def mssql_add_column(
+ compiler: MSDDLCompiler, column: Column[Any], **kw
+) -> str:
+ return "ADD %s" % compiler.get_column_specification(column, **kw)
+
+
+@compiles(ColumnNullable, "mssql")
+def visit_column_nullable(
+ element: ColumnNullable, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "%s %s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ format_type(compiler, element.existing_type), # type: ignore[arg-type]
+ "NULL" if element.nullable else "NOT NULL",
+ )
+
+
+@compiles(ColumnDefault, "mssql")
+def visit_column_default(
+ element: ColumnDefault, compiler: MSDDLCompiler, **kw
+) -> str:
+ # TODO: there can also be a named constraint
+ # with ADD CONSTRAINT here
+ return "%s ADD DEFAULT %s FOR %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_server_default(compiler, element.default),
+ format_column_name(compiler, element.column_name),
+ )
+
+
+@compiles(ColumnName, "mssql")
+def visit_rename_column(
+ element: ColumnName, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "EXEC sp_rename '%s.%s', %s, 'COLUMN'" % (
+ format_table_name(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ )
+
+
+@compiles(ColumnType, "mssql")
+def visit_column_type(
+ element: ColumnType, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ format_type(compiler, element.type_),
+ )
+
+
+@compiles(RenameTable, "mssql")
+def visit_rename_table(
+ element: RenameTable, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "EXEC sp_rename '%s', %s" % (
+ format_table_name(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/mysql.py b/venv/lib/python3.12/site-packages/alembic/ddl/mysql.py
new file mode 100644
index 0000000..f312173
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/mysql.py
@@ -0,0 +1,474 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import schema
+from sqlalchemy import types as sqltypes
+
+from .base import alter_table
+from .base import AlterColumn
+from .base import ColumnDefault
+from .base import ColumnName
+from .base import ColumnNullable
+from .base import ColumnType
+from .base import format_column_name
+from .base import format_server_default
+from .impl import DefaultImpl
+from .. import util
+from ..util import sqla_compat
+from ..util.sqla_compat import _is_mariadb
+from ..util.sqla_compat import _is_type_bound
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.dialects.mysql.base import MySQLDDLCompiler
+ from sqlalchemy.sql.ddl import DropConstraint
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+
+
+class MySQLImpl(DefaultImpl):
+ __dialect__ = "mysql"
+
+ transactional_ddl = False
+ type_synonyms = DefaultImpl.type_synonyms + (
+ {"BOOL", "TINYINT"},
+ {"JSON", "LONGTEXT"},
+ )
+ type_arg_extract = [r"character set ([\w\-_]+)", r"collate ([\w\-_]+)"]
+
+ def alter_column( # type:ignore[override]
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Union[_ServerDefault, Literal[False]] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ autoincrement: Optional[bool] = None,
+ existing_autoincrement: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ existing_comment: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ if sqla_compat._server_default_is_identity(
+ server_default, existing_server_default
+ ) or sqla_compat._server_default_is_computed(
+ server_default, existing_server_default
+ ):
+ # modifying computed or identity columns is not supported
+ # the default will raise
+ super().alter_column(
+ table_name,
+ column_name,
+ nullable=nullable,
+ type_=type_,
+ schema=schema,
+ existing_type=existing_type,
+ existing_nullable=existing_nullable,
+ server_default=server_default,
+ existing_server_default=existing_server_default,
+ **kw,
+ )
+ if name is not None or self._is_mysql_allowed_functional_default(
+ type_ if type_ is not None else existing_type, server_default
+ ):
+ self._exec(
+ MySQLChangeColumn(
+ table_name,
+ column_name,
+ schema=schema,
+ newname=name if name is not None else column_name,
+ nullable=nullable
+ if nullable is not None
+ else existing_nullable
+ if existing_nullable is not None
+ else True,
+ type_=type_ if type_ is not None else existing_type,
+ default=server_default
+ if server_default is not False
+ else existing_server_default,
+ autoincrement=autoincrement
+ if autoincrement is not None
+ else existing_autoincrement,
+ comment=comment
+ if comment is not False
+ else existing_comment,
+ )
+ )
+ elif (
+ nullable is not None
+ or type_ is not None
+ or autoincrement is not None
+ or comment is not False
+ ):
+ self._exec(
+ MySQLModifyColumn(
+ table_name,
+ column_name,
+ schema=schema,
+ newname=name if name is not None else column_name,
+ nullable=nullable
+ if nullable is not None
+ else existing_nullable
+ if existing_nullable is not None
+ else True,
+ type_=type_ if type_ is not None else existing_type,
+ default=server_default
+ if server_default is not False
+ else existing_server_default,
+ autoincrement=autoincrement
+ if autoincrement is not None
+ else existing_autoincrement,
+ comment=comment
+ if comment is not False
+ else existing_comment,
+ )
+ )
+ elif server_default is not False:
+ self._exec(
+ MySQLAlterDefault(
+ table_name, column_name, server_default, schema=schema
+ )
+ )
+
+ def drop_constraint(
+ self,
+ const: Constraint,
+ ) -> None:
+ if isinstance(const, schema.CheckConstraint) and _is_type_bound(const):
+ return
+
+ super().drop_constraint(const)
+
+ def _is_mysql_allowed_functional_default(
+ self,
+ type_: Optional[TypeEngine],
+ server_default: Union[_ServerDefault, Literal[False]],
+ ) -> bool:
+ return (
+ type_ is not None
+ and type_._type_affinity is sqltypes.DateTime
+ and server_default is not None
+ )
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ # partially a workaround for SQLAlchemy issue #3023; if the
+ # column were created without "NOT NULL", MySQL may have added
+ # an implicit default of '0' which we need to skip
+ # TODO: this is not really covered anymore ?
+ if (
+ metadata_column.type._type_affinity is sqltypes.Integer
+ and inspector_column.primary_key
+ and not inspector_column.autoincrement
+ and not rendered_metadata_default
+ and rendered_inspector_default == "'0'"
+ ):
+ return False
+ elif (
+ rendered_inspector_default
+ and inspector_column.type._type_affinity is sqltypes.Integer
+ ):
+ rendered_inspector_default = (
+ re.sub(r"^'|'$", "", rendered_inspector_default)
+ if rendered_inspector_default is not None
+ else None
+ )
+ return rendered_inspector_default != rendered_metadata_default
+ elif (
+ rendered_metadata_default
+ and metadata_column.type._type_affinity is sqltypes.String
+ ):
+ metadata_default = re.sub(r"^'|'$", "", rendered_metadata_default)
+ return rendered_inspector_default != f"'{metadata_default}'"
+ elif rendered_inspector_default and rendered_metadata_default:
+ # adjust for "function()" vs. "FUNCTION" as can occur particularly
+ # for the CURRENT_TIMESTAMP function on newer MariaDB versions
+
+ # SQLAlchemy MySQL dialect bundles ON UPDATE into the server
+ # default; adjust for this possibly being present.
+ onupdate_ins = re.match(
+ r"(.*) (on update.*?)(?:\(\))?$",
+ rendered_inspector_default.lower(),
+ )
+ onupdate_met = re.match(
+ r"(.*) (on update.*?)(?:\(\))?$",
+ rendered_metadata_default.lower(),
+ )
+
+ if onupdate_ins:
+ if not onupdate_met:
+ return True
+ elif onupdate_ins.group(2) != onupdate_met.group(2):
+ return True
+
+ rendered_inspector_default = onupdate_ins.group(1)
+ rendered_metadata_default = onupdate_met.group(1)
+
+ return re.sub(
+ r"(.*?)(?:\(\))?$", r"\1", rendered_inspector_default.lower()
+ ) != re.sub(
+ r"(.*?)(?:\(\))?$", r"\1", rendered_metadata_default.lower()
+ )
+ else:
+ return rendered_inspector_default != rendered_metadata_default
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ ):
+ # TODO: if SQLA 1.0, make use of "duplicates_index"
+ # metadata
+ removed = set()
+ for idx in list(conn_indexes):
+ if idx.unique:
+ continue
+ # MySQL puts implicit indexes on FK columns, even if
+ # composite and even if MyISAM, so can't check this too easily.
+ # the name of the index may be the column name or it may
+ # be the name of the FK constraint.
+ for col in idx.columns:
+ if idx.name == col.name:
+ conn_indexes.remove(idx)
+ removed.add(idx.name)
+ break
+ for fk in col.foreign_keys:
+ if fk.name == idx.name:
+ conn_indexes.remove(idx)
+ removed.add(idx.name)
+ break
+ if idx.name in removed:
+ break
+
+ # then remove indexes from the "metadata_indexes"
+ # that we've removed from reflected, otherwise they come out
+ # as adds (see #202)
+ for idx in list(metadata_indexes):
+ if idx.name in removed:
+ metadata_indexes.remove(idx)
+
+ def correct_for_autogen_foreignkeys(self, conn_fks, metadata_fks):
+ conn_fk_by_sig = {
+ self._create_reflected_constraint_sig(fk).unnamed_no_options: fk
+ for fk in conn_fks
+ }
+ metadata_fk_by_sig = {
+ self._create_metadata_constraint_sig(fk).unnamed_no_options: fk
+ for fk in metadata_fks
+ }
+
+ for sig in set(conn_fk_by_sig).intersection(metadata_fk_by_sig):
+ mdfk = metadata_fk_by_sig[sig]
+ cnfk = conn_fk_by_sig[sig]
+ # MySQL considers RESTRICT to be the default and doesn't
+ # report on it. if the model has explicit RESTRICT and
+ # the conn FK has None, set it to RESTRICT
+ if (
+ mdfk.ondelete is not None
+ and mdfk.ondelete.lower() == "restrict"
+ and cnfk.ondelete is None
+ ):
+ cnfk.ondelete = "RESTRICT"
+ if (
+ mdfk.onupdate is not None
+ and mdfk.onupdate.lower() == "restrict"
+ and cnfk.onupdate is None
+ ):
+ cnfk.onupdate = "RESTRICT"
+
+
+class MariaDBImpl(MySQLImpl):
+ __dialect__ = "mariadb"
+
+
+class MySQLAlterDefault(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ default: _ServerDefault,
+ schema: Optional[str] = None,
+ ) -> None:
+ super(AlterColumn, self).__init__(name, schema=schema)
+ self.column_name = column_name
+ self.default = default
+
+
+class MySQLChangeColumn(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ schema: Optional[str] = None,
+ newname: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ nullable: Optional[bool] = None,
+ default: Optional[Union[_ServerDefault, Literal[False]]] = False,
+ autoincrement: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ ) -> None:
+ super(AlterColumn, self).__init__(name, schema=schema)
+ self.column_name = column_name
+ self.nullable = nullable
+ self.newname = newname
+ self.default = default
+ self.autoincrement = autoincrement
+ self.comment = comment
+ if type_ is None:
+ raise util.CommandError(
+ "All MySQL CHANGE/MODIFY COLUMN operations "
+ "require the existing type."
+ )
+
+ self.type_ = sqltypes.to_instance(type_)
+
+
+class MySQLModifyColumn(MySQLChangeColumn):
+ pass
+
+
+@compiles(ColumnNullable, "mysql", "mariadb")
+@compiles(ColumnName, "mysql", "mariadb")
+@compiles(ColumnDefault, "mysql", "mariadb")
+@compiles(ColumnType, "mysql", "mariadb")
+def _mysql_doesnt_support_individual(element, compiler, **kw):
+ raise NotImplementedError(
+ "Individual alter column constructs not supported by MySQL"
+ )
+
+
+@compiles(MySQLAlterDefault, "mysql", "mariadb")
+def _mysql_alter_default(
+ element: MySQLAlterDefault, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ return "%s ALTER COLUMN %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ "SET DEFAULT %s" % format_server_default(compiler, element.default)
+ if element.default is not None
+ else "DROP DEFAULT",
+ )
+
+
+@compiles(MySQLModifyColumn, "mysql", "mariadb")
+def _mysql_modify_column(
+ element: MySQLModifyColumn, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ return "%s MODIFY %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ _mysql_colspec(
+ compiler,
+ nullable=element.nullable,
+ server_default=element.default,
+ type_=element.type_,
+ autoincrement=element.autoincrement,
+ comment=element.comment,
+ ),
+ )
+
+
+@compiles(MySQLChangeColumn, "mysql", "mariadb")
+def _mysql_change_column(
+ element: MySQLChangeColumn, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ return "%s CHANGE %s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ _mysql_colspec(
+ compiler,
+ nullable=element.nullable,
+ server_default=element.default,
+ type_=element.type_,
+ autoincrement=element.autoincrement,
+ comment=element.comment,
+ ),
+ )
+
+
+def _mysql_colspec(
+ compiler: MySQLDDLCompiler,
+ nullable: Optional[bool],
+ server_default: Optional[Union[_ServerDefault, Literal[False]]],
+ type_: TypeEngine,
+ autoincrement: Optional[bool],
+ comment: Optional[Union[str, Literal[False]]],
+) -> str:
+ spec = "%s %s" % (
+ compiler.dialect.type_compiler.process(type_),
+ "NULL" if nullable else "NOT NULL",
+ )
+ if autoincrement:
+ spec += " AUTO_INCREMENT"
+ if server_default is not False and server_default is not None:
+ spec += " DEFAULT %s" % format_server_default(compiler, server_default)
+ if comment:
+ spec += " COMMENT %s" % compiler.sql_compiler.render_literal_value(
+ comment, sqltypes.String()
+ )
+
+ return spec
+
+
+@compiles(schema.DropConstraint, "mysql", "mariadb")
+def _mysql_drop_constraint(
+ element: DropConstraint, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ """Redefine SQLAlchemy's drop constraint to
+ raise errors for invalid constraint type."""
+
+ constraint = element.element
+ if isinstance(
+ constraint,
+ (
+ schema.ForeignKeyConstraint,
+ schema.PrimaryKeyConstraint,
+ schema.UniqueConstraint,
+ ),
+ ):
+ assert not kw
+ return compiler.visit_drop_constraint(element)
+ elif isinstance(constraint, schema.CheckConstraint):
+ # note that SQLAlchemy as of 1.2 does not yet support
+ # DROP CONSTRAINT for MySQL/MariaDB, so we implement fully
+ # here.
+ if _is_mariadb(compiler.dialect):
+ return "ALTER TABLE %s DROP CONSTRAINT %s" % (
+ compiler.preparer.format_table(constraint.table),
+ compiler.preparer.format_constraint(constraint),
+ )
+ else:
+ return "ALTER TABLE %s DROP CHECK %s" % (
+ compiler.preparer.format_table(constraint.table),
+ compiler.preparer.format_constraint(constraint),
+ )
+ else:
+ raise NotImplementedError(
+ "No generic 'DROP CONSTRAINT' in MySQL - "
+ "please specify constraint type"
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/oracle.py b/venv/lib/python3.12/site-packages/alembic/ddl/oracle.py
new file mode 100644
index 0000000..5401174
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/oracle.py
@@ -0,0 +1,200 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Optional
+from typing import TYPE_CHECKING
+
+from sqlalchemy.sql import sqltypes
+
+from .base import AddColumn
+from .base import alter_table
+from .base import ColumnComment
+from .base import ColumnDefault
+from .base import ColumnName
+from .base import ColumnNullable
+from .base import ColumnType
+from .base import format_column_name
+from .base import format_server_default
+from .base import format_table_name
+from .base import format_type
+from .base import IdentityColumnDefault
+from .base import RenameTable
+from .impl import DefaultImpl
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from sqlalchemy.dialects.oracle.base import OracleDDLCompiler
+ from sqlalchemy.engine.cursor import CursorResult
+ from sqlalchemy.sql.schema import Column
+
+
+class OracleImpl(DefaultImpl):
+ __dialect__ = "oracle"
+ transactional_ddl = False
+ batch_separator = "/"
+ command_terminator = ""
+ type_synonyms = DefaultImpl.type_synonyms + (
+ {"VARCHAR", "VARCHAR2"},
+ {"BIGINT", "INTEGER", "SMALLINT", "DECIMAL", "NUMERIC", "NUMBER"},
+ {"DOUBLE", "FLOAT", "DOUBLE_PRECISION"},
+ )
+ identity_attrs_ignore = ()
+
+ def __init__(self, *arg, **kw) -> None:
+ super().__init__(*arg, **kw)
+ self.batch_separator = self.context_opts.get(
+ "oracle_batch_separator", self.batch_separator
+ )
+
+ def _exec(self, construct: Any, *args, **kw) -> Optional[CursorResult]:
+ result = super()._exec(construct, *args, **kw)
+ if self.as_sql and self.batch_separator:
+ self.static_output(self.batch_separator)
+ return result
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ if rendered_metadata_default is not None:
+ rendered_metadata_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_metadata_default
+ )
+
+ rendered_metadata_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_metadata_default
+ )
+
+ if rendered_inspector_default is not None:
+ rendered_inspector_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_inspector_default
+ )
+
+ rendered_inspector_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_inspector_default
+ )
+
+ rendered_inspector_default = rendered_inspector_default.strip()
+ return rendered_inspector_default != rendered_metadata_default
+
+ def emit_begin(self) -> None:
+ self._exec("SET TRANSACTION READ WRITE")
+
+ def emit_commit(self) -> None:
+ self._exec("COMMIT")
+
+
+@compiles(AddColumn, "oracle")
+def visit_add_column(
+ element: AddColumn, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ add_column(compiler, element.column, **kw),
+ )
+
+
+@compiles(ColumnNullable, "oracle")
+def visit_column_nullable(
+ element: ColumnNullable, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "NULL" if element.nullable else "NOT NULL",
+ )
+
+
+@compiles(ColumnType, "oracle")
+def visit_column_type(
+ element: ColumnType, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "%s" % format_type(compiler, element.type_),
+ )
+
+
+@compiles(ColumnName, "oracle")
+def visit_column_name(
+ element: ColumnName, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s RENAME COLUMN %s TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ )
+
+
+@compiles(ColumnDefault, "oracle")
+def visit_column_default(
+ element: ColumnDefault, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "DEFAULT %s" % format_server_default(compiler, element.default)
+ if element.default is not None
+ else "DEFAULT NULL",
+ )
+
+
+@compiles(ColumnComment, "oracle")
+def visit_column_comment(
+ element: ColumnComment, compiler: OracleDDLCompiler, **kw
+) -> str:
+ ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}"
+
+ comment = compiler.sql_compiler.render_literal_value(
+ (element.comment if element.comment is not None else ""),
+ sqltypes.String(),
+ )
+
+ return ddl.format(
+ table_name=element.table_name,
+ column_name=element.column_name,
+ comment=comment,
+ )
+
+
+@compiles(RenameTable, "oracle")
+def visit_rename_table(
+ element: RenameTable, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
+
+
+def alter_column(compiler: OracleDDLCompiler, name: str) -> str:
+ return "MODIFY %s" % format_column_name(compiler, name)
+
+
+def add_column(compiler: OracleDDLCompiler, column: Column[Any], **kw) -> str:
+ return "ADD %s" % compiler.get_column_specification(column, **kw)
+
+
+@compiles(IdentityColumnDefault, "oracle")
+def visit_identity_column(
+ element: IdentityColumnDefault, compiler: OracleDDLCompiler, **kw
+):
+ text = "%s %s " % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ )
+ if element.default is None:
+ # drop identity
+ text += "DROP IDENTITY"
+ return text
+ else:
+ text += compiler.visit_identity_column(element.default)
+ return text
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/postgresql.py b/venv/lib/python3.12/site-packages/alembic/ddl/postgresql.py
new file mode 100644
index 0000000..6507fcb
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/postgresql.py
@@ -0,0 +1,848 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import logging
+import re
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import Column
+from sqlalchemy import literal_column
+from sqlalchemy import Numeric
+from sqlalchemy import text
+from sqlalchemy import types as sqltypes
+from sqlalchemy.dialects.postgresql import BIGINT
+from sqlalchemy.dialects.postgresql import ExcludeConstraint
+from sqlalchemy.dialects.postgresql import INTEGER
+from sqlalchemy.schema import CreateIndex
+from sqlalchemy.sql.elements import ColumnClause
+from sqlalchemy.sql.elements import TextClause
+from sqlalchemy.sql.functions import FunctionElement
+from sqlalchemy.types import NULLTYPE
+
+from .base import alter_column
+from .base import alter_table
+from .base import AlterColumn
+from .base import ColumnComment
+from .base import format_column_name
+from .base import format_table_name
+from .base import format_type
+from .base import IdentityColumnDefault
+from .base import RenameTable
+from .impl import ComparisonResult
+from .impl import DefaultImpl
+from .. import util
+from ..autogenerate import render
+from ..operations import ops
+from ..operations import schemaobj
+from ..operations.base import BatchOperations
+from ..operations.base import Operations
+from ..util import sqla_compat
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy import Index
+ from sqlalchemy import UniqueConstraint
+ from sqlalchemy.dialects.postgresql.array import ARRAY
+ from sqlalchemy.dialects.postgresql.base import PGDDLCompiler
+ from sqlalchemy.dialects.postgresql.hstore import HSTORE
+ from sqlalchemy.dialects.postgresql.json import JSON
+ from sqlalchemy.dialects.postgresql.json import JSONB
+ from sqlalchemy.sql.elements import ClauseElement
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+ from ..autogenerate.api import AutogenContext
+ from ..autogenerate.render import _f_name
+ from ..runtime.migration import MigrationContext
+
+
+log = logging.getLogger(__name__)
+
+
+class PostgresqlImpl(DefaultImpl):
+ __dialect__ = "postgresql"
+ transactional_ddl = True
+ type_synonyms = DefaultImpl.type_synonyms + (
+ {"FLOAT", "DOUBLE PRECISION"},
+ )
+
+ def create_index(self, index: Index, **kw: Any) -> None:
+ # this likely defaults to None if not present, so get()
+ # should normally not return the default value. being
+ # defensive in any case
+ postgresql_include = index.kwargs.get("postgresql_include", None) or ()
+ for col in postgresql_include:
+ if col not in index.table.c: # type: ignore[union-attr]
+ index.table.append_column( # type: ignore[union-attr]
+ Column(col, sqltypes.NullType)
+ )
+ self._exec(CreateIndex(index, **kw))
+
+ def prep_table_for_batch(self, batch_impl, table):
+ for constraint in table.constraints:
+ if (
+ constraint.name is not None
+ and constraint.name in batch_impl.named_constraints
+ ):
+ self.drop_constraint(constraint)
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ # don't do defaults for SERIAL columns
+ if (
+ metadata_column.primary_key
+ and metadata_column is metadata_column.table._autoincrement_column
+ ):
+ return False
+
+ conn_col_default = rendered_inspector_default
+
+ defaults_equal = conn_col_default == rendered_metadata_default
+ if defaults_equal:
+ return False
+
+ if None in (
+ conn_col_default,
+ rendered_metadata_default,
+ metadata_column.server_default,
+ ):
+ return not defaults_equal
+
+ metadata_default = metadata_column.server_default.arg
+
+ if isinstance(metadata_default, str):
+ if not isinstance(inspector_column.type, Numeric):
+ metadata_default = re.sub(r"^'|'$", "", metadata_default)
+ metadata_default = f"'{metadata_default}'"
+
+ metadata_default = literal_column(metadata_default)
+
+ # run a real compare against the server
+ conn = self.connection
+ assert conn is not None
+ return not conn.scalar(
+ sqla_compat._select(
+ literal_column(conn_col_default) == metadata_default
+ )
+ )
+
+ def alter_column( # type:ignore[override]
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Union[_ServerDefault, Literal[False]] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ autoincrement: Optional[bool] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ existing_autoincrement: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ using = kw.pop("postgresql_using", None)
+
+ if using is not None and type_ is None:
+ raise util.CommandError(
+ "postgresql_using must be used with the type_ parameter"
+ )
+
+ if type_ is not None:
+ self._exec(
+ PostgresqlColumnType(
+ table_name,
+ column_name,
+ type_,
+ schema=schema,
+ using=using,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ )
+ )
+
+ super().alter_column(
+ table_name,
+ column_name,
+ nullable=nullable,
+ server_default=server_default,
+ name=name,
+ schema=schema,
+ autoincrement=autoincrement,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_autoincrement=existing_autoincrement,
+ **kw,
+ )
+
+ def autogen_column_reflect(self, inspector, table, column_info):
+ if column_info.get("default") and isinstance(
+ column_info["type"], (INTEGER, BIGINT)
+ ):
+ seq_match = re.match(
+ r"nextval\('(.+?)'::regclass\)", column_info["default"]
+ )
+ if seq_match:
+ info = sqla_compat._exec_on_inspector(
+ inspector,
+ text(
+ "select c.relname, a.attname "
+ "from pg_class as c join "
+ "pg_depend d on d.objid=c.oid and "
+ "d.classid='pg_class'::regclass and "
+ "d.refclassid='pg_class'::regclass "
+ "join pg_class t on t.oid=d.refobjid "
+ "join pg_attribute a on a.attrelid=t.oid and "
+ "a.attnum=d.refobjsubid "
+ "where c.relkind='S' and c.relname=:seqname"
+ ),
+ seqname=seq_match.group(1),
+ ).first()
+ if info:
+ seqname, colname = info
+ if colname == column_info["name"]:
+ log.info(
+ "Detected sequence named '%s' as "
+ "owned by integer column '%s(%s)', "
+ "assuming SERIAL and omitting",
+ seqname,
+ table.name,
+ colname,
+ )
+ # sequence, and the owner is this column,
+ # its a SERIAL - whack it!
+ del column_info["default"]
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ ):
+ doubled_constraints = {
+ index
+ for index in conn_indexes
+ if index.info.get("duplicates_constraint")
+ }
+
+ for ix in doubled_constraints:
+ conn_indexes.remove(ix)
+
+ if not sqla_compat.sqla_2:
+ self._skip_functional_indexes(metadata_indexes, conn_indexes)
+
+ # pg behavior regarding modifiers
+ # | # | compiled sql | returned sql | regexp. group is removed |
+ # | - | ---------------- | -----------------| ------------------------ |
+ # | 1 | nulls first | nulls first | - |
+ # | 2 | nulls last | | (? str:
+ expr = expr.lower().replace('"', "").replace("'", "")
+ if index.table is not None:
+ # should not be needed, since include_table=False is in compile
+ expr = expr.replace(f"{index.table.name.lower()}.", "")
+
+ if "::" in expr:
+ # strip :: cast. types can have spaces in them
+ expr = re.sub(r"(::[\w ]+\w)", "", expr)
+
+ while expr and expr[0] == "(" and expr[-1] == ")":
+ expr = expr[1:-1]
+
+ # NOTE: when parsing the connection expression this cleanup could
+ # be skipped
+ for rs in self._default_modifiers_re:
+ if match := rs.search(expr):
+ start, end = match.span(1)
+ expr = expr[:start] + expr[end:]
+ break
+
+ while expr and expr[0] == "(" and expr[-1] == ")":
+ expr = expr[1:-1]
+
+ # strip casts
+ cast_re = re.compile(r"cast\s*\(")
+ if cast_re.match(expr):
+ expr = cast_re.sub("", expr)
+ # remove the as type
+ expr = re.sub(r"as\s+[^)]+\)", "", expr)
+ # remove spaces
+ expr = expr.replace(" ", "")
+ return expr
+
+ def _dialect_options(
+ self, item: Union[Index, UniqueConstraint]
+ ) -> Tuple[Any, ...]:
+ # only the positive case is returned by sqlalchemy reflection so
+ # None and False are threated the same
+ if item.dialect_kwargs.get("postgresql_nulls_not_distinct"):
+ return ("nulls_not_distinct",)
+ return ()
+
+ def compare_indexes(
+ self,
+ metadata_index: Index,
+ reflected_index: Index,
+ ) -> ComparisonResult:
+ msg = []
+ unique_msg = self._compare_index_unique(
+ metadata_index, reflected_index
+ )
+ if unique_msg:
+ msg.append(unique_msg)
+ m_exprs = metadata_index.expressions
+ r_exprs = reflected_index.expressions
+ if len(m_exprs) != len(r_exprs):
+ msg.append(f"expression number {len(r_exprs)} to {len(m_exprs)}")
+ if msg:
+ # no point going further, return early
+ return ComparisonResult.Different(msg)
+ skip = []
+ for pos, (m_e, r_e) in enumerate(zip(m_exprs, r_exprs), 1):
+ m_compile = self._compile_element(m_e)
+ m_text = self._cleanup_index_expr(metadata_index, m_compile)
+ # print(f"META ORIG: {m_compile!r} CLEANUP: {m_text!r}")
+ r_compile = self._compile_element(r_e)
+ r_text = self._cleanup_index_expr(metadata_index, r_compile)
+ # print(f"CONN ORIG: {r_compile!r} CLEANUP: {r_text!r}")
+ if m_text == r_text:
+ continue # expressions these are equal
+ elif m_compile.strip().endswith("_ops") and (
+ " " in m_compile or ")" in m_compile # is an expression
+ ):
+ skip.append(
+ f"expression #{pos} {m_compile!r} detected "
+ "as including operator clause."
+ )
+ util.warn(
+ f"Expression #{pos} {m_compile!r} in index "
+ f"{reflected_index.name!r} detected to include "
+ "an operator clause. Expression compare cannot proceed. "
+ "Please move the operator clause to the "
+ "``postgresql_ops`` dict to enable proper compare "
+ "of the index expressions: "
+ "https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#operator-classes", # noqa: E501
+ )
+ else:
+ msg.append(f"expression #{pos} {r_compile!r} to {m_compile!r}")
+
+ m_options = self._dialect_options(metadata_index)
+ r_options = self._dialect_options(reflected_index)
+ if m_options != r_options:
+ msg.extend(f"options {r_options} to {m_options}")
+
+ if msg:
+ return ComparisonResult.Different(msg)
+ elif skip:
+ # if there are other changes detected don't skip the index
+ return ComparisonResult.Skip(skip)
+ else:
+ return ComparisonResult.Equal()
+
+ def compare_unique_constraint(
+ self,
+ metadata_constraint: UniqueConstraint,
+ reflected_constraint: UniqueConstraint,
+ ) -> ComparisonResult:
+ metadata_tup = self._create_metadata_constraint_sig(
+ metadata_constraint
+ )
+ reflected_tup = self._create_reflected_constraint_sig(
+ reflected_constraint
+ )
+
+ meta_sig = metadata_tup.unnamed
+ conn_sig = reflected_tup.unnamed
+ if conn_sig != meta_sig:
+ return ComparisonResult.Different(
+ f"expression {conn_sig} to {meta_sig}"
+ )
+
+ metadata_do = self._dialect_options(metadata_tup.const)
+ conn_do = self._dialect_options(reflected_tup.const)
+ if metadata_do != conn_do:
+ return ComparisonResult.Different(
+ f"expression {conn_do} to {metadata_do}"
+ )
+
+ return ComparisonResult.Equal()
+
+ def adjust_reflected_dialect_options(
+ self, reflected_options: Dict[str, Any], kind: str
+ ) -> Dict[str, Any]:
+ options: Dict[str, Any]
+ options = reflected_options.get("dialect_options", {}).copy()
+ if not options.get("postgresql_include"):
+ options.pop("postgresql_include", None)
+ return options
+
+ def _compile_element(self, element: Union[ClauseElement, str]) -> str:
+ if isinstance(element, str):
+ return element
+ return element.compile(
+ dialect=self.dialect,
+ compile_kwargs={"literal_binds": True, "include_table": False},
+ ).string
+
+ def render_ddl_sql_expr(
+ self,
+ expr: ClauseElement,
+ is_server_default: bool = False,
+ is_index: bool = False,
+ **kw: Any,
+ ) -> str:
+ """Render a SQL expression that is typically a server default,
+ index expression, etc.
+
+ """
+
+ # apply self_group to index expressions;
+ # see https://github.com/sqlalchemy/sqlalchemy/blob/
+ # 82fa95cfce070fab401d020c6e6e4a6a96cc2578/
+ # lib/sqlalchemy/dialects/postgresql/base.py#L2261
+ if is_index and not isinstance(expr, ColumnClause):
+ expr = expr.self_group()
+
+ return super().render_ddl_sql_expr(
+ expr, is_server_default=is_server_default, is_index=is_index, **kw
+ )
+
+ def render_type(
+ self, type_: TypeEngine, autogen_context: AutogenContext
+ ) -> Union[str, Literal[False]]:
+ mod = type(type_).__module__
+ if not mod.startswith("sqlalchemy.dialects.postgresql"):
+ return False
+
+ if hasattr(self, "_render_%s_type" % type_.__visit_name__):
+ meth = getattr(self, "_render_%s_type" % type_.__visit_name__)
+ return meth(type_, autogen_context)
+
+ return False
+
+ def _render_HSTORE_type(
+ self, type_: HSTORE, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "text_type", r"(.+?\(.*text_type=)"
+ ),
+ )
+
+ def _render_ARRAY_type(
+ self, type_: ARRAY, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "item_type", r"(.+?\()"
+ ),
+ )
+
+ def _render_JSON_type(
+ self, type_: JSON, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "astext_type", r"(.+?\(.*astext_type=)"
+ ),
+ )
+
+ def _render_JSONB_type(
+ self, type_: JSONB, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "astext_type", r"(.+?\(.*astext_type=)"
+ ),
+ )
+
+
+class PostgresqlColumnType(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, type_: TypeEngine, **kw
+ ) -> None:
+ using = kw.pop("using", None)
+ super().__init__(name, column_name, **kw)
+ self.type_ = sqltypes.to_instance(type_)
+ self.using = using
+
+
+@compiles(RenameTable, "postgresql")
+def visit_rename_table(
+ element: RenameTable, compiler: PGDDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
+
+
+@compiles(PostgresqlColumnType, "postgresql")
+def visit_column_type(
+ element: PostgresqlColumnType, compiler: PGDDLCompiler, **kw
+) -> str:
+ return "%s %s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "TYPE %s" % format_type(compiler, element.type_),
+ "USING %s" % element.using if element.using else "",
+ )
+
+
+@compiles(ColumnComment, "postgresql")
+def visit_column_comment(
+ element: ColumnComment, compiler: PGDDLCompiler, **kw
+) -> str:
+ ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}"
+ comment = (
+ compiler.sql_compiler.render_literal_value(
+ element.comment, sqltypes.String()
+ )
+ if element.comment is not None
+ else "NULL"
+ )
+
+ return ddl.format(
+ table_name=format_table_name(
+ compiler, element.table_name, element.schema
+ ),
+ column_name=format_column_name(compiler, element.column_name),
+ comment=comment,
+ )
+
+
+@compiles(IdentityColumnDefault, "postgresql")
+def visit_identity_column(
+ element: IdentityColumnDefault, compiler: PGDDLCompiler, **kw
+):
+ text = "%s %s " % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ )
+ if element.default is None:
+ # drop identity
+ text += "DROP IDENTITY"
+ return text
+ elif element.existing_server_default is None:
+ # add identity options
+ text += "ADD "
+ text += compiler.visit_identity_column(element.default)
+ return text
+ else:
+ # alter identity
+ diff, _, _ = element.impl._compare_identity_default(
+ element.default, element.existing_server_default
+ )
+ identity = element.default
+ for attr in sorted(diff):
+ if attr == "always":
+ text += "SET GENERATED %s " % (
+ "ALWAYS" if identity.always else "BY DEFAULT"
+ )
+ else:
+ text += "SET %s " % compiler.get_identity_options(
+ sqla_compat.Identity(**{attr: getattr(identity, attr)})
+ )
+ return text
+
+
+@Operations.register_operation("create_exclude_constraint")
+@BatchOperations.register_operation(
+ "create_exclude_constraint", "batch_create_exclude_constraint"
+)
+@ops.AddConstraintOp.register_add_constraint("exclude_constraint")
+class CreateExcludeConstraintOp(ops.AddConstraintOp):
+ """Represent a create exclude constraint operation."""
+
+ constraint_type = "exclude"
+
+ def __init__(
+ self,
+ constraint_name: sqla_compat._ConstraintName,
+ table_name: Union[str, quoted_name],
+ elements: Union[
+ Sequence[Tuple[str, str]],
+ Sequence[Tuple[ColumnClause[Any], str]],
+ ],
+ where: Optional[Union[ColumnElement[bool], str]] = None,
+ schema: Optional[str] = None,
+ _orig_constraint: Optional[ExcludeConstraint] = None,
+ **kw,
+ ) -> None:
+ self.constraint_name = constraint_name
+ self.table_name = table_name
+ self.elements = elements
+ self.where = where
+ self.schema = schema
+ self._orig_constraint = _orig_constraint
+ self.kw = kw
+
+ @classmethod
+ def from_constraint( # type:ignore[override]
+ cls, constraint: ExcludeConstraint
+ ) -> CreateExcludeConstraintOp:
+ constraint_table = sqla_compat._table_for_constraint(constraint)
+ return cls(
+ constraint.name,
+ constraint_table.name,
+ [ # type: ignore
+ (expr, op) for expr, name, op in constraint._render_exprs
+ ],
+ where=cast("ColumnElement[bool] | None", constraint.where),
+ schema=constraint_table.schema,
+ _orig_constraint=constraint,
+ deferrable=constraint.deferrable,
+ initially=constraint.initially,
+ using=constraint.using,
+ )
+
+ def to_constraint(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> ExcludeConstraint:
+ if self._orig_constraint is not None:
+ return self._orig_constraint
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+ t = schema_obj.table(self.table_name, schema=self.schema)
+ excl = ExcludeConstraint(
+ *self.elements,
+ name=self.constraint_name,
+ where=self.where,
+ **self.kw,
+ )
+ for (
+ expr,
+ name,
+ oper,
+ ) in excl._render_exprs:
+ t.append_column(Column(name, NULLTYPE))
+ t.append_constraint(excl)
+ return excl
+
+ @classmethod
+ def create_exclude_constraint(
+ cls,
+ operations: Operations,
+ constraint_name: str,
+ table_name: str,
+ *elements: Any,
+ **kw: Any,
+ ) -> Optional[Table]:
+ """Issue an alter to create an EXCLUDE constraint using the
+ current migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_exclude_constraint(
+ "user_excl",
+ "user",
+ ("period", "&&"),
+ ("group", "="),
+ where=("group != 'some group'"),
+ )
+
+ Note that the expressions work the same way as that of
+ the ``ExcludeConstraint`` object itself; if plain strings are
+ passed, quoting rules must be applied manually.
+
+ :param name: Name of the constraint.
+ :param table_name: String name of the source table.
+ :param elements: exclude conditions.
+ :param where: SQL expression or SQL string with optional WHERE
+ clause.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within.
+
+ """
+ op = cls(constraint_name, table_name, elements, **kw)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_exclude_constraint(
+ cls,
+ operations: BatchOperations,
+ constraint_name: str,
+ *elements: Any,
+ **kw: Any,
+ ) -> Optional[Table]:
+ """Issue a "create exclude constraint" instruction using the
+ current batch migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ .. seealso::
+
+ :meth:`.Operations.create_exclude_constraint`
+
+ """
+ kw["schema"] = operations.impl.schema
+ op = cls(constraint_name, operations.impl.table_name, elements, **kw)
+ return operations.invoke(op)
+
+
+@render.renderers.dispatch_for(CreateExcludeConstraintOp)
+def _add_exclude_constraint(
+ autogen_context: AutogenContext, op: CreateExcludeConstraintOp
+) -> str:
+ return _exclude_constraint(op.to_constraint(), autogen_context, alter=True)
+
+
+@render._constraint_renderers.dispatch_for(ExcludeConstraint)
+def _render_inline_exclude_constraint(
+ constraint: ExcludeConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: MetaData,
+) -> str:
+ rendered = render._user_defined_render(
+ "exclude", constraint, autogen_context
+ )
+ if rendered is not False:
+ return rendered
+
+ return _exclude_constraint(constraint, autogen_context, False)
+
+
+def _postgresql_autogenerate_prefix(autogen_context: AutogenContext) -> str:
+ imports = autogen_context.imports
+ if imports is not None:
+ imports.add("from sqlalchemy.dialects import postgresql")
+ return "postgresql."
+
+
+def _exclude_constraint(
+ constraint: ExcludeConstraint,
+ autogen_context: AutogenContext,
+ alter: bool,
+) -> str:
+ opts: List[Tuple[str, Union[quoted_name, str, _f_name, None]]] = []
+
+ has_batch = autogen_context._has_batch
+
+ if constraint.deferrable:
+ opts.append(("deferrable", str(constraint.deferrable)))
+ if constraint.initially:
+ opts.append(("initially", str(constraint.initially)))
+ if constraint.using:
+ opts.append(("using", str(constraint.using)))
+ if not has_batch and alter and constraint.table.schema:
+ opts.append(("schema", render._ident(constraint.table.schema)))
+ if not alter and constraint.name:
+ opts.append(
+ ("name", render._render_gen_name(autogen_context, constraint.name))
+ )
+
+ def do_expr_where_opts():
+ args = [
+ "(%s, %r)"
+ % (
+ _render_potential_column(
+ sqltext, # type:ignore[arg-type]
+ autogen_context,
+ ),
+ opstring,
+ )
+ for sqltext, name, opstring in constraint._render_exprs
+ ]
+ if constraint.where is not None:
+ args.append(
+ "where=%s"
+ % render._render_potential_expr(
+ constraint.where, autogen_context
+ )
+ )
+ args.extend(["%s=%r" % (k, v) for k, v in opts])
+ return args
+
+ if alter:
+ args = [
+ repr(render._render_gen_name(autogen_context, constraint.name))
+ ]
+ if not has_batch:
+ args += [repr(render._ident(constraint.table.name))]
+ args.extend(do_expr_where_opts())
+ return "%(prefix)screate_exclude_constraint(%(args)s)" % {
+ "prefix": render._alembic_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+ else:
+ args = do_expr_where_opts()
+ return "%(prefix)sExcludeConstraint(%(args)s)" % {
+ "prefix": _postgresql_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+
+
+def _render_potential_column(
+ value: Union[
+ ColumnClause[Any], Column[Any], TextClause, FunctionElement[Any]
+ ],
+ autogen_context: AutogenContext,
+) -> str:
+ if isinstance(value, ColumnClause):
+ if value.is_literal:
+ # like literal_column("int8range(from, to)") in ExcludeConstraint
+ template = "%(prefix)sliteral_column(%(name)r)"
+ else:
+ template = "%(prefix)scolumn(%(name)r)"
+
+ return template % {
+ "prefix": render._sqlalchemy_autogenerate_prefix(autogen_context),
+ "name": value.name,
+ }
+ else:
+ return render._render_potential_expr(
+ value,
+ autogen_context,
+ wrap_in_text=isinstance(value, (TextClause, FunctionElement)),
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/ddl/sqlite.py b/venv/lib/python3.12/site-packages/alembic/ddl/sqlite.py
new file mode 100644
index 0000000..762e8ca
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/ddl/sqlite.py
@@ -0,0 +1,225 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Dict
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import cast
+from sqlalchemy import JSON
+from sqlalchemy import schema
+from sqlalchemy import sql
+
+from .base import alter_table
+from .base import format_table_name
+from .base import RenameTable
+from .impl import DefaultImpl
+from .. import util
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine.reflection import Inspector
+ from sqlalchemy.sql.compiler import DDLCompiler
+ from sqlalchemy.sql.elements import Cast
+ from sqlalchemy.sql.elements import ClauseElement
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from ..operations.batch import BatchOperationsImpl
+
+
+class SQLiteImpl(DefaultImpl):
+ __dialect__ = "sqlite"
+
+ transactional_ddl = False
+ """SQLite supports transactional DDL, but pysqlite does not:
+ see: http://bugs.python.org/issue10740
+ """
+
+ def requires_recreate_in_batch(
+ self, batch_op: BatchOperationsImpl
+ ) -> bool:
+ """Return True if the given :class:`.BatchOperationsImpl`
+ would need the table to be recreated and copied in order to
+ proceed.
+
+ Normally, only returns True on SQLite when operations other
+ than add_column are present.
+
+ """
+ for op in batch_op.batch:
+ if op[0] == "add_column":
+ col = op[1][1]
+ if isinstance(
+ col.server_default, schema.DefaultClause
+ ) and isinstance(col.server_default.arg, sql.ClauseElement):
+ return True
+ elif (
+ isinstance(col.server_default, util.sqla_compat.Computed)
+ and col.server_default.persisted
+ ):
+ return True
+ elif op[0] not in ("create_index", "drop_index"):
+ return True
+ else:
+ return False
+
+ def add_constraint(self, const: Constraint):
+ # attempt to distinguish between an
+ # auto-gen constraint and an explicit one
+ if const._create_rule is None:
+ raise NotImplementedError(
+ "No support for ALTER of constraints in SQLite dialect. "
+ "Please refer to the batch mode feature which allows for "
+ "SQLite migrations using a copy-and-move strategy."
+ )
+ elif const._create_rule(self):
+ util.warn(
+ "Skipping unsupported ALTER for "
+ "creation of implicit constraint. "
+ "Please refer to the batch mode feature which allows for "
+ "SQLite migrations using a copy-and-move strategy."
+ )
+
+ def drop_constraint(self, const: Constraint):
+ if const._create_rule is None:
+ raise NotImplementedError(
+ "No support for ALTER of constraints in SQLite dialect. "
+ "Please refer to the batch mode feature which allows for "
+ "SQLite migrations using a copy-and-move strategy."
+ )
+
+ def compare_server_default(
+ self,
+ inspector_column: Column[Any],
+ metadata_column: Column[Any],
+ rendered_metadata_default: Optional[str],
+ rendered_inspector_default: Optional[str],
+ ) -> bool:
+ if rendered_metadata_default is not None:
+ rendered_metadata_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_metadata_default
+ )
+
+ rendered_metadata_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_metadata_default
+ )
+
+ if rendered_inspector_default is not None:
+ rendered_inspector_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_inspector_default
+ )
+
+ rendered_inspector_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_inspector_default
+ )
+
+ return rendered_inspector_default != rendered_metadata_default
+
+ def _guess_if_default_is_unparenthesized_sql_expr(
+ self, expr: Optional[str]
+ ) -> bool:
+ """Determine if a server default is a SQL expression or a constant.
+
+ There are too many assertions that expect server defaults to round-trip
+ identically without parenthesis added so we will add parens only in
+ very specific cases.
+
+ """
+ if not expr:
+ return False
+ elif re.match(r"^[0-9\.]$", expr):
+ return False
+ elif re.match(r"^'.+'$", expr):
+ return False
+ elif re.match(r"^\(.+\)$", expr):
+ return False
+ else:
+ return True
+
+ def autogen_column_reflect(
+ self,
+ inspector: Inspector,
+ table: Table,
+ column_info: Dict[str, Any],
+ ) -> None:
+ # SQLite expression defaults require parenthesis when sent
+ # as DDL
+ if self._guess_if_default_is_unparenthesized_sql_expr(
+ column_info.get("default", None)
+ ):
+ column_info["default"] = "(%s)" % (column_info["default"],)
+
+ def render_ddl_sql_expr(
+ self, expr: ClauseElement, is_server_default: bool = False, **kw
+ ) -> str:
+ # SQLite expression defaults require parenthesis when sent
+ # as DDL
+ str_expr = super().render_ddl_sql_expr(
+ expr, is_server_default=is_server_default, **kw
+ )
+
+ if (
+ is_server_default
+ and self._guess_if_default_is_unparenthesized_sql_expr(str_expr)
+ ):
+ str_expr = "(%s)" % (str_expr,)
+ return str_expr
+
+ def cast_for_batch_migrate(
+ self,
+ existing: Column[Any],
+ existing_transfer: Dict[str, Union[TypeEngine, Cast]],
+ new_type: TypeEngine,
+ ) -> None:
+ if (
+ existing.type._type_affinity is not new_type._type_affinity
+ and not isinstance(new_type, JSON)
+ ):
+ existing_transfer["expr"] = cast(
+ existing_transfer["expr"], new_type
+ )
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ ):
+ self._skip_functional_indexes(metadata_indexes, conn_indexes)
+
+
+@compiles(RenameTable, "sqlite")
+def visit_rename_table(
+ element: RenameTable, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
+
+
+# @compiles(AddColumn, 'sqlite')
+# def visit_add_column(element, compiler, **kw):
+# return "%s %s" % (
+# alter_table(compiler, element.table_name, element.schema),
+# add_column(compiler, element.column, **kw)
+# )
+
+
+# def add_column(compiler, column, **kw):
+# text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
+# need to modify SQLAlchemy so that the CHECK associated with a Boolean
+# or Enum gets placed as part of the column constraints, not the Table
+# see ticket 98
+# for const in column.constraints:
+# text += compiler.process(AddConstraint(const))
+# return text
diff --git a/venv/lib/python3.12/site-packages/alembic/environment.py b/venv/lib/python3.12/site-packages/alembic/environment.py
new file mode 100644
index 0000000..adfc93e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/environment.py
@@ -0,0 +1 @@
+from .runtime.environment import * # noqa
diff --git a/venv/lib/python3.12/site-packages/alembic/migration.py b/venv/lib/python3.12/site-packages/alembic/migration.py
new file mode 100644
index 0000000..02626e2
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/migration.py
@@ -0,0 +1 @@
+from .runtime.migration import * # noqa
diff --git a/venv/lib/python3.12/site-packages/alembic/op.py b/venv/lib/python3.12/site-packages/alembic/op.py
new file mode 100644
index 0000000..f3f5fac
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/op.py
@@ -0,0 +1,5 @@
+from .operations.base import Operations
+
+# create proxy functions for
+# each method on the Operations class.
+Operations.create_module_class_proxy(globals(), locals())
diff --git a/venv/lib/python3.12/site-packages/alembic/op.pyi b/venv/lib/python3.12/site-packages/alembic/op.pyi
new file mode 100644
index 0000000..83deac1
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/op.pyi
@@ -0,0 +1,1321 @@
+# ### this file stubs are generated by tools/write_pyi.py - do not edit ###
+# ### imports are manually managed
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import Any
+from typing import Awaitable
+from typing import Callable
+from typing import Dict
+from typing import Iterator
+from typing import List
+from typing import Literal
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import conv
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.expression import TableClause
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Computed
+ from sqlalchemy.sql.schema import Identity
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.type_api import TypeEngine
+ from sqlalchemy.util import immutabledict
+
+ from .operations.base import BatchOperations
+ from .operations.ops import AddColumnOp
+ from .operations.ops import AddConstraintOp
+ from .operations.ops import AlterColumnOp
+ from .operations.ops import AlterTableOp
+ from .operations.ops import BulkInsertOp
+ from .operations.ops import CreateIndexOp
+ from .operations.ops import CreateTableCommentOp
+ from .operations.ops import CreateTableOp
+ from .operations.ops import DropColumnOp
+ from .operations.ops import DropConstraintOp
+ from .operations.ops import DropIndexOp
+ from .operations.ops import DropTableCommentOp
+ from .operations.ops import DropTableOp
+ from .operations.ops import ExecuteSQLOp
+ from .operations.ops import MigrateOperation
+ from .runtime.migration import MigrationContext
+ from .util.sqla_compat import _literal_bindparam
+
+_T = TypeVar("_T")
+_C = TypeVar("_C", bound=Callable[..., Any])
+
+### end imports ###
+
+def add_column(
+ table_name: str, column: Column[Any], *, schema: Optional[str] = None
+) -> None:
+ """Issue an "add column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy import Column, String
+
+ op.add_column("organization", Column("name", String()))
+
+ The :meth:`.Operations.add_column` method typically corresponds
+ to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope
+ of this command, the column's name, datatype, nullability,
+ and optional server-generated defaults may be indicated.
+
+ .. note::
+
+ With the exception of NOT NULL constraints or single-column FOREIGN
+ KEY constraints, other kinds of constraints such as PRIMARY KEY,
+ UNIQUE or CHECK constraints **cannot** be generated using this
+ method; for these constraints, refer to operations such as
+ :meth:`.Operations.create_primary_key` and
+ :meth:`.Operations.create_check_constraint`. In particular, the
+ following :class:`~sqlalchemy.schema.Column` parameters are
+ **ignored**:
+
+ * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases
+ typically do not support an ALTER operation that can add
+ individual columns one at a time to an existing primary key
+ constraint, therefore it's less ambiguous to use the
+ :meth:`.Operations.create_primary_key` method, which assumes no
+ existing primary key constraint is present.
+ * :paramref:`~sqlalchemy.schema.Column.unique` - use the
+ :meth:`.Operations.create_unique_constraint` method
+ * :paramref:`~sqlalchemy.schema.Column.index` - use the
+ :meth:`.Operations.create_index` method
+
+
+ The provided :class:`~sqlalchemy.schema.Column` object may include a
+ :class:`~sqlalchemy.schema.ForeignKey` constraint directive,
+ referencing a remote table name. For this specific type of constraint,
+ Alembic will automatically emit a second ALTER statement in order to
+ add the single-column FOREIGN KEY constraint separately::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ )
+
+ The column argument passed to :meth:`.Operations.add_column` is a
+ :class:`~sqlalchemy.schema.Column` construct, used in the same way it's
+ used in SQLAlchemy. In particular, values or functions to be indicated
+ as producing the column's default value on the database side are
+ specified using the ``server_default`` parameter, and not ``default``
+ which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the column add
+ op.add_column(
+ "account",
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ :param table_name: String name of the parent table.
+ :param column: a :class:`sqlalchemy.schema.Column` object
+ representing the new column.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def alter_column(
+ table_name: str,
+ column_name: str,
+ *,
+ nullable: Optional[bool] = None,
+ comment: Union[str, Literal[False], None] = False,
+ server_default: Any = False,
+ new_column_name: Optional[str] = None,
+ type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None,
+ existing_type: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None,
+ existing_server_default: Union[
+ str, bool, Identity, Computed, None
+ ] = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> None:
+ r"""Issue an "alter column" instruction using the
+ current migration context.
+
+ Generally, only that aspect of the column which
+ is being changed, i.e. name, type, nullability,
+ default, needs to be specified. Multiple changes
+ can also be specified at once and the backend should
+ "do the right thing", emitting each change either
+ separately or together as the backend allows.
+
+ MySQL has special requirements here, since MySQL
+ cannot ALTER a column without a full specification.
+ When producing MySQL-compatible migration files,
+ it is recommended that the ``existing_type``,
+ ``existing_server_default``, and ``existing_nullable``
+ parameters be present, if not being altered.
+
+ Type changes which are against the SQLAlchemy
+ "schema" types :class:`~sqlalchemy.types.Boolean`
+ and :class:`~sqlalchemy.types.Enum` may also
+ add or drop constraints which accompany those
+ types on backends that don't support them natively.
+ The ``existing_type`` argument is
+ used in this case to identify and remove a previous
+ constraint that was bound to the type object.
+
+ :param table_name: string name of the target table.
+ :param column_name: string name of the target column,
+ as it exists before the operation begins.
+ :param nullable: Optional; specify ``True`` or ``False``
+ to alter the column's nullability.
+ :param server_default: Optional; specify a string
+ SQL expression, :func:`~sqlalchemy.sql.expression.text`,
+ or :class:`~sqlalchemy.schema.DefaultClause` to indicate
+ an alteration to the column's default value.
+ Set to ``None`` to have the default removed.
+ :param comment: optional string text of a new comment to add to the
+ column.
+ :param new_column_name: Optional; specify a string name here to
+ indicate the new name within a column rename operation.
+ :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify a change to the column's type.
+ For SQLAlchemy types that also indicate a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`),
+ the constraint is also generated.
+ :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column;
+ currently understood by the MySQL dialect.
+ :param existing_type: Optional; a
+ :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify the previous type. This
+ is required for all MySQL column alter operations that
+ don't otherwise specify a new type, as well as for
+ when nullability is being changed on a SQL Server
+ column. It is also used if the type is a so-called
+ SQLAlchemy "schema" type which may define a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`,
+ :class:`~sqlalchemy.types.Enum`),
+ so that the constraint can be dropped.
+ :param existing_server_default: Optional; The existing
+ default value of the column. Required on MySQL if
+ an existing default is not being changed; else MySQL
+ removes the default.
+ :param existing_nullable: Optional; the existing nullability
+ of the column. Required on MySQL if the existing nullability
+ is not being changed; else MySQL sets this to NULL.
+ :param existing_autoincrement: Optional; the existing autoincrement
+ of the column. Used for MySQL's system of altering a column
+ that specifies ``AUTO_INCREMENT``.
+ :param existing_comment: string text of the existing comment on the
+ column to be maintained. Required on MySQL if the existing comment
+ on the column is not being changed.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param postgresql_using: String argument which will indicate a
+ SQL expression to render within the Postgresql-specific USING clause
+ within ALTER COLUMN. This string is taken directly as raw SQL which
+ must explicitly include any necessary quoting or escaping of tokens
+ within the expression.
+
+ """
+
+@contextmanager
+def batch_alter_table(
+ table_name: str,
+ schema: Optional[str] = None,
+ recreate: Literal["auto", "always", "never"] = "auto",
+ partial_reordering: Optional[Tuple[Any, ...]] = None,
+ copy_from: Optional[Table] = None,
+ table_args: Tuple[Any, ...] = (),
+ table_kwargs: Mapping[str, Any] = immutabledict({}),
+ reflect_args: Tuple[Any, ...] = (),
+ reflect_kwargs: Mapping[str, Any] = immutabledict({}),
+ naming_convention: Optional[Dict[str, str]] = None,
+) -> Iterator[BatchOperations]:
+ """Invoke a series of per-table migrations in batch.
+
+ Batch mode allows a series of operations specific to a table
+ to be syntactically grouped together, and allows for alternate
+ modes of table migration, in particular the "recreate" style of
+ migration required by SQLite.
+
+ "recreate" style is as follows:
+
+ 1. A new table is created with the new specification, based on the
+ migration directives within the batch, using a temporary name.
+
+ 2. the data copied from the existing table to the new table.
+
+ 3. the existing table is dropped.
+
+ 4. the new table is renamed to the existing table name.
+
+ The directive by default will only use "recreate" style on the
+ SQLite backend, and only if directives are present which require
+ this form, e.g. anything other than ``add_column()``. The batch
+ operation on other backends will proceed using standard ALTER TABLE
+ operations.
+
+ The method is used as a context manager, which returns an instance
+ of :class:`.BatchOperations`; this object is the same as
+ :class:`.Operations` except that table names and schema names
+ are omitted. E.g.::
+
+ with op.batch_alter_table("some_table") as batch_op:
+ batch_op.add_column(Column("foo", Integer))
+ batch_op.drop_column("bar")
+
+ The operations within the context manager are invoked at once
+ when the context is ended. When run against SQLite, if the
+ migrations include operations not supported by SQLite's ALTER TABLE,
+ the entire table will be copied to a new one with the new
+ specification, moving all data across as well.
+
+ The copy operation by default uses reflection to retrieve the current
+ structure of the table, and therefore :meth:`.batch_alter_table`
+ in this mode requires that the migration is run in "online" mode.
+ The ``copy_from`` parameter may be passed which refers to an existing
+ :class:`.Table` object, which will bypass this reflection step.
+
+ .. note:: The table copy operation will currently not copy
+ CHECK constraints, and may not copy UNIQUE constraints that are
+ unnamed, as is possible on SQLite. See the section
+ :ref:`sqlite_batch_constraints` for workarounds.
+
+ :param table_name: name of table
+ :param schema: optional schema name.
+ :param recreate: under what circumstances the table should be
+ recreated. At its default of ``"auto"``, the SQLite dialect will
+ recreate the table if any operations other than ``add_column()``,
+ ``create_index()``, or ``drop_index()`` are
+ present. Other options include ``"always"`` and ``"never"``.
+ :param copy_from: optional :class:`~sqlalchemy.schema.Table` object
+ that will act as the structure of the table being copied. If omitted,
+ table reflection is used to retrieve the structure of the table.
+
+ .. seealso::
+
+ :ref:`batch_offline_mode`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_args`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_kwargs`
+
+ :param reflect_args: a sequence of additional positional arguments that
+ will be applied to the table structure being reflected / copied;
+ this may be used to pass column and constraint overrides to the
+ table that will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param reflect_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the table structure being copied; this may be
+ used to pass additional table and reflection options to the table that
+ will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param table_args: a sequence of additional positional arguments that
+ will be applied to the new :class:`~sqlalchemy.schema.Table` when
+ created, in addition to those copied from the source table.
+ This may be used to provide additional constraints such as CHECK
+ constraints that may not be reflected.
+ :param table_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the new :class:`~sqlalchemy.schema.Table`
+ when created, in addition to those copied from the source table.
+ This may be used to provide for additional table options that may
+ not be reflected.
+ :param naming_convention: a naming convention dictionary of the form
+ described at :ref:`autogen_naming_conventions` which will be applied
+ to the :class:`~sqlalchemy.schema.MetaData` during the reflection
+ process. This is typically required if one wants to drop SQLite
+ constraints, as these constraints will not have names when
+ reflected on this backend. Requires SQLAlchemy **0.9.4** or greater.
+
+ .. seealso::
+
+ :ref:`dropping_sqlite_foreign_keys`
+
+ :param partial_reordering: a list of tuples, each suggesting a desired
+ ordering of two or more columns in the newly created table. Requires
+ that :paramref:`.batch_alter_table.recreate` is set to ``"always"``.
+ Examples, given a table with columns "a", "b", "c", and "d":
+
+ Specify the order of all columns::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("c", "d", "a", "b")],
+ ) as batch_op:
+ pass
+
+ Ensure "d" appears before "c", and "b", appears before "a"::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("d", "c"), ("b", "a")],
+ ) as batch_op:
+ pass
+
+ The ordering of columns not included in the partial_reordering
+ set is undefined. Therefore it is best to specify the complete
+ ordering of all columns for best results.
+
+ .. note:: batch mode requires SQLAlchemy 0.8 or above.
+
+ .. seealso::
+
+ :ref:`batch_migrations`
+
+ """
+
+def bulk_insert(
+ table: Union[Table, TableClause],
+ rows: List[Dict[str, Any]],
+ *,
+ multiinsert: bool = True,
+) -> None:
+ """Issue a "bulk insert" operation using the current
+ migration context.
+
+ This provides a means of representing an INSERT of multiple rows
+ which works equally well in the context of executing on a live
+ connection as well as that of generating a SQL script. In the
+ case of a SQL script, the values are rendered inline into the
+ statement.
+
+ e.g.::
+
+ from alembic import op
+ from datetime import date
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String, Integer, Date
+
+ # Create an ad-hoc table to use for the insert statement.
+ accounts_table = table(
+ "account",
+ column("id", Integer),
+ column("name", String),
+ column("create_date", Date),
+ )
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": date(2010, 10, 5),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": date(2007, 5, 27),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": date(2008, 8, 15),
+ },
+ ],
+ )
+
+ When using --sql mode, some datatypes may not render inline
+ automatically, such as dates and other special types. When this
+ issue is present, :meth:`.Operations.inline_literal` may be used::
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": op.inline_literal("2010-10-05"),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": op.inline_literal("2007-05-27"),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": op.inline_literal("2008-08-15"),
+ },
+ ],
+ multiinsert=False,
+ )
+
+ When using :meth:`.Operations.inline_literal` in conjunction with
+ :meth:`.Operations.bulk_insert`, in order for the statement to work
+ in "online" (e.g. non --sql) mode, the
+ :paramref:`~.Operations.bulk_insert.multiinsert`
+ flag should be set to ``False``, which will have the effect of
+ individual INSERT statements being emitted to the database, each
+ with a distinct VALUES clause, so that the "inline" values can
+ still be rendered, rather than attempting to pass the values
+ as bound parameters.
+
+ :param table: a table object which represents the target of the INSERT.
+
+ :param rows: a list of dictionaries indicating rows.
+
+ :param multiinsert: when at its default of True and --sql mode is not
+ enabled, the INSERT statement will be executed using
+ "executemany()" style, where all elements in the list of
+ dictionaries are passed as bound parameters in a single
+ list. Setting this to False results in individual INSERT
+ statements being emitted per parameter set, and is needed
+ in those cases where non-literal values are present in the
+ parameter sets.
+
+ """
+
+def create_check_constraint(
+ constraint_name: Optional[str],
+ table_name: str,
+ condition: Union[str, ColumnElement[bool], TextClause],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> None:
+ """Issue a "create check constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy.sql import column, func
+
+ op.create_check_constraint(
+ "ck_user_name_len",
+ "user",
+ func.len(column("name")) > 5,
+ )
+
+ CHECK constraints are usually against a SQL expression, so ad-hoc
+ table metadata is usually needed. The function will convert the given
+ arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound
+ to an anonymous table in order to emit the CREATE statement.
+
+ :param name: Name of the check constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param condition: SQL expression that's the condition of the
+ constraint. Can be a string or SQLAlchemy expression language
+ structure.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def create_exclude_constraint(
+ constraint_name: str, table_name: str, *elements: Any, **kw: Any
+) -> Optional[Table]:
+ """Issue an alter to create an EXCLUDE constraint using the
+ current migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_exclude_constraint(
+ "user_excl",
+ "user",
+ ("period", "&&"),
+ ("group", "="),
+ where=("group != 'some group'"),
+ )
+
+ Note that the expressions work the same way as that of
+ the ``ExcludeConstraint`` object itself; if plain strings are
+ passed, quoting rules must be applied manually.
+
+ :param name: Name of the constraint.
+ :param table_name: String name of the source table.
+ :param elements: exclude conditions.
+ :param where: SQL expression or SQL string with optional WHERE
+ clause.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within.
+
+ """
+
+def create_foreign_key(
+ constraint_name: Optional[str],
+ source_table: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ *,
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ source_schema: Optional[str] = None,
+ referent_schema: Optional[str] = None,
+ **dialect_kw: Any,
+) -> None:
+ """Issue a "create foreign key" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_foreign_key(
+ "fk_user_address",
+ "address",
+ "user",
+ ["user_id"],
+ ["id"],
+ )
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.ForeignKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the foreign key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param source_table: String name of the source table.
+ :param referent_table: String name of the destination table.
+ :param local_cols: a list of string column names in the
+ source table.
+ :param remote_cols: a list of string column names in the
+ remote table.
+ :param onupdate: Optional string. If set, emit ON UPDATE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param ondelete: Optional string. If set, emit ON DELETE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or NOT
+ DEFERRABLE when issuing DDL for this constraint.
+ :param source_schema: Optional schema name of the source table.
+ :param referent_schema: Optional schema name of the destination table.
+
+ """
+
+def create_index(
+ index_name: Optional[str],
+ table_name: str,
+ columns: Sequence[Union[str, TextClause, Function[Any]]],
+ *,
+ schema: Optional[str] = None,
+ unique: bool = False,
+ if_not_exists: Optional[bool] = None,
+ **kw: Any,
+) -> None:
+ r"""Issue a "create index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_index("ik_test", "t1", ["foo", "bar"])
+
+ Functional indexes can be produced by using the
+ :func:`sqlalchemy.sql.expression.text` construct::
+
+ from alembic import op
+ from sqlalchemy import text
+
+ op.create_index("ik_test", "t1", [text("lower(foo)")])
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table.
+ :param columns: a list consisting of string column names and/or
+ :func:`~sqlalchemy.sql.expression.text` constructs.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param unique: If True, create a unique index.
+
+ :param quote: Force quoting of this column's name on or off,
+ corresponding to ``True`` or ``False``. When left at its default
+ of ``None``, the column identifier will be quoted according to
+ whether the name is case sensitive (identifiers with at least one
+ upper case character are treated as case sensitive), or if it's a
+ reserved word. This flag is only needed to force quoting of a
+ reserved word which is not known by the SQLAlchemy dialect.
+
+ :param if_not_exists: If True, adds IF NOT EXISTS operator when
+ creating the new index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """
+
+def create_primary_key(
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: List[str],
+ *,
+ schema: Optional[str] = None,
+) -> None:
+ """Issue a "create primary key" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_primary_key("pk_my_table", "my_table", ["id", "version"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.PrimaryKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the primary key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the target table.
+ :param columns: a list of string column names to be applied to the
+ primary key constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def create_table(table_name: str, *columns: SchemaItem, **kw: Any) -> Table:
+ r"""Issue a "create table" instruction using the current migration
+ context.
+
+ This directive receives an argument list similar to that of the
+ traditional :class:`sqlalchemy.schema.Table` construct, but without the
+ metadata::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ Note that :meth:`.create_table` accepts
+ :class:`~sqlalchemy.schema.Column`
+ constructs directly from the SQLAlchemy library. In particular,
+ default values to be created on the database side are
+ specified using the ``server_default`` parameter, and not
+ ``default`` which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the "timestamp" column
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ The function also returns a newly created
+ :class:`~sqlalchemy.schema.Table` object, corresponding to the table
+ specification given, which is suitable for
+ immediate SQL operations, in particular
+ :meth:`.Operations.bulk_insert`::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ account_table = op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ op.bulk_insert(
+ account_table,
+ [
+ {"name": "A1", "description": "account 1"},
+ {"name": "A2", "description": "account 2"},
+ ],
+ )
+
+ :param table_name: Name of the table
+ :param \*columns: collection of :class:`~sqlalchemy.schema.Column`
+ objects within
+ the table, as well as optional :class:`~sqlalchemy.schema.Constraint`
+ objects
+ and :class:`~.sqlalchemy.schema.Index` objects.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ :return: the :class:`~sqlalchemy.schema.Table` object corresponding
+ to the parameters given.
+
+ """
+
+def create_table_comment(
+ table_name: str,
+ comment: Optional[str],
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+) -> None:
+ """Emit a COMMENT ON operation to set the comment for a table.
+
+ :param table_name: string name of the target table.
+ :param comment: string value of the comment being registered against
+ the specified table.
+ :param existing_comment: String value of a comment
+ already registered on the specified table, used within autogenerate
+ so that the operation is reversible, but not required for direct
+ use.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """
+
+def create_unique_constraint(
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: Sequence[str],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> Any:
+ """Issue a "create unique constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ op.create_unique_constraint("uq_user_name", "user", ["name"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.UniqueConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param name: Name of the unique constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param columns: a list of string column names in the
+ source table.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def drop_column(
+ table_name: str,
+ column_name: str,
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> None:
+ """Issue a "drop column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_column("organization", "account_id")
+
+ :param table_name: name of table
+ :param column_name: name of column
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param mssql_drop_check: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the CHECK constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.check_constraints,
+ then exec's a separate DROP CONSTRAINT for that constraint.
+ :param mssql_drop_default: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the DEFAULT constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.default_constraints,
+ then exec's a separate DROP CONSTRAINT for that default.
+ :param mssql_drop_foreign_key: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop a single FOREIGN KEY constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from
+ sys.foreign_keys/sys.foreign_key_columns,
+ then exec's a separate DROP CONSTRAINT for that default. Only
+ works if the column has exactly one FK constraint which refers to
+ it, at the moment.
+
+ """
+
+def drop_constraint(
+ constraint_name: str,
+ table_name: str,
+ type_: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+) -> None:
+ r"""Drop a constraint of the given name, typically via DROP CONSTRAINT.
+
+ :param constraint_name: name of the constraint.
+ :param table_name: table name.
+ :param type\_: optional, required on MySQL. can be
+ 'foreignkey', 'primary', 'unique', or 'check'.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def drop_index(
+ index_name: str,
+ table_name: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ if_exists: Optional[bool] = None,
+ **kw: Any,
+) -> None:
+ r"""Issue a "drop index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_index("accounts")
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table. Some
+ backends such as Microsoft SQL Server require this.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ :param if_exists: If True, adds IF EXISTS operator when
+ dropping the index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """
+
+def drop_table(
+ table_name: str, *, schema: Optional[str] = None, **kw: Any
+) -> None:
+ r"""Issue a "drop table" instruction using the current
+ migration context.
+
+
+ e.g.::
+
+ drop_table("accounts")
+
+ :param table_name: Name of the table
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ """
+
+def drop_table_comment(
+ table_name: str,
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+) -> None:
+ """Issue a "drop table comment" operation to
+ remove an existing comment set on a table.
+
+ :param table_name: string name of the target table.
+ :param existing_comment: An optional string value of a comment already
+ registered on the specified table.
+
+ .. seealso::
+
+ :meth:`.Operations.create_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """
+
+def execute(
+ sqltext: Union[Executable, str],
+ *,
+ execution_options: Optional[dict[str, Any]] = None,
+) -> None:
+ r"""Execute the given SQL using the current migration context.
+
+ The given SQL can be a plain string, e.g.::
+
+ op.execute("INSERT INTO table (foo) VALUES ('some value')")
+
+ Or it can be any kind of Core SQL Expression construct, such as
+ below where we use an update construct::
+
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String
+ from alembic import op
+
+ account = table("account", column("name", String))
+ op.execute(
+ account.update()
+ .where(account.c.name == op.inline_literal("account 1"))
+ .values({"name": op.inline_literal("account 2")})
+ )
+
+ Above, we made use of the SQLAlchemy
+ :func:`sqlalchemy.sql.expression.table` and
+ :func:`sqlalchemy.sql.expression.column` constructs to make a brief,
+ ad-hoc table construct just for our UPDATE statement. A full
+ :class:`~sqlalchemy.schema.Table` construct of course works perfectly
+ fine as well, though note it's a recommended practice to at least
+ ensure the definition of a table is self-contained within the migration
+ script, rather than imported from a module that may break compatibility
+ with older migrations.
+
+ In a SQL script context, the statement is emitted directly to the
+ output stream. There is *no* return result, however, as this
+ function is oriented towards generating a change script
+ that can run in "offline" mode. Additionally, parameterized
+ statements are discouraged here, as they *will not work* in offline
+ mode. Above, we use :meth:`.inline_literal` where parameters are
+ to be used.
+
+ For full interaction with a connected database where parameters can
+ also be used normally, use the "bind" available from the context::
+
+ from alembic import op
+
+ connection = op.get_bind()
+
+ connection.execute(
+ account.update()
+ .where(account.c.name == "account 1")
+ .values({"name": "account 2"})
+ )
+
+ Additionally, when passing the statement as a plain string, it is first
+ coerced into a :func:`sqlalchemy.sql.expression.text` construct
+ before being passed along. In the less likely case that the
+ literal SQL string contains a colon, it must be escaped with a
+ backslash, as::
+
+ op.execute(r"INSERT INTO table (foo) VALUES ('\:colon_value')")
+
+
+ :param sqltext: Any legal SQLAlchemy expression, including:
+
+ * a string
+ * a :func:`sqlalchemy.sql.expression.text` construct.
+ * a :func:`sqlalchemy.sql.expression.insert` construct.
+ * a :func:`sqlalchemy.sql.expression.update` construct.
+ * a :func:`sqlalchemy.sql.expression.delete` construct.
+ * Any "executable" described in SQLAlchemy Core documentation,
+ noting that no result set is returned.
+
+ .. note:: when passing a plain string, the statement is coerced into
+ a :func:`sqlalchemy.sql.expression.text` construct. This construct
+ considers symbols with colons, e.g. ``:foo`` to be bound parameters.
+ To avoid this, ensure that colon symbols are escaped, e.g.
+ ``\:foo``.
+
+ :param execution_options: Optional dictionary of
+ execution options, will be passed to
+ :meth:`sqlalchemy.engine.Connection.execution_options`.
+ """
+
+def f(name: str) -> conv:
+ """Indicate a string name that has already had a naming convention
+ applied to it.
+
+ This feature combines with the SQLAlchemy ``naming_convention`` feature
+ to disambiguate constraint names that have already had naming
+ conventions applied to them, versus those that have not. This is
+ necessary in the case that the ``"%(constraint_name)s"`` token
+ is used within a naming convention, so that it can be identified
+ that this particular name should remain fixed.
+
+ If the :meth:`.Operations.f` is used on a constraint, the naming
+ convention will not take effect::
+
+ op.add_column("t", "x", Boolean(name=op.f("ck_bool_t_x")))
+
+ Above, the CHECK constraint generated will have the name
+ ``ck_bool_t_x`` regardless of whether or not a naming convention is
+ in use.
+
+ Alternatively, if a naming convention is in use, and 'f' is not used,
+ names will be converted along conventions. If the ``target_metadata``
+ contains the naming convention
+ ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the
+ output of the following:
+
+ op.add_column("t", "x", Boolean(name="x"))
+
+ will be::
+
+ CONSTRAINT ck_bool_t_x CHECK (x in (1, 0)))
+
+ The function is rendered in the output of autogenerate when
+ a particular constraint name is already converted.
+
+ """
+
+def get_bind() -> Connection:
+ """Return the current 'bind'.
+
+ Under normal circumstances, this is the
+ :class:`~sqlalchemy.engine.Connection` currently being used
+ to emit SQL to the database.
+
+ In a SQL script context, this value is ``None``. [TODO: verify this]
+
+ """
+
+def get_context() -> MigrationContext:
+ """Return the :class:`.MigrationContext` object that's
+ currently in use.
+
+ """
+
+def implementation_for(op_cls: Any) -> Callable[[_C], _C]:
+ """Register an implementation for a given :class:`.MigrateOperation`.
+
+ This is part of the operation extensibility API.
+
+ .. seealso::
+
+ :ref:`operation_plugins` - example of use
+
+ """
+
+def inline_literal(
+ value: Union[str, int], type_: Optional[TypeEngine[Any]] = None
+) -> _literal_bindparam:
+ r"""Produce an 'inline literal' expression, suitable for
+ using in an INSERT, UPDATE, or DELETE statement.
+
+ When using Alembic in "offline" mode, CRUD operations
+ aren't compatible with SQLAlchemy's default behavior surrounding
+ literal values,
+ which is that they are converted into bound values and passed
+ separately into the ``execute()`` method of the DBAPI cursor.
+ An offline SQL
+ script needs to have these rendered inline. While it should
+ always be noted that inline literal values are an **enormous**
+ security hole in an application that handles untrusted input,
+ a schema migration is not run in this context, so
+ literals are safe to render inline, with the caveat that
+ advanced types like dates may not be supported directly
+ by SQLAlchemy.
+
+ See :meth:`.Operations.execute` for an example usage of
+ :meth:`.Operations.inline_literal`.
+
+ The environment can also be configured to attempt to render
+ "literal" values inline automatically, for those simple types
+ that are supported by the dialect; see
+ :paramref:`.EnvironmentContext.configure.literal_binds` for this
+ more recently added feature.
+
+ :param value: The value to render. Strings, integers, and simple
+ numerics should be supported. Other types like boolean,
+ dates, etc. may or may not be supported yet by various
+ backends.
+ :param type\_: optional - a :class:`sqlalchemy.types.TypeEngine`
+ subclass stating the type of this value. In SQLAlchemy
+ expressions, this is usually derived automatically
+ from the Python type of the value itself, as well as
+ based on the context in which the value is used.
+
+ .. seealso::
+
+ :paramref:`.EnvironmentContext.configure.literal_binds`
+
+ """
+
+@overload
+def invoke(operation: CreateTableOp) -> Table: ...
+@overload
+def invoke(
+ operation: Union[
+ AddConstraintOp,
+ DropConstraintOp,
+ CreateIndexOp,
+ DropIndexOp,
+ AddColumnOp,
+ AlterColumnOp,
+ AlterTableOp,
+ CreateTableCommentOp,
+ DropTableCommentOp,
+ DropColumnOp,
+ BulkInsertOp,
+ DropTableOp,
+ ExecuteSQLOp,
+ ]
+) -> None: ...
+@overload
+def invoke(operation: MigrateOperation) -> Any:
+ """Given a :class:`.MigrateOperation`, invoke it in terms of
+ this :class:`.Operations` instance.
+
+ """
+
+def register_operation(
+ name: str, sourcename: Optional[str] = None
+) -> Callable[[Type[_T]], Type[_T]]:
+ """Register a new operation for this class.
+
+ This method is normally used to add new operations
+ to the :class:`.Operations` class, and possibly the
+ :class:`.BatchOperations` class as well. All Alembic migration
+ operations are implemented via this system, however the system
+ is also available as a public API to facilitate adding custom
+ operations.
+
+ .. seealso::
+
+ :ref:`operation_plugins`
+
+
+ """
+
+def rename_table(
+ old_table_name: str, new_table_name: str, *, schema: Optional[str] = None
+) -> None:
+ """Emit an ALTER TABLE to rename a table.
+
+ :param old_table_name: old name.
+ :param new_table_name: new name.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def run_async(
+ async_function: Callable[..., Awaitable[_T]], *args: Any, **kw_args: Any
+) -> _T:
+ """Invoke the given asynchronous callable, passing an asynchronous
+ :class:`~sqlalchemy.ext.asyncio.AsyncConnection` as the first
+ argument.
+
+ This method allows calling async functions from within the
+ synchronous ``upgrade()`` or ``downgrade()`` alembic migration
+ method.
+
+ The async connection passed to the callable shares the same
+ transaction as the connection running in the migration context.
+
+ Any additional arg or kw_arg passed to this function are passed
+ to the provided async function.
+
+ .. versionadded: 1.11
+
+ .. note::
+
+ This method can be called only when alembic is called using
+ an async dialect.
+ """
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/__init__.py b/venv/lib/python3.12/site-packages/alembic/operations/__init__.py
new file mode 100644
index 0000000..26197cb
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/operations/__init__.py
@@ -0,0 +1,15 @@
+from . import toimpl
+from .base import AbstractOperations
+from .base import BatchOperations
+from .base import Operations
+from .ops import MigrateOperation
+from .ops import MigrationScript
+
+
+__all__ = [
+ "AbstractOperations",
+ "Operations",
+ "BatchOperations",
+ "MigrateOperation",
+ "MigrationScript",
+]
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..5b28a7f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/base.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..d99615e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/base.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/batch.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/batch.cpython-312.pyc
new file mode 100644
index 0000000..50140e2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/batch.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/ops.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/ops.cpython-312.pyc
new file mode 100644
index 0000000..9e886e7
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/ops.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/schemaobj.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/schemaobj.cpython-312.pyc
new file mode 100644
index 0000000..9412e8e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/schemaobj.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/toimpl.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/toimpl.cpython-312.pyc
new file mode 100644
index 0000000..8274183
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/operations/__pycache__/toimpl.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/base.py b/venv/lib/python3.12/site-packages/alembic/operations/base.py
new file mode 100644
index 0000000..bafe441
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/operations/base.py
@@ -0,0 +1,1893 @@
+# mypy: allow-untyped-calls
+
+from __future__ import annotations
+
+from contextlib import contextmanager
+import re
+import textwrap
+from typing import Any
+from typing import Awaitable
+from typing import Callable
+from typing import Dict
+from typing import Iterator
+from typing import List # noqa
+from typing import Mapping
+from typing import NoReturn
+from typing import Optional
+from typing import overload
+from typing import Sequence # noqa
+from typing import Tuple
+from typing import Type # noqa
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy.sql.elements import conv
+
+from . import batch
+from . import schemaobj
+from .. import util
+from ..util import sqla_compat
+from ..util.compat import formatannotation_fwdref
+from ..util.compat import inspect_formatargspec
+from ..util.compat import inspect_getfullargspec
+from ..util.sqla_compat import _literal_bindparam
+
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy import Table
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.expression import ColumnElement
+ from sqlalchemy.sql.expression import TableClause
+ from sqlalchemy.sql.expression import TextClause
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Computed
+ from sqlalchemy.sql.schema import Identity
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.types import TypeEngine
+
+ from .batch import BatchOperationsImpl
+ from .ops import AddColumnOp
+ from .ops import AddConstraintOp
+ from .ops import AlterColumnOp
+ from .ops import AlterTableOp
+ from .ops import BulkInsertOp
+ from .ops import CreateIndexOp
+ from .ops import CreateTableCommentOp
+ from .ops import CreateTableOp
+ from .ops import DropColumnOp
+ from .ops import DropConstraintOp
+ from .ops import DropIndexOp
+ from .ops import DropTableCommentOp
+ from .ops import DropTableOp
+ from .ops import ExecuteSQLOp
+ from .ops import MigrateOperation
+ from ..ddl import DefaultImpl
+ from ..runtime.migration import MigrationContext
+__all__ = ("Operations", "BatchOperations")
+_T = TypeVar("_T")
+
+_C = TypeVar("_C", bound=Callable[..., Any])
+
+
+class AbstractOperations(util.ModuleClsProxy):
+ """Base class for Operations and BatchOperations.
+
+ .. versionadded:: 1.11.0
+
+ """
+
+ impl: Union[DefaultImpl, BatchOperationsImpl]
+ _to_impl = util.Dispatcher()
+
+ def __init__(
+ self,
+ migration_context: MigrationContext,
+ impl: Optional[BatchOperationsImpl] = None,
+ ) -> None:
+ """Construct a new :class:`.Operations`
+
+ :param migration_context: a :class:`.MigrationContext`
+ instance.
+
+ """
+ self.migration_context = migration_context
+ if impl is None:
+ self.impl = migration_context.impl
+ else:
+ self.impl = impl
+
+ self.schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ @classmethod
+ def register_operation(
+ cls, name: str, sourcename: Optional[str] = None
+ ) -> Callable[[Type[_T]], Type[_T]]:
+ """Register a new operation for this class.
+
+ This method is normally used to add new operations
+ to the :class:`.Operations` class, and possibly the
+ :class:`.BatchOperations` class as well. All Alembic migration
+ operations are implemented via this system, however the system
+ is also available as a public API to facilitate adding custom
+ operations.
+
+ .. seealso::
+
+ :ref:`operation_plugins`
+
+
+ """
+
+ def register(op_cls: Type[_T]) -> Type[_T]:
+ if sourcename is None:
+ fn = getattr(op_cls, name)
+ source_name = fn.__name__
+ else:
+ fn = getattr(op_cls, sourcename)
+ source_name = fn.__name__
+
+ spec = inspect_getfullargspec(fn)
+
+ name_args = spec[0]
+ assert name_args[0:2] == ["cls", "operations"]
+
+ name_args[0:2] = ["self"]
+
+ args = inspect_formatargspec(
+ *spec, formatannotation=formatannotation_fwdref
+ )
+ num_defaults = len(spec[3]) if spec[3] else 0
+
+ defaulted_vals: Tuple[Any, ...]
+
+ if num_defaults:
+ defaulted_vals = tuple(name_args[0 - num_defaults :])
+ else:
+ defaulted_vals = ()
+
+ defaulted_vals += tuple(spec[4])
+ # here, we are using formatargspec in a different way in order
+ # to get a string that will re-apply incoming arguments to a new
+ # function call
+
+ apply_kw = inspect_formatargspec(
+ name_args + spec[4],
+ spec[1],
+ spec[2],
+ defaulted_vals,
+ formatvalue=lambda x: "=" + x,
+ formatannotation=formatannotation_fwdref,
+ )
+
+ args = re.sub(
+ r'[_]?ForwardRef\(([\'"].+?[\'"])\)',
+ lambda m: m.group(1),
+ args,
+ )
+
+ func_text = textwrap.dedent(
+ """\
+ def %(name)s%(args)s:
+ %(doc)r
+ return op_cls.%(source_name)s%(apply_kw)s
+ """
+ % {
+ "name": name,
+ "source_name": source_name,
+ "args": args,
+ "apply_kw": apply_kw,
+ "doc": fn.__doc__,
+ }
+ )
+
+ globals_ = dict(globals())
+ globals_.update({"op_cls": op_cls})
+ lcl: Dict[str, Any] = {}
+
+ exec(func_text, globals_, lcl)
+ setattr(cls, name, lcl[name])
+ fn.__func__.__doc__ = (
+ "This method is proxied on "
+ "the :class:`.%s` class, via the :meth:`.%s.%s` method."
+ % (cls.__name__, cls.__name__, name)
+ )
+ if hasattr(fn, "_legacy_translations"):
+ lcl[name]._legacy_translations = fn._legacy_translations
+ return op_cls
+
+ return register
+
+ @classmethod
+ def implementation_for(cls, op_cls: Any) -> Callable[[_C], _C]:
+ """Register an implementation for a given :class:`.MigrateOperation`.
+
+ This is part of the operation extensibility API.
+
+ .. seealso::
+
+ :ref:`operation_plugins` - example of use
+
+ """
+
+ def decorate(fn: _C) -> _C:
+ cls._to_impl.dispatch_for(op_cls)(fn)
+ return fn
+
+ return decorate
+
+ @classmethod
+ @contextmanager
+ def context(
+ cls, migration_context: MigrationContext
+ ) -> Iterator[Operations]:
+ op = Operations(migration_context)
+ op._install_proxy()
+ yield op
+ op._remove_proxy()
+
+ @contextmanager
+ def batch_alter_table(
+ self,
+ table_name: str,
+ schema: Optional[str] = None,
+ recreate: Literal["auto", "always", "never"] = "auto",
+ partial_reordering: Optional[Tuple[Any, ...]] = None,
+ copy_from: Optional[Table] = None,
+ table_args: Tuple[Any, ...] = (),
+ table_kwargs: Mapping[str, Any] = util.immutabledict(),
+ reflect_args: Tuple[Any, ...] = (),
+ reflect_kwargs: Mapping[str, Any] = util.immutabledict(),
+ naming_convention: Optional[Dict[str, str]] = None,
+ ) -> Iterator[BatchOperations]:
+ """Invoke a series of per-table migrations in batch.
+
+ Batch mode allows a series of operations specific to a table
+ to be syntactically grouped together, and allows for alternate
+ modes of table migration, in particular the "recreate" style of
+ migration required by SQLite.
+
+ "recreate" style is as follows:
+
+ 1. A new table is created with the new specification, based on the
+ migration directives within the batch, using a temporary name.
+
+ 2. the data copied from the existing table to the new table.
+
+ 3. the existing table is dropped.
+
+ 4. the new table is renamed to the existing table name.
+
+ The directive by default will only use "recreate" style on the
+ SQLite backend, and only if directives are present which require
+ this form, e.g. anything other than ``add_column()``. The batch
+ operation on other backends will proceed using standard ALTER TABLE
+ operations.
+
+ The method is used as a context manager, which returns an instance
+ of :class:`.BatchOperations`; this object is the same as
+ :class:`.Operations` except that table names and schema names
+ are omitted. E.g.::
+
+ with op.batch_alter_table("some_table") as batch_op:
+ batch_op.add_column(Column("foo", Integer))
+ batch_op.drop_column("bar")
+
+ The operations within the context manager are invoked at once
+ when the context is ended. When run against SQLite, if the
+ migrations include operations not supported by SQLite's ALTER TABLE,
+ the entire table will be copied to a new one with the new
+ specification, moving all data across as well.
+
+ The copy operation by default uses reflection to retrieve the current
+ structure of the table, and therefore :meth:`.batch_alter_table`
+ in this mode requires that the migration is run in "online" mode.
+ The ``copy_from`` parameter may be passed which refers to an existing
+ :class:`.Table` object, which will bypass this reflection step.
+
+ .. note:: The table copy operation will currently not copy
+ CHECK constraints, and may not copy UNIQUE constraints that are
+ unnamed, as is possible on SQLite. See the section
+ :ref:`sqlite_batch_constraints` for workarounds.
+
+ :param table_name: name of table
+ :param schema: optional schema name.
+ :param recreate: under what circumstances the table should be
+ recreated. At its default of ``"auto"``, the SQLite dialect will
+ recreate the table if any operations other than ``add_column()``,
+ ``create_index()``, or ``drop_index()`` are
+ present. Other options include ``"always"`` and ``"never"``.
+ :param copy_from: optional :class:`~sqlalchemy.schema.Table` object
+ that will act as the structure of the table being copied. If omitted,
+ table reflection is used to retrieve the structure of the table.
+
+ .. seealso::
+
+ :ref:`batch_offline_mode`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_args`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_kwargs`
+
+ :param reflect_args: a sequence of additional positional arguments that
+ will be applied to the table structure being reflected / copied;
+ this may be used to pass column and constraint overrides to the
+ table that will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param reflect_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the table structure being copied; this may be
+ used to pass additional table and reflection options to the table that
+ will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param table_args: a sequence of additional positional arguments that
+ will be applied to the new :class:`~sqlalchemy.schema.Table` when
+ created, in addition to those copied from the source table.
+ This may be used to provide additional constraints such as CHECK
+ constraints that may not be reflected.
+ :param table_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the new :class:`~sqlalchemy.schema.Table`
+ when created, in addition to those copied from the source table.
+ This may be used to provide for additional table options that may
+ not be reflected.
+ :param naming_convention: a naming convention dictionary of the form
+ described at :ref:`autogen_naming_conventions` which will be applied
+ to the :class:`~sqlalchemy.schema.MetaData` during the reflection
+ process. This is typically required if one wants to drop SQLite
+ constraints, as these constraints will not have names when
+ reflected on this backend. Requires SQLAlchemy **0.9.4** or greater.
+
+ .. seealso::
+
+ :ref:`dropping_sqlite_foreign_keys`
+
+ :param partial_reordering: a list of tuples, each suggesting a desired
+ ordering of two or more columns in the newly created table. Requires
+ that :paramref:`.batch_alter_table.recreate` is set to ``"always"``.
+ Examples, given a table with columns "a", "b", "c", and "d":
+
+ Specify the order of all columns::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("c", "d", "a", "b")],
+ ) as batch_op:
+ pass
+
+ Ensure "d" appears before "c", and "b", appears before "a"::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("d", "c"), ("b", "a")],
+ ) as batch_op:
+ pass
+
+ The ordering of columns not included in the partial_reordering
+ set is undefined. Therefore it is best to specify the complete
+ ordering of all columns for best results.
+
+ .. note:: batch mode requires SQLAlchemy 0.8 or above.
+
+ .. seealso::
+
+ :ref:`batch_migrations`
+
+ """
+ impl = batch.BatchOperationsImpl(
+ self,
+ table_name,
+ schema,
+ recreate,
+ copy_from,
+ table_args,
+ table_kwargs,
+ reflect_args,
+ reflect_kwargs,
+ naming_convention,
+ partial_reordering,
+ )
+ batch_op = BatchOperations(self.migration_context, impl=impl)
+ yield batch_op
+ impl.flush()
+
+ def get_context(self) -> MigrationContext:
+ """Return the :class:`.MigrationContext` object that's
+ currently in use.
+
+ """
+
+ return self.migration_context
+
+ @overload
+ def invoke(self, operation: CreateTableOp) -> Table:
+ ...
+
+ @overload
+ def invoke(
+ self,
+ operation: Union[
+ AddConstraintOp,
+ DropConstraintOp,
+ CreateIndexOp,
+ DropIndexOp,
+ AddColumnOp,
+ AlterColumnOp,
+ AlterTableOp,
+ CreateTableCommentOp,
+ DropTableCommentOp,
+ DropColumnOp,
+ BulkInsertOp,
+ DropTableOp,
+ ExecuteSQLOp,
+ ],
+ ) -> None:
+ ...
+
+ @overload
+ def invoke(self, operation: MigrateOperation) -> Any:
+ ...
+
+ def invoke(self, operation: MigrateOperation) -> Any:
+ """Given a :class:`.MigrateOperation`, invoke it in terms of
+ this :class:`.Operations` instance.
+
+ """
+ fn = self._to_impl.dispatch(
+ operation, self.migration_context.impl.__dialect__
+ )
+ return fn(self, operation)
+
+ def f(self, name: str) -> conv:
+ """Indicate a string name that has already had a naming convention
+ applied to it.
+
+ This feature combines with the SQLAlchemy ``naming_convention`` feature
+ to disambiguate constraint names that have already had naming
+ conventions applied to them, versus those that have not. This is
+ necessary in the case that the ``"%(constraint_name)s"`` token
+ is used within a naming convention, so that it can be identified
+ that this particular name should remain fixed.
+
+ If the :meth:`.Operations.f` is used on a constraint, the naming
+ convention will not take effect::
+
+ op.add_column("t", "x", Boolean(name=op.f("ck_bool_t_x")))
+
+ Above, the CHECK constraint generated will have the name
+ ``ck_bool_t_x`` regardless of whether or not a naming convention is
+ in use.
+
+ Alternatively, if a naming convention is in use, and 'f' is not used,
+ names will be converted along conventions. If the ``target_metadata``
+ contains the naming convention
+ ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the
+ output of the following:
+
+ op.add_column("t", "x", Boolean(name="x"))
+
+ will be::
+
+ CONSTRAINT ck_bool_t_x CHECK (x in (1, 0)))
+
+ The function is rendered in the output of autogenerate when
+ a particular constraint name is already converted.
+
+ """
+ return conv(name)
+
+ def inline_literal(
+ self, value: Union[str, int], type_: Optional[TypeEngine[Any]] = None
+ ) -> _literal_bindparam:
+ r"""Produce an 'inline literal' expression, suitable for
+ using in an INSERT, UPDATE, or DELETE statement.
+
+ When using Alembic in "offline" mode, CRUD operations
+ aren't compatible with SQLAlchemy's default behavior surrounding
+ literal values,
+ which is that they are converted into bound values and passed
+ separately into the ``execute()`` method of the DBAPI cursor.
+ An offline SQL
+ script needs to have these rendered inline. While it should
+ always be noted that inline literal values are an **enormous**
+ security hole in an application that handles untrusted input,
+ a schema migration is not run in this context, so
+ literals are safe to render inline, with the caveat that
+ advanced types like dates may not be supported directly
+ by SQLAlchemy.
+
+ See :meth:`.Operations.execute` for an example usage of
+ :meth:`.Operations.inline_literal`.
+
+ The environment can also be configured to attempt to render
+ "literal" values inline automatically, for those simple types
+ that are supported by the dialect; see
+ :paramref:`.EnvironmentContext.configure.literal_binds` for this
+ more recently added feature.
+
+ :param value: The value to render. Strings, integers, and simple
+ numerics should be supported. Other types like boolean,
+ dates, etc. may or may not be supported yet by various
+ backends.
+ :param type\_: optional - a :class:`sqlalchemy.types.TypeEngine`
+ subclass stating the type of this value. In SQLAlchemy
+ expressions, this is usually derived automatically
+ from the Python type of the value itself, as well as
+ based on the context in which the value is used.
+
+ .. seealso::
+
+ :paramref:`.EnvironmentContext.configure.literal_binds`
+
+ """
+ return sqla_compat._literal_bindparam(None, value, type_=type_)
+
+ def get_bind(self) -> Connection:
+ """Return the current 'bind'.
+
+ Under normal circumstances, this is the
+ :class:`~sqlalchemy.engine.Connection` currently being used
+ to emit SQL to the database.
+
+ In a SQL script context, this value is ``None``. [TODO: verify this]
+
+ """
+ return self.migration_context.impl.bind # type: ignore[return-value]
+
+ def run_async(
+ self,
+ async_function: Callable[..., Awaitable[_T]],
+ *args: Any,
+ **kw_args: Any,
+ ) -> _T:
+ """Invoke the given asynchronous callable, passing an asynchronous
+ :class:`~sqlalchemy.ext.asyncio.AsyncConnection` as the first
+ argument.
+
+ This method allows calling async functions from within the
+ synchronous ``upgrade()`` or ``downgrade()`` alembic migration
+ method.
+
+ The async connection passed to the callable shares the same
+ transaction as the connection running in the migration context.
+
+ Any additional arg or kw_arg passed to this function are passed
+ to the provided async function.
+
+ .. versionadded: 1.11
+
+ .. note::
+
+ This method can be called only when alembic is called using
+ an async dialect.
+ """
+ if not sqla_compat.sqla_14_18:
+ raise NotImplementedError("SQLAlchemy 1.4.18+ required")
+ sync_conn = self.get_bind()
+ if sync_conn is None:
+ raise NotImplementedError("Cannot call run_async in SQL mode")
+ if not sync_conn.dialect.is_async:
+ raise ValueError("Cannot call run_async with a sync engine")
+ from sqlalchemy.ext.asyncio import AsyncConnection
+ from sqlalchemy.util import await_only
+
+ async_conn = AsyncConnection._retrieve_proxy_for_target(sync_conn)
+ return await_only(async_function(async_conn, *args, **kw_args))
+
+
+class Operations(AbstractOperations):
+ """Define high level migration operations.
+
+ Each operation corresponds to some schema migration operation,
+ executed against a particular :class:`.MigrationContext`
+ which in turn represents connectivity to a database,
+ or a file output stream.
+
+ While :class:`.Operations` is normally configured as
+ part of the :meth:`.EnvironmentContext.run_migrations`
+ method called from an ``env.py`` script, a standalone
+ :class:`.Operations` instance can be
+ made for use cases external to regular Alembic
+ migrations by passing in a :class:`.MigrationContext`::
+
+ from alembic.migration import MigrationContext
+ from alembic.operations import Operations
+
+ conn = myengine.connect()
+ ctx = MigrationContext.configure(conn)
+ op = Operations(ctx)
+
+ op.alter_column("t", "c", nullable=True)
+
+ Note that as of 0.8, most of the methods on this class are produced
+ dynamically using the :meth:`.Operations.register_operation`
+ method.
+
+ """
+
+ if TYPE_CHECKING:
+ # START STUB FUNCTIONS: op_cls
+ # ### the following stubs are generated by tools/write_pyi.py ###
+ # ### do not edit ###
+
+ def add_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue an "add column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy import Column, String
+
+ op.add_column("organization", Column("name", String()))
+
+ The :meth:`.Operations.add_column` method typically corresponds
+ to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope
+ of this command, the column's name, datatype, nullability,
+ and optional server-generated defaults may be indicated.
+
+ .. note::
+
+ With the exception of NOT NULL constraints or single-column FOREIGN
+ KEY constraints, other kinds of constraints such as PRIMARY KEY,
+ UNIQUE or CHECK constraints **cannot** be generated using this
+ method; for these constraints, refer to operations such as
+ :meth:`.Operations.create_primary_key` and
+ :meth:`.Operations.create_check_constraint`. In particular, the
+ following :class:`~sqlalchemy.schema.Column` parameters are
+ **ignored**:
+
+ * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases
+ typically do not support an ALTER operation that can add
+ individual columns one at a time to an existing primary key
+ constraint, therefore it's less ambiguous to use the
+ :meth:`.Operations.create_primary_key` method, which assumes no
+ existing primary key constraint is present.
+ * :paramref:`~sqlalchemy.schema.Column.unique` - use the
+ :meth:`.Operations.create_unique_constraint` method
+ * :paramref:`~sqlalchemy.schema.Column.index` - use the
+ :meth:`.Operations.create_index` method
+
+
+ The provided :class:`~sqlalchemy.schema.Column` object may include a
+ :class:`~sqlalchemy.schema.ForeignKey` constraint directive,
+ referencing a remote table name. For this specific type of constraint,
+ Alembic will automatically emit a second ALTER statement in order to
+ add the single-column FOREIGN KEY constraint separately::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ )
+
+ The column argument passed to :meth:`.Operations.add_column` is a
+ :class:`~sqlalchemy.schema.Column` construct, used in the same way it's
+ used in SQLAlchemy. In particular, values or functions to be indicated
+ as producing the column's default value on the database side are
+ specified using the ``server_default`` parameter, and not ``default``
+ which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the column add
+ op.add_column(
+ "account",
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ :param table_name: String name of the parent table.
+ :param column: a :class:`sqlalchemy.schema.Column` object
+ representing the new column.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def alter_column(
+ self,
+ table_name: str,
+ column_name: str,
+ *,
+ nullable: Optional[bool] = None,
+ comment: Union[str, Literal[False], None] = False,
+ server_default: Any = False,
+ new_column_name: Optional[str] = None,
+ type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None,
+ existing_type: Union[
+ TypeEngine[Any], Type[TypeEngine[Any]], None
+ ] = None,
+ existing_server_default: Union[
+ str, bool, Identity, Computed, None
+ ] = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue an "alter column" instruction using the
+ current migration context.
+
+ Generally, only that aspect of the column which
+ is being changed, i.e. name, type, nullability,
+ default, needs to be specified. Multiple changes
+ can also be specified at once and the backend should
+ "do the right thing", emitting each change either
+ separately or together as the backend allows.
+
+ MySQL has special requirements here, since MySQL
+ cannot ALTER a column without a full specification.
+ When producing MySQL-compatible migration files,
+ it is recommended that the ``existing_type``,
+ ``existing_server_default``, and ``existing_nullable``
+ parameters be present, if not being altered.
+
+ Type changes which are against the SQLAlchemy
+ "schema" types :class:`~sqlalchemy.types.Boolean`
+ and :class:`~sqlalchemy.types.Enum` may also
+ add or drop constraints which accompany those
+ types on backends that don't support them natively.
+ The ``existing_type`` argument is
+ used in this case to identify and remove a previous
+ constraint that was bound to the type object.
+
+ :param table_name: string name of the target table.
+ :param column_name: string name of the target column,
+ as it exists before the operation begins.
+ :param nullable: Optional; specify ``True`` or ``False``
+ to alter the column's nullability.
+ :param server_default: Optional; specify a string
+ SQL expression, :func:`~sqlalchemy.sql.expression.text`,
+ or :class:`~sqlalchemy.schema.DefaultClause` to indicate
+ an alteration to the column's default value.
+ Set to ``None`` to have the default removed.
+ :param comment: optional string text of a new comment to add to the
+ column.
+ :param new_column_name: Optional; specify a string name here to
+ indicate the new name within a column rename operation.
+ :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify a change to the column's type.
+ For SQLAlchemy types that also indicate a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`),
+ the constraint is also generated.
+ :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column;
+ currently understood by the MySQL dialect.
+ :param existing_type: Optional; a
+ :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify the previous type. This
+ is required for all MySQL column alter operations that
+ don't otherwise specify a new type, as well as for
+ when nullability is being changed on a SQL Server
+ column. It is also used if the type is a so-called
+ SQLAlchemy "schema" type which may define a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`,
+ :class:`~sqlalchemy.types.Enum`),
+ so that the constraint can be dropped.
+ :param existing_server_default: Optional; The existing
+ default value of the column. Required on MySQL if
+ an existing default is not being changed; else MySQL
+ removes the default.
+ :param existing_nullable: Optional; the existing nullability
+ of the column. Required on MySQL if the existing nullability
+ is not being changed; else MySQL sets this to NULL.
+ :param existing_autoincrement: Optional; the existing autoincrement
+ of the column. Used for MySQL's system of altering a column
+ that specifies ``AUTO_INCREMENT``.
+ :param existing_comment: string text of the existing comment on the
+ column to be maintained. Required on MySQL if the existing comment
+ on the column is not being changed.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param postgresql_using: String argument which will indicate a
+ SQL expression to render within the Postgresql-specific USING clause
+ within ALTER COLUMN. This string is taken directly as raw SQL which
+ must explicitly include any necessary quoting or escaping of tokens
+ within the expression.
+
+ """ # noqa: E501
+ ...
+
+ def bulk_insert(
+ self,
+ table: Union[Table, TableClause],
+ rows: List[Dict[str, Any]],
+ *,
+ multiinsert: bool = True,
+ ) -> None:
+ """Issue a "bulk insert" operation using the current
+ migration context.
+
+ This provides a means of representing an INSERT of multiple rows
+ which works equally well in the context of executing on a live
+ connection as well as that of generating a SQL script. In the
+ case of a SQL script, the values are rendered inline into the
+ statement.
+
+ e.g.::
+
+ from alembic import op
+ from datetime import date
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String, Integer, Date
+
+ # Create an ad-hoc table to use for the insert statement.
+ accounts_table = table(
+ "account",
+ column("id", Integer),
+ column("name", String),
+ column("create_date", Date),
+ )
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": date(2010, 10, 5),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": date(2007, 5, 27),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": date(2008, 8, 15),
+ },
+ ],
+ )
+
+ When using --sql mode, some datatypes may not render inline
+ automatically, such as dates and other special types. When this
+ issue is present, :meth:`.Operations.inline_literal` may be used::
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": op.inline_literal("2010-10-05"),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": op.inline_literal("2007-05-27"),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": op.inline_literal("2008-08-15"),
+ },
+ ],
+ multiinsert=False,
+ )
+
+ When using :meth:`.Operations.inline_literal` in conjunction with
+ :meth:`.Operations.bulk_insert`, in order for the statement to work
+ in "online" (e.g. non --sql) mode, the
+ :paramref:`~.Operations.bulk_insert.multiinsert`
+ flag should be set to ``False``, which will have the effect of
+ individual INSERT statements being emitted to the database, each
+ with a distinct VALUES clause, so that the "inline" values can
+ still be rendered, rather than attempting to pass the values
+ as bound parameters.
+
+ :param table: a table object which represents the target of the INSERT.
+
+ :param rows: a list of dictionaries indicating rows.
+
+ :param multiinsert: when at its default of True and --sql mode is not
+ enabled, the INSERT statement will be executed using
+ "executemany()" style, where all elements in the list of
+ dictionaries are passed as bound parameters in a single
+ list. Setting this to False results in individual INSERT
+ statements being emitted per parameter set, and is needed
+ in those cases where non-literal values are present in the
+ parameter sets.
+
+ """ # noqa: E501
+ ...
+
+ def create_check_constraint(
+ self,
+ constraint_name: Optional[str],
+ table_name: str,
+ condition: Union[str, ColumnElement[bool], TextClause],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue a "create check constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy.sql import column, func
+
+ op.create_check_constraint(
+ "ck_user_name_len",
+ "user",
+ func.len(column("name")) > 5,
+ )
+
+ CHECK constraints are usually against a SQL expression, so ad-hoc
+ table metadata is usually needed. The function will convert the given
+ arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound
+ to an anonymous table in order to emit the CREATE statement.
+
+ :param name: Name of the check constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param condition: SQL expression that's the condition of the
+ constraint. Can be a string or SQLAlchemy expression language
+ structure.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def create_exclude_constraint(
+ self,
+ constraint_name: str,
+ table_name: str,
+ *elements: Any,
+ **kw: Any,
+ ) -> Optional[Table]:
+ """Issue an alter to create an EXCLUDE constraint using the
+ current migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_exclude_constraint(
+ "user_excl",
+ "user",
+ ("period", "&&"),
+ ("group", "="),
+ where=("group != 'some group'"),
+ )
+
+ Note that the expressions work the same way as that of
+ the ``ExcludeConstraint`` object itself; if plain strings are
+ passed, quoting rules must be applied manually.
+
+ :param name: Name of the constraint.
+ :param table_name: String name of the source table.
+ :param elements: exclude conditions.
+ :param where: SQL expression or SQL string with optional WHERE
+ clause.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within.
+
+ """ # noqa: E501
+ ...
+
+ def create_foreign_key(
+ self,
+ constraint_name: Optional[str],
+ source_table: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ *,
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ source_schema: Optional[str] = None,
+ referent_schema: Optional[str] = None,
+ **dialect_kw: Any,
+ ) -> None:
+ """Issue a "create foreign key" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_foreign_key(
+ "fk_user_address",
+ "address",
+ "user",
+ ["user_id"],
+ ["id"],
+ )
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.ForeignKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the foreign key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param source_table: String name of the source table.
+ :param referent_table: String name of the destination table.
+ :param local_cols: a list of string column names in the
+ source table.
+ :param remote_cols: a list of string column names in the
+ remote table.
+ :param onupdate: Optional string. If set, emit ON UPDATE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param ondelete: Optional string. If set, emit ON DELETE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or NOT
+ DEFERRABLE when issuing DDL for this constraint.
+ :param source_schema: Optional schema name of the source table.
+ :param referent_schema: Optional schema name of the destination table.
+
+ """ # noqa: E501
+ ...
+
+ def create_index(
+ self,
+ index_name: Optional[str],
+ table_name: str,
+ columns: Sequence[Union[str, TextClause, Function[Any]]],
+ *,
+ schema: Optional[str] = None,
+ unique: bool = False,
+ if_not_exists: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue a "create index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_index("ik_test", "t1", ["foo", "bar"])
+
+ Functional indexes can be produced by using the
+ :func:`sqlalchemy.sql.expression.text` construct::
+
+ from alembic import op
+ from sqlalchemy import text
+
+ op.create_index("ik_test", "t1", [text("lower(foo)")])
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table.
+ :param columns: a list consisting of string column names and/or
+ :func:`~sqlalchemy.sql.expression.text` constructs.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param unique: If True, create a unique index.
+
+ :param quote: Force quoting of this column's name on or off,
+ corresponding to ``True`` or ``False``. When left at its default
+ of ``None``, the column identifier will be quoted according to
+ whether the name is case sensitive (identifiers with at least one
+ upper case character are treated as case sensitive), or if it's a
+ reserved word. This flag is only needed to force quoting of a
+ reserved word which is not known by the SQLAlchemy dialect.
+
+ :param if_not_exists: If True, adds IF NOT EXISTS operator when
+ creating the new index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """ # noqa: E501
+ ...
+
+ def create_primary_key(
+ self,
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: List[str],
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue a "create primary key" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_primary_key("pk_my_table", "my_table", ["id", "version"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.PrimaryKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the primary key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the target table.
+ :param columns: a list of string column names to be applied to the
+ primary key constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def create_table(
+ self, table_name: str, *columns: SchemaItem, **kw: Any
+ ) -> Table:
+ r"""Issue a "create table" instruction using the current migration
+ context.
+
+ This directive receives an argument list similar to that of the
+ traditional :class:`sqlalchemy.schema.Table` construct, but without the
+ metadata::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ Note that :meth:`.create_table` accepts
+ :class:`~sqlalchemy.schema.Column`
+ constructs directly from the SQLAlchemy library. In particular,
+ default values to be created on the database side are
+ specified using the ``server_default`` parameter, and not
+ ``default`` which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the "timestamp" column
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ The function also returns a newly created
+ :class:`~sqlalchemy.schema.Table` object, corresponding to the table
+ specification given, which is suitable for
+ immediate SQL operations, in particular
+ :meth:`.Operations.bulk_insert`::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ account_table = op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ op.bulk_insert(
+ account_table,
+ [
+ {"name": "A1", "description": "account 1"},
+ {"name": "A2", "description": "account 2"},
+ ],
+ )
+
+ :param table_name: Name of the table
+ :param \*columns: collection of :class:`~sqlalchemy.schema.Column`
+ objects within
+ the table, as well as optional :class:`~sqlalchemy.schema.Constraint`
+ objects
+ and :class:`~.sqlalchemy.schema.Index` objects.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ :return: the :class:`~sqlalchemy.schema.Table` object corresponding
+ to the parameters given.
+
+ """ # noqa: E501
+ ...
+
+ def create_table_comment(
+ self,
+ table_name: str,
+ comment: Optional[str],
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Emit a COMMENT ON operation to set the comment for a table.
+
+ :param table_name: string name of the target table.
+ :param comment: string value of the comment being registered against
+ the specified table.
+ :param existing_comment: String value of a comment
+ already registered on the specified table, used within autogenerate
+ so that the operation is reversible, but not required for direct
+ use.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """ # noqa: E501
+ ...
+
+ def create_unique_constraint(
+ self,
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: Sequence[str],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> Any:
+ """Issue a "create unique constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ op.create_unique_constraint("uq_user_name", "user", ["name"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.UniqueConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param name: Name of the unique constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param columns: a list of string column names in the
+ source table.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def drop_column(
+ self,
+ table_name: str,
+ column_name: str,
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue a "drop column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_column("organization", "account_id")
+
+ :param table_name: name of table
+ :param column_name: name of column
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param mssql_drop_check: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the CHECK constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.check_constraints,
+ then exec's a separate DROP CONSTRAINT for that constraint.
+ :param mssql_drop_default: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the DEFAULT constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.default_constraints,
+ then exec's a separate DROP CONSTRAINT for that default.
+ :param mssql_drop_foreign_key: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop a single FOREIGN KEY constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from
+ sys.foreign_keys/sys.foreign_key_columns,
+ then exec's a separate DROP CONSTRAINT for that default. Only
+ works if the column has exactly one FK constraint which refers to
+ it, at the moment.
+
+ """ # noqa: E501
+ ...
+
+ def drop_constraint(
+ self,
+ constraint_name: str,
+ table_name: str,
+ type_: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ r"""Drop a constraint of the given name, typically via DROP CONSTRAINT.
+
+ :param constraint_name: name of the constraint.
+ :param table_name: table name.
+ :param type\_: optional, required on MySQL. can be
+ 'foreignkey', 'primary', 'unique', or 'check'.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def drop_index(
+ self,
+ index_name: str,
+ table_name: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ if_exists: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue a "drop index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_index("accounts")
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table. Some
+ backends such as Microsoft SQL Server require this.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ :param if_exists: If True, adds IF EXISTS operator when
+ dropping the index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """ # noqa: E501
+ ...
+
+ def drop_table(
+ self, table_name: str, *, schema: Optional[str] = None, **kw: Any
+ ) -> None:
+ r"""Issue a "drop table" instruction using the current
+ migration context.
+
+
+ e.g.::
+
+ drop_table("accounts")
+
+ :param table_name: Name of the table
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ """ # noqa: E501
+ ...
+
+ def drop_table_comment(
+ self,
+ table_name: str,
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue a "drop table comment" operation to
+ remove an existing comment set on a table.
+
+ :param table_name: string name of the target table.
+ :param existing_comment: An optional string value of a comment already
+ registered on the specified table.
+
+ .. seealso::
+
+ :meth:`.Operations.create_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """ # noqa: E501
+ ...
+
+ def execute(
+ self,
+ sqltext: Union[Executable, str],
+ *,
+ execution_options: Optional[dict[str, Any]] = None,
+ ) -> None:
+ r"""Execute the given SQL using the current migration context.
+
+ The given SQL can be a plain string, e.g.::
+
+ op.execute("INSERT INTO table (foo) VALUES ('some value')")
+
+ Or it can be any kind of Core SQL Expression construct, such as
+ below where we use an update construct::
+
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String
+ from alembic import op
+
+ account = table("account", column("name", String))
+ op.execute(
+ account.update()
+ .where(account.c.name == op.inline_literal("account 1"))
+ .values({"name": op.inline_literal("account 2")})
+ )
+
+ Above, we made use of the SQLAlchemy
+ :func:`sqlalchemy.sql.expression.table` and
+ :func:`sqlalchemy.sql.expression.column` constructs to make a brief,
+ ad-hoc table construct just for our UPDATE statement. A full
+ :class:`~sqlalchemy.schema.Table` construct of course works perfectly
+ fine as well, though note it's a recommended practice to at least
+ ensure the definition of a table is self-contained within the migration
+ script, rather than imported from a module that may break compatibility
+ with older migrations.
+
+ In a SQL script context, the statement is emitted directly to the
+ output stream. There is *no* return result, however, as this
+ function is oriented towards generating a change script
+ that can run in "offline" mode. Additionally, parameterized
+ statements are discouraged here, as they *will not work* in offline
+ mode. Above, we use :meth:`.inline_literal` where parameters are
+ to be used.
+
+ For full interaction with a connected database where parameters can
+ also be used normally, use the "bind" available from the context::
+
+ from alembic import op
+
+ connection = op.get_bind()
+
+ connection.execute(
+ account.update()
+ .where(account.c.name == "account 1")
+ .values({"name": "account 2"})
+ )
+
+ Additionally, when passing the statement as a plain string, it is first
+ coerced into a :func:`sqlalchemy.sql.expression.text` construct
+ before being passed along. In the less likely case that the
+ literal SQL string contains a colon, it must be escaped with a
+ backslash, as::
+
+ op.execute(r"INSERT INTO table (foo) VALUES ('\:colon_value')")
+
+
+ :param sqltext: Any legal SQLAlchemy expression, including:
+
+ * a string
+ * a :func:`sqlalchemy.sql.expression.text` construct.
+ * a :func:`sqlalchemy.sql.expression.insert` construct.
+ * a :func:`sqlalchemy.sql.expression.update` construct.
+ * a :func:`sqlalchemy.sql.expression.delete` construct.
+ * Any "executable" described in SQLAlchemy Core documentation,
+ noting that no result set is returned.
+
+ .. note:: when passing a plain string, the statement is coerced into
+ a :func:`sqlalchemy.sql.expression.text` construct. This construct
+ considers symbols with colons, e.g. ``:foo`` to be bound parameters.
+ To avoid this, ensure that colon symbols are escaped, e.g.
+ ``\:foo``.
+
+ :param execution_options: Optional dictionary of
+ execution options, will be passed to
+ :meth:`sqlalchemy.engine.Connection.execution_options`.
+ """ # noqa: E501
+ ...
+
+ def rename_table(
+ self,
+ old_table_name: str,
+ new_table_name: str,
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Emit an ALTER TABLE to rename a table.
+
+ :param old_table_name: old name.
+ :param new_table_name: new name.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ # END STUB FUNCTIONS: op_cls
+
+
+class BatchOperations(AbstractOperations):
+ """Modifies the interface :class:`.Operations` for batch mode.
+
+ This basically omits the ``table_name`` and ``schema`` parameters
+ from associated methods, as these are a given when running under batch
+ mode.
+
+ .. seealso::
+
+ :meth:`.Operations.batch_alter_table`
+
+ Note that as of 0.8, most of the methods on this class are produced
+ dynamically using the :meth:`.Operations.register_operation`
+ method.
+
+ """
+
+ impl: BatchOperationsImpl
+
+ def _noop(self, operation: Any) -> NoReturn:
+ raise NotImplementedError(
+ "The %s method does not apply to a batch table alter operation."
+ % operation
+ )
+
+ if TYPE_CHECKING:
+ # START STUB FUNCTIONS: batch_op
+ # ### the following stubs are generated by tools/write_pyi.py ###
+ # ### do not edit ###
+
+ def add_column(
+ self,
+ column: Column[Any],
+ *,
+ insert_before: Optional[str] = None,
+ insert_after: Optional[str] = None,
+ ) -> None:
+ """Issue an "add column" instruction using the current
+ batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.add_column`
+
+ """ # noqa: E501
+ ...
+
+ def alter_column(
+ self,
+ column_name: str,
+ *,
+ nullable: Optional[bool] = None,
+ comment: Union[str, Literal[False], None] = False,
+ server_default: Any = False,
+ new_column_name: Optional[str] = None,
+ type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None,
+ existing_type: Union[
+ TypeEngine[Any], Type[TypeEngine[Any]], None
+ ] = None,
+ existing_server_default: Union[
+ str, bool, Identity, Computed, None
+ ] = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ insert_before: Optional[str] = None,
+ insert_after: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue an "alter column" instruction using the current
+ batch migration context.
+
+ Parameters are the same as that of :meth:`.Operations.alter_column`,
+ as well as the following option(s):
+
+ :param insert_before: String name of an existing column which this
+ column should be placed before, when creating the new table.
+
+ :param insert_after: String name of an existing column which this
+ column should be placed after, when creating the new table. If
+ both :paramref:`.BatchOperations.alter_column.insert_before`
+ and :paramref:`.BatchOperations.alter_column.insert_after` are
+ omitted, the column is inserted after the last existing column
+ in the table.
+
+ .. seealso::
+
+ :meth:`.Operations.alter_column`
+
+
+ """ # noqa: E501
+ ...
+
+ def create_check_constraint(
+ self,
+ constraint_name: str,
+ condition: Union[str, ColumnElement[bool], TextClause],
+ **kw: Any,
+ ) -> None:
+ """Issue a "create check constraint" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``source`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.create_check_constraint`
+
+ """ # noqa: E501
+ ...
+
+ def create_exclude_constraint(
+ self, constraint_name: str, *elements: Any, **kw: Any
+ ) -> Optional[Table]:
+ """Issue a "create exclude constraint" instruction using the
+ current batch migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ .. seealso::
+
+ :meth:`.Operations.create_exclude_constraint`
+
+ """ # noqa: E501
+ ...
+
+ def create_foreign_key(
+ self,
+ constraint_name: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ *,
+ referent_schema: Optional[str] = None,
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ **dialect_kw: Any,
+ ) -> None:
+ """Issue a "create foreign key" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``source`` and ``source_schema``
+ arguments from the call.
+
+ e.g.::
+
+ with batch_alter_table("address") as batch_op:
+ batch_op.create_foreign_key(
+ "fk_user_address",
+ "user",
+ ["user_id"],
+ ["id"],
+ )
+
+ .. seealso::
+
+ :meth:`.Operations.create_foreign_key`
+
+ """ # noqa: E501
+ ...
+
+ def create_index(
+ self, index_name: str, columns: List[str], **kw: Any
+ ) -> None:
+ """Issue a "create index" instruction using the
+ current batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.create_index`
+
+ """ # noqa: E501
+ ...
+
+ def create_primary_key(
+ self, constraint_name: str, columns: List[str]
+ ) -> None:
+ """Issue a "create primary key" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``table_name`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.create_primary_key`
+
+ """ # noqa: E501
+ ...
+
+ def create_table_comment(
+ self,
+ comment: Optional[str],
+ *,
+ existing_comment: Optional[str] = None,
+ ) -> None:
+ """Emit a COMMENT ON operation to set the comment for a table
+ using the current batch migration context.
+
+ :param comment: string value of the comment being registered against
+ the specified table.
+ :param existing_comment: String value of a comment
+ already registered on the specified table, used within autogenerate
+ so that the operation is reversible, but not required for direct
+ use.
+
+ """ # noqa: E501
+ ...
+
+ def create_unique_constraint(
+ self, constraint_name: str, columns: Sequence[str], **kw: Any
+ ) -> Any:
+ """Issue a "create unique constraint" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``source`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.create_unique_constraint`
+
+ """ # noqa: E501
+ ...
+
+ def drop_column(self, column_name: str, **kw: Any) -> None:
+ """Issue a "drop column" instruction using the current
+ batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_column`
+
+ """ # noqa: E501
+ ...
+
+ def drop_constraint(
+ self, constraint_name: str, type_: Optional[str] = None
+ ) -> None:
+ """Issue a "drop constraint" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``table_name`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_constraint`
+
+ """ # noqa: E501
+ ...
+
+ def drop_index(self, index_name: str, **kw: Any) -> None:
+ """Issue a "drop index" instruction using the
+ current batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_index`
+
+ """ # noqa: E501
+ ...
+
+ def drop_table_comment(
+ self, *, existing_comment: Optional[str] = None
+ ) -> None:
+ """Issue a "drop table comment" operation to
+ remove an existing comment set on a table using the current
+ batch operations context.
+
+ :param existing_comment: An optional string value of a comment already
+ registered on the specified table.
+
+ """ # noqa: E501
+ ...
+
+ def execute(
+ self,
+ sqltext: Union[Executable, str],
+ *,
+ execution_options: Optional[dict[str, Any]] = None,
+ ) -> None:
+ """Execute the given SQL using the current migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.execute`
+
+ """ # noqa: E501
+ ...
+
+ # END STUB FUNCTIONS: batch_op
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/batch.py b/venv/lib/python3.12/site-packages/alembic/operations/batch.py
new file mode 100644
index 0000000..fd7ab99
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/operations/batch.py
@@ -0,0 +1,717 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import CheckConstraint
+from sqlalchemy import Column
+from sqlalchemy import ForeignKeyConstraint
+from sqlalchemy import Index
+from sqlalchemy import MetaData
+from sqlalchemy import PrimaryKeyConstraint
+from sqlalchemy import schema as sql_schema
+from sqlalchemy import Table
+from sqlalchemy import types as sqltypes
+from sqlalchemy.sql.schema import SchemaEventTarget
+from sqlalchemy.util import OrderedDict
+from sqlalchemy.util import topological
+
+from ..util import exc
+from ..util.sqla_compat import _columns_for_constraint
+from ..util.sqla_compat import _copy
+from ..util.sqla_compat import _copy_expression
+from ..util.sqla_compat import _ensure_scope_for_ddl
+from ..util.sqla_compat import _fk_is_self_referential
+from ..util.sqla_compat import _idx_table_bound_expressions
+from ..util.sqla_compat import _insert_inline
+from ..util.sqla_compat import _is_type_bound
+from ..util.sqla_compat import _remove_column_from_collection
+from ..util.sqla_compat import _resolve_for_variant
+from ..util.sqla_compat import _select
+from ..util.sqla_compat import constraint_name_defined
+from ..util.sqla_compat import constraint_name_string
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.engine import Dialect
+ from sqlalchemy.sql.elements import ColumnClause
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from ..ddl.impl import DefaultImpl
+
+
+class BatchOperationsImpl:
+ def __init__(
+ self,
+ operations,
+ table_name,
+ schema,
+ recreate,
+ copy_from,
+ table_args,
+ table_kwargs,
+ reflect_args,
+ reflect_kwargs,
+ naming_convention,
+ partial_reordering,
+ ):
+ self.operations = operations
+ self.table_name = table_name
+ self.schema = schema
+ if recreate not in ("auto", "always", "never"):
+ raise ValueError(
+ "recreate may be one of 'auto', 'always', or 'never'."
+ )
+ self.recreate = recreate
+ self.copy_from = copy_from
+ self.table_args = table_args
+ self.table_kwargs = dict(table_kwargs)
+ self.reflect_args = reflect_args
+ self.reflect_kwargs = dict(reflect_kwargs)
+ self.reflect_kwargs.setdefault(
+ "listeners", list(self.reflect_kwargs.get("listeners", ()))
+ )
+ self.reflect_kwargs["listeners"].append(
+ ("column_reflect", operations.impl.autogen_column_reflect)
+ )
+ self.naming_convention = naming_convention
+ self.partial_reordering = partial_reordering
+ self.batch = []
+
+ @property
+ def dialect(self) -> Dialect:
+ return self.operations.impl.dialect
+
+ @property
+ def impl(self) -> DefaultImpl:
+ return self.operations.impl
+
+ def _should_recreate(self) -> bool:
+ if self.recreate == "auto":
+ return self.operations.impl.requires_recreate_in_batch(self)
+ elif self.recreate == "always":
+ return True
+ else:
+ return False
+
+ def flush(self) -> None:
+ should_recreate = self._should_recreate()
+
+ with _ensure_scope_for_ddl(self.impl.connection):
+ if not should_recreate:
+ for opname, arg, kw in self.batch:
+ fn = getattr(self.operations.impl, opname)
+ fn(*arg, **kw)
+ else:
+ if self.naming_convention:
+ m1 = MetaData(naming_convention=self.naming_convention)
+ else:
+ m1 = MetaData()
+
+ if self.copy_from is not None:
+ existing_table = self.copy_from
+ reflected = False
+ else:
+ if self.operations.migration_context.as_sql:
+ raise exc.CommandError(
+ f"This operation cannot proceed in --sql mode; "
+ f"batch mode with dialect "
+ f"{self.operations.migration_context.dialect.name} " # noqa: E501
+ f"requires a live database connection with which "
+ f'to reflect the table "{self.table_name}". '
+ f"To generate a batch SQL migration script using "
+ "table "
+ '"move and copy", a complete Table object '
+ f'should be passed to the "copy_from" argument '
+ "of the batch_alter_table() method so that table "
+ "reflection can be skipped."
+ )
+
+ existing_table = Table(
+ self.table_name,
+ m1,
+ schema=self.schema,
+ autoload_with=self.operations.get_bind(),
+ *self.reflect_args,
+ **self.reflect_kwargs,
+ )
+ reflected = True
+
+ batch_impl = ApplyBatchImpl(
+ self.impl,
+ existing_table,
+ self.table_args,
+ self.table_kwargs,
+ reflected,
+ partial_reordering=self.partial_reordering,
+ )
+ for opname, arg, kw in self.batch:
+ fn = getattr(batch_impl, opname)
+ fn(*arg, **kw)
+
+ batch_impl._create(self.impl)
+
+ def alter_column(self, *arg, **kw) -> None:
+ self.batch.append(("alter_column", arg, kw))
+
+ def add_column(self, *arg, **kw) -> None:
+ if (
+ "insert_before" in kw or "insert_after" in kw
+ ) and not self._should_recreate():
+ raise exc.CommandError(
+ "Can't specify insert_before or insert_after when using "
+ "ALTER; please specify recreate='always'"
+ )
+ self.batch.append(("add_column", arg, kw))
+
+ def drop_column(self, *arg, **kw) -> None:
+ self.batch.append(("drop_column", arg, kw))
+
+ def add_constraint(self, const: Constraint) -> None:
+ self.batch.append(("add_constraint", (const,), {}))
+
+ def drop_constraint(self, const: Constraint) -> None:
+ self.batch.append(("drop_constraint", (const,), {}))
+
+ def rename_table(self, *arg, **kw):
+ self.batch.append(("rename_table", arg, kw))
+
+ def create_index(self, idx: Index, **kw: Any) -> None:
+ self.batch.append(("create_index", (idx,), kw))
+
+ def drop_index(self, idx: Index, **kw: Any) -> None:
+ self.batch.append(("drop_index", (idx,), kw))
+
+ def create_table_comment(self, table):
+ self.batch.append(("create_table_comment", (table,), {}))
+
+ def drop_table_comment(self, table):
+ self.batch.append(("drop_table_comment", (table,), {}))
+
+ def create_table(self, table):
+ raise NotImplementedError("Can't create table in batch mode")
+
+ def drop_table(self, table):
+ raise NotImplementedError("Can't drop table in batch mode")
+
+ def create_column_comment(self, column):
+ self.batch.append(("create_column_comment", (column,), {}))
+
+
+class ApplyBatchImpl:
+ def __init__(
+ self,
+ impl: DefaultImpl,
+ table: Table,
+ table_args: tuple,
+ table_kwargs: Dict[str, Any],
+ reflected: bool,
+ partial_reordering: tuple = (),
+ ) -> None:
+ self.impl = impl
+ self.table = table # this is a Table object
+ self.table_args = table_args
+ self.table_kwargs = table_kwargs
+ self.temp_table_name = self._calc_temp_name(table.name)
+ self.new_table: Optional[Table] = None
+
+ self.partial_reordering = partial_reordering # tuple of tuples
+ self.add_col_ordering: Tuple[
+ Tuple[str, str], ...
+ ] = () # tuple of tuples
+
+ self.column_transfers = OrderedDict(
+ (c.name, {"expr": c}) for c in self.table.c
+ )
+ self.existing_ordering = list(self.column_transfers)
+
+ self.reflected = reflected
+ self._grab_table_elements()
+
+ @classmethod
+ def _calc_temp_name(cls, tablename: Union[quoted_name, str]) -> str:
+ return ("_alembic_tmp_%s" % tablename)[0:50]
+
+ def _grab_table_elements(self) -> None:
+ schema = self.table.schema
+ self.columns: Dict[str, Column[Any]] = OrderedDict()
+ for c in self.table.c:
+ c_copy = _copy(c, schema=schema)
+ c_copy.unique = c_copy.index = False
+ # ensure that the type object was copied,
+ # as we may need to modify it in-place
+ if isinstance(c.type, SchemaEventTarget):
+ assert c_copy.type is not c.type
+ self.columns[c.name] = c_copy
+ self.named_constraints: Dict[str, Constraint] = {}
+ self.unnamed_constraints = []
+ self.col_named_constraints = {}
+ self.indexes: Dict[str, Index] = {}
+ self.new_indexes: Dict[str, Index] = {}
+
+ for const in self.table.constraints:
+ if _is_type_bound(const):
+ continue
+ elif (
+ self.reflected
+ and isinstance(const, CheckConstraint)
+ and not const.name
+ ):
+ # TODO: we are skipping unnamed reflected CheckConstraint
+ # because
+ # we have no way to determine _is_type_bound() for these.
+ pass
+ elif constraint_name_string(const.name):
+ self.named_constraints[const.name] = const
+ else:
+ self.unnamed_constraints.append(const)
+
+ if not self.reflected:
+ for col in self.table.c:
+ for const in col.constraints:
+ if const.name:
+ self.col_named_constraints[const.name] = (col, const)
+
+ for idx in self.table.indexes:
+ self.indexes[idx.name] = idx # type: ignore[index]
+
+ for k in self.table.kwargs:
+ self.table_kwargs.setdefault(k, self.table.kwargs[k])
+
+ def _adjust_self_columns_for_partial_reordering(self) -> None:
+ pairs = set()
+
+ col_by_idx = list(self.columns)
+
+ if self.partial_reordering:
+ for tuple_ in self.partial_reordering:
+ for index, elem in enumerate(tuple_):
+ if index > 0:
+ pairs.add((tuple_[index - 1], elem))
+ else:
+ for index, elem in enumerate(self.existing_ordering):
+ if index > 0:
+ pairs.add((col_by_idx[index - 1], elem))
+
+ pairs.update(self.add_col_ordering)
+
+ # this can happen if some columns were dropped and not removed
+ # from existing_ordering. this should be prevented already, but
+ # conservatively making sure this didn't happen
+ pairs_list = [p for p in pairs if p[0] != p[1]]
+
+ sorted_ = list(
+ topological.sort(pairs_list, col_by_idx, deterministic_order=True)
+ )
+ self.columns = OrderedDict((k, self.columns[k]) for k in sorted_)
+ self.column_transfers = OrderedDict(
+ (k, self.column_transfers[k]) for k in sorted_
+ )
+
+ def _transfer_elements_to_new_table(self) -> None:
+ assert self.new_table is None, "Can only create new table once"
+
+ m = MetaData()
+ schema = self.table.schema
+
+ if self.partial_reordering or self.add_col_ordering:
+ self._adjust_self_columns_for_partial_reordering()
+
+ self.new_table = new_table = Table(
+ self.temp_table_name,
+ m,
+ *(list(self.columns.values()) + list(self.table_args)),
+ schema=schema,
+ **self.table_kwargs,
+ )
+
+ for const in (
+ list(self.named_constraints.values()) + self.unnamed_constraints
+ ):
+ const_columns = {c.key for c in _columns_for_constraint(const)}
+
+ if not const_columns.issubset(self.column_transfers):
+ continue
+
+ const_copy: Constraint
+ if isinstance(const, ForeignKeyConstraint):
+ if _fk_is_self_referential(const):
+ # for self-referential constraint, refer to the
+ # *original* table name, and not _alembic_batch_temp.
+ # This is consistent with how we're handling
+ # FK constraints from other tables; we assume SQLite
+ # no foreign keys just keeps the names unchanged, so
+ # when we rename back, they match again.
+ const_copy = _copy(
+ const, schema=schema, target_table=self.table
+ )
+ else:
+ # "target_table" for ForeignKeyConstraint.copy() is
+ # only used if the FK is detected as being
+ # self-referential, which we are handling above.
+ const_copy = _copy(const, schema=schema)
+ else:
+ const_copy = _copy(
+ const, schema=schema, target_table=new_table
+ )
+ if isinstance(const, ForeignKeyConstraint):
+ self._setup_referent(m, const)
+ new_table.append_constraint(const_copy)
+
+ def _gather_indexes_from_both_tables(self) -> List[Index]:
+ assert self.new_table is not None
+ idx: List[Index] = []
+
+ for idx_existing in self.indexes.values():
+ # this is a lift-and-move from Table.to_metadata
+
+ if idx_existing._column_flag:
+ continue
+
+ idx_copy = Index(
+ idx_existing.name,
+ unique=idx_existing.unique,
+ *[
+ _copy_expression(expr, self.new_table)
+ for expr in _idx_table_bound_expressions(idx_existing)
+ ],
+ _table=self.new_table,
+ **idx_existing.kwargs,
+ )
+ idx.append(idx_copy)
+
+ for index in self.new_indexes.values():
+ idx.append(
+ Index(
+ index.name,
+ unique=index.unique,
+ *[self.new_table.c[col] for col in index.columns.keys()],
+ **index.kwargs,
+ )
+ )
+ return idx
+
+ def _setup_referent(
+ self, metadata: MetaData, constraint: ForeignKeyConstraint
+ ) -> None:
+ spec = constraint.elements[0]._get_colspec()
+ parts = spec.split(".")
+ tname = parts[-2]
+ if len(parts) == 3:
+ referent_schema = parts[0]
+ else:
+ referent_schema = None
+
+ if tname != self.temp_table_name:
+ key = sql_schema._get_table_key(tname, referent_schema)
+
+ def colspec(elem: Any):
+ return elem._get_colspec()
+
+ if key in metadata.tables:
+ t = metadata.tables[key]
+ for elem in constraint.elements:
+ colname = colspec(elem).split(".")[-1]
+ if colname not in t.c:
+ t.append_column(Column(colname, sqltypes.NULLTYPE))
+ else:
+ Table(
+ tname,
+ metadata,
+ *[
+ Column(n, sqltypes.NULLTYPE)
+ for n in [
+ colspec(elem).split(".")[-1]
+ for elem in constraint.elements
+ ]
+ ],
+ schema=referent_schema,
+ )
+
+ def _create(self, op_impl: DefaultImpl) -> None:
+ self._transfer_elements_to_new_table()
+
+ op_impl.prep_table_for_batch(self, self.table)
+ assert self.new_table is not None
+ op_impl.create_table(self.new_table)
+
+ try:
+ op_impl._exec(
+ _insert_inline(self.new_table).from_select(
+ list(
+ k
+ for k, transfer in self.column_transfers.items()
+ if "expr" in transfer
+ ),
+ _select(
+ *[
+ transfer["expr"]
+ for transfer in self.column_transfers.values()
+ if "expr" in transfer
+ ]
+ ),
+ )
+ )
+ op_impl.drop_table(self.table)
+ except:
+ op_impl.drop_table(self.new_table)
+ raise
+ else:
+ op_impl.rename_table(
+ self.temp_table_name, self.table.name, schema=self.table.schema
+ )
+ self.new_table.name = self.table.name
+ try:
+ for idx in self._gather_indexes_from_both_tables():
+ op_impl.create_index(idx)
+ finally:
+ self.new_table.name = self.temp_table_name
+
+ def alter_column(
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Optional[Union[Function[Any], str, bool]] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ autoincrement: Optional[Union[bool, Literal["auto"]]] = None,
+ comment: Union[str, Literal[False]] = False,
+ **kw,
+ ) -> None:
+ existing = self.columns[column_name]
+ existing_transfer: Dict[str, Any] = self.column_transfers[column_name]
+ if name is not None and name != column_name:
+ # note that we don't change '.key' - we keep referring
+ # to the renamed column by its old key in _create(). neat!
+ existing.name = name
+ existing_transfer["name"] = name
+
+ existing_type = kw.get("existing_type", None)
+ if existing_type:
+ resolved_existing_type = _resolve_for_variant(
+ kw["existing_type"], self.impl.dialect
+ )
+
+ # pop named constraints for Boolean/Enum for rename
+ if (
+ isinstance(resolved_existing_type, SchemaEventTarget)
+ and resolved_existing_type.name # type:ignore[attr-defined] # noqa E501
+ ):
+ self.named_constraints.pop(
+ resolved_existing_type.name, # type:ignore[attr-defined] # noqa E501
+ None,
+ )
+
+ if type_ is not None:
+ type_ = sqltypes.to_instance(type_)
+ # old type is being discarded so turn off eventing
+ # rules. Alternatively we can
+ # erase the events set up by this type, but this is simpler.
+ # we also ignore the drop_constraint that will come here from
+ # Operations.implementation_for(alter_column)
+
+ if isinstance(existing.type, SchemaEventTarget):
+ existing.type._create_events = ( # type:ignore[attr-defined]
+ existing.type.create_constraint # type:ignore[attr-defined] # noqa
+ ) = False
+
+ self.impl.cast_for_batch_migrate(
+ existing, existing_transfer, type_
+ )
+
+ existing.type = type_
+
+ # we *dont* however set events for the new type, because
+ # alter_column is invoked from
+ # Operations.implementation_for(alter_column) which already
+ # will emit an add_constraint()
+
+ if nullable is not None:
+ existing.nullable = nullable
+ if server_default is not False:
+ if server_default is None:
+ existing.server_default = None
+ else:
+ sql_schema.DefaultClause(
+ server_default # type: ignore[arg-type]
+ )._set_parent(existing)
+ if autoincrement is not None:
+ existing.autoincrement = bool(autoincrement)
+
+ if comment is not False:
+ existing.comment = comment
+
+ def _setup_dependencies_for_add_column(
+ self,
+ colname: str,
+ insert_before: Optional[str],
+ insert_after: Optional[str],
+ ) -> None:
+ index_cols = self.existing_ordering
+ col_indexes = {name: i for i, name in enumerate(index_cols)}
+
+ if not self.partial_reordering:
+ if insert_after:
+ if not insert_before:
+ if insert_after in col_indexes:
+ # insert after an existing column
+ idx = col_indexes[insert_after] + 1
+ if idx < len(index_cols):
+ insert_before = index_cols[idx]
+ else:
+ # insert after a column that is also new
+ insert_before = dict(self.add_col_ordering)[
+ insert_after
+ ]
+ if insert_before:
+ if not insert_after:
+ if insert_before in col_indexes:
+ # insert before an existing column
+ idx = col_indexes[insert_before] - 1
+ if idx >= 0:
+ insert_after = index_cols[idx]
+ else:
+ # insert before a column that is also new
+ insert_after = {
+ b: a for a, b in self.add_col_ordering
+ }[insert_before]
+
+ if insert_before:
+ self.add_col_ordering += ((colname, insert_before),)
+ if insert_after:
+ self.add_col_ordering += ((insert_after, colname),)
+
+ if (
+ not self.partial_reordering
+ and not insert_before
+ and not insert_after
+ and col_indexes
+ ):
+ self.add_col_ordering += ((index_cols[-1], colname),)
+
+ def add_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ insert_before: Optional[str] = None,
+ insert_after: Optional[str] = None,
+ **kw,
+ ) -> None:
+ self._setup_dependencies_for_add_column(
+ column.name, insert_before, insert_after
+ )
+ # we copy the column because operations.add_column()
+ # gives us a Column that is part of a Table already.
+ self.columns[column.name] = _copy(column, schema=self.table.schema)
+ self.column_transfers[column.name] = {}
+
+ def drop_column(
+ self,
+ table_name: str,
+ column: Union[ColumnClause[Any], Column[Any]],
+ **kw,
+ ) -> None:
+ if column.name in self.table.primary_key.columns:
+ _remove_column_from_collection(
+ self.table.primary_key.columns, column
+ )
+ del self.columns[column.name]
+ del self.column_transfers[column.name]
+ self.existing_ordering.remove(column.name)
+
+ # pop named constraints for Boolean/Enum for rename
+ if (
+ "existing_type" in kw
+ and isinstance(kw["existing_type"], SchemaEventTarget)
+ and kw["existing_type"].name # type:ignore[attr-defined]
+ ):
+ self.named_constraints.pop(
+ kw["existing_type"].name, None # type:ignore[attr-defined]
+ )
+
+ def create_column_comment(self, column):
+ """the batch table creation function will issue create_column_comment
+ on the real "impl" as part of the create table process.
+
+ That is, the Column object will have the comment on it already,
+ so when it is received by add_column() it will be a normal part of
+ the CREATE TABLE and doesn't need an extra step here.
+
+ """
+
+ def create_table_comment(self, table):
+ """the batch table creation function will issue create_table_comment
+ on the real "impl" as part of the create table process.
+
+ """
+
+ def drop_table_comment(self, table):
+ """the batch table creation function will issue drop_table_comment
+ on the real "impl" as part of the create table process.
+
+ """
+
+ def add_constraint(self, const: Constraint) -> None:
+ if not constraint_name_defined(const.name):
+ raise ValueError("Constraint must have a name")
+ if isinstance(const, sql_schema.PrimaryKeyConstraint):
+ if self.table.primary_key in self.unnamed_constraints:
+ self.unnamed_constraints.remove(self.table.primary_key)
+
+ if constraint_name_string(const.name):
+ self.named_constraints[const.name] = const
+ else:
+ self.unnamed_constraints.append(const)
+
+ def drop_constraint(self, const: Constraint) -> None:
+ if not const.name:
+ raise ValueError("Constraint must have a name")
+ try:
+ if const.name in self.col_named_constraints:
+ col, const = self.col_named_constraints.pop(const.name)
+
+ for col_const in list(self.columns[col.name].constraints):
+ if col_const.name == const.name:
+ self.columns[col.name].constraints.remove(col_const)
+ elif constraint_name_string(const.name):
+ const = self.named_constraints.pop(const.name)
+ elif const in self.unnamed_constraints:
+ self.unnamed_constraints.remove(const)
+
+ except KeyError:
+ if _is_type_bound(const):
+ # type-bound constraints are only included in the new
+ # table via their type object in any case, so ignore the
+ # drop_constraint() that comes here via the
+ # Operations.implementation_for(alter_column)
+ return
+ raise ValueError("No such constraint: '%s'" % const.name)
+ else:
+ if isinstance(const, PrimaryKeyConstraint):
+ for col in const.columns:
+ self.columns[col.name].primary_key = False
+
+ def create_index(self, idx: Index) -> None:
+ self.new_indexes[idx.name] = idx # type: ignore[index]
+
+ def drop_index(self, idx: Index) -> None:
+ try:
+ del self.indexes[idx.name] # type: ignore[arg-type]
+ except KeyError:
+ raise ValueError("No such index: '%s'" % idx.name)
+
+ def rename_table(self, *arg, **kw):
+ raise NotImplementedError("TODO")
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/ops.py b/venv/lib/python3.12/site-packages/alembic/operations/ops.py
new file mode 100644
index 0000000..7b65191
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/operations/ops.py
@@ -0,0 +1,2786 @@
+from __future__ import annotations
+
+from abc import abstractmethod
+import re
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import FrozenSet
+from typing import Iterator
+from typing import List
+from typing import MutableMapping
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy.types import NULLTYPE
+
+from . import schemaobj
+from .base import BatchOperations
+from .base import Operations
+from .. import util
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import conv
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import CheckConstraint
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Computed
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import ForeignKeyConstraint
+ from sqlalchemy.sql.schema import Identity
+ from sqlalchemy.sql.schema import Index
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import PrimaryKeyConstraint
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.schema import UniqueConstraint
+ from sqlalchemy.sql.selectable import TableClause
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from ..autogenerate.rewriter import Rewriter
+ from ..runtime.migration import MigrationContext
+ from ..script.revision import _RevIdType
+
+_T = TypeVar("_T", bound=Any)
+_AC = TypeVar("_AC", bound="AddConstraintOp")
+
+
+class MigrateOperation:
+ """base class for migration command and organization objects.
+
+ This system is part of the operation extensibility API.
+
+ .. seealso::
+
+ :ref:`operation_objects`
+
+ :ref:`operation_plugins`
+
+ :ref:`customizing_revision`
+
+ """
+
+ @util.memoized_property
+ def info(self) -> Dict[Any, Any]:
+ """A dictionary that may be used to store arbitrary information
+ along with this :class:`.MigrateOperation` object.
+
+ """
+ return {}
+
+ _mutations: FrozenSet[Rewriter] = frozenset()
+
+ def reverse(self) -> MigrateOperation:
+ raise NotImplementedError
+
+ def to_diff_tuple(self) -> Tuple[Any, ...]:
+ raise NotImplementedError
+
+
+class AddConstraintOp(MigrateOperation):
+ """Represent an add constraint operation."""
+
+ add_constraint_ops = util.Dispatcher()
+
+ @property
+ def constraint_type(self) -> str:
+ raise NotImplementedError()
+
+ @classmethod
+ def register_add_constraint(
+ cls, type_: str
+ ) -> Callable[[Type[_AC]], Type[_AC]]:
+ def go(klass: Type[_AC]) -> Type[_AC]:
+ cls.add_constraint_ops.dispatch_for(type_)(klass.from_constraint)
+ return klass
+
+ return go
+
+ @classmethod
+ def from_constraint(cls, constraint: Constraint) -> AddConstraintOp:
+ return cls.add_constraint_ops.dispatch(constraint.__visit_name__)( # type: ignore[no-any-return] # noqa: E501
+ constraint
+ )
+
+ @abstractmethod
+ def to_constraint(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Constraint:
+ pass
+
+ def reverse(self) -> DropConstraintOp:
+ return DropConstraintOp.from_constraint(self.to_constraint())
+
+ def to_diff_tuple(self) -> Tuple[str, Constraint]:
+ return ("add_constraint", self.to_constraint())
+
+
+@Operations.register_operation("drop_constraint")
+@BatchOperations.register_operation("drop_constraint", "batch_drop_constraint")
+class DropConstraintOp(MigrateOperation):
+ """Represent a drop constraint operation."""
+
+ def __init__(
+ self,
+ constraint_name: Optional[sqla_compat._ConstraintNameDefined],
+ table_name: str,
+ type_: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ _reverse: Optional[AddConstraintOp] = None,
+ ) -> None:
+ self.constraint_name = constraint_name
+ self.table_name = table_name
+ self.constraint_type = type_
+ self.schema = schema
+ self._reverse = _reverse
+
+ def reverse(self) -> AddConstraintOp:
+ return AddConstraintOp.from_constraint(self.to_constraint())
+
+ def to_diff_tuple(
+ self,
+ ) -> Tuple[str, SchemaItem]:
+ if self.constraint_type == "foreignkey":
+ return ("remove_fk", self.to_constraint())
+ else:
+ return ("remove_constraint", self.to_constraint())
+
+ @classmethod
+ def from_constraint(cls, constraint: Constraint) -> DropConstraintOp:
+ types = {
+ "unique_constraint": "unique",
+ "foreign_key_constraint": "foreignkey",
+ "primary_key_constraint": "primary",
+ "check_constraint": "check",
+ "column_check_constraint": "check",
+ "table_or_column_check_constraint": "check",
+ }
+
+ constraint_table = sqla_compat._table_for_constraint(constraint)
+ return cls(
+ sqla_compat.constraint_name_or_none(constraint.name),
+ constraint_table.name,
+ schema=constraint_table.schema,
+ type_=types.get(constraint.__visit_name__),
+ _reverse=AddConstraintOp.from_constraint(constraint),
+ )
+
+ def to_constraint(self) -> Constraint:
+ if self._reverse is not None:
+ constraint = self._reverse.to_constraint()
+ constraint.name = self.constraint_name
+ constraint_table = sqla_compat._table_for_constraint(constraint)
+ constraint_table.name = self.table_name
+ constraint_table.schema = self.schema
+
+ return constraint
+ else:
+ raise ValueError(
+ "constraint cannot be produced; "
+ "original constraint is not present"
+ )
+
+ @classmethod
+ def drop_constraint(
+ cls,
+ operations: Operations,
+ constraint_name: str,
+ table_name: str,
+ type_: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ r"""Drop a constraint of the given name, typically via DROP CONSTRAINT.
+
+ :param constraint_name: name of the constraint.
+ :param table_name: table name.
+ :param type\_: optional, required on MySQL. can be
+ 'foreignkey', 'primary', 'unique', or 'check'.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+ op = cls(constraint_name, table_name, type_=type_, schema=schema)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_drop_constraint(
+ cls,
+ operations: BatchOperations,
+ constraint_name: str,
+ type_: Optional[str] = None,
+ ) -> None:
+ """Issue a "drop constraint" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``table_name`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_constraint`
+
+ """
+ op = cls(
+ constraint_name,
+ operations.impl.table_name,
+ type_=type_,
+ schema=operations.impl.schema,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("create_primary_key")
+@BatchOperations.register_operation(
+ "create_primary_key", "batch_create_primary_key"
+)
+@AddConstraintOp.register_add_constraint("primary_key_constraint")
+class CreatePrimaryKeyOp(AddConstraintOp):
+ """Represent a create primary key operation."""
+
+ constraint_type = "primarykey"
+
+ def __init__(
+ self,
+ constraint_name: Optional[sqla_compat._ConstraintNameDefined],
+ table_name: str,
+ columns: Sequence[str],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ self.constraint_name = constraint_name
+ self.table_name = table_name
+ self.columns = columns
+ self.schema = schema
+ self.kw = kw
+
+ @classmethod
+ def from_constraint(cls, constraint: Constraint) -> CreatePrimaryKeyOp:
+ constraint_table = sqla_compat._table_for_constraint(constraint)
+ pk_constraint = cast("PrimaryKeyConstraint", constraint)
+ return cls(
+ sqla_compat.constraint_name_or_none(pk_constraint.name),
+ constraint_table.name,
+ pk_constraint.columns.keys(),
+ schema=constraint_table.schema,
+ **pk_constraint.dialect_kwargs,
+ )
+
+ def to_constraint(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> PrimaryKeyConstraint:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ return schema_obj.primary_key_constraint(
+ self.constraint_name,
+ self.table_name,
+ self.columns,
+ schema=self.schema,
+ **self.kw,
+ )
+
+ @classmethod
+ def create_primary_key(
+ cls,
+ operations: Operations,
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: List[str],
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue a "create primary key" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_primary_key("pk_my_table", "my_table", ["id", "version"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.PrimaryKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the primary key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the target table.
+ :param columns: a list of string column names to be applied to the
+ primary key constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+ op = cls(constraint_name, table_name, columns, schema=schema)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_primary_key(
+ cls,
+ operations: BatchOperations,
+ constraint_name: str,
+ columns: List[str],
+ ) -> None:
+ """Issue a "create primary key" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``table_name`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.create_primary_key`
+
+ """
+ op = cls(
+ constraint_name,
+ operations.impl.table_name,
+ columns,
+ schema=operations.impl.schema,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("create_unique_constraint")
+@BatchOperations.register_operation(
+ "create_unique_constraint", "batch_create_unique_constraint"
+)
+@AddConstraintOp.register_add_constraint("unique_constraint")
+class CreateUniqueConstraintOp(AddConstraintOp):
+ """Represent a create unique constraint operation."""
+
+ constraint_type = "unique"
+
+ def __init__(
+ self,
+ constraint_name: Optional[sqla_compat._ConstraintNameDefined],
+ table_name: str,
+ columns: Sequence[str],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ self.constraint_name = constraint_name
+ self.table_name = table_name
+ self.columns = columns
+ self.schema = schema
+ self.kw = kw
+
+ @classmethod
+ def from_constraint(
+ cls, constraint: Constraint
+ ) -> CreateUniqueConstraintOp:
+ constraint_table = sqla_compat._table_for_constraint(constraint)
+
+ uq_constraint = cast("UniqueConstraint", constraint)
+
+ kw: Dict[str, Any] = {}
+ if uq_constraint.deferrable:
+ kw["deferrable"] = uq_constraint.deferrable
+ if uq_constraint.initially:
+ kw["initially"] = uq_constraint.initially
+ kw.update(uq_constraint.dialect_kwargs)
+ return cls(
+ sqla_compat.constraint_name_or_none(uq_constraint.name),
+ constraint_table.name,
+ [c.name for c in uq_constraint.columns],
+ schema=constraint_table.schema,
+ **kw,
+ )
+
+ def to_constraint(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> UniqueConstraint:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+ return schema_obj.unique_constraint(
+ self.constraint_name,
+ self.table_name,
+ self.columns,
+ schema=self.schema,
+ **self.kw,
+ )
+
+ @classmethod
+ def create_unique_constraint(
+ cls,
+ operations: Operations,
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: Sequence[str],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> Any:
+ """Issue a "create unique constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ op.create_unique_constraint("uq_user_name", "user", ["name"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.UniqueConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param name: Name of the unique constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param columns: a list of string column names in the
+ source table.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+ op = cls(constraint_name, table_name, columns, schema=schema, **kw)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_unique_constraint(
+ cls,
+ operations: BatchOperations,
+ constraint_name: str,
+ columns: Sequence[str],
+ **kw: Any,
+ ) -> Any:
+ """Issue a "create unique constraint" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``source`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.create_unique_constraint`
+
+ """
+ kw["schema"] = operations.impl.schema
+ op = cls(constraint_name, operations.impl.table_name, columns, **kw)
+ return operations.invoke(op)
+
+
+@Operations.register_operation("create_foreign_key")
+@BatchOperations.register_operation(
+ "create_foreign_key", "batch_create_foreign_key"
+)
+@AddConstraintOp.register_add_constraint("foreign_key_constraint")
+class CreateForeignKeyOp(AddConstraintOp):
+ """Represent a create foreign key constraint operation."""
+
+ constraint_type = "foreignkey"
+
+ def __init__(
+ self,
+ constraint_name: Optional[sqla_compat._ConstraintNameDefined],
+ source_table: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ **kw: Any,
+ ) -> None:
+ self.constraint_name = constraint_name
+ self.source_table = source_table
+ self.referent_table = referent_table
+ self.local_cols = local_cols
+ self.remote_cols = remote_cols
+ self.kw = kw
+
+ def to_diff_tuple(self) -> Tuple[str, ForeignKeyConstraint]:
+ return ("add_fk", self.to_constraint())
+
+ @classmethod
+ def from_constraint(cls, constraint: Constraint) -> CreateForeignKeyOp:
+ fk_constraint = cast("ForeignKeyConstraint", constraint)
+ kw: Dict[str, Any] = {}
+ if fk_constraint.onupdate:
+ kw["onupdate"] = fk_constraint.onupdate
+ if fk_constraint.ondelete:
+ kw["ondelete"] = fk_constraint.ondelete
+ if fk_constraint.initially:
+ kw["initially"] = fk_constraint.initially
+ if fk_constraint.deferrable:
+ kw["deferrable"] = fk_constraint.deferrable
+ if fk_constraint.use_alter:
+ kw["use_alter"] = fk_constraint.use_alter
+ if fk_constraint.match:
+ kw["match"] = fk_constraint.match
+
+ (
+ source_schema,
+ source_table,
+ source_columns,
+ target_schema,
+ target_table,
+ target_columns,
+ onupdate,
+ ondelete,
+ deferrable,
+ initially,
+ ) = sqla_compat._fk_spec(fk_constraint)
+
+ kw["source_schema"] = source_schema
+ kw["referent_schema"] = target_schema
+ kw.update(fk_constraint.dialect_kwargs)
+ return cls(
+ sqla_compat.constraint_name_or_none(fk_constraint.name),
+ source_table,
+ target_table,
+ source_columns,
+ target_columns,
+ **kw,
+ )
+
+ def to_constraint(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> ForeignKeyConstraint:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+ return schema_obj.foreign_key_constraint(
+ self.constraint_name,
+ self.source_table,
+ self.referent_table,
+ self.local_cols,
+ self.remote_cols,
+ **self.kw,
+ )
+
+ @classmethod
+ def create_foreign_key(
+ cls,
+ operations: Operations,
+ constraint_name: Optional[str],
+ source_table: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ *,
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ source_schema: Optional[str] = None,
+ referent_schema: Optional[str] = None,
+ **dialect_kw: Any,
+ ) -> None:
+ """Issue a "create foreign key" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_foreign_key(
+ "fk_user_address",
+ "address",
+ "user",
+ ["user_id"],
+ ["id"],
+ )
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.ForeignKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the foreign key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param source_table: String name of the source table.
+ :param referent_table: String name of the destination table.
+ :param local_cols: a list of string column names in the
+ source table.
+ :param remote_cols: a list of string column names in the
+ remote table.
+ :param onupdate: Optional string. If set, emit ON UPDATE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param ondelete: Optional string. If set, emit ON DELETE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or NOT
+ DEFERRABLE when issuing DDL for this constraint.
+ :param source_schema: Optional schema name of the source table.
+ :param referent_schema: Optional schema name of the destination table.
+
+ """
+
+ op = cls(
+ constraint_name,
+ source_table,
+ referent_table,
+ local_cols,
+ remote_cols,
+ onupdate=onupdate,
+ ondelete=ondelete,
+ deferrable=deferrable,
+ source_schema=source_schema,
+ referent_schema=referent_schema,
+ initially=initially,
+ match=match,
+ **dialect_kw,
+ )
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_foreign_key(
+ cls,
+ operations: BatchOperations,
+ constraint_name: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ *,
+ referent_schema: Optional[str] = None,
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ **dialect_kw: Any,
+ ) -> None:
+ """Issue a "create foreign key" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``source`` and ``source_schema``
+ arguments from the call.
+
+ e.g.::
+
+ with batch_alter_table("address") as batch_op:
+ batch_op.create_foreign_key(
+ "fk_user_address",
+ "user",
+ ["user_id"],
+ ["id"],
+ )
+
+ .. seealso::
+
+ :meth:`.Operations.create_foreign_key`
+
+ """
+ op = cls(
+ constraint_name,
+ operations.impl.table_name,
+ referent_table,
+ local_cols,
+ remote_cols,
+ onupdate=onupdate,
+ ondelete=ondelete,
+ deferrable=deferrable,
+ source_schema=operations.impl.schema,
+ referent_schema=referent_schema,
+ initially=initially,
+ match=match,
+ **dialect_kw,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("create_check_constraint")
+@BatchOperations.register_operation(
+ "create_check_constraint", "batch_create_check_constraint"
+)
+@AddConstraintOp.register_add_constraint("check_constraint")
+@AddConstraintOp.register_add_constraint("table_or_column_check_constraint")
+@AddConstraintOp.register_add_constraint("column_check_constraint")
+class CreateCheckConstraintOp(AddConstraintOp):
+ """Represent a create check constraint operation."""
+
+ constraint_type = "check"
+
+ def __init__(
+ self,
+ constraint_name: Optional[sqla_compat._ConstraintNameDefined],
+ table_name: str,
+ condition: Union[str, TextClause, ColumnElement[Any]],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ self.constraint_name = constraint_name
+ self.table_name = table_name
+ self.condition = condition
+ self.schema = schema
+ self.kw = kw
+
+ @classmethod
+ def from_constraint(
+ cls, constraint: Constraint
+ ) -> CreateCheckConstraintOp:
+ constraint_table = sqla_compat._table_for_constraint(constraint)
+
+ ck_constraint = cast("CheckConstraint", constraint)
+ return cls(
+ sqla_compat.constraint_name_or_none(ck_constraint.name),
+ constraint_table.name,
+ cast("ColumnElement[Any]", ck_constraint.sqltext),
+ schema=constraint_table.schema,
+ **ck_constraint.dialect_kwargs,
+ )
+
+ def to_constraint(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> CheckConstraint:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+ return schema_obj.check_constraint(
+ self.constraint_name,
+ self.table_name,
+ self.condition,
+ schema=self.schema,
+ **self.kw,
+ )
+
+ @classmethod
+ def create_check_constraint(
+ cls,
+ operations: Operations,
+ constraint_name: Optional[str],
+ table_name: str,
+ condition: Union[str, ColumnElement[bool], TextClause],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue a "create check constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy.sql import column, func
+
+ op.create_check_constraint(
+ "ck_user_name_len",
+ "user",
+ func.len(column("name")) > 5,
+ )
+
+ CHECK constraints are usually against a SQL expression, so ad-hoc
+ table metadata is usually needed. The function will convert the given
+ arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound
+ to an anonymous table in order to emit the CREATE statement.
+
+ :param name: Name of the check constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param condition: SQL expression that's the condition of the
+ constraint. Can be a string or SQLAlchemy expression language
+ structure.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+ op = cls(constraint_name, table_name, condition, schema=schema, **kw)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_check_constraint(
+ cls,
+ operations: BatchOperations,
+ constraint_name: str,
+ condition: Union[str, ColumnElement[bool], TextClause],
+ **kw: Any,
+ ) -> None:
+ """Issue a "create check constraint" instruction using the
+ current batch migration context.
+
+ The batch form of this call omits the ``source`` and ``schema``
+ arguments from the call.
+
+ .. seealso::
+
+ :meth:`.Operations.create_check_constraint`
+
+ """
+ op = cls(
+ constraint_name,
+ operations.impl.table_name,
+ condition,
+ schema=operations.impl.schema,
+ **kw,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("create_index")
+@BatchOperations.register_operation("create_index", "batch_create_index")
+class CreateIndexOp(MigrateOperation):
+ """Represent a create index operation."""
+
+ def __init__(
+ self,
+ index_name: Optional[str],
+ table_name: str,
+ columns: Sequence[Union[str, TextClause, ColumnElement[Any]]],
+ *,
+ schema: Optional[str] = None,
+ unique: bool = False,
+ if_not_exists: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ self.index_name = index_name
+ self.table_name = table_name
+ self.columns = columns
+ self.schema = schema
+ self.unique = unique
+ self.if_not_exists = if_not_exists
+ self.kw = kw
+
+ def reverse(self) -> DropIndexOp:
+ return DropIndexOp.from_index(self.to_index())
+
+ def to_diff_tuple(self) -> Tuple[str, Index]:
+ return ("add_index", self.to_index())
+
+ @classmethod
+ def from_index(cls, index: Index) -> CreateIndexOp:
+ assert index.table is not None
+ return cls(
+ index.name,
+ index.table.name,
+ index.expressions,
+ schema=index.table.schema,
+ unique=index.unique,
+ **index.kwargs,
+ )
+
+ def to_index(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Index:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ idx = schema_obj.index(
+ self.index_name,
+ self.table_name,
+ self.columns,
+ schema=self.schema,
+ unique=self.unique,
+ **self.kw,
+ )
+ return idx
+
+ @classmethod
+ def create_index(
+ cls,
+ operations: Operations,
+ index_name: Optional[str],
+ table_name: str,
+ columns: Sequence[Union[str, TextClause, Function[Any]]],
+ *,
+ schema: Optional[str] = None,
+ unique: bool = False,
+ if_not_exists: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue a "create index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_index("ik_test", "t1", ["foo", "bar"])
+
+ Functional indexes can be produced by using the
+ :func:`sqlalchemy.sql.expression.text` construct::
+
+ from alembic import op
+ from sqlalchemy import text
+
+ op.create_index("ik_test", "t1", [text("lower(foo)")])
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table.
+ :param columns: a list consisting of string column names and/or
+ :func:`~sqlalchemy.sql.expression.text` constructs.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param unique: If True, create a unique index.
+
+ :param quote: Force quoting of this column's name on or off,
+ corresponding to ``True`` or ``False``. When left at its default
+ of ``None``, the column identifier will be quoted according to
+ whether the name is case sensitive (identifiers with at least one
+ upper case character are treated as case sensitive), or if it's a
+ reserved word. This flag is only needed to force quoting of a
+ reserved word which is not known by the SQLAlchemy dialect.
+
+ :param if_not_exists: If True, adds IF NOT EXISTS operator when
+ creating the new index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """
+ op = cls(
+ index_name,
+ table_name,
+ columns,
+ schema=schema,
+ unique=unique,
+ if_not_exists=if_not_exists,
+ **kw,
+ )
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_index(
+ cls,
+ operations: BatchOperations,
+ index_name: str,
+ columns: List[str],
+ **kw: Any,
+ ) -> None:
+ """Issue a "create index" instruction using the
+ current batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.create_index`
+
+ """
+
+ op = cls(
+ index_name,
+ operations.impl.table_name,
+ columns,
+ schema=operations.impl.schema,
+ **kw,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("drop_index")
+@BatchOperations.register_operation("drop_index", "batch_drop_index")
+class DropIndexOp(MigrateOperation):
+ """Represent a drop index operation."""
+
+ def __init__(
+ self,
+ index_name: Union[quoted_name, str, conv],
+ table_name: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ if_exists: Optional[bool] = None,
+ _reverse: Optional[CreateIndexOp] = None,
+ **kw: Any,
+ ) -> None:
+ self.index_name = index_name
+ self.table_name = table_name
+ self.schema = schema
+ self.if_exists = if_exists
+ self._reverse = _reverse
+ self.kw = kw
+
+ def to_diff_tuple(self) -> Tuple[str, Index]:
+ return ("remove_index", self.to_index())
+
+ def reverse(self) -> CreateIndexOp:
+ return CreateIndexOp.from_index(self.to_index())
+
+ @classmethod
+ def from_index(cls, index: Index) -> DropIndexOp:
+ assert index.table is not None
+ return cls(
+ index.name, # type: ignore[arg-type]
+ table_name=index.table.name,
+ schema=index.table.schema,
+ _reverse=CreateIndexOp.from_index(index),
+ unique=index.unique,
+ **index.kwargs,
+ )
+
+ def to_index(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Index:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ # need a dummy column name here since SQLAlchemy
+ # 0.7.6 and further raises on Index with no columns
+ return schema_obj.index(
+ self.index_name,
+ self.table_name,
+ self._reverse.columns if self._reverse else ["x"],
+ schema=self.schema,
+ **self.kw,
+ )
+
+ @classmethod
+ def drop_index(
+ cls,
+ operations: Operations,
+ index_name: str,
+ table_name: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ if_exists: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue a "drop index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_index("accounts")
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table. Some
+ backends such as Microsoft SQL Server require this.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ :param if_exists: If True, adds IF EXISTS operator when
+ dropping the index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """
+ op = cls(
+ index_name,
+ table_name=table_name,
+ schema=schema,
+ if_exists=if_exists,
+ **kw,
+ )
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_drop_index(
+ cls, operations: BatchOperations, index_name: str, **kw: Any
+ ) -> None:
+ """Issue a "drop index" instruction using the
+ current batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_index`
+
+ """
+
+ op = cls(
+ index_name,
+ table_name=operations.impl.table_name,
+ schema=operations.impl.schema,
+ **kw,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("create_table")
+class CreateTableOp(MigrateOperation):
+ """Represent a create table operation."""
+
+ def __init__(
+ self,
+ table_name: str,
+ columns: Sequence[SchemaItem],
+ *,
+ schema: Optional[str] = None,
+ _namespace_metadata: Optional[MetaData] = None,
+ _constraints_included: bool = False,
+ **kw: Any,
+ ) -> None:
+ self.table_name = table_name
+ self.columns = columns
+ self.schema = schema
+ self.info = kw.pop("info", {})
+ self.comment = kw.pop("comment", None)
+ self.prefixes = kw.pop("prefixes", None)
+ self.kw = kw
+ self._namespace_metadata = _namespace_metadata
+ self._constraints_included = _constraints_included
+
+ def reverse(self) -> DropTableOp:
+ return DropTableOp.from_table(
+ self.to_table(), _namespace_metadata=self._namespace_metadata
+ )
+
+ def to_diff_tuple(self) -> Tuple[str, Table]:
+ return ("add_table", self.to_table())
+
+ @classmethod
+ def from_table(
+ cls, table: Table, *, _namespace_metadata: Optional[MetaData] = None
+ ) -> CreateTableOp:
+ if _namespace_metadata is None:
+ _namespace_metadata = table.metadata
+
+ return cls(
+ table.name,
+ list(table.c) + list(table.constraints),
+ schema=table.schema,
+ _namespace_metadata=_namespace_metadata,
+ # given a Table() object, this Table will contain full Index()
+ # and UniqueConstraint objects already constructed in response to
+ # each unique=True / index=True flag on a Column. Carry this
+ # state along so that when we re-convert back into a Table, we
+ # skip unique=True/index=True so that these constraints are
+ # not doubled up. see #844 #848
+ _constraints_included=True,
+ comment=table.comment,
+ info=dict(table.info),
+ prefixes=list(table._prefixes),
+ **table.kwargs,
+ )
+
+ def to_table(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Table:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ return schema_obj.table(
+ self.table_name,
+ *self.columns,
+ schema=self.schema,
+ prefixes=list(self.prefixes) if self.prefixes else [],
+ comment=self.comment,
+ info=self.info.copy() if self.info else {},
+ _constraints_included=self._constraints_included,
+ **self.kw,
+ )
+
+ @classmethod
+ def create_table(
+ cls,
+ operations: Operations,
+ table_name: str,
+ *columns: SchemaItem,
+ **kw: Any,
+ ) -> Table:
+ r"""Issue a "create table" instruction using the current migration
+ context.
+
+ This directive receives an argument list similar to that of the
+ traditional :class:`sqlalchemy.schema.Table` construct, but without the
+ metadata::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ Note that :meth:`.create_table` accepts
+ :class:`~sqlalchemy.schema.Column`
+ constructs directly from the SQLAlchemy library. In particular,
+ default values to be created on the database side are
+ specified using the ``server_default`` parameter, and not
+ ``default`` which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the "timestamp" column
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ The function also returns a newly created
+ :class:`~sqlalchemy.schema.Table` object, corresponding to the table
+ specification given, which is suitable for
+ immediate SQL operations, in particular
+ :meth:`.Operations.bulk_insert`::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ account_table = op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ op.bulk_insert(
+ account_table,
+ [
+ {"name": "A1", "description": "account 1"},
+ {"name": "A2", "description": "account 2"},
+ ],
+ )
+
+ :param table_name: Name of the table
+ :param \*columns: collection of :class:`~sqlalchemy.schema.Column`
+ objects within
+ the table, as well as optional :class:`~sqlalchemy.schema.Constraint`
+ objects
+ and :class:`~.sqlalchemy.schema.Index` objects.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ :return: the :class:`~sqlalchemy.schema.Table` object corresponding
+ to the parameters given.
+
+ """
+ op = cls(table_name, columns, **kw)
+ return operations.invoke(op)
+
+
+@Operations.register_operation("drop_table")
+class DropTableOp(MigrateOperation):
+ """Represent a drop table operation."""
+
+ def __init__(
+ self,
+ table_name: str,
+ *,
+ schema: Optional[str] = None,
+ table_kw: Optional[MutableMapping[Any, Any]] = None,
+ _reverse: Optional[CreateTableOp] = None,
+ ) -> None:
+ self.table_name = table_name
+ self.schema = schema
+ self.table_kw = table_kw or {}
+ self.comment = self.table_kw.pop("comment", None)
+ self.info = self.table_kw.pop("info", None)
+ self.prefixes = self.table_kw.pop("prefixes", None)
+ self._reverse = _reverse
+
+ def to_diff_tuple(self) -> Tuple[str, Table]:
+ return ("remove_table", self.to_table())
+
+ def reverse(self) -> CreateTableOp:
+ return CreateTableOp.from_table(self.to_table())
+
+ @classmethod
+ def from_table(
+ cls, table: Table, *, _namespace_metadata: Optional[MetaData] = None
+ ) -> DropTableOp:
+ return cls(
+ table.name,
+ schema=table.schema,
+ table_kw={
+ "comment": table.comment,
+ "info": dict(table.info),
+ "prefixes": list(table._prefixes),
+ **table.kwargs,
+ },
+ _reverse=CreateTableOp.from_table(
+ table, _namespace_metadata=_namespace_metadata
+ ),
+ )
+
+ def to_table(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Table:
+ if self._reverse:
+ cols_and_constraints = self._reverse.columns
+ else:
+ cols_and_constraints = []
+
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+ t = schema_obj.table(
+ self.table_name,
+ *cols_and_constraints,
+ comment=self.comment,
+ info=self.info.copy() if self.info else {},
+ prefixes=list(self.prefixes) if self.prefixes else [],
+ schema=self.schema,
+ _constraints_included=self._reverse._constraints_included
+ if self._reverse
+ else False,
+ **self.table_kw,
+ )
+ return t
+
+ @classmethod
+ def drop_table(
+ cls,
+ operations: Operations,
+ table_name: str,
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue a "drop table" instruction using the current
+ migration context.
+
+
+ e.g.::
+
+ drop_table("accounts")
+
+ :param table_name: Name of the table
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ """
+ op = cls(table_name, schema=schema, table_kw=kw)
+ operations.invoke(op)
+
+
+class AlterTableOp(MigrateOperation):
+ """Represent an alter table operation."""
+
+ def __init__(
+ self,
+ table_name: str,
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ self.table_name = table_name
+ self.schema = schema
+
+
+@Operations.register_operation("rename_table")
+class RenameTableOp(AlterTableOp):
+ """Represent a rename table operation."""
+
+ def __init__(
+ self,
+ old_table_name: str,
+ new_table_name: str,
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ super().__init__(old_table_name, schema=schema)
+ self.new_table_name = new_table_name
+
+ @classmethod
+ def rename_table(
+ cls,
+ operations: Operations,
+ old_table_name: str,
+ new_table_name: str,
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Emit an ALTER TABLE to rename a table.
+
+ :param old_table_name: old name.
+ :param new_table_name: new name.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+ op = cls(old_table_name, new_table_name, schema=schema)
+ return operations.invoke(op)
+
+
+@Operations.register_operation("create_table_comment")
+@BatchOperations.register_operation(
+ "create_table_comment", "batch_create_table_comment"
+)
+class CreateTableCommentOp(AlterTableOp):
+ """Represent a COMMENT ON `table` operation."""
+
+ def __init__(
+ self,
+ table_name: str,
+ comment: Optional[str],
+ *,
+ schema: Optional[str] = None,
+ existing_comment: Optional[str] = None,
+ ) -> None:
+ self.table_name = table_name
+ self.comment = comment
+ self.existing_comment = existing_comment
+ self.schema = schema
+
+ @classmethod
+ def create_table_comment(
+ cls,
+ operations: Operations,
+ table_name: str,
+ comment: Optional[str],
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Emit a COMMENT ON operation to set the comment for a table.
+
+ :param table_name: string name of the target table.
+ :param comment: string value of the comment being registered against
+ the specified table.
+ :param existing_comment: String value of a comment
+ already registered on the specified table, used within autogenerate
+ so that the operation is reversible, but not required for direct
+ use.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """
+
+ op = cls(
+ table_name,
+ comment,
+ existing_comment=existing_comment,
+ schema=schema,
+ )
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_table_comment(
+ cls,
+ operations: BatchOperations,
+ comment: Optional[str],
+ *,
+ existing_comment: Optional[str] = None,
+ ) -> None:
+ """Emit a COMMENT ON operation to set the comment for a table
+ using the current batch migration context.
+
+ :param comment: string value of the comment being registered against
+ the specified table.
+ :param existing_comment: String value of a comment
+ already registered on the specified table, used within autogenerate
+ so that the operation is reversible, but not required for direct
+ use.
+
+ """
+
+ op = cls(
+ operations.impl.table_name,
+ comment,
+ existing_comment=existing_comment,
+ schema=operations.impl.schema,
+ )
+ return operations.invoke(op)
+
+ def reverse(self) -> Union[CreateTableCommentOp, DropTableCommentOp]:
+ """Reverses the COMMENT ON operation against a table."""
+ if self.existing_comment is None:
+ return DropTableCommentOp(
+ self.table_name,
+ existing_comment=self.comment,
+ schema=self.schema,
+ )
+ else:
+ return CreateTableCommentOp(
+ self.table_name,
+ self.existing_comment,
+ existing_comment=self.comment,
+ schema=self.schema,
+ )
+
+ def to_table(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Table:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ return schema_obj.table(
+ self.table_name, schema=self.schema, comment=self.comment
+ )
+
+ def to_diff_tuple(self) -> Tuple[Any, ...]:
+ return ("add_table_comment", self.to_table(), self.existing_comment)
+
+
+@Operations.register_operation("drop_table_comment")
+@BatchOperations.register_operation(
+ "drop_table_comment", "batch_drop_table_comment"
+)
+class DropTableCommentOp(AlterTableOp):
+ """Represent an operation to remove the comment from a table."""
+
+ def __init__(
+ self,
+ table_name: str,
+ *,
+ schema: Optional[str] = None,
+ existing_comment: Optional[str] = None,
+ ) -> None:
+ self.table_name = table_name
+ self.existing_comment = existing_comment
+ self.schema = schema
+
+ @classmethod
+ def drop_table_comment(
+ cls,
+ operations: Operations,
+ table_name: str,
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue a "drop table comment" operation to
+ remove an existing comment set on a table.
+
+ :param table_name: string name of the target table.
+ :param existing_comment: An optional string value of a comment already
+ registered on the specified table.
+
+ .. seealso::
+
+ :meth:`.Operations.create_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """
+
+ op = cls(table_name, existing_comment=existing_comment, schema=schema)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_drop_table_comment(
+ cls,
+ operations: BatchOperations,
+ *,
+ existing_comment: Optional[str] = None,
+ ) -> None:
+ """Issue a "drop table comment" operation to
+ remove an existing comment set on a table using the current
+ batch operations context.
+
+ :param existing_comment: An optional string value of a comment already
+ registered on the specified table.
+
+ """
+
+ op = cls(
+ operations.impl.table_name,
+ existing_comment=existing_comment,
+ schema=operations.impl.schema,
+ )
+ return operations.invoke(op)
+
+ def reverse(self) -> CreateTableCommentOp:
+ """Reverses the COMMENT ON operation against a table."""
+ return CreateTableCommentOp(
+ self.table_name, self.existing_comment, schema=self.schema
+ )
+
+ def to_table(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Table:
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ return schema_obj.table(self.table_name, schema=self.schema)
+
+ def to_diff_tuple(self) -> Tuple[Any, ...]:
+ return ("remove_table_comment", self.to_table())
+
+
+@Operations.register_operation("alter_column")
+@BatchOperations.register_operation("alter_column", "batch_alter_column")
+class AlterColumnOp(AlterTableOp):
+ """Represent an alter column operation."""
+
+ def __init__(
+ self,
+ table_name: str,
+ column_name: str,
+ *,
+ schema: Optional[str] = None,
+ existing_type: Optional[Any] = None,
+ existing_server_default: Any = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ modify_nullable: Optional[bool] = None,
+ modify_comment: Optional[Union[str, Literal[False]]] = False,
+ modify_server_default: Any = False,
+ modify_name: Optional[str] = None,
+ modify_type: Optional[Any] = None,
+ **kw: Any,
+ ) -> None:
+ super().__init__(table_name, schema=schema)
+ self.column_name = column_name
+ self.existing_type = existing_type
+ self.existing_server_default = existing_server_default
+ self.existing_nullable = existing_nullable
+ self.existing_comment = existing_comment
+ self.modify_nullable = modify_nullable
+ self.modify_comment = modify_comment
+ self.modify_server_default = modify_server_default
+ self.modify_name = modify_name
+ self.modify_type = modify_type
+ self.kw = kw
+
+ def to_diff_tuple(self) -> Any:
+ col_diff = []
+ schema, tname, cname = self.schema, self.table_name, self.column_name
+
+ if self.modify_type is not None:
+ col_diff.append(
+ (
+ "modify_type",
+ schema,
+ tname,
+ cname,
+ {
+ "existing_nullable": self.existing_nullable,
+ "existing_server_default": (
+ self.existing_server_default
+ ),
+ "existing_comment": self.existing_comment,
+ },
+ self.existing_type,
+ self.modify_type,
+ )
+ )
+
+ if self.modify_nullable is not None:
+ col_diff.append(
+ (
+ "modify_nullable",
+ schema,
+ tname,
+ cname,
+ {
+ "existing_type": self.existing_type,
+ "existing_server_default": (
+ self.existing_server_default
+ ),
+ "existing_comment": self.existing_comment,
+ },
+ self.existing_nullable,
+ self.modify_nullable,
+ )
+ )
+
+ if self.modify_server_default is not False:
+ col_diff.append(
+ (
+ "modify_default",
+ schema,
+ tname,
+ cname,
+ {
+ "existing_nullable": self.existing_nullable,
+ "existing_type": self.existing_type,
+ "existing_comment": self.existing_comment,
+ },
+ self.existing_server_default,
+ self.modify_server_default,
+ )
+ )
+
+ if self.modify_comment is not False:
+ col_diff.append(
+ (
+ "modify_comment",
+ schema,
+ tname,
+ cname,
+ {
+ "existing_nullable": self.existing_nullable,
+ "existing_type": self.existing_type,
+ "existing_server_default": (
+ self.existing_server_default
+ ),
+ },
+ self.existing_comment,
+ self.modify_comment,
+ )
+ )
+
+ return col_diff
+
+ def has_changes(self) -> bool:
+ hc1 = (
+ self.modify_nullable is not None
+ or self.modify_server_default is not False
+ or self.modify_type is not None
+ or self.modify_comment is not False
+ )
+ if hc1:
+ return True
+ for kw in self.kw:
+ if kw.startswith("modify_"):
+ return True
+ else:
+ return False
+
+ def reverse(self) -> AlterColumnOp:
+ kw = self.kw.copy()
+ kw["existing_type"] = self.existing_type
+ kw["existing_nullable"] = self.existing_nullable
+ kw["existing_server_default"] = self.existing_server_default
+ kw["existing_comment"] = self.existing_comment
+ if self.modify_type is not None:
+ kw["modify_type"] = self.modify_type
+ if self.modify_nullable is not None:
+ kw["modify_nullable"] = self.modify_nullable
+ if self.modify_server_default is not False:
+ kw["modify_server_default"] = self.modify_server_default
+ if self.modify_comment is not False:
+ kw["modify_comment"] = self.modify_comment
+
+ # TODO: make this a little simpler
+ all_keys = {
+ m.group(1)
+ for m in [re.match(r"^(?:existing_|modify_)(.+)$", k) for k in kw]
+ if m
+ }
+
+ for k in all_keys:
+ if "modify_%s" % k in kw:
+ swap = kw["existing_%s" % k]
+ kw["existing_%s" % k] = kw["modify_%s" % k]
+ kw["modify_%s" % k] = swap
+
+ return self.__class__(
+ self.table_name, self.column_name, schema=self.schema, **kw
+ )
+
+ @classmethod
+ def alter_column(
+ cls,
+ operations: Operations,
+ table_name: str,
+ column_name: str,
+ *,
+ nullable: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ server_default: Any = False,
+ new_column_name: Optional[str] = None,
+ type_: Optional[Union[TypeEngine[Any], Type[TypeEngine[Any]]]] = None,
+ existing_type: Optional[
+ Union[TypeEngine[Any], Type[TypeEngine[Any]]]
+ ] = None,
+ existing_server_default: Optional[
+ Union[str, bool, Identity, Computed]
+ ] = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue an "alter column" instruction using the
+ current migration context.
+
+ Generally, only that aspect of the column which
+ is being changed, i.e. name, type, nullability,
+ default, needs to be specified. Multiple changes
+ can also be specified at once and the backend should
+ "do the right thing", emitting each change either
+ separately or together as the backend allows.
+
+ MySQL has special requirements here, since MySQL
+ cannot ALTER a column without a full specification.
+ When producing MySQL-compatible migration files,
+ it is recommended that the ``existing_type``,
+ ``existing_server_default``, and ``existing_nullable``
+ parameters be present, if not being altered.
+
+ Type changes which are against the SQLAlchemy
+ "schema" types :class:`~sqlalchemy.types.Boolean`
+ and :class:`~sqlalchemy.types.Enum` may also
+ add or drop constraints which accompany those
+ types on backends that don't support them natively.
+ The ``existing_type`` argument is
+ used in this case to identify and remove a previous
+ constraint that was bound to the type object.
+
+ :param table_name: string name of the target table.
+ :param column_name: string name of the target column,
+ as it exists before the operation begins.
+ :param nullable: Optional; specify ``True`` or ``False``
+ to alter the column's nullability.
+ :param server_default: Optional; specify a string
+ SQL expression, :func:`~sqlalchemy.sql.expression.text`,
+ or :class:`~sqlalchemy.schema.DefaultClause` to indicate
+ an alteration to the column's default value.
+ Set to ``None`` to have the default removed.
+ :param comment: optional string text of a new comment to add to the
+ column.
+ :param new_column_name: Optional; specify a string name here to
+ indicate the new name within a column rename operation.
+ :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify a change to the column's type.
+ For SQLAlchemy types that also indicate a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`),
+ the constraint is also generated.
+ :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column;
+ currently understood by the MySQL dialect.
+ :param existing_type: Optional; a
+ :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify the previous type. This
+ is required for all MySQL column alter operations that
+ don't otherwise specify a new type, as well as for
+ when nullability is being changed on a SQL Server
+ column. It is also used if the type is a so-called
+ SQLAlchemy "schema" type which may define a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`,
+ :class:`~sqlalchemy.types.Enum`),
+ so that the constraint can be dropped.
+ :param existing_server_default: Optional; The existing
+ default value of the column. Required on MySQL if
+ an existing default is not being changed; else MySQL
+ removes the default.
+ :param existing_nullable: Optional; the existing nullability
+ of the column. Required on MySQL if the existing nullability
+ is not being changed; else MySQL sets this to NULL.
+ :param existing_autoincrement: Optional; the existing autoincrement
+ of the column. Used for MySQL's system of altering a column
+ that specifies ``AUTO_INCREMENT``.
+ :param existing_comment: string text of the existing comment on the
+ column to be maintained. Required on MySQL if the existing comment
+ on the column is not being changed.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param postgresql_using: String argument which will indicate a
+ SQL expression to render within the Postgresql-specific USING clause
+ within ALTER COLUMN. This string is taken directly as raw SQL which
+ must explicitly include any necessary quoting or escaping of tokens
+ within the expression.
+
+ """
+
+ alt = cls(
+ table_name,
+ column_name,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ modify_name=new_column_name,
+ modify_type=type_,
+ modify_server_default=server_default,
+ modify_nullable=nullable,
+ modify_comment=comment,
+ **kw,
+ )
+
+ return operations.invoke(alt)
+
+ @classmethod
+ def batch_alter_column(
+ cls,
+ operations: BatchOperations,
+ column_name: str,
+ *,
+ nullable: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ server_default: Any = False,
+ new_column_name: Optional[str] = None,
+ type_: Optional[Union[TypeEngine[Any], Type[TypeEngine[Any]]]] = None,
+ existing_type: Optional[
+ Union[TypeEngine[Any], Type[TypeEngine[Any]]]
+ ] = None,
+ existing_server_default: Optional[
+ Union[str, bool, Identity, Computed]
+ ] = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ insert_before: Optional[str] = None,
+ insert_after: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue an "alter column" instruction using the current
+ batch migration context.
+
+ Parameters are the same as that of :meth:`.Operations.alter_column`,
+ as well as the following option(s):
+
+ :param insert_before: String name of an existing column which this
+ column should be placed before, when creating the new table.
+
+ :param insert_after: String name of an existing column which this
+ column should be placed after, when creating the new table. If
+ both :paramref:`.BatchOperations.alter_column.insert_before`
+ and :paramref:`.BatchOperations.alter_column.insert_after` are
+ omitted, the column is inserted after the last existing column
+ in the table.
+
+ .. seealso::
+
+ :meth:`.Operations.alter_column`
+
+
+ """
+ alt = cls(
+ operations.impl.table_name,
+ column_name,
+ schema=operations.impl.schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ modify_name=new_column_name,
+ modify_type=type_,
+ modify_server_default=server_default,
+ modify_nullable=nullable,
+ modify_comment=comment,
+ insert_before=insert_before,
+ insert_after=insert_after,
+ **kw,
+ )
+
+ return operations.invoke(alt)
+
+
+@Operations.register_operation("add_column")
+@BatchOperations.register_operation("add_column", "batch_add_column")
+class AddColumnOp(AlterTableOp):
+ """Represent an add column operation."""
+
+ def __init__(
+ self,
+ table_name: str,
+ column: Column[Any],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ super().__init__(table_name, schema=schema)
+ self.column = column
+ self.kw = kw
+
+ def reverse(self) -> DropColumnOp:
+ return DropColumnOp.from_column_and_tablename(
+ self.schema, self.table_name, self.column
+ )
+
+ def to_diff_tuple(
+ self,
+ ) -> Tuple[str, Optional[str], str, Column[Any]]:
+ return ("add_column", self.schema, self.table_name, self.column)
+
+ def to_column(self) -> Column[Any]:
+ return self.column
+
+ @classmethod
+ def from_column(cls, col: Column[Any]) -> AddColumnOp:
+ return cls(col.table.name, col, schema=col.table.schema)
+
+ @classmethod
+ def from_column_and_tablename(
+ cls,
+ schema: Optional[str],
+ tname: str,
+ col: Column[Any],
+ ) -> AddColumnOp:
+ return cls(tname, col, schema=schema)
+
+ @classmethod
+ def add_column(
+ cls,
+ operations: Operations,
+ table_name: str,
+ column: Column[Any],
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue an "add column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy import Column, String
+
+ op.add_column("organization", Column("name", String()))
+
+ The :meth:`.Operations.add_column` method typically corresponds
+ to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope
+ of this command, the column's name, datatype, nullability,
+ and optional server-generated defaults may be indicated.
+
+ .. note::
+
+ With the exception of NOT NULL constraints or single-column FOREIGN
+ KEY constraints, other kinds of constraints such as PRIMARY KEY,
+ UNIQUE or CHECK constraints **cannot** be generated using this
+ method; for these constraints, refer to operations such as
+ :meth:`.Operations.create_primary_key` and
+ :meth:`.Operations.create_check_constraint`. In particular, the
+ following :class:`~sqlalchemy.schema.Column` parameters are
+ **ignored**:
+
+ * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases
+ typically do not support an ALTER operation that can add
+ individual columns one at a time to an existing primary key
+ constraint, therefore it's less ambiguous to use the
+ :meth:`.Operations.create_primary_key` method, which assumes no
+ existing primary key constraint is present.
+ * :paramref:`~sqlalchemy.schema.Column.unique` - use the
+ :meth:`.Operations.create_unique_constraint` method
+ * :paramref:`~sqlalchemy.schema.Column.index` - use the
+ :meth:`.Operations.create_index` method
+
+
+ The provided :class:`~sqlalchemy.schema.Column` object may include a
+ :class:`~sqlalchemy.schema.ForeignKey` constraint directive,
+ referencing a remote table name. For this specific type of constraint,
+ Alembic will automatically emit a second ALTER statement in order to
+ add the single-column FOREIGN KEY constraint separately::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ )
+
+ The column argument passed to :meth:`.Operations.add_column` is a
+ :class:`~sqlalchemy.schema.Column` construct, used in the same way it's
+ used in SQLAlchemy. In particular, values or functions to be indicated
+ as producing the column's default value on the database side are
+ specified using the ``server_default`` parameter, and not ``default``
+ which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the column add
+ op.add_column(
+ "account",
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ :param table_name: String name of the parent table.
+ :param column: a :class:`sqlalchemy.schema.Column` object
+ representing the new column.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+ op = cls(table_name, column, schema=schema)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_add_column(
+ cls,
+ operations: BatchOperations,
+ column: Column[Any],
+ *,
+ insert_before: Optional[str] = None,
+ insert_after: Optional[str] = None,
+ ) -> None:
+ """Issue an "add column" instruction using the current
+ batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.add_column`
+
+ """
+
+ kw = {}
+ if insert_before:
+ kw["insert_before"] = insert_before
+ if insert_after:
+ kw["insert_after"] = insert_after
+
+ op = cls(
+ operations.impl.table_name,
+ column,
+ schema=operations.impl.schema,
+ **kw,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("drop_column")
+@BatchOperations.register_operation("drop_column", "batch_drop_column")
+class DropColumnOp(AlterTableOp):
+ """Represent a drop column operation."""
+
+ def __init__(
+ self,
+ table_name: str,
+ column_name: str,
+ *,
+ schema: Optional[str] = None,
+ _reverse: Optional[AddColumnOp] = None,
+ **kw: Any,
+ ) -> None:
+ super().__init__(table_name, schema=schema)
+ self.column_name = column_name
+ self.kw = kw
+ self._reverse = _reverse
+
+ def to_diff_tuple(
+ self,
+ ) -> Tuple[str, Optional[str], str, Column[Any]]:
+ return (
+ "remove_column",
+ self.schema,
+ self.table_name,
+ self.to_column(),
+ )
+
+ def reverse(self) -> AddColumnOp:
+ if self._reverse is None:
+ raise ValueError(
+ "operation is not reversible; "
+ "original column is not present"
+ )
+
+ return AddColumnOp.from_column_and_tablename(
+ self.schema, self.table_name, self._reverse.column
+ )
+
+ @classmethod
+ def from_column_and_tablename(
+ cls,
+ schema: Optional[str],
+ tname: str,
+ col: Column[Any],
+ ) -> DropColumnOp:
+ return cls(
+ tname,
+ col.name,
+ schema=schema,
+ _reverse=AddColumnOp.from_column_and_tablename(schema, tname, col),
+ )
+
+ def to_column(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> Column[Any]:
+ if self._reverse is not None:
+ return self._reverse.column
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+ return schema_obj.column(self.column_name, NULLTYPE)
+
+ @classmethod
+ def drop_column(
+ cls,
+ operations: Operations,
+ table_name: str,
+ column_name: str,
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue a "drop column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_column("organization", "account_id")
+
+ :param table_name: name of table
+ :param column_name: name of column
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param mssql_drop_check: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the CHECK constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.check_constraints,
+ then exec's a separate DROP CONSTRAINT for that constraint.
+ :param mssql_drop_default: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the DEFAULT constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.default_constraints,
+ then exec's a separate DROP CONSTRAINT for that default.
+ :param mssql_drop_foreign_key: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop a single FOREIGN KEY constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from
+ sys.foreign_keys/sys.foreign_key_columns,
+ then exec's a separate DROP CONSTRAINT for that default. Only
+ works if the column has exactly one FK constraint which refers to
+ it, at the moment.
+
+ """
+
+ op = cls(table_name, column_name, schema=schema, **kw)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_drop_column(
+ cls, operations: BatchOperations, column_name: str, **kw: Any
+ ) -> None:
+ """Issue a "drop column" instruction using the current
+ batch migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_column`
+
+ """
+ op = cls(
+ operations.impl.table_name,
+ column_name,
+ schema=operations.impl.schema,
+ **kw,
+ )
+ return operations.invoke(op)
+
+
+@Operations.register_operation("bulk_insert")
+class BulkInsertOp(MigrateOperation):
+ """Represent a bulk insert operation."""
+
+ def __init__(
+ self,
+ table: Union[Table, TableClause],
+ rows: List[Dict[str, Any]],
+ *,
+ multiinsert: bool = True,
+ ) -> None:
+ self.table = table
+ self.rows = rows
+ self.multiinsert = multiinsert
+
+ @classmethod
+ def bulk_insert(
+ cls,
+ operations: Operations,
+ table: Union[Table, TableClause],
+ rows: List[Dict[str, Any]],
+ *,
+ multiinsert: bool = True,
+ ) -> None:
+ """Issue a "bulk insert" operation using the current
+ migration context.
+
+ This provides a means of representing an INSERT of multiple rows
+ which works equally well in the context of executing on a live
+ connection as well as that of generating a SQL script. In the
+ case of a SQL script, the values are rendered inline into the
+ statement.
+
+ e.g.::
+
+ from alembic import op
+ from datetime import date
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String, Integer, Date
+
+ # Create an ad-hoc table to use for the insert statement.
+ accounts_table = table(
+ "account",
+ column("id", Integer),
+ column("name", String),
+ column("create_date", Date),
+ )
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": date(2010, 10, 5),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": date(2007, 5, 27),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": date(2008, 8, 15),
+ },
+ ],
+ )
+
+ When using --sql mode, some datatypes may not render inline
+ automatically, such as dates and other special types. When this
+ issue is present, :meth:`.Operations.inline_literal` may be used::
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": op.inline_literal("2010-10-05"),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": op.inline_literal("2007-05-27"),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": op.inline_literal("2008-08-15"),
+ },
+ ],
+ multiinsert=False,
+ )
+
+ When using :meth:`.Operations.inline_literal` in conjunction with
+ :meth:`.Operations.bulk_insert`, in order for the statement to work
+ in "online" (e.g. non --sql) mode, the
+ :paramref:`~.Operations.bulk_insert.multiinsert`
+ flag should be set to ``False``, which will have the effect of
+ individual INSERT statements being emitted to the database, each
+ with a distinct VALUES clause, so that the "inline" values can
+ still be rendered, rather than attempting to pass the values
+ as bound parameters.
+
+ :param table: a table object which represents the target of the INSERT.
+
+ :param rows: a list of dictionaries indicating rows.
+
+ :param multiinsert: when at its default of True and --sql mode is not
+ enabled, the INSERT statement will be executed using
+ "executemany()" style, where all elements in the list of
+ dictionaries are passed as bound parameters in a single
+ list. Setting this to False results in individual INSERT
+ statements being emitted per parameter set, and is needed
+ in those cases where non-literal values are present in the
+ parameter sets.
+
+ """
+
+ op = cls(table, rows, multiinsert=multiinsert)
+ operations.invoke(op)
+
+
+@Operations.register_operation("execute")
+@BatchOperations.register_operation("execute", "batch_execute")
+class ExecuteSQLOp(MigrateOperation):
+ """Represent an execute SQL operation."""
+
+ def __init__(
+ self,
+ sqltext: Union[Executable, str],
+ *,
+ execution_options: Optional[dict[str, Any]] = None,
+ ) -> None:
+ self.sqltext = sqltext
+ self.execution_options = execution_options
+
+ @classmethod
+ def execute(
+ cls,
+ operations: Operations,
+ sqltext: Union[Executable, str],
+ *,
+ execution_options: Optional[dict[str, Any]] = None,
+ ) -> None:
+ r"""Execute the given SQL using the current migration context.
+
+ The given SQL can be a plain string, e.g.::
+
+ op.execute("INSERT INTO table (foo) VALUES ('some value')")
+
+ Or it can be any kind of Core SQL Expression construct, such as
+ below where we use an update construct::
+
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String
+ from alembic import op
+
+ account = table("account", column("name", String))
+ op.execute(
+ account.update()
+ .where(account.c.name == op.inline_literal("account 1"))
+ .values({"name": op.inline_literal("account 2")})
+ )
+
+ Above, we made use of the SQLAlchemy
+ :func:`sqlalchemy.sql.expression.table` and
+ :func:`sqlalchemy.sql.expression.column` constructs to make a brief,
+ ad-hoc table construct just for our UPDATE statement. A full
+ :class:`~sqlalchemy.schema.Table` construct of course works perfectly
+ fine as well, though note it's a recommended practice to at least
+ ensure the definition of a table is self-contained within the migration
+ script, rather than imported from a module that may break compatibility
+ with older migrations.
+
+ In a SQL script context, the statement is emitted directly to the
+ output stream. There is *no* return result, however, as this
+ function is oriented towards generating a change script
+ that can run in "offline" mode. Additionally, parameterized
+ statements are discouraged here, as they *will not work* in offline
+ mode. Above, we use :meth:`.inline_literal` where parameters are
+ to be used.
+
+ For full interaction with a connected database where parameters can
+ also be used normally, use the "bind" available from the context::
+
+ from alembic import op
+
+ connection = op.get_bind()
+
+ connection.execute(
+ account.update()
+ .where(account.c.name == "account 1")
+ .values({"name": "account 2"})
+ )
+
+ Additionally, when passing the statement as a plain string, it is first
+ coerced into a :func:`sqlalchemy.sql.expression.text` construct
+ before being passed along. In the less likely case that the
+ literal SQL string contains a colon, it must be escaped with a
+ backslash, as::
+
+ op.execute(r"INSERT INTO table (foo) VALUES ('\:colon_value')")
+
+
+ :param sqltext: Any legal SQLAlchemy expression, including:
+
+ * a string
+ * a :func:`sqlalchemy.sql.expression.text` construct.
+ * a :func:`sqlalchemy.sql.expression.insert` construct.
+ * a :func:`sqlalchemy.sql.expression.update` construct.
+ * a :func:`sqlalchemy.sql.expression.delete` construct.
+ * Any "executable" described in SQLAlchemy Core documentation,
+ noting that no result set is returned.
+
+ .. note:: when passing a plain string, the statement is coerced into
+ a :func:`sqlalchemy.sql.expression.text` construct. This construct
+ considers symbols with colons, e.g. ``:foo`` to be bound parameters.
+ To avoid this, ensure that colon symbols are escaped, e.g.
+ ``\:foo``.
+
+ :param execution_options: Optional dictionary of
+ execution options, will be passed to
+ :meth:`sqlalchemy.engine.Connection.execution_options`.
+ """
+ op = cls(sqltext, execution_options=execution_options)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_execute(
+ cls,
+ operations: Operations,
+ sqltext: Union[Executable, str],
+ *,
+ execution_options: Optional[dict[str, Any]] = None,
+ ) -> None:
+ """Execute the given SQL using the current migration context.
+
+ .. seealso::
+
+ :meth:`.Operations.execute`
+
+ """
+ return cls.execute(
+ operations, sqltext, execution_options=execution_options
+ )
+
+ def to_diff_tuple(self) -> Tuple[str, Union[Executable, str]]:
+ return ("execute", self.sqltext)
+
+
+class OpContainer(MigrateOperation):
+ """Represent a sequence of operations operation."""
+
+ def __init__(self, ops: Sequence[MigrateOperation] = ()) -> None:
+ self.ops = list(ops)
+
+ def is_empty(self) -> bool:
+ return not self.ops
+
+ def as_diffs(self) -> Any:
+ return list(OpContainer._ops_as_diffs(self))
+
+ @classmethod
+ def _ops_as_diffs(
+ cls, migrations: OpContainer
+ ) -> Iterator[Tuple[Any, ...]]:
+ for op in migrations.ops:
+ if hasattr(op, "ops"):
+ yield from cls._ops_as_diffs(cast("OpContainer", op))
+ else:
+ yield op.to_diff_tuple()
+
+
+class ModifyTableOps(OpContainer):
+ """Contains a sequence of operations that all apply to a single Table."""
+
+ def __init__(
+ self,
+ table_name: str,
+ ops: Sequence[MigrateOperation],
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ super().__init__(ops)
+ self.table_name = table_name
+ self.schema = schema
+
+ def reverse(self) -> ModifyTableOps:
+ return ModifyTableOps(
+ self.table_name,
+ ops=list(reversed([op.reverse() for op in self.ops])),
+ schema=self.schema,
+ )
+
+
+class UpgradeOps(OpContainer):
+ """contains a sequence of operations that would apply to the
+ 'upgrade' stream of a script.
+
+ .. seealso::
+
+ :ref:`customizing_revision`
+
+ """
+
+ def __init__(
+ self,
+ ops: Sequence[MigrateOperation] = (),
+ upgrade_token: str = "upgrades",
+ ) -> None:
+ super().__init__(ops=ops)
+ self.upgrade_token = upgrade_token
+
+ def reverse_into(self, downgrade_ops: DowngradeOps) -> DowngradeOps:
+ downgrade_ops.ops[:] = list(
+ reversed([op.reverse() for op in self.ops])
+ )
+ return downgrade_ops
+
+ def reverse(self) -> DowngradeOps:
+ return self.reverse_into(DowngradeOps(ops=[]))
+
+
+class DowngradeOps(OpContainer):
+ """contains a sequence of operations that would apply to the
+ 'downgrade' stream of a script.
+
+ .. seealso::
+
+ :ref:`customizing_revision`
+
+ """
+
+ def __init__(
+ self,
+ ops: Sequence[MigrateOperation] = (),
+ downgrade_token: str = "downgrades",
+ ) -> None:
+ super().__init__(ops=ops)
+ self.downgrade_token = downgrade_token
+
+ def reverse(self) -> UpgradeOps:
+ return UpgradeOps(
+ ops=list(reversed([op.reverse() for op in self.ops]))
+ )
+
+
+class MigrationScript(MigrateOperation):
+ """represents a migration script.
+
+ E.g. when autogenerate encounters this object, this corresponds to the
+ production of an actual script file.
+
+ A normal :class:`.MigrationScript` object would contain a single
+ :class:`.UpgradeOps` and a single :class:`.DowngradeOps` directive.
+ These are accessible via the ``.upgrade_ops`` and ``.downgrade_ops``
+ attributes.
+
+ In the case of an autogenerate operation that runs multiple times,
+ such as the multiple database example in the "multidb" template,
+ the ``.upgrade_ops`` and ``.downgrade_ops`` attributes are disabled,
+ and instead these objects should be accessed via the ``.upgrade_ops_list``
+ and ``.downgrade_ops_list`` list-based attributes. These latter
+ attributes are always available at the very least as single-element lists.
+
+ .. seealso::
+
+ :ref:`customizing_revision`
+
+ """
+
+ _needs_render: Optional[bool]
+ _upgrade_ops: List[UpgradeOps]
+ _downgrade_ops: List[DowngradeOps]
+
+ def __init__(
+ self,
+ rev_id: Optional[str],
+ upgrade_ops: UpgradeOps,
+ downgrade_ops: DowngradeOps,
+ *,
+ message: Optional[str] = None,
+ imports: Set[str] = set(),
+ head: Optional[str] = None,
+ splice: Optional[bool] = None,
+ branch_label: Optional[_RevIdType] = None,
+ version_path: Optional[str] = None,
+ depends_on: Optional[_RevIdType] = None,
+ ) -> None:
+ self.rev_id = rev_id
+ self.message = message
+ self.imports = imports
+ self.head = head
+ self.splice = splice
+ self.branch_label = branch_label
+ self.version_path = version_path
+ self.depends_on = depends_on
+ self.upgrade_ops = upgrade_ops
+ self.downgrade_ops = downgrade_ops
+
+ @property
+ def upgrade_ops(self) -> Optional[UpgradeOps]:
+ """An instance of :class:`.UpgradeOps`.
+
+ .. seealso::
+
+ :attr:`.MigrationScript.upgrade_ops_list`
+ """
+ if len(self._upgrade_ops) > 1:
+ raise ValueError(
+ "This MigrationScript instance has a multiple-entry "
+ "list for UpgradeOps; please use the "
+ "upgrade_ops_list attribute."
+ )
+ elif not self._upgrade_ops:
+ return None
+ else:
+ return self._upgrade_ops[0]
+
+ @upgrade_ops.setter
+ def upgrade_ops(
+ self, upgrade_ops: Union[UpgradeOps, List[UpgradeOps]]
+ ) -> None:
+ self._upgrade_ops = util.to_list(upgrade_ops)
+ for elem in self._upgrade_ops:
+ assert isinstance(elem, UpgradeOps)
+
+ @property
+ def downgrade_ops(self) -> Optional[DowngradeOps]:
+ """An instance of :class:`.DowngradeOps`.
+
+ .. seealso::
+
+ :attr:`.MigrationScript.downgrade_ops_list`
+ """
+ if len(self._downgrade_ops) > 1:
+ raise ValueError(
+ "This MigrationScript instance has a multiple-entry "
+ "list for DowngradeOps; please use the "
+ "downgrade_ops_list attribute."
+ )
+ elif not self._downgrade_ops:
+ return None
+ else:
+ return self._downgrade_ops[0]
+
+ @downgrade_ops.setter
+ def downgrade_ops(
+ self, downgrade_ops: Union[DowngradeOps, List[DowngradeOps]]
+ ) -> None:
+ self._downgrade_ops = util.to_list(downgrade_ops)
+ for elem in self._downgrade_ops:
+ assert isinstance(elem, DowngradeOps)
+
+ @property
+ def upgrade_ops_list(self) -> List[UpgradeOps]:
+ """A list of :class:`.UpgradeOps` instances.
+
+ This is used in place of the :attr:`.MigrationScript.upgrade_ops`
+ attribute when dealing with a revision operation that does
+ multiple autogenerate passes.
+
+ """
+ return self._upgrade_ops
+
+ @property
+ def downgrade_ops_list(self) -> List[DowngradeOps]:
+ """A list of :class:`.DowngradeOps` instances.
+
+ This is used in place of the :attr:`.MigrationScript.downgrade_ops`
+ attribute when dealing with a revision operation that does
+ multiple autogenerate passes.
+
+ """
+ return self._downgrade_ops
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/schemaobj.py b/venv/lib/python3.12/site-packages/alembic/operations/schemaobj.py
new file mode 100644
index 0000000..32b26e9
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/operations/schemaobj.py
@@ -0,0 +1,288 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import schema as sa_schema
+from sqlalchemy.sql.schema import Column
+from sqlalchemy.sql.schema import Constraint
+from sqlalchemy.sql.schema import Index
+from sqlalchemy.types import Integer
+from sqlalchemy.types import NULLTYPE
+
+from .. import util
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.schema import CheckConstraint
+ from sqlalchemy.sql.schema import ForeignKey
+ from sqlalchemy.sql.schema import ForeignKeyConstraint
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import PrimaryKeyConstraint
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.schema import UniqueConstraint
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from ..runtime.migration import MigrationContext
+
+
+class SchemaObjects:
+ def __init__(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> None:
+ self.migration_context = migration_context
+
+ def primary_key_constraint(
+ self,
+ name: Optional[sqla_compat._ConstraintNameDefined],
+ table_name: str,
+ cols: Sequence[str],
+ schema: Optional[str] = None,
+ **dialect_kw,
+ ) -> PrimaryKeyConstraint:
+ m = self.metadata()
+ columns = [sa_schema.Column(n, NULLTYPE) for n in cols]
+ t = sa_schema.Table(table_name, m, *columns, schema=schema)
+ # SQLAlchemy primary key constraint name arg is wrongly typed on
+ # the SQLAlchemy side through 2.0.5 at least
+ p = sa_schema.PrimaryKeyConstraint(
+ *[t.c[n] for n in cols], name=name, **dialect_kw # type: ignore
+ )
+ return p
+
+ def foreign_key_constraint(
+ self,
+ name: Optional[sqla_compat._ConstraintNameDefined],
+ source: str,
+ referent: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ source_schema: Optional[str] = None,
+ referent_schema: Optional[str] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ **dialect_kw,
+ ) -> ForeignKeyConstraint:
+ m = self.metadata()
+ if source == referent and source_schema == referent_schema:
+ t1_cols = local_cols + remote_cols
+ else:
+ t1_cols = local_cols
+ sa_schema.Table(
+ referent,
+ m,
+ *[sa_schema.Column(n, NULLTYPE) for n in remote_cols],
+ schema=referent_schema,
+ )
+
+ t1 = sa_schema.Table(
+ source,
+ m,
+ *[
+ sa_schema.Column(n, NULLTYPE)
+ for n in util.unique_list(t1_cols)
+ ],
+ schema=source_schema,
+ )
+
+ tname = (
+ "%s.%s" % (referent_schema, referent)
+ if referent_schema
+ else referent
+ )
+
+ dialect_kw["match"] = match
+
+ f = sa_schema.ForeignKeyConstraint(
+ local_cols,
+ ["%s.%s" % (tname, n) for n in remote_cols],
+ name=name,
+ onupdate=onupdate,
+ ondelete=ondelete,
+ deferrable=deferrable,
+ initially=initially,
+ **dialect_kw,
+ )
+ t1.append_constraint(f)
+
+ return f
+
+ def unique_constraint(
+ self,
+ name: Optional[sqla_compat._ConstraintNameDefined],
+ source: str,
+ local_cols: Sequence[str],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> UniqueConstraint:
+ t = sa_schema.Table(
+ source,
+ self.metadata(),
+ *[sa_schema.Column(n, NULLTYPE) for n in local_cols],
+ schema=schema,
+ )
+ kw["name"] = name
+ uq = sa_schema.UniqueConstraint(*[t.c[n] for n in local_cols], **kw)
+ # TODO: need event tests to ensure the event
+ # is fired off here
+ t.append_constraint(uq)
+ return uq
+
+ def check_constraint(
+ self,
+ name: Optional[sqla_compat._ConstraintNameDefined],
+ source: str,
+ condition: Union[str, TextClause, ColumnElement[Any]],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> Union[CheckConstraint]:
+ t = sa_schema.Table(
+ source,
+ self.metadata(),
+ sa_schema.Column("x", Integer),
+ schema=schema,
+ )
+ ck = sa_schema.CheckConstraint(condition, name=name, **kw)
+ t.append_constraint(ck)
+ return ck
+
+ def generic_constraint(
+ self,
+ name: Optional[sqla_compat._ConstraintNameDefined],
+ table_name: str,
+ type_: Optional[str],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> Any:
+ t = self.table(table_name, schema=schema)
+ types: Dict[Optional[str], Any] = {
+ "foreignkey": lambda name: sa_schema.ForeignKeyConstraint(
+ [], [], name=name
+ ),
+ "primary": sa_schema.PrimaryKeyConstraint,
+ "unique": sa_schema.UniqueConstraint,
+ "check": lambda name: sa_schema.CheckConstraint("", name=name),
+ None: sa_schema.Constraint,
+ }
+ try:
+ const = types[type_]
+ except KeyError as ke:
+ raise TypeError(
+ "'type' can be one of %s"
+ % ", ".join(sorted(repr(x) for x in types))
+ ) from ke
+ else:
+ const = const(name=name)
+ t.append_constraint(const)
+ return const
+
+ def metadata(self) -> MetaData:
+ kw = {}
+ if (
+ self.migration_context is not None
+ and "target_metadata" in self.migration_context.opts
+ ):
+ mt = self.migration_context.opts["target_metadata"]
+ if hasattr(mt, "naming_convention"):
+ kw["naming_convention"] = mt.naming_convention
+ return sa_schema.MetaData(**kw)
+
+ def table(self, name: str, *columns, **kw) -> Table:
+ m = self.metadata()
+
+ cols = [
+ sqla_compat._copy(c) if c.table is not None else c
+ for c in columns
+ if isinstance(c, Column)
+ ]
+ # these flags have already added their UniqueConstraint /
+ # Index objects to the table, so flip them off here.
+ # SQLAlchemy tometadata() avoids this instead by preserving the
+ # flags and skipping the constraints that have _type_bound on them,
+ # but for a migration we'd rather list out the constraints
+ # explicitly.
+ _constraints_included = kw.pop("_constraints_included", False)
+ if _constraints_included:
+ for c in cols:
+ c.unique = c.index = False
+
+ t = sa_schema.Table(name, m, *cols, **kw)
+
+ constraints = [
+ sqla_compat._copy(elem, target_table=t)
+ if getattr(elem, "parent", None) is not t
+ and getattr(elem, "parent", None) is not None
+ else elem
+ for elem in columns
+ if isinstance(elem, (Constraint, Index))
+ ]
+
+ for const in constraints:
+ t.append_constraint(const)
+
+ for f in t.foreign_keys:
+ self._ensure_table_for_fk(m, f)
+ return t
+
+ def column(self, name: str, type_: TypeEngine, **kw) -> Column:
+ return sa_schema.Column(name, type_, **kw)
+
+ def index(
+ self,
+ name: Optional[str],
+ tablename: Optional[str],
+ columns: Sequence[Union[str, TextClause, ColumnElement[Any]]],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> Index:
+ t = sa_schema.Table(
+ tablename or "no_table",
+ self.metadata(),
+ schema=schema,
+ )
+ kw["_table"] = t
+ idx = sa_schema.Index(
+ name,
+ *[util.sqla_compat._textual_index_column(t, n) for n in columns],
+ **kw,
+ )
+ return idx
+
+ def _parse_table_key(self, table_key: str) -> Tuple[Optional[str], str]:
+ if "." in table_key:
+ tokens = table_key.split(".")
+ sname: Optional[str] = ".".join(tokens[0:-1])
+ tname = tokens[-1]
+ else:
+ tname = table_key
+ sname = None
+ return (sname, tname)
+
+ def _ensure_table_for_fk(self, metadata: MetaData, fk: ForeignKey) -> None:
+ """create a placeholder Table object for the referent of a
+ ForeignKey.
+
+ """
+ if isinstance(fk._colspec, str):
+ table_key, cname = fk._colspec.rsplit(".", 1)
+ sname, tname = self._parse_table_key(table_key)
+ if table_key not in metadata.tables:
+ rel_t = sa_schema.Table(tname, metadata, schema=sname)
+ else:
+ rel_t = metadata.tables[table_key]
+ if cname not in rel_t.c:
+ rel_t.append_column(sa_schema.Column(cname, NULLTYPE))
diff --git a/venv/lib/python3.12/site-packages/alembic/operations/toimpl.py b/venv/lib/python3.12/site-packages/alembic/operations/toimpl.py
new file mode 100644
index 0000000..4759f7f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/operations/toimpl.py
@@ -0,0 +1,226 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from typing import TYPE_CHECKING
+
+from sqlalchemy import schema as sa_schema
+
+from . import ops
+from .base import Operations
+from ..util.sqla_compat import _copy
+from ..util.sqla_compat import sqla_14
+
+if TYPE_CHECKING:
+ from sqlalchemy.sql.schema import Table
+
+
+@Operations.implementation_for(ops.AlterColumnOp)
+def alter_column(
+ operations: "Operations", operation: "ops.AlterColumnOp"
+) -> None:
+ compiler = operations.impl.dialect.statement_compiler(
+ operations.impl.dialect, None
+ )
+
+ existing_type = operation.existing_type
+ existing_nullable = operation.existing_nullable
+ existing_server_default = operation.existing_server_default
+ type_ = operation.modify_type
+ column_name = operation.column_name
+ table_name = operation.table_name
+ schema = operation.schema
+ server_default = operation.modify_server_default
+ new_column_name = operation.modify_name
+ nullable = operation.modify_nullable
+ comment = operation.modify_comment
+ existing_comment = operation.existing_comment
+
+ def _count_constraint(constraint):
+ return not isinstance(constraint, sa_schema.PrimaryKeyConstraint) and (
+ not constraint._create_rule or constraint._create_rule(compiler)
+ )
+
+ if existing_type and type_:
+ t = operations.schema_obj.table(
+ table_name,
+ sa_schema.Column(column_name, existing_type),
+ schema=schema,
+ )
+ for constraint in t.constraints:
+ if _count_constraint(constraint):
+ operations.impl.drop_constraint(constraint)
+
+ operations.impl.alter_column(
+ table_name,
+ column_name,
+ nullable=nullable,
+ server_default=server_default,
+ name=new_column_name,
+ type_=type_,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ comment=comment,
+ existing_comment=existing_comment,
+ **operation.kw,
+ )
+
+ if type_:
+ t = operations.schema_obj.table(
+ table_name,
+ operations.schema_obj.column(column_name, type_),
+ schema=schema,
+ )
+ for constraint in t.constraints:
+ if _count_constraint(constraint):
+ operations.impl.add_constraint(constraint)
+
+
+@Operations.implementation_for(ops.DropTableOp)
+def drop_table(operations: "Operations", operation: "ops.DropTableOp") -> None:
+ operations.impl.drop_table(
+ operation.to_table(operations.migration_context)
+ )
+
+
+@Operations.implementation_for(ops.DropColumnOp)
+def drop_column(
+ operations: "Operations", operation: "ops.DropColumnOp"
+) -> None:
+ column = operation.to_column(operations.migration_context)
+ operations.impl.drop_column(
+ operation.table_name, column, schema=operation.schema, **operation.kw
+ )
+
+
+@Operations.implementation_for(ops.CreateIndexOp)
+def create_index(
+ operations: "Operations", operation: "ops.CreateIndexOp"
+) -> None:
+ idx = operation.to_index(operations.migration_context)
+ kw = {}
+ if operation.if_not_exists is not None:
+ if not sqla_14:
+ raise NotImplementedError("SQLAlchemy 1.4+ required")
+
+ kw["if_not_exists"] = operation.if_not_exists
+ operations.impl.create_index(idx, **kw)
+
+
+@Operations.implementation_for(ops.DropIndexOp)
+def drop_index(operations: "Operations", operation: "ops.DropIndexOp") -> None:
+ kw = {}
+ if operation.if_exists is not None:
+ if not sqla_14:
+ raise NotImplementedError("SQLAlchemy 1.4+ required")
+
+ kw["if_exists"] = operation.if_exists
+
+ operations.impl.drop_index(
+ operation.to_index(operations.migration_context),
+ **kw,
+ )
+
+
+@Operations.implementation_for(ops.CreateTableOp)
+def create_table(
+ operations: "Operations", operation: "ops.CreateTableOp"
+) -> "Table":
+ table = operation.to_table(operations.migration_context)
+ operations.impl.create_table(table)
+ return table
+
+
+@Operations.implementation_for(ops.RenameTableOp)
+def rename_table(
+ operations: "Operations", operation: "ops.RenameTableOp"
+) -> None:
+ operations.impl.rename_table(
+ operation.table_name, operation.new_table_name, schema=operation.schema
+ )
+
+
+@Operations.implementation_for(ops.CreateTableCommentOp)
+def create_table_comment(
+ operations: "Operations", operation: "ops.CreateTableCommentOp"
+) -> None:
+ table = operation.to_table(operations.migration_context)
+ operations.impl.create_table_comment(table)
+
+
+@Operations.implementation_for(ops.DropTableCommentOp)
+def drop_table_comment(
+ operations: "Operations", operation: "ops.DropTableCommentOp"
+) -> None:
+ table = operation.to_table(operations.migration_context)
+ operations.impl.drop_table_comment(table)
+
+
+@Operations.implementation_for(ops.AddColumnOp)
+def add_column(operations: "Operations", operation: "ops.AddColumnOp") -> None:
+ table_name = operation.table_name
+ column = operation.column
+ schema = operation.schema
+ kw = operation.kw
+
+ if column.table is not None:
+ column = _copy(column)
+
+ t = operations.schema_obj.table(table_name, column, schema=schema)
+ operations.impl.add_column(table_name, column, schema=schema, **kw)
+
+ for constraint in t.constraints:
+ if not isinstance(constraint, sa_schema.PrimaryKeyConstraint):
+ operations.impl.add_constraint(constraint)
+ for index in t.indexes:
+ operations.impl.create_index(index)
+
+ with_comment = (
+ operations.impl.dialect.supports_comments
+ and not operations.impl.dialect.inline_comments
+ )
+ comment = column.comment
+ if comment and with_comment:
+ operations.impl.create_column_comment(column)
+
+
+@Operations.implementation_for(ops.AddConstraintOp)
+def create_constraint(
+ operations: "Operations", operation: "ops.AddConstraintOp"
+) -> None:
+ operations.impl.add_constraint(
+ operation.to_constraint(operations.migration_context)
+ )
+
+
+@Operations.implementation_for(ops.DropConstraintOp)
+def drop_constraint(
+ operations: "Operations", operation: "ops.DropConstraintOp"
+) -> None:
+ operations.impl.drop_constraint(
+ operations.schema_obj.generic_constraint(
+ operation.constraint_name,
+ operation.table_name,
+ operation.constraint_type,
+ schema=operation.schema,
+ )
+ )
+
+
+@Operations.implementation_for(ops.BulkInsertOp)
+def bulk_insert(
+ operations: "Operations", operation: "ops.BulkInsertOp"
+) -> None:
+ operations.impl.bulk_insert( # type: ignore[union-attr]
+ operation.table, operation.rows, multiinsert=operation.multiinsert
+ )
+
+
+@Operations.implementation_for(ops.ExecuteSQLOp)
+def execute_sql(
+ operations: "Operations", operation: "ops.ExecuteSQLOp"
+) -> None:
+ operations.migration_context.impl.execute(
+ operation.sqltext, execution_options=operation.execution_options
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/py.typed b/venv/lib/python3.12/site-packages/alembic/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/alembic/runtime/__init__.py b/venv/lib/python3.12/site-packages/alembic/runtime/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..e7d0a32
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/environment.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/environment.cpython-312.pyc
new file mode 100644
index 0000000..c64ca2e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/environment.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/migration.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/migration.cpython-312.pyc
new file mode 100644
index 0000000..043ee0e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/runtime/__pycache__/migration.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/runtime/environment.py b/venv/lib/python3.12/site-packages/alembic/runtime/environment.py
new file mode 100644
index 0000000..d64b2ad
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/runtime/environment.py
@@ -0,0 +1,1053 @@
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Collection
+from typing import ContextManager
+from typing import Dict
+from typing import List
+from typing import Mapping
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import TextIO
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy.sql.schema import Column
+from sqlalchemy.sql.schema import FetchedValue
+from typing_extensions import Literal
+
+from .migration import _ProxyTransaction
+from .migration import MigrationContext
+from .. import util
+from ..operations import Operations
+from ..script.revision import _GetRevArg
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine import URL
+ from sqlalchemy.engine.base import Connection
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .migration import MigrationInfo
+ from ..autogenerate.api import AutogenContext
+ from ..config import Config
+ from ..ddl import DefaultImpl
+ from ..operations.ops import MigrationScript
+ from ..script.base import ScriptDirectory
+
+_RevNumber = Optional[Union[str, Tuple[str, ...]]]
+
+ProcessRevisionDirectiveFn = Callable[
+ [MigrationContext, _GetRevArg, List["MigrationScript"]], None
+]
+
+RenderItemFn = Callable[
+ [str, Any, "AutogenContext"], Union[str, Literal[False]]
+]
+
+NameFilterType = Literal[
+ "schema",
+ "table",
+ "column",
+ "index",
+ "unique_constraint",
+ "foreign_key_constraint",
+]
+NameFilterParentNames = MutableMapping[
+ Literal["schema_name", "table_name", "schema_qualified_table_name"],
+ Optional[str],
+]
+IncludeNameFn = Callable[
+ [Optional[str], NameFilterType, NameFilterParentNames], bool
+]
+
+IncludeObjectFn = Callable[
+ [
+ "SchemaItem",
+ Optional[str],
+ NameFilterType,
+ bool,
+ Optional["SchemaItem"],
+ ],
+ bool,
+]
+
+OnVersionApplyFn = Callable[
+ [MigrationContext, "MigrationInfo", Collection[Any], Mapping[str, Any]],
+ None,
+]
+
+CompareServerDefault = Callable[
+ [
+ MigrationContext,
+ "Column[Any]",
+ "Column[Any]",
+ Optional[str],
+ Optional[FetchedValue],
+ Optional[str],
+ ],
+ Optional[bool],
+]
+
+CompareType = Callable[
+ [
+ MigrationContext,
+ "Column[Any]",
+ "Column[Any]",
+ "TypeEngine[Any]",
+ "TypeEngine[Any]",
+ ],
+ Optional[bool],
+]
+
+
+class EnvironmentContext(util.ModuleClsProxy):
+
+ """A configurational facade made available in an ``env.py`` script.
+
+ The :class:`.EnvironmentContext` acts as a *facade* to the more
+ nuts-and-bolts objects of :class:`.MigrationContext` as well as certain
+ aspects of :class:`.Config`,
+ within the context of the ``env.py`` script that is invoked by
+ most Alembic commands.
+
+ :class:`.EnvironmentContext` is normally instantiated
+ when a command in :mod:`alembic.command` is run. It then makes
+ itself available in the ``alembic.context`` module for the scope
+ of the command. From within an ``env.py`` script, the current
+ :class:`.EnvironmentContext` is available by importing this module.
+
+ :class:`.EnvironmentContext` also supports programmatic usage.
+ At this level, it acts as a Python context manager, that is, is
+ intended to be used using the
+ ``with:`` statement. A typical use of :class:`.EnvironmentContext`::
+
+ from alembic.config import Config
+ from alembic.script import ScriptDirectory
+
+ config = Config()
+ config.set_main_option("script_location", "myapp:migrations")
+ script = ScriptDirectory.from_config(config)
+
+
+ def my_function(rev, context):
+ '''do something with revision "rev", which
+ will be the current database revision,
+ and "context", which is the MigrationContext
+ that the env.py will create'''
+
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=my_function,
+ as_sql=False,
+ starting_rev="base",
+ destination_rev="head",
+ tag="sometag",
+ ):
+ script.run_env()
+
+ The above script will invoke the ``env.py`` script
+ within the migration environment. If and when ``env.py``
+ calls :meth:`.MigrationContext.run_migrations`, the
+ ``my_function()`` function above will be called
+ by the :class:`.MigrationContext`, given the context
+ itself as well as the current revision in the database.
+
+ .. note::
+
+ For most API usages other than full blown
+ invocation of migration scripts, the :class:`.MigrationContext`
+ and :class:`.ScriptDirectory` objects can be created and
+ used directly. The :class:`.EnvironmentContext` object
+ is *only* needed when you need to actually invoke the
+ ``env.py`` module present in the migration environment.
+
+ """
+
+ _migration_context: Optional[MigrationContext] = None
+
+ config: Config = None # type:ignore[assignment]
+ """An instance of :class:`.Config` representing the
+ configuration file contents as well as other variables
+ set programmatically within it."""
+
+ script: ScriptDirectory = None # type:ignore[assignment]
+ """An instance of :class:`.ScriptDirectory` which provides
+ programmatic access to version files within the ``versions/``
+ directory.
+
+ """
+
+ def __init__(
+ self, config: Config, script: ScriptDirectory, **kw: Any
+ ) -> None:
+ r"""Construct a new :class:`.EnvironmentContext`.
+
+ :param config: a :class:`.Config` instance.
+ :param script: a :class:`.ScriptDirectory` instance.
+ :param \**kw: keyword options that will be ultimately
+ passed along to the :class:`.MigrationContext` when
+ :meth:`.EnvironmentContext.configure` is called.
+
+ """
+ self.config = config
+ self.script = script
+ self.context_opts = kw
+
+ def __enter__(self) -> EnvironmentContext:
+ """Establish a context which provides a
+ :class:`.EnvironmentContext` object to
+ env.py scripts.
+
+ The :class:`.EnvironmentContext` will
+ be made available as ``from alembic import context``.
+
+ """
+ self._install_proxy()
+ return self
+
+ def __exit__(self, *arg: Any, **kw: Any) -> None:
+ self._remove_proxy()
+
+ def is_offline_mode(self) -> bool:
+ """Return True if the current migrations environment
+ is running in "offline mode".
+
+ This is ``True`` or ``False`` depending
+ on the ``--sql`` flag passed.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+ return self.context_opts.get("as_sql", False) # type: ignore[no-any-return] # noqa: E501
+
+ def is_transactional_ddl(self) -> bool:
+ """Return True if the context is configured to expect a
+ transactional DDL capable backend.
+
+ This defaults to the type of database in use, and
+ can be overridden by the ``transactional_ddl`` argument
+ to :meth:`.configure`
+
+ This function requires that a :class:`.MigrationContext`
+ has first been made available via :meth:`.configure`.
+
+ """
+ return self.get_context().impl.transactional_ddl
+
+ def requires_connection(self) -> bool:
+ return not self.is_offline_mode()
+
+ def get_head_revision(self) -> _RevNumber:
+ """Return the hex identifier of the 'head' script revision.
+
+ If the script directory has multiple heads, this
+ method raises a :class:`.CommandError`;
+ :meth:`.EnvironmentContext.get_head_revisions` should be preferred.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso:: :meth:`.EnvironmentContext.get_head_revisions`
+
+ """
+ return self.script.as_revision_number("head")
+
+ def get_head_revisions(self) -> _RevNumber:
+ """Return the hex identifier of the 'heads' script revision(s).
+
+ This returns a tuple containing the version number of all
+ heads in the script directory.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+ return self.script.as_revision_number("heads")
+
+ def get_starting_revision_argument(self) -> _RevNumber:
+ """Return the 'starting revision' argument,
+ if the revision was passed using ``start:end``.
+
+ This is only meaningful in "offline" mode.
+ Returns ``None`` if no value is available
+ or was configured.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+ if self._migration_context is not None:
+ return self.script.as_revision_number(
+ self.get_context()._start_from_rev
+ )
+ elif "starting_rev" in self.context_opts:
+ return self.script.as_revision_number(
+ self.context_opts["starting_rev"]
+ )
+ else:
+ # this should raise only in the case that a command
+ # is being run where the "starting rev" is never applicable;
+ # this is to catch scripts which rely upon this in
+ # non-sql mode or similar
+ raise util.CommandError(
+ "No starting revision argument is available."
+ )
+
+ def get_revision_argument(self) -> _RevNumber:
+ """Get the 'destination' revision argument.
+
+ This is typically the argument passed to the
+ ``upgrade`` or ``downgrade`` command.
+
+ If it was specified as ``head``, the actual
+ version number is returned; if specified
+ as ``base``, ``None`` is returned.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+ return self.script.as_revision_number(
+ self.context_opts["destination_rev"]
+ )
+
+ def get_tag_argument(self) -> Optional[str]:
+ """Return the value passed for the ``--tag`` argument, if any.
+
+ The ``--tag`` argument is not used directly by Alembic,
+ but is available for custom ``env.py`` configurations that
+ wish to use it; particularly for offline generation scripts
+ that wish to generate tagged filenames.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_x_argument` - a newer and more
+ open ended system of extending ``env.py`` scripts via the command
+ line.
+
+ """
+ return self.context_opts.get("tag", None) # type: ignore[no-any-return] # noqa: E501
+
+ @overload
+ def get_x_argument(self, as_dictionary: Literal[False]) -> List[str]:
+ ...
+
+ @overload
+ def get_x_argument(self, as_dictionary: Literal[True]) -> Dict[str, str]:
+ ...
+
+ @overload
+ def get_x_argument(
+ self, as_dictionary: bool = ...
+ ) -> Union[List[str], Dict[str, str]]:
+ ...
+
+ def get_x_argument(
+ self, as_dictionary: bool = False
+ ) -> Union[List[str], Dict[str, str]]:
+ """Return the value(s) passed for the ``-x`` argument, if any.
+
+ The ``-x`` argument is an open ended flag that allows any user-defined
+ value or values to be passed on the command line, then available
+ here for consumption by a custom ``env.py`` script.
+
+ The return value is a list, returned directly from the ``argparse``
+ structure. If ``as_dictionary=True`` is passed, the ``x`` arguments
+ are parsed using ``key=value`` format into a dictionary that is
+ then returned. If there is no ``=`` in the argument, value is an empty
+ string.
+
+ .. versionchanged:: 1.13.1 Support ``as_dictionary=True`` when
+ arguments are passed without the ``=`` symbol.
+
+ For example, to support passing a database URL on the command line,
+ the standard ``env.py`` script can be modified like this::
+
+ cmd_line_url = context.get_x_argument(
+ as_dictionary=True).get('dbname')
+ if cmd_line_url:
+ engine = create_engine(cmd_line_url)
+ else:
+ engine = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix='sqlalchemy.',
+ poolclass=pool.NullPool)
+
+ This then takes effect by running the ``alembic`` script as::
+
+ alembic -x dbname=postgresql://user:pass@host/dbname upgrade head
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_tag_argument`
+
+ :attr:`.Config.cmd_opts`
+
+ """
+ if self.config.cmd_opts is not None:
+ value = self.config.cmd_opts.x or []
+ else:
+ value = []
+ if as_dictionary:
+ dict_value = {}
+ for arg in value:
+ x_key, _, x_value = arg.partition("=")
+ dict_value[x_key] = x_value
+ value = dict_value
+
+ return value
+
+ def configure(
+ self,
+ connection: Optional[Connection] = None,
+ url: Optional[Union[str, URL]] = None,
+ dialect_name: Optional[str] = None,
+ dialect_opts: Optional[Dict[str, Any]] = None,
+ transactional_ddl: Optional[bool] = None,
+ transaction_per_migration: bool = False,
+ output_buffer: Optional[TextIO] = None,
+ starting_rev: Optional[str] = None,
+ tag: Optional[str] = None,
+ template_args: Optional[Dict[str, Any]] = None,
+ render_as_batch: bool = False,
+ target_metadata: Union[MetaData, Sequence[MetaData], None] = None,
+ include_name: Optional[IncludeNameFn] = None,
+ include_object: Optional[IncludeObjectFn] = None,
+ include_schemas: bool = False,
+ process_revision_directives: Optional[
+ ProcessRevisionDirectiveFn
+ ] = None,
+ compare_type: Union[bool, CompareType] = True,
+ compare_server_default: Union[bool, CompareServerDefault] = False,
+ render_item: Optional[RenderItemFn] = None,
+ literal_binds: bool = False,
+ upgrade_token: str = "upgrades",
+ downgrade_token: str = "downgrades",
+ alembic_module_prefix: str = "op.",
+ sqlalchemy_module_prefix: str = "sa.",
+ user_module_prefix: Optional[str] = None,
+ on_version_apply: Optional[OnVersionApplyFn] = None,
+ **kw: Any,
+ ) -> None:
+ """Configure a :class:`.MigrationContext` within this
+ :class:`.EnvironmentContext` which will provide database
+ connectivity and other configuration to a series of
+ migration scripts.
+
+ Many methods on :class:`.EnvironmentContext` require that
+ this method has been called in order to function, as they
+ ultimately need to have database access or at least access
+ to the dialect in use. Those which do are documented as such.
+
+ The important thing needed by :meth:`.configure` is a
+ means to determine what kind of database dialect is in use.
+ An actual connection to that database is needed only if
+ the :class:`.MigrationContext` is to be used in
+ "online" mode.
+
+ If the :meth:`.is_offline_mode` function returns ``True``,
+ then no connection is needed here. Otherwise, the
+ ``connection`` parameter should be present as an
+ instance of :class:`sqlalchemy.engine.Connection`.
+
+ This function is typically called from the ``env.py``
+ script within a migration environment. It can be called
+ multiple times for an invocation. The most recent
+ :class:`~sqlalchemy.engine.Connection`
+ for which it was called is the one that will be operated upon
+ by the next call to :meth:`.run_migrations`.
+
+ General parameters:
+
+ :param connection: a :class:`~sqlalchemy.engine.Connection`
+ to use
+ for SQL execution in "online" mode. When present, is also
+ used to determine the type of dialect in use.
+ :param url: a string database url, or a
+ :class:`sqlalchemy.engine.url.URL` object.
+ The type of dialect to be used will be derived from this if
+ ``connection`` is not passed.
+ :param dialect_name: string name of a dialect, such as
+ "postgresql", "mssql", etc.
+ The type of dialect to be used will be derived from this if
+ ``connection`` and ``url`` are not passed.
+ :param dialect_opts: dictionary of options to be passed to dialect
+ constructor.
+ :param transactional_ddl: Force the usage of "transactional"
+ DDL on or off;
+ this otherwise defaults to whether or not the dialect in
+ use supports it.
+ :param transaction_per_migration: if True, nest each migration script
+ in a transaction rather than the full series of migrations to
+ run.
+ :param output_buffer: a file-like object that will be used
+ for textual output
+ when the ``--sql`` option is used to generate SQL scripts.
+ Defaults to
+ ``sys.stdout`` if not passed here and also not present on
+ the :class:`.Config`
+ object. The value here overrides that of the :class:`.Config`
+ object.
+ :param output_encoding: when using ``--sql`` to generate SQL
+ scripts, apply this encoding to the string output.
+ :param literal_binds: when using ``--sql`` to generate SQL
+ scripts, pass through the ``literal_binds`` flag to the compiler
+ so that any literal values that would ordinarily be bound
+ parameters are converted to plain strings.
+
+ .. warning:: Dialects can typically only handle simple datatypes
+ like strings and numbers for auto-literal generation. Datatypes
+ like dates, intervals, and others may still require manual
+ formatting, typically using :meth:`.Operations.inline_literal`.
+
+ .. note:: the ``literal_binds`` flag is ignored on SQLAlchemy
+ versions prior to 0.8 where this feature is not supported.
+
+ .. seealso::
+
+ :meth:`.Operations.inline_literal`
+
+ :param starting_rev: Override the "starting revision" argument
+ when using ``--sql`` mode.
+ :param tag: a string tag for usage by custom ``env.py`` scripts.
+ Set via the ``--tag`` option, can be overridden here.
+ :param template_args: dictionary of template arguments which
+ will be added to the template argument environment when
+ running the "revision" command. Note that the script environment
+ is only run within the "revision" command if the --autogenerate
+ option is used, or if the option "revision_environment=true"
+ is present in the alembic.ini file.
+
+ :param version_table: The name of the Alembic version table.
+ The default is ``'alembic_version'``.
+ :param version_table_schema: Optional schema to place version
+ table within.
+ :param version_table_pk: boolean, whether the Alembic version table
+ should use a primary key constraint for the "value" column; this
+ only takes effect when the table is first created.
+ Defaults to True; setting to False should not be necessary and is
+ here for backwards compatibility reasons.
+ :param on_version_apply: a callable or collection of callables to be
+ run for each migration step.
+ The callables will be run in the order they are given, once for
+ each migration step, after the respective operation has been
+ applied but before its transaction is finalized.
+ Each callable accepts no positional arguments and the following
+ keyword arguments:
+
+ * ``ctx``: the :class:`.MigrationContext` running the migration,
+ * ``step``: a :class:`.MigrationInfo` representing the
+ step currently being applied,
+ * ``heads``: a collection of version strings representing the
+ current heads,
+ * ``run_args``: the ``**kwargs`` passed to :meth:`.run_migrations`.
+
+ Parameters specific to the autogenerate feature, when
+ ``alembic revision`` is run with the ``--autogenerate`` feature:
+
+ :param target_metadata: a :class:`sqlalchemy.schema.MetaData`
+ object, or a sequence of :class:`~sqlalchemy.schema.MetaData`
+ objects, that will be consulted during autogeneration.
+ The tables present in each :class:`~sqlalchemy.schema.MetaData`
+ will be compared against
+ what is locally available on the target
+ :class:`~sqlalchemy.engine.Connection`
+ to produce candidate upgrade/downgrade operations.
+ :param compare_type: Indicates type comparison behavior during
+ an autogenerate
+ operation. Defaults to ``True`` turning on type comparison, which
+ has good accuracy on most backends. See :ref:`compare_types`
+ for an example as well as information on other type
+ comparison options. Set to ``False`` which disables type
+ comparison. A callable can also be passed to provide custom type
+ comparison, see :ref:`compare_types` for additional details.
+
+ .. versionchanged:: 1.12.0 The default value of
+ :paramref:`.EnvironmentContext.configure.compare_type` has been
+ changed to ``True``.
+
+ .. seealso::
+
+ :ref:`compare_types`
+
+ :paramref:`.EnvironmentContext.configure.compare_server_default`
+
+ :param compare_server_default: Indicates server default comparison
+ behavior during
+ an autogenerate operation. Defaults to ``False`` which disables
+ server default
+ comparison. Set to ``True`` to turn on server default comparison,
+ which has
+ varied accuracy depending on backend.
+
+ To customize server default comparison behavior, a callable may
+ be specified
+ which can filter server default comparisons during an
+ autogenerate operation.
+ defaults during an autogenerate operation. The format of this
+ callable is::
+
+ def my_compare_server_default(context, inspected_column,
+ metadata_column, inspected_default, metadata_default,
+ rendered_metadata_default):
+ # return True if the defaults are different,
+ # False if not, or None to allow the default implementation
+ # to compare these defaults
+ return None
+
+ context.configure(
+ # ...
+ compare_server_default = my_compare_server_default
+ )
+
+ ``inspected_column`` is a dictionary structure as returned by
+ :meth:`sqlalchemy.engine.reflection.Inspector.get_columns`, whereas
+ ``metadata_column`` is a :class:`sqlalchemy.schema.Column` from
+ the local model environment.
+
+ A return value of ``None`` indicates to allow default server default
+ comparison
+ to proceed. Note that some backends such as Postgresql actually
+ execute
+ the two defaults on the database side to compare for equivalence.
+
+ .. seealso::
+
+ :paramref:`.EnvironmentContext.configure.compare_type`
+
+ :param include_name: A callable function which is given
+ the chance to return ``True`` or ``False`` for any database reflected
+ object based on its name, including database schema names when
+ the :paramref:`.EnvironmentContext.configure.include_schemas` flag
+ is set to ``True``.
+
+ The function accepts the following positional arguments:
+
+ * ``name``: the name of the object, such as schema name or table name.
+ Will be ``None`` when indicating the default schema name of the
+ database connection.
+ * ``type``: a string describing the type of object; currently
+ ``"schema"``, ``"table"``, ``"column"``, ``"index"``,
+ ``"unique_constraint"``, or ``"foreign_key_constraint"``
+ * ``parent_names``: a dictionary of "parent" object names, that are
+ relative to the name being given. Keys in this dictionary may
+ include: ``"schema_name"``, ``"table_name"`` or
+ ``"schema_qualified_table_name"``.
+
+ E.g.::
+
+ def include_name(name, type_, parent_names):
+ if type_ == "schema":
+ return name in ["schema_one", "schema_two"]
+ else:
+ return True
+
+ context.configure(
+ # ...
+ include_schemas = True,
+ include_name = include_name
+ )
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_object`
+
+ :paramref:`.EnvironmentContext.configure.include_schemas`
+
+
+ :param include_object: A callable function which is given
+ the chance to return ``True`` or ``False`` for any object,
+ indicating if the given object should be considered in the
+ autogenerate sweep.
+
+ The function accepts the following positional arguments:
+
+ * ``object``: a :class:`~sqlalchemy.schema.SchemaItem` object such
+ as a :class:`~sqlalchemy.schema.Table`,
+ :class:`~sqlalchemy.schema.Column`,
+ :class:`~sqlalchemy.schema.Index`
+ :class:`~sqlalchemy.schema.UniqueConstraint`,
+ or :class:`~sqlalchemy.schema.ForeignKeyConstraint` object
+ * ``name``: the name of the object. This is typically available
+ via ``object.name``.
+ * ``type``: a string describing the type of object; currently
+ ``"table"``, ``"column"``, ``"index"``, ``"unique_constraint"``,
+ or ``"foreign_key_constraint"``
+ * ``reflected``: ``True`` if the given object was produced based on
+ table reflection, ``False`` if it's from a local :class:`.MetaData`
+ object.
+ * ``compare_to``: the object being compared against, if available,
+ else ``None``.
+
+ E.g.::
+
+ def include_object(object, name, type_, reflected, compare_to):
+ if (type_ == "column" and
+ not reflected and
+ object.info.get("skip_autogenerate", False)):
+ return False
+ else:
+ return True
+
+ context.configure(
+ # ...
+ include_object = include_object
+ )
+
+ For the use case of omitting specific schemas from a target database
+ when :paramref:`.EnvironmentContext.configure.include_schemas` is
+ set to ``True``, the :attr:`~sqlalchemy.schema.Table.schema`
+ attribute can be checked for each :class:`~sqlalchemy.schema.Table`
+ object passed to the hook, however it is much more efficient
+ to filter on schemas before reflection of objects takes place
+ using the :paramref:`.EnvironmentContext.configure.include_name`
+ hook.
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_name`
+
+ :paramref:`.EnvironmentContext.configure.include_schemas`
+
+ :param render_as_batch: if True, commands which alter elements
+ within a table will be placed under a ``with batch_alter_table():``
+ directive, so that batch migrations will take place.
+
+ .. seealso::
+
+ :ref:`batch_migrations`
+
+ :param include_schemas: If True, autogenerate will scan across
+ all schemas located by the SQLAlchemy
+ :meth:`~sqlalchemy.engine.reflection.Inspector.get_schema_names`
+ method, and include all differences in tables found across all
+ those schemas. When using this option, you may want to also
+ use the :paramref:`.EnvironmentContext.configure.include_name`
+ parameter to specify a callable which
+ can filter the tables/schemas that get included.
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_name`
+
+ :paramref:`.EnvironmentContext.configure.include_object`
+
+ :param render_item: Callable that can be used to override how
+ any schema item, i.e. column, constraint, type,
+ etc., is rendered for autogenerate. The callable receives a
+ string describing the type of object, the object, and
+ the autogen context. If it returns False, the
+ default rendering method will be used. If it returns None,
+ the item will not be rendered in the context of a Table
+ construct, that is, can be used to skip columns or constraints
+ within op.create_table()::
+
+ def my_render_column(type_, col, autogen_context):
+ if type_ == "column" and isinstance(col, MySpecialCol):
+ return repr(col)
+ else:
+ return False
+
+ context.configure(
+ # ...
+ render_item = my_render_column
+ )
+
+ Available values for the type string include: ``"column"``,
+ ``"primary_key"``, ``"foreign_key"``, ``"unique"``, ``"check"``,
+ ``"type"``, ``"server_default"``.
+
+ .. seealso::
+
+ :ref:`autogen_render_types`
+
+ :param upgrade_token: When autogenerate completes, the text of the
+ candidate upgrade operations will be present in this template
+ variable when ``script.py.mako`` is rendered. Defaults to
+ ``upgrades``.
+ :param downgrade_token: When autogenerate completes, the text of the
+ candidate downgrade operations will be present in this
+ template variable when ``script.py.mako`` is rendered. Defaults to
+ ``downgrades``.
+
+ :param alembic_module_prefix: When autogenerate refers to Alembic
+ :mod:`alembic.operations` constructs, this prefix will be used
+ (i.e. ``op.create_table``) Defaults to "``op.``".
+ Can be ``None`` to indicate no prefix.
+
+ :param sqlalchemy_module_prefix: When autogenerate refers to
+ SQLAlchemy
+ :class:`~sqlalchemy.schema.Column` or type classes, this prefix
+ will be used
+ (i.e. ``sa.Column("somename", sa.Integer)``) Defaults to "``sa.``".
+ Can be ``None`` to indicate no prefix.
+ Note that when dialect-specific types are rendered, autogenerate
+ will render them using the dialect module name, i.e. ``mssql.BIT()``,
+ ``postgresql.UUID()``.
+
+ :param user_module_prefix: When autogenerate refers to a SQLAlchemy
+ type (e.g. :class:`.TypeEngine`) where the module name is not
+ under the ``sqlalchemy`` namespace, this prefix will be used
+ within autogenerate. If left at its default of
+ ``None``, the ``__module__`` attribute of the type is used to
+ render the import module. It's a good practice to set this
+ and to have all custom types be available from a fixed module space,
+ in order to future-proof migration files against reorganizations
+ in modules.
+
+ .. seealso::
+
+ :ref:`autogen_module_prefix`
+
+ :param process_revision_directives: a callable function that will
+ be passed a structure representing the end result of an autogenerate
+ or plain "revision" operation, which can be manipulated to affect
+ how the ``alembic revision`` command ultimately outputs new
+ revision scripts. The structure of the callable is::
+
+ def process_revision_directives(context, revision, directives):
+ pass
+
+ The ``directives`` parameter is a Python list containing
+ a single :class:`.MigrationScript` directive, which represents
+ the revision file to be generated. This list as well as its
+ contents may be freely modified to produce any set of commands.
+ The section :ref:`customizing_revision` shows an example of
+ doing this. The ``context`` parameter is the
+ :class:`.MigrationContext` in use,
+ and ``revision`` is a tuple of revision identifiers representing the
+ current revision of the database.
+
+ The callable is invoked at all times when the ``--autogenerate``
+ option is passed to ``alembic revision``. If ``--autogenerate``
+ is not passed, the callable is invoked only if the
+ ``revision_environment`` variable is set to True in the Alembic
+ configuration, in which case the given ``directives`` collection
+ will contain empty :class:`.UpgradeOps` and :class:`.DowngradeOps`
+ collections for ``.upgrade_ops`` and ``.downgrade_ops``. The
+ ``--autogenerate`` option itself can be inferred by inspecting
+ ``context.config.cmd_opts.autogenerate``.
+
+ The callable function may optionally be an instance of
+ a :class:`.Rewriter` object. This is a helper object that
+ assists in the production of autogenerate-stream rewriter functions.
+
+ .. seealso::
+
+ :ref:`customizing_revision`
+
+ :ref:`autogen_rewriter`
+
+ :paramref:`.command.revision.process_revision_directives`
+
+ Parameters specific to individual backends:
+
+ :param mssql_batch_separator: The "batch separator" which will
+ be placed between each statement when generating offline SQL Server
+ migrations. Defaults to ``GO``. Note this is in addition to the
+ customary semicolon ``;`` at the end of each statement; SQL Server
+ considers the "batch separator" to denote the end of an
+ individual statement execution, and cannot group certain
+ dependent operations in one step.
+ :param oracle_batch_separator: The "batch separator" which will
+ be placed between each statement when generating offline
+ Oracle migrations. Defaults to ``/``. Oracle doesn't add a
+ semicolon between statements like most other backends.
+
+ """
+ opts = self.context_opts
+ if transactional_ddl is not None:
+ opts["transactional_ddl"] = transactional_ddl
+ if output_buffer is not None:
+ opts["output_buffer"] = output_buffer
+ elif self.config.output_buffer is not None:
+ opts["output_buffer"] = self.config.output_buffer
+ if starting_rev:
+ opts["starting_rev"] = starting_rev
+ if tag:
+ opts["tag"] = tag
+ if template_args and "template_args" in opts:
+ opts["template_args"].update(template_args)
+ opts["transaction_per_migration"] = transaction_per_migration
+ opts["target_metadata"] = target_metadata
+ opts["include_name"] = include_name
+ opts["include_object"] = include_object
+ opts["include_schemas"] = include_schemas
+ opts["render_as_batch"] = render_as_batch
+ opts["upgrade_token"] = upgrade_token
+ opts["downgrade_token"] = downgrade_token
+ opts["sqlalchemy_module_prefix"] = sqlalchemy_module_prefix
+ opts["alembic_module_prefix"] = alembic_module_prefix
+ opts["user_module_prefix"] = user_module_prefix
+ opts["literal_binds"] = literal_binds
+ opts["process_revision_directives"] = process_revision_directives
+ opts["on_version_apply"] = util.to_tuple(on_version_apply, default=())
+
+ if render_item is not None:
+ opts["render_item"] = render_item
+ opts["compare_type"] = compare_type
+ if compare_server_default is not None:
+ opts["compare_server_default"] = compare_server_default
+ opts["script"] = self.script
+
+ opts.update(kw)
+
+ self._migration_context = MigrationContext.configure(
+ connection=connection,
+ url=url,
+ dialect_name=dialect_name,
+ environment_context=self,
+ dialect_opts=dialect_opts,
+ opts=opts,
+ )
+
+ def run_migrations(self, **kw: Any) -> None:
+ """Run migrations as determined by the current command line
+ configuration
+ as well as versioning information present (or not) in the current
+ database connection (if one is present).
+
+ The function accepts optional ``**kw`` arguments. If these are
+ passed, they are sent directly to the ``upgrade()`` and
+ ``downgrade()``
+ functions within each target revision file. By modifying the
+ ``script.py.mako`` file so that the ``upgrade()`` and ``downgrade()``
+ functions accept arguments, parameters can be passed here so that
+ contextual information, usually information to identify a particular
+ database in use, can be passed from a custom ``env.py`` script
+ to the migration functions.
+
+ This function requires that a :class:`.MigrationContext` has
+ first been made available via :meth:`.configure`.
+
+ """
+ assert self._migration_context is not None
+ with Operations.context(self._migration_context):
+ self.get_context().run_migrations(**kw)
+
+ def execute(
+ self,
+ sql: Union[Executable, str],
+ execution_options: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """Execute the given SQL using the current change context.
+
+ The behavior of :meth:`.execute` is the same
+ as that of :meth:`.Operations.execute`. Please see that
+ function's documentation for full detail including
+ caveats and limitations.
+
+ This function requires that a :class:`.MigrationContext` has
+ first been made available via :meth:`.configure`.
+
+ """
+ self.get_context().execute(sql, execution_options=execution_options)
+
+ def static_output(self, text: str) -> None:
+ """Emit text directly to the "offline" SQL stream.
+
+ Typically this is for emitting comments that
+ start with --. The statement is not treated
+ as a SQL execution, no ; or batch separator
+ is added, etc.
+
+ """
+ self.get_context().impl.static_output(text)
+
+ def begin_transaction(
+ self,
+ ) -> Union[_ProxyTransaction, ContextManager[None]]:
+ """Return a context manager that will
+ enclose an operation within a "transaction",
+ as defined by the environment's offline
+ and transactional DDL settings.
+
+ e.g.::
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+ :meth:`.begin_transaction` is intended to
+ "do the right thing" regardless of
+ calling context:
+
+ * If :meth:`.is_transactional_ddl` is ``False``,
+ returns a "do nothing" context manager
+ which otherwise produces no transactional
+ state or directives.
+ * If :meth:`.is_offline_mode` is ``True``,
+ returns a context manager that will
+ invoke the :meth:`.DefaultImpl.emit_begin`
+ and :meth:`.DefaultImpl.emit_commit`
+ methods, which will produce the string
+ directives ``BEGIN`` and ``COMMIT`` on
+ the output stream, as rendered by the
+ target backend (e.g. SQL Server would
+ emit ``BEGIN TRANSACTION``).
+ * Otherwise, calls :meth:`sqlalchemy.engine.Connection.begin`
+ on the current online connection, which
+ returns a :class:`sqlalchemy.engine.Transaction`
+ object. This object demarcates a real
+ transaction and is itself a context manager,
+ which will roll back if an exception
+ is raised.
+
+ Note that a custom ``env.py`` script which
+ has more specific transactional needs can of course
+ manipulate the :class:`~sqlalchemy.engine.Connection`
+ directly to produce transactional state in "online"
+ mode.
+
+ """
+
+ return self.get_context().begin_transaction()
+
+ def get_context(self) -> MigrationContext:
+ """Return the current :class:`.MigrationContext` object.
+
+ If :meth:`.EnvironmentContext.configure` has not been
+ called yet, raises an exception.
+
+ """
+
+ if self._migration_context is None:
+ raise Exception("No context has been configured yet.")
+ return self._migration_context
+
+ def get_bind(self) -> Connection:
+ """Return the current 'bind'.
+
+ In "online" mode, this is the
+ :class:`sqlalchemy.engine.Connection` currently being used
+ to emit SQL to the database.
+
+ This function requires that a :class:`.MigrationContext`
+ has first been made available via :meth:`.configure`.
+
+ """
+ return self.get_context().bind # type: ignore[return-value]
+
+ def get_impl(self) -> DefaultImpl:
+ return self.get_context().impl
diff --git a/venv/lib/python3.12/site-packages/alembic/runtime/migration.py b/venv/lib/python3.12/site-packages/alembic/runtime/migration.py
new file mode 100644
index 0000000..95c69bc
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/runtime/migration.py
@@ -0,0 +1,1396 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+from contextlib import contextmanager
+from contextlib import nullcontext
+import logging
+import sys
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Collection
+from typing import ContextManager
+from typing import Dict
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import Column
+from sqlalchemy import literal_column
+from sqlalchemy import MetaData
+from sqlalchemy import PrimaryKeyConstraint
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy.engine import Engine
+from sqlalchemy.engine import url as sqla_url
+from sqlalchemy.engine.strategies import MockEngineStrategy
+
+from .. import ddl
+from .. import util
+from ..util import sqla_compat
+from ..util.compat import EncodedIO
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine import Dialect
+ from sqlalchemy.engine import URL
+ from sqlalchemy.engine.base import Connection
+ from sqlalchemy.engine.base import Transaction
+ from sqlalchemy.engine.mock import MockConnection
+ from sqlalchemy.sql import Executable
+
+ from .environment import EnvironmentContext
+ from ..config import Config
+ from ..script.base import Script
+ from ..script.base import ScriptDirectory
+ from ..script.revision import _RevisionOrBase
+ from ..script.revision import Revision
+ from ..script.revision import RevisionMap
+
+log = logging.getLogger(__name__)
+
+
+class _ProxyTransaction:
+ def __init__(self, migration_context: MigrationContext) -> None:
+ self.migration_context = migration_context
+
+ @property
+ def _proxied_transaction(self) -> Optional[Transaction]:
+ return self.migration_context._transaction
+
+ def rollback(self) -> None:
+ t = self._proxied_transaction
+ assert t is not None
+ t.rollback()
+ self.migration_context._transaction = None
+
+ def commit(self) -> None:
+ t = self._proxied_transaction
+ assert t is not None
+ t.commit()
+ self.migration_context._transaction = None
+
+ def __enter__(self) -> _ProxyTransaction:
+ return self
+
+ def __exit__(self, type_: Any, value: Any, traceback: Any) -> None:
+ if self._proxied_transaction is not None:
+ self._proxied_transaction.__exit__(type_, value, traceback)
+ self.migration_context._transaction = None
+
+
+class MigrationContext:
+
+ """Represent the database state made available to a migration
+ script.
+
+ :class:`.MigrationContext` is the front end to an actual
+ database connection, or alternatively a string output
+ stream given a particular database dialect,
+ from an Alembic perspective.
+
+ When inside the ``env.py`` script, the :class:`.MigrationContext`
+ is available via the
+ :meth:`.EnvironmentContext.get_context` method,
+ which is available at ``alembic.context``::
+
+ # from within env.py script
+ from alembic import context
+
+ migration_context = context.get_context()
+
+ For usage outside of an ``env.py`` script, such as for
+ utility routines that want to check the current version
+ in the database, the :meth:`.MigrationContext.configure`
+ method to create new :class:`.MigrationContext` objects.
+ For example, to get at the current revision in the
+ database using :meth:`.MigrationContext.get_current_revision`::
+
+ # in any application, outside of an env.py script
+ from alembic.migration import MigrationContext
+ from sqlalchemy import create_engine
+
+ engine = create_engine("postgresql://mydatabase")
+ conn = engine.connect()
+
+ context = MigrationContext.configure(conn)
+ current_rev = context.get_current_revision()
+
+ The above context can also be used to produce
+ Alembic migration operations with an :class:`.Operations`
+ instance::
+
+ # in any application, outside of the normal Alembic environment
+ from alembic.operations import Operations
+
+ op = Operations(context)
+ op.alter_column("mytable", "somecolumn", nullable=True)
+
+ """
+
+ def __init__(
+ self,
+ dialect: Dialect,
+ connection: Optional[Connection],
+ opts: Dict[str, Any],
+ environment_context: Optional[EnvironmentContext] = None,
+ ) -> None:
+ self.environment_context = environment_context
+ self.opts = opts
+ self.dialect = dialect
+ self.script: Optional[ScriptDirectory] = opts.get("script")
+ as_sql: bool = opts.get("as_sql", False)
+ transactional_ddl = opts.get("transactional_ddl")
+ self._transaction_per_migration = opts.get(
+ "transaction_per_migration", False
+ )
+ self.on_version_apply_callbacks = opts.get("on_version_apply", ())
+ self._transaction: Optional[Transaction] = None
+
+ if as_sql:
+ self.connection = cast(
+ Optional["Connection"], self._stdout_connection(connection)
+ )
+ assert self.connection is not None
+ self._in_external_transaction = False
+ else:
+ self.connection = connection
+ self._in_external_transaction = (
+ sqla_compat._get_connection_in_transaction(connection)
+ )
+
+ self._migrations_fn: Optional[
+ Callable[..., Iterable[RevisionStep]]
+ ] = opts.get("fn")
+ self.as_sql = as_sql
+
+ self.purge = opts.get("purge", False)
+
+ if "output_encoding" in opts:
+ self.output_buffer = EncodedIO(
+ opts.get("output_buffer")
+ or sys.stdout, # type:ignore[arg-type]
+ opts["output_encoding"],
+ )
+ else:
+ self.output_buffer = opts.get("output_buffer", sys.stdout)
+
+ self._user_compare_type = opts.get("compare_type", True)
+ self._user_compare_server_default = opts.get(
+ "compare_server_default", False
+ )
+ self.version_table = version_table = opts.get(
+ "version_table", "alembic_version"
+ )
+ self.version_table_schema = version_table_schema = opts.get(
+ "version_table_schema", None
+ )
+ self._version = Table(
+ version_table,
+ MetaData(),
+ Column("version_num", String(32), nullable=False),
+ schema=version_table_schema,
+ )
+ if opts.get("version_table_pk", True):
+ self._version.append_constraint(
+ PrimaryKeyConstraint(
+ "version_num", name="%s_pkc" % version_table
+ )
+ )
+
+ self._start_from_rev: Optional[str] = opts.get("starting_rev")
+ self.impl = ddl.DefaultImpl.get_by_dialect(dialect)(
+ dialect,
+ self.connection,
+ self.as_sql,
+ transactional_ddl,
+ self.output_buffer,
+ opts,
+ )
+ log.info("Context impl %s.", self.impl.__class__.__name__)
+ if self.as_sql:
+ log.info("Generating static SQL")
+ log.info(
+ "Will assume %s DDL.",
+ "transactional"
+ if self.impl.transactional_ddl
+ else "non-transactional",
+ )
+
+ @classmethod
+ def configure(
+ cls,
+ connection: Optional[Connection] = None,
+ url: Optional[Union[str, URL]] = None,
+ dialect_name: Optional[str] = None,
+ dialect: Optional[Dialect] = None,
+ environment_context: Optional[EnvironmentContext] = None,
+ dialect_opts: Optional[Dict[str, str]] = None,
+ opts: Optional[Any] = None,
+ ) -> MigrationContext:
+ """Create a new :class:`.MigrationContext`.
+
+ This is a factory method usually called
+ by :meth:`.EnvironmentContext.configure`.
+
+ :param connection: a :class:`~sqlalchemy.engine.Connection`
+ to use for SQL execution in "online" mode. When present,
+ is also used to determine the type of dialect in use.
+ :param url: a string database url, or a
+ :class:`sqlalchemy.engine.url.URL` object.
+ The type of dialect to be used will be derived from this if
+ ``connection`` is not passed.
+ :param dialect_name: string name of a dialect, such as
+ "postgresql", "mssql", etc. The type of dialect to be used will be
+ derived from this if ``connection`` and ``url`` are not passed.
+ :param opts: dictionary of options. Most other options
+ accepted by :meth:`.EnvironmentContext.configure` are passed via
+ this dictionary.
+
+ """
+ if opts is None:
+ opts = {}
+ if dialect_opts is None:
+ dialect_opts = {}
+
+ if connection:
+ if isinstance(connection, Engine):
+ raise util.CommandError(
+ "'connection' argument to configure() is expected "
+ "to be a sqlalchemy.engine.Connection instance, "
+ "got %r" % connection,
+ )
+
+ dialect = connection.dialect
+ elif url:
+ url_obj = sqla_url.make_url(url)
+ dialect = url_obj.get_dialect()(**dialect_opts)
+ elif dialect_name:
+ url_obj = sqla_url.make_url("%s://" % dialect_name)
+ dialect = url_obj.get_dialect()(**dialect_opts)
+ elif not dialect:
+ raise Exception("Connection, url, or dialect_name is required.")
+ assert dialect is not None
+ return MigrationContext(dialect, connection, opts, environment_context)
+
+ @contextmanager
+ def autocommit_block(self) -> Iterator[None]:
+ """Enter an "autocommit" block, for databases that support AUTOCOMMIT
+ isolation levels.
+
+ This special directive is intended to support the occasional database
+ DDL or system operation that specifically has to be run outside of
+ any kind of transaction block. The PostgreSQL database platform
+ is the most common target for this style of operation, as many
+ of its DDL operations must be run outside of transaction blocks, even
+ though the database overall supports transactional DDL.
+
+ The method is used as a context manager within a migration script, by
+ calling on :meth:`.Operations.get_context` to retrieve the
+ :class:`.MigrationContext`, then invoking
+ :meth:`.MigrationContext.autocommit_block` using the ``with:``
+ statement::
+
+ def upgrade():
+ with op.get_context().autocommit_block():
+ op.execute("ALTER TYPE mood ADD VALUE 'soso'")
+
+ Above, a PostgreSQL "ALTER TYPE..ADD VALUE" directive is emitted,
+ which must be run outside of a transaction block at the database level.
+ The :meth:`.MigrationContext.autocommit_block` method makes use of the
+ SQLAlchemy ``AUTOCOMMIT`` isolation level setting, which against the
+ psycogp2 DBAPI corresponds to the ``connection.autocommit`` setting,
+ to ensure that the database driver is not inside of a DBAPI level
+ transaction block.
+
+ .. warning::
+
+ As is necessary, **the database transaction preceding the block is
+ unconditionally committed**. This means that the run of migrations
+ preceding the operation will be committed, before the overall
+ migration operation is complete.
+
+ It is recommended that when an application includes migrations with
+ "autocommit" blocks, that
+ :paramref:`.EnvironmentContext.transaction_per_migration` be used
+ so that the calling environment is tuned to expect short per-file
+ migrations whether or not one of them has an autocommit block.
+
+
+ """
+ _in_connection_transaction = self._in_connection_transaction()
+
+ if self.impl.transactional_ddl and self.as_sql:
+ self.impl.emit_commit()
+
+ elif _in_connection_transaction:
+ assert self._transaction is not None
+
+ self._transaction.commit()
+ self._transaction = None
+
+ if not self.as_sql:
+ assert self.connection is not None
+ current_level = self.connection.get_isolation_level()
+ base_connection = self.connection
+
+ # in 1.3 and 1.4 non-future mode, the connection gets switched
+ # out. we can use the base connection with the new mode
+ # except that it will not know it's in "autocommit" and will
+ # emit deprecation warnings when an autocommit action takes
+ # place.
+ self.connection = (
+ self.impl.connection
+ ) = base_connection.execution_options(isolation_level="AUTOCOMMIT")
+
+ # sqlalchemy future mode will "autobegin" in any case, so take
+ # control of that "transaction" here
+ fake_trans: Optional[Transaction] = self.connection.begin()
+ else:
+ fake_trans = None
+ try:
+ yield
+ finally:
+ if not self.as_sql:
+ assert self.connection is not None
+ if fake_trans is not None:
+ fake_trans.commit()
+ self.connection.execution_options(
+ isolation_level=current_level
+ )
+ self.connection = self.impl.connection = base_connection
+
+ if self.impl.transactional_ddl and self.as_sql:
+ self.impl.emit_begin()
+
+ elif _in_connection_transaction:
+ assert self.connection is not None
+ self._transaction = self.connection.begin()
+
+ def begin_transaction(
+ self, _per_migration: bool = False
+ ) -> Union[_ProxyTransaction, ContextManager[None]]:
+ """Begin a logical transaction for migration operations.
+
+ This method is used within an ``env.py`` script to demarcate where
+ the outer "transaction" for a series of migrations begins. Example::
+
+ def run_migrations_online():
+ connectable = create_engine(...)
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection, target_metadata=target_metadata
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+ Above, :meth:`.MigrationContext.begin_transaction` is used to demarcate
+ where the outer logical transaction occurs around the
+ :meth:`.MigrationContext.run_migrations` operation.
+
+ A "Logical" transaction means that the operation may or may not
+ correspond to a real database transaction. If the target database
+ supports transactional DDL (or
+ :paramref:`.EnvironmentContext.configure.transactional_ddl` is true),
+ the :paramref:`.EnvironmentContext.configure.transaction_per_migration`
+ flag is not set, and the migration is against a real database
+ connection (as opposed to using "offline" ``--sql`` mode), a real
+ transaction will be started. If ``--sql`` mode is in effect, the
+ operation would instead correspond to a string such as "BEGIN" being
+ emitted to the string output.
+
+ The returned object is a Python context manager that should only be
+ used in the context of a ``with:`` statement as indicated above.
+ The object has no other guaranteed API features present.
+
+ .. seealso::
+
+ :meth:`.MigrationContext.autocommit_block`
+
+ """
+
+ if self._in_external_transaction:
+ return nullcontext()
+
+ if self.impl.transactional_ddl:
+ transaction_now = _per_migration == self._transaction_per_migration
+ else:
+ transaction_now = _per_migration is True
+
+ if not transaction_now:
+ return nullcontext()
+
+ elif not self.impl.transactional_ddl:
+ assert _per_migration
+
+ if self.as_sql:
+ return nullcontext()
+ else:
+ # track our own notion of a "transaction block", which must be
+ # committed when complete. Don't rely upon whether or not the
+ # SQLAlchemy connection reports as "in transaction"; this
+ # because SQLAlchemy future connection features autobegin
+ # behavior, so it may already be in a transaction from our
+ # emitting of queries like "has_version_table", etc. While we
+ # could track these operations as well, that leaves open the
+ # possibility of new operations or other things happening in
+ # the user environment that still may be triggering
+ # "autobegin".
+
+ in_transaction = self._transaction is not None
+
+ if in_transaction:
+ return nullcontext()
+ else:
+ assert self.connection is not None
+ self._transaction = (
+ sqla_compat._safe_begin_connection_transaction(
+ self.connection
+ )
+ )
+ return _ProxyTransaction(self)
+ elif self.as_sql:
+
+ @contextmanager
+ def begin_commit():
+ self.impl.emit_begin()
+ yield
+ self.impl.emit_commit()
+
+ return begin_commit()
+ else:
+ assert self.connection is not None
+ self._transaction = sqla_compat._safe_begin_connection_transaction(
+ self.connection
+ )
+ return _ProxyTransaction(self)
+
+ def get_current_revision(self) -> Optional[str]:
+ """Return the current revision, usually that which is present
+ in the ``alembic_version`` table in the database.
+
+ This method intends to be used only for a migration stream that
+ does not contain unmerged branches in the target database;
+ if there are multiple branches present, an exception is raised.
+ The :meth:`.MigrationContext.get_current_heads` should be preferred
+ over this method going forward in order to be compatible with
+ branch migration support.
+
+ If this :class:`.MigrationContext` was configured in "offline"
+ mode, that is with ``as_sql=True``, the ``starting_rev``
+ parameter is returned instead, if any.
+
+ """
+ heads = self.get_current_heads()
+ if len(heads) == 0:
+ return None
+ elif len(heads) > 1:
+ raise util.CommandError(
+ "Version table '%s' has more than one head present; "
+ "please use get_current_heads()" % self.version_table
+ )
+ else:
+ return heads[0]
+
+ def get_current_heads(self) -> Tuple[str, ...]:
+ """Return a tuple of the current 'head versions' that are represented
+ in the target database.
+
+ For a migration stream without branches, this will be a single
+ value, synonymous with that of
+ :meth:`.MigrationContext.get_current_revision`. However when multiple
+ unmerged branches exist within the target database, the returned tuple
+ will contain a value for each head.
+
+ If this :class:`.MigrationContext` was configured in "offline"
+ mode, that is with ``as_sql=True``, the ``starting_rev``
+ parameter is returned in a one-length tuple.
+
+ If no version table is present, or if there are no revisions
+ present, an empty tuple is returned.
+
+ """
+ if self.as_sql:
+ start_from_rev: Any = self._start_from_rev
+ if start_from_rev == "base":
+ start_from_rev = None
+ elif start_from_rev is not None and self.script:
+ start_from_rev = [
+ self.script.get_revision(sfr).revision
+ for sfr in util.to_list(start_from_rev)
+ if sfr not in (None, "base")
+ ]
+ return util.to_tuple(start_from_rev, default=())
+ else:
+ if self._start_from_rev:
+ raise util.CommandError(
+ "Can't specify current_rev to context "
+ "when using a database connection"
+ )
+ if not self._has_version_table():
+ return ()
+ assert self.connection is not None
+ return tuple(
+ row[0] for row in self.connection.execute(self._version.select())
+ )
+
+ def _ensure_version_table(self, purge: bool = False) -> None:
+ with sqla_compat._ensure_scope_for_ddl(self.connection):
+ assert self.connection is not None
+ self._version.create(self.connection, checkfirst=True)
+ if purge:
+ assert self.connection is not None
+ self.connection.execute(self._version.delete())
+
+ def _has_version_table(self) -> bool:
+ assert self.connection is not None
+ return sqla_compat._connectable_has_table(
+ self.connection, self.version_table, self.version_table_schema
+ )
+
+ def stamp(self, script_directory: ScriptDirectory, revision: str) -> None:
+ """Stamp the version table with a specific revision.
+
+ This method calculates those branches to which the given revision
+ can apply, and updates those branches as though they were migrated
+ towards that revision (either up or down). If no current branches
+ include the revision, it is added as a new branch head.
+
+ """
+ heads = self.get_current_heads()
+ if not self.as_sql and not heads:
+ self._ensure_version_table()
+ head_maintainer = HeadMaintainer(self, heads)
+ for step in script_directory._stamp_revs(revision, heads):
+ head_maintainer.update_to_step(step)
+
+ def run_migrations(self, **kw: Any) -> None:
+ r"""Run the migration scripts established for this
+ :class:`.MigrationContext`, if any.
+
+ The commands in :mod:`alembic.command` will set up a function
+ that is ultimately passed to the :class:`.MigrationContext`
+ as the ``fn`` argument. This function represents the "work"
+ that will be done when :meth:`.MigrationContext.run_migrations`
+ is called, typically from within the ``env.py`` script of the
+ migration environment. The "work function" then provides an iterable
+ of version callables and other version information which
+ in the case of the ``upgrade`` or ``downgrade`` commands are the
+ list of version scripts to invoke. Other commands yield nothing,
+ in the case that a command wants to run some other operation
+ against the database such as the ``current`` or ``stamp`` commands.
+
+ :param \**kw: keyword arguments here will be passed to each
+ migration callable, that is the ``upgrade()`` or ``downgrade()``
+ method within revision scripts.
+
+ """
+ self.impl.start_migrations()
+
+ heads: Tuple[str, ...]
+ if self.purge:
+ if self.as_sql:
+ raise util.CommandError("Can't use --purge with --sql mode")
+ self._ensure_version_table(purge=True)
+ heads = ()
+ else:
+ heads = self.get_current_heads()
+
+ dont_mutate = self.opts.get("dont_mutate", False)
+
+ if not self.as_sql and not heads and not dont_mutate:
+ self._ensure_version_table()
+
+ head_maintainer = HeadMaintainer(self, heads)
+
+ assert self._migrations_fn is not None
+ for step in self._migrations_fn(heads, self):
+ with self.begin_transaction(_per_migration=True):
+ if self.as_sql and not head_maintainer.heads:
+ # for offline mode, include a CREATE TABLE from
+ # the base
+ assert self.connection is not None
+ self._version.create(self.connection)
+ log.info("Running %s", step)
+ if self.as_sql:
+ self.impl.static_output(
+ "-- Running %s" % (step.short_log,)
+ )
+ step.migration_fn(**kw)
+
+ # previously, we wouldn't stamp per migration
+ # if we were in a transaction, however given the more
+ # complex model that involves any number of inserts
+ # and row-targeted updates and deletes, it's simpler for now
+ # just to run the operations on every version
+ head_maintainer.update_to_step(step)
+ for callback in self.on_version_apply_callbacks:
+ callback(
+ ctx=self,
+ step=step.info,
+ heads=set(head_maintainer.heads),
+ run_args=kw,
+ )
+
+ if self.as_sql and not head_maintainer.heads:
+ assert self.connection is not None
+ self._version.drop(self.connection)
+
+ def _in_connection_transaction(self) -> bool:
+ try:
+ meth = self.connection.in_transaction # type:ignore[union-attr]
+ except AttributeError:
+ return False
+ else:
+ return meth()
+
+ def execute(
+ self,
+ sql: Union[Executable, str],
+ execution_options: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """Execute a SQL construct or string statement.
+
+ The underlying execution mechanics are used, that is
+ if this is "offline mode" the SQL is written to the
+ output buffer, otherwise the SQL is emitted on
+ the current SQLAlchemy connection.
+
+ """
+ self.impl._exec(sql, execution_options)
+
+ def _stdout_connection(
+ self, connection: Optional[Connection]
+ ) -> MockConnection:
+ def dump(construct, *multiparams, **params):
+ self.impl._exec(construct)
+
+ return MockEngineStrategy.MockConnection(self.dialect, dump)
+
+ @property
+ def bind(self) -> Optional[Connection]:
+ """Return the current "bind".
+
+ In online mode, this is an instance of
+ :class:`sqlalchemy.engine.Connection`, and is suitable
+ for ad-hoc execution of any kind of usage described
+ in SQLAlchemy Core documentation as well as
+ for usage with the :meth:`sqlalchemy.schema.Table.create`
+ and :meth:`sqlalchemy.schema.MetaData.create_all` methods
+ of :class:`~sqlalchemy.schema.Table`,
+ :class:`~sqlalchemy.schema.MetaData`.
+
+ Note that when "standard output" mode is enabled,
+ this bind will be a "mock" connection handler that cannot
+ return results and is only appropriate for a very limited
+ subset of commands.
+
+ """
+ return self.connection
+
+ @property
+ def config(self) -> Optional[Config]:
+ """Return the :class:`.Config` used by the current environment,
+ if any."""
+
+ if self.environment_context:
+ return self.environment_context.config
+ else:
+ return None
+
+ def _compare_type(
+ self, inspector_column: Column[Any], metadata_column: Column
+ ) -> bool:
+ if self._user_compare_type is False:
+ return False
+
+ if callable(self._user_compare_type):
+ user_value = self._user_compare_type(
+ self,
+ inspector_column,
+ metadata_column,
+ inspector_column.type,
+ metadata_column.type,
+ )
+ if user_value is not None:
+ return user_value
+
+ return self.impl.compare_type(inspector_column, metadata_column)
+
+ def _compare_server_default(
+ self,
+ inspector_column: Column[Any],
+ metadata_column: Column[Any],
+ rendered_metadata_default: Optional[str],
+ rendered_column_default: Optional[str],
+ ) -> bool:
+ if self._user_compare_server_default is False:
+ return False
+
+ if callable(self._user_compare_server_default):
+ user_value = self._user_compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_column_default,
+ metadata_column.server_default,
+ rendered_metadata_default,
+ )
+ if user_value is not None:
+ return user_value
+
+ return self.impl.compare_server_default(
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_column_default,
+ )
+
+
+class HeadMaintainer:
+ def __init__(self, context: MigrationContext, heads: Any) -> None:
+ self.context = context
+ self.heads = set(heads)
+
+ def _insert_version(self, version: str) -> None:
+ assert version not in self.heads
+ self.heads.add(version)
+
+ self.context.impl._exec(
+ self.context._version.insert().values(
+ version_num=literal_column("'%s'" % version)
+ )
+ )
+
+ def _delete_version(self, version: str) -> None:
+ self.heads.remove(version)
+
+ ret = self.context.impl._exec(
+ self.context._version.delete().where(
+ self.context._version.c.version_num
+ == literal_column("'%s'" % version)
+ )
+ )
+
+ if (
+ not self.context.as_sql
+ and self.context.dialect.supports_sane_rowcount
+ and ret is not None
+ and ret.rowcount != 1
+ ):
+ raise util.CommandError(
+ "Online migration expected to match one "
+ "row when deleting '%s' in '%s'; "
+ "%d found"
+ % (version, self.context.version_table, ret.rowcount)
+ )
+
+ def _update_version(self, from_: str, to_: str) -> None:
+ assert to_ not in self.heads
+ self.heads.remove(from_)
+ self.heads.add(to_)
+
+ ret = self.context.impl._exec(
+ self.context._version.update()
+ .values(version_num=literal_column("'%s'" % to_))
+ .where(
+ self.context._version.c.version_num
+ == literal_column("'%s'" % from_)
+ )
+ )
+
+ if (
+ not self.context.as_sql
+ and self.context.dialect.supports_sane_rowcount
+ and ret is not None
+ and ret.rowcount != 1
+ ):
+ raise util.CommandError(
+ "Online migration expected to match one "
+ "row when updating '%s' to '%s' in '%s'; "
+ "%d found"
+ % (from_, to_, self.context.version_table, ret.rowcount)
+ )
+
+ def update_to_step(self, step: Union[RevisionStep, StampStep]) -> None:
+ if step.should_delete_branch(self.heads):
+ vers = step.delete_version_num
+ log.debug("branch delete %s", vers)
+ self._delete_version(vers)
+ elif step.should_create_branch(self.heads):
+ vers = step.insert_version_num
+ log.debug("new branch insert %s", vers)
+ self._insert_version(vers)
+ elif step.should_merge_branches(self.heads):
+ # delete revs, update from rev, update to rev
+ (
+ delete_revs,
+ update_from_rev,
+ update_to_rev,
+ ) = step.merge_branch_idents(self.heads)
+ log.debug(
+ "merge, delete %s, update %s to %s",
+ delete_revs,
+ update_from_rev,
+ update_to_rev,
+ )
+ for delrev in delete_revs:
+ self._delete_version(delrev)
+ self._update_version(update_from_rev, update_to_rev)
+ elif step.should_unmerge_branches(self.heads):
+ (
+ update_from_rev,
+ update_to_rev,
+ insert_revs,
+ ) = step.unmerge_branch_idents(self.heads)
+ log.debug(
+ "unmerge, insert %s, update %s to %s",
+ insert_revs,
+ update_from_rev,
+ update_to_rev,
+ )
+ for insrev in insert_revs:
+ self._insert_version(insrev)
+ self._update_version(update_from_rev, update_to_rev)
+ else:
+ from_, to_ = step.update_version_num(self.heads)
+ log.debug("update %s to %s", from_, to_)
+ self._update_version(from_, to_)
+
+
+class MigrationInfo:
+ """Exposes information about a migration step to a callback listener.
+
+ The :class:`.MigrationInfo` object is available exclusively for the
+ benefit of the :paramref:`.EnvironmentContext.on_version_apply`
+ callback hook.
+
+ """
+
+ is_upgrade: bool
+ """True/False: indicates whether this operation ascends or descends the
+ version tree."""
+
+ is_stamp: bool
+ """True/False: indicates whether this operation is a stamp (i.e. whether
+ it results in any actual database operations)."""
+
+ up_revision_id: Optional[str]
+ """Version string corresponding to :attr:`.Revision.revision`.
+
+ In the case of a stamp operation, it is advised to use the
+ :attr:`.MigrationInfo.up_revision_ids` tuple as a stamp operation can
+ make a single movement from one or more branches down to a single
+ branchpoint, in which case there will be multiple "up" revisions.
+
+ .. seealso::
+
+ :attr:`.MigrationInfo.up_revision_ids`
+
+ """
+
+ up_revision_ids: Tuple[str, ...]
+ """Tuple of version strings corresponding to :attr:`.Revision.revision`.
+
+ In the majority of cases, this tuple will be a single value, synonymous
+ with the scalar value of :attr:`.MigrationInfo.up_revision_id`.
+ It can be multiple revision identifiers only in the case of an
+ ``alembic stamp`` operation which is moving downwards from multiple
+ branches down to their common branch point.
+
+ """
+
+ down_revision_ids: Tuple[str, ...]
+ """Tuple of strings representing the base revisions of this migration step.
+
+ If empty, this represents a root revision; otherwise, the first item
+ corresponds to :attr:`.Revision.down_revision`, and the rest are inferred
+ from dependencies.
+ """
+
+ revision_map: RevisionMap
+ """The revision map inside of which this operation occurs."""
+
+ def __init__(
+ self,
+ revision_map: RevisionMap,
+ is_upgrade: bool,
+ is_stamp: bool,
+ up_revisions: Union[str, Tuple[str, ...]],
+ down_revisions: Union[str, Tuple[str, ...]],
+ ) -> None:
+ self.revision_map = revision_map
+ self.is_upgrade = is_upgrade
+ self.is_stamp = is_stamp
+ self.up_revision_ids = util.to_tuple(up_revisions, default=())
+ if self.up_revision_ids:
+ self.up_revision_id = self.up_revision_ids[0]
+ else:
+ # this should never be the case with
+ # "upgrade", "downgrade", or "stamp" as we are always
+ # measuring movement in terms of at least one upgrade version
+ self.up_revision_id = None
+ self.down_revision_ids = util.to_tuple(down_revisions, default=())
+
+ @property
+ def is_migration(self) -> bool:
+ """True/False: indicates whether this operation is a migration.
+
+ At present this is true if and only the migration is not a stamp.
+ If other operation types are added in the future, both this attribute
+ and :attr:`~.MigrationInfo.is_stamp` will be false.
+ """
+ return not self.is_stamp
+
+ @property
+ def source_revision_ids(self) -> Tuple[str, ...]:
+ """Active revisions before this migration step is applied."""
+ return (
+ self.down_revision_ids if self.is_upgrade else self.up_revision_ids
+ )
+
+ @property
+ def destination_revision_ids(self) -> Tuple[str, ...]:
+ """Active revisions after this migration step is applied."""
+ return (
+ self.up_revision_ids if self.is_upgrade else self.down_revision_ids
+ )
+
+ @property
+ def up_revision(self) -> Optional[Revision]:
+ """Get :attr:`~.MigrationInfo.up_revision_id` as
+ a :class:`.Revision`.
+
+ """
+ return self.revision_map.get_revision(self.up_revision_id)
+
+ @property
+ def up_revisions(self) -> Tuple[Optional[_RevisionOrBase], ...]:
+ """Get :attr:`~.MigrationInfo.up_revision_ids` as a
+ :class:`.Revision`."""
+ return self.revision_map.get_revisions(self.up_revision_ids)
+
+ @property
+ def down_revisions(self) -> Tuple[Optional[_RevisionOrBase], ...]:
+ """Get :attr:`~.MigrationInfo.down_revision_ids` as a tuple of
+ :class:`Revisions <.Revision>`."""
+ return self.revision_map.get_revisions(self.down_revision_ids)
+
+ @property
+ def source_revisions(self) -> Tuple[Optional[_RevisionOrBase], ...]:
+ """Get :attr:`~MigrationInfo.source_revision_ids` as a tuple of
+ :class:`Revisions <.Revision>`."""
+ return self.revision_map.get_revisions(self.source_revision_ids)
+
+ @property
+ def destination_revisions(self) -> Tuple[Optional[_RevisionOrBase], ...]:
+ """Get :attr:`~MigrationInfo.destination_revision_ids` as a tuple of
+ :class:`Revisions <.Revision>`."""
+ return self.revision_map.get_revisions(self.destination_revision_ids)
+
+
+class MigrationStep:
+ from_revisions_no_deps: Tuple[str, ...]
+ to_revisions_no_deps: Tuple[str, ...]
+ is_upgrade: bool
+ migration_fn: Any
+
+ if TYPE_CHECKING:
+
+ @property
+ def doc(self) -> Optional[str]:
+ ...
+
+ @property
+ def name(self) -> str:
+ return self.migration_fn.__name__
+
+ @classmethod
+ def upgrade_from_script(
+ cls, revision_map: RevisionMap, script: Script
+ ) -> RevisionStep:
+ return RevisionStep(revision_map, script, True)
+
+ @classmethod
+ def downgrade_from_script(
+ cls, revision_map: RevisionMap, script: Script
+ ) -> RevisionStep:
+ return RevisionStep(revision_map, script, False)
+
+ @property
+ def is_downgrade(self) -> bool:
+ return not self.is_upgrade
+
+ @property
+ def short_log(self) -> str:
+ return "%s %s -> %s" % (
+ self.name,
+ util.format_as_comma(self.from_revisions_no_deps),
+ util.format_as_comma(self.to_revisions_no_deps),
+ )
+
+ def __str__(self):
+ if self.doc:
+ return "%s %s -> %s, %s" % (
+ self.name,
+ util.format_as_comma(self.from_revisions_no_deps),
+ util.format_as_comma(self.to_revisions_no_deps),
+ self.doc,
+ )
+ else:
+ return self.short_log
+
+
+class RevisionStep(MigrationStep):
+ def __init__(
+ self, revision_map: RevisionMap, revision: Script, is_upgrade: bool
+ ) -> None:
+ self.revision_map = revision_map
+ self.revision = revision
+ self.is_upgrade = is_upgrade
+ if is_upgrade:
+ self.migration_fn = revision.module.upgrade
+ else:
+ self.migration_fn = revision.module.downgrade
+
+ def __repr__(self):
+ return "RevisionStep(%r, is_upgrade=%r)" % (
+ self.revision.revision,
+ self.is_upgrade,
+ )
+
+ def __eq__(self, other: object) -> bool:
+ return (
+ isinstance(other, RevisionStep)
+ and other.revision == self.revision
+ and self.is_upgrade == other.is_upgrade
+ )
+
+ @property
+ def doc(self) -> Optional[str]:
+ return self.revision.doc
+
+ @property
+ def from_revisions(self) -> Tuple[str, ...]:
+ if self.is_upgrade:
+ return self.revision._normalized_down_revisions
+ else:
+ return (self.revision.revision,)
+
+ @property
+ def from_revisions_no_deps( # type:ignore[override]
+ self,
+ ) -> Tuple[str, ...]:
+ if self.is_upgrade:
+ return self.revision._versioned_down_revisions
+ else:
+ return (self.revision.revision,)
+
+ @property
+ def to_revisions(self) -> Tuple[str, ...]:
+ if self.is_upgrade:
+ return (self.revision.revision,)
+ else:
+ return self.revision._normalized_down_revisions
+
+ @property
+ def to_revisions_no_deps( # type:ignore[override]
+ self,
+ ) -> Tuple[str, ...]:
+ if self.is_upgrade:
+ return (self.revision.revision,)
+ else:
+ return self.revision._versioned_down_revisions
+
+ @property
+ def _has_scalar_down_revision(self) -> bool:
+ return len(self.revision._normalized_down_revisions) == 1
+
+ def should_delete_branch(self, heads: Set[str]) -> bool:
+ """A delete is when we are a. in a downgrade and b.
+ we are going to the "base" or we are going to a version that
+ is implied as a dependency on another version that is remaining.
+
+ """
+ if not self.is_downgrade:
+ return False
+
+ if self.revision.revision not in heads:
+ return False
+
+ downrevs = self.revision._normalized_down_revisions
+
+ if not downrevs:
+ # is a base
+ return True
+ else:
+ # determine what the ultimate "to_revisions" for an
+ # unmerge would be. If there are none, then we're a delete.
+ to_revisions = self._unmerge_to_revisions(heads)
+ return not to_revisions
+
+ def merge_branch_idents(
+ self, heads: Set[str]
+ ) -> Tuple[List[str], str, str]:
+ other_heads = set(heads).difference(self.from_revisions)
+
+ if other_heads:
+ ancestors = {
+ r.revision
+ for r in self.revision_map._get_ancestor_nodes(
+ self.revision_map.get_revisions(other_heads), check=False
+ )
+ }
+ from_revisions = list(
+ set(self.from_revisions).difference(ancestors)
+ )
+ else:
+ from_revisions = list(self.from_revisions)
+
+ return (
+ # delete revs, update from rev, update to rev
+ list(from_revisions[0:-1]),
+ from_revisions[-1],
+ self.to_revisions[0],
+ )
+
+ def _unmerge_to_revisions(self, heads: Set[str]) -> Tuple[str, ...]:
+ other_heads = set(heads).difference([self.revision.revision])
+ if other_heads:
+ ancestors = {
+ r.revision
+ for r in self.revision_map._get_ancestor_nodes(
+ self.revision_map.get_revisions(other_heads), check=False
+ )
+ }
+ return tuple(set(self.to_revisions).difference(ancestors))
+ else:
+ # for each revision we plan to return, compute its ancestors
+ # (excluding self), and remove those from the final output since
+ # they are already accounted for.
+ ancestors = {
+ r.revision
+ for to_revision in self.to_revisions
+ for r in self.revision_map._get_ancestor_nodes(
+ self.revision_map.get_revisions(to_revision), check=False
+ )
+ if r.revision != to_revision
+ }
+ return tuple(set(self.to_revisions).difference(ancestors))
+
+ def unmerge_branch_idents(
+ self, heads: Set[str]
+ ) -> Tuple[str, str, Tuple[str, ...]]:
+ to_revisions = self._unmerge_to_revisions(heads)
+
+ return (
+ # update from rev, update to rev, insert revs
+ self.from_revisions[0],
+ to_revisions[-1],
+ to_revisions[0:-1],
+ )
+
+ def should_create_branch(self, heads: Set[str]) -> bool:
+ if not self.is_upgrade:
+ return False
+
+ downrevs = self.revision._normalized_down_revisions
+
+ if not downrevs:
+ # is a base
+ return True
+ else:
+ # none of our downrevs are present, so...
+ # we have to insert our version. This is true whether
+ # or not there is only one downrev, or multiple (in the latter
+ # case, we're a merge point.)
+ if not heads.intersection(downrevs):
+ return True
+ else:
+ return False
+
+ def should_merge_branches(self, heads: Set[str]) -> bool:
+ if not self.is_upgrade:
+ return False
+
+ downrevs = self.revision._normalized_down_revisions
+
+ if len(downrevs) > 1 and len(heads.intersection(downrevs)) > 1:
+ return True
+
+ return False
+
+ def should_unmerge_branches(self, heads: Set[str]) -> bool:
+ if not self.is_downgrade:
+ return False
+
+ downrevs = self.revision._normalized_down_revisions
+
+ if self.revision.revision in heads and len(downrevs) > 1:
+ return True
+
+ return False
+
+ def update_version_num(self, heads: Set[str]) -> Tuple[str, str]:
+ if not self._has_scalar_down_revision:
+ downrev = heads.intersection(
+ self.revision._normalized_down_revisions
+ )
+ assert (
+ len(downrev) == 1
+ ), "Can't do an UPDATE because downrevision is ambiguous"
+ down_revision = list(downrev)[0]
+ else:
+ down_revision = self.revision._normalized_down_revisions[0]
+
+ if self.is_upgrade:
+ return down_revision, self.revision.revision
+ else:
+ return self.revision.revision, down_revision
+
+ @property
+ def delete_version_num(self) -> str:
+ return self.revision.revision
+
+ @property
+ def insert_version_num(self) -> str:
+ return self.revision.revision
+
+ @property
+ def info(self) -> MigrationInfo:
+ return MigrationInfo(
+ revision_map=self.revision_map,
+ up_revisions=self.revision.revision,
+ down_revisions=self.revision._normalized_down_revisions,
+ is_upgrade=self.is_upgrade,
+ is_stamp=False,
+ )
+
+
+class StampStep(MigrationStep):
+ def __init__(
+ self,
+ from_: Optional[Union[str, Collection[str]]],
+ to_: Optional[Union[str, Collection[str]]],
+ is_upgrade: bool,
+ branch_move: bool,
+ revision_map: Optional[RevisionMap] = None,
+ ) -> None:
+ self.from_: Tuple[str, ...] = util.to_tuple(from_, default=())
+ self.to_: Tuple[str, ...] = util.to_tuple(to_, default=())
+ self.is_upgrade = is_upgrade
+ self.branch_move = branch_move
+ self.migration_fn = self.stamp_revision
+ self.revision_map = revision_map
+
+ doc: Optional[str] = None
+
+ def stamp_revision(self, **kw: Any) -> None:
+ return None
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, StampStep)
+ and other.from_revisions == self.from_revisions
+ and other.to_revisions == self.to_revisions
+ and other.branch_move == self.branch_move
+ and self.is_upgrade == other.is_upgrade
+ )
+
+ @property
+ def from_revisions(self):
+ return self.from_
+
+ @property
+ def to_revisions(self) -> Tuple[str, ...]:
+ return self.to_
+
+ @property
+ def from_revisions_no_deps( # type:ignore[override]
+ self,
+ ) -> Tuple[str, ...]:
+ return self.from_
+
+ @property
+ def to_revisions_no_deps( # type:ignore[override]
+ self,
+ ) -> Tuple[str, ...]:
+ return self.to_
+
+ @property
+ def delete_version_num(self) -> str:
+ assert len(self.from_) == 1
+ return self.from_[0]
+
+ @property
+ def insert_version_num(self) -> str:
+ assert len(self.to_) == 1
+ return self.to_[0]
+
+ def update_version_num(self, heads: Set[str]) -> Tuple[str, str]:
+ assert len(self.from_) == 1
+ assert len(self.to_) == 1
+ return self.from_[0], self.to_[0]
+
+ def merge_branch_idents(
+ self, heads: Union[Set[str], List[str]]
+ ) -> Union[Tuple[List[Any], str, str], Tuple[List[str], str, str]]:
+ return (
+ # delete revs, update from rev, update to rev
+ list(self.from_[0:-1]),
+ self.from_[-1],
+ self.to_[0],
+ )
+
+ def unmerge_branch_idents(
+ self, heads: Set[str]
+ ) -> Tuple[str, str, List[str]]:
+ return (
+ # update from rev, update to rev, insert revs
+ self.from_[0],
+ self.to_[-1],
+ list(self.to_[0:-1]),
+ )
+
+ def should_delete_branch(self, heads: Set[str]) -> bool:
+ # TODO: we probably need to look for self.to_ inside of heads,
+ # in a similar manner as should_create_branch, however we have
+ # no tests for this yet (stamp downgrades w/ branches)
+ return self.is_downgrade and self.branch_move
+
+ def should_create_branch(self, heads: Set[str]) -> Union[Set[str], bool]:
+ return (
+ self.is_upgrade
+ and (self.branch_move or set(self.from_).difference(heads))
+ and set(self.to_).difference(heads)
+ )
+
+ def should_merge_branches(self, heads: Set[str]) -> bool:
+ return len(self.from_) > 1
+
+ def should_unmerge_branches(self, heads: Set[str]) -> bool:
+ return len(self.to_) > 1
+
+ @property
+ def info(self) -> MigrationInfo:
+ up, down = (
+ (self.to_, self.from_)
+ if self.is_upgrade
+ else (self.from_, self.to_)
+ )
+ assert self.revision_map is not None
+ return MigrationInfo(
+ revision_map=self.revision_map,
+ up_revisions=up,
+ down_revisions=down,
+ is_upgrade=self.is_upgrade,
+ is_stamp=True,
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/script/__init__.py b/venv/lib/python3.12/site-packages/alembic/script/__init__.py
new file mode 100644
index 0000000..d78f3f1
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/script/__init__.py
@@ -0,0 +1,4 @@
+from .base import Script
+from .base import ScriptDirectory
+
+__all__ = ["ScriptDirectory", "Script"]
diff --git a/venv/lib/python3.12/site-packages/alembic/script/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..786dcdb
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/script/__pycache__/base.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..ca8cb98
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/base.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/script/__pycache__/revision.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/revision.cpython-312.pyc
new file mode 100644
index 0000000..2cbbf9e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/revision.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/script/__pycache__/write_hooks.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/write_hooks.cpython-312.pyc
new file mode 100644
index 0000000..99cecbf
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/script/__pycache__/write_hooks.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/script/base.py b/venv/lib/python3.12/site-packages/alembic/script/base.py
new file mode 100644
index 0000000..5945ca5
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/script/base.py
@@ -0,0 +1,1060 @@
+from __future__ import annotations
+
+from contextlib import contextmanager
+import datetime
+import os
+import re
+import shutil
+import sys
+from types import ModuleType
+from typing import Any
+from typing import cast
+from typing import Iterator
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from . import revision
+from . import write_hooks
+from .. import util
+from ..runtime import migration
+from ..util import compat
+from ..util import not_none
+
+if TYPE_CHECKING:
+ from .revision import _GetRevArg
+ from .revision import _RevIdType
+ from .revision import Revision
+ from ..config import Config
+ from ..config import MessagingOptions
+ from ..runtime.migration import RevisionStep
+ from ..runtime.migration import StampStep
+
+try:
+ if compat.py39:
+ from zoneinfo import ZoneInfo
+ from zoneinfo import ZoneInfoNotFoundError
+ else:
+ from backports.zoneinfo import ZoneInfo # type: ignore[import-not-found,no-redef] # noqa: E501
+ from backports.zoneinfo import ZoneInfoNotFoundError # type: ignore[no-redef] # noqa: E501
+except ImportError:
+ ZoneInfo = None # type: ignore[assignment, misc]
+
+_sourceless_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)(c|o)?$")
+_only_source_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)$")
+_legacy_rev = re.compile(r"([a-f0-9]+)\.py$")
+_slug_re = re.compile(r"\w+")
+_default_file_template = "%(rev)s_%(slug)s"
+_split_on_space_comma = re.compile(r", *|(?: +)")
+
+_split_on_space_comma_colon = re.compile(r", *|(?: +)|\:")
+
+
+class ScriptDirectory:
+
+ """Provides operations upon an Alembic script directory.
+
+ This object is useful to get information as to current revisions,
+ most notably being able to get at the "head" revision, for schemes
+ that want to test if the current revision in the database is the most
+ recent::
+
+ from alembic.script import ScriptDirectory
+ from alembic.config import Config
+ config = Config()
+ config.set_main_option("script_location", "myapp:migrations")
+ script = ScriptDirectory.from_config(config)
+
+ head_revision = script.get_current_head()
+
+
+
+ """
+
+ def __init__(
+ self,
+ dir: str, # noqa
+ file_template: str = _default_file_template,
+ truncate_slug_length: Optional[int] = 40,
+ version_locations: Optional[List[str]] = None,
+ sourceless: bool = False,
+ output_encoding: str = "utf-8",
+ timezone: Optional[str] = None,
+ hook_config: Optional[Mapping[str, str]] = None,
+ recursive_version_locations: bool = False,
+ messaging_opts: MessagingOptions = cast(
+ "MessagingOptions", util.EMPTY_DICT
+ ),
+ ) -> None:
+ self.dir = dir
+ self.file_template = file_template
+ self.version_locations = version_locations
+ self.truncate_slug_length = truncate_slug_length or 40
+ self.sourceless = sourceless
+ self.output_encoding = output_encoding
+ self.revision_map = revision.RevisionMap(self._load_revisions)
+ self.timezone = timezone
+ self.hook_config = hook_config
+ self.recursive_version_locations = recursive_version_locations
+ self.messaging_opts = messaging_opts
+
+ if not os.access(dir, os.F_OK):
+ raise util.CommandError(
+ "Path doesn't exist: %r. Please use "
+ "the 'init' command to create a new "
+ "scripts folder." % os.path.abspath(dir)
+ )
+
+ @property
+ def versions(self) -> str:
+ loc = self._version_locations
+ if len(loc) > 1:
+ raise util.CommandError("Multiple version_locations present")
+ else:
+ return loc[0]
+
+ @util.memoized_property
+ def _version_locations(self) -> Sequence[str]:
+ if self.version_locations:
+ return [
+ os.path.abspath(util.coerce_resource_to_filename(location))
+ for location in self.version_locations
+ ]
+ else:
+ return (os.path.abspath(os.path.join(self.dir, "versions")),)
+
+ def _load_revisions(self) -> Iterator[Script]:
+ if self.version_locations:
+ paths = [
+ vers
+ for vers in self._version_locations
+ if os.path.exists(vers)
+ ]
+ else:
+ paths = [self.versions]
+
+ dupes = set()
+ for vers in paths:
+ for file_path in Script._list_py_dir(self, vers):
+ real_path = os.path.realpath(file_path)
+ if real_path in dupes:
+ util.warn(
+ "File %s loaded twice! ignoring. Please ensure "
+ "version_locations is unique." % real_path
+ )
+ continue
+ dupes.add(real_path)
+
+ filename = os.path.basename(real_path)
+ dir_name = os.path.dirname(real_path)
+ script = Script._from_filename(self, dir_name, filename)
+ if script is None:
+ continue
+ yield script
+
+ @classmethod
+ def from_config(cls, config: Config) -> ScriptDirectory:
+ """Produce a new :class:`.ScriptDirectory` given a :class:`.Config`
+ instance.
+
+ The :class:`.Config` need only have the ``script_location`` key
+ present.
+
+ """
+ script_location = config.get_main_option("script_location")
+ if script_location is None:
+ raise util.CommandError(
+ "No 'script_location' key " "found in configuration."
+ )
+ truncate_slug_length: Optional[int]
+ tsl = config.get_main_option("truncate_slug_length")
+ if tsl is not None:
+ truncate_slug_length = int(tsl)
+ else:
+ truncate_slug_length = None
+
+ version_locations_str = config.get_main_option("version_locations")
+ version_locations: Optional[List[str]]
+ if version_locations_str:
+ version_path_separator = config.get_main_option(
+ "version_path_separator"
+ )
+
+ split_on_path = {
+ None: None,
+ "space": " ",
+ "os": os.pathsep,
+ ":": ":",
+ ";": ";",
+ }
+
+ try:
+ split_char: Optional[str] = split_on_path[
+ version_path_separator
+ ]
+ except KeyError as ke:
+ raise ValueError(
+ "'%s' is not a valid value for "
+ "version_path_separator; "
+ "expected 'space', 'os', ':', ';'" % version_path_separator
+ ) from ke
+ else:
+ if split_char is None:
+ # legacy behaviour for backwards compatibility
+ version_locations = _split_on_space_comma.split(
+ version_locations_str
+ )
+ else:
+ version_locations = [
+ x for x in version_locations_str.split(split_char) if x
+ ]
+ else:
+ version_locations = None
+
+ prepend_sys_path = config.get_main_option("prepend_sys_path")
+ if prepend_sys_path:
+ sys.path[:0] = list(
+ _split_on_space_comma_colon.split(prepend_sys_path)
+ )
+
+ rvl = config.get_main_option("recursive_version_locations") == "true"
+ return ScriptDirectory(
+ util.coerce_resource_to_filename(script_location),
+ file_template=config.get_main_option(
+ "file_template", _default_file_template
+ ),
+ truncate_slug_length=truncate_slug_length,
+ sourceless=config.get_main_option("sourceless") == "true",
+ output_encoding=config.get_main_option("output_encoding", "utf-8"),
+ version_locations=version_locations,
+ timezone=config.get_main_option("timezone"),
+ hook_config=config.get_section("post_write_hooks", {}),
+ recursive_version_locations=rvl,
+ messaging_opts=config.messaging_opts,
+ )
+
+ @contextmanager
+ def _catch_revision_errors(
+ self,
+ ancestor: Optional[str] = None,
+ multiple_heads: Optional[str] = None,
+ start: Optional[str] = None,
+ end: Optional[str] = None,
+ resolution: Optional[str] = None,
+ ) -> Iterator[None]:
+ try:
+ yield
+ except revision.RangeNotAncestorError as rna:
+ if start is None:
+ start = cast(Any, rna.lower)
+ if end is None:
+ end = cast(Any, rna.upper)
+ if not ancestor:
+ ancestor = (
+ "Requested range %(start)s:%(end)s does not refer to "
+ "ancestor/descendant revisions along the same branch"
+ )
+ ancestor = ancestor % {"start": start, "end": end}
+ raise util.CommandError(ancestor) from rna
+ except revision.MultipleHeads as mh:
+ if not multiple_heads:
+ multiple_heads = (
+ "Multiple head revisions are present for given "
+ "argument '%(head_arg)s'; please "
+ "specify a specific target revision, "
+ "'@%(head_arg)s' to "
+ "narrow to a specific head, or 'heads' for all heads"
+ )
+ multiple_heads = multiple_heads % {
+ "head_arg": end or mh.argument,
+ "heads": util.format_as_comma(mh.heads),
+ }
+ raise util.CommandError(multiple_heads) from mh
+ except revision.ResolutionError as re:
+ if resolution is None:
+ resolution = "Can't locate revision identified by '%s'" % (
+ re.argument
+ )
+ raise util.CommandError(resolution) from re
+ except revision.RevisionError as err:
+ raise util.CommandError(err.args[0]) from err
+
+ def walk_revisions(
+ self, base: str = "base", head: str = "heads"
+ ) -> Iterator[Script]:
+ """Iterate through all revisions.
+
+ :param base: the base revision, or "base" to start from the
+ empty revision.
+
+ :param head: the head revision; defaults to "heads" to indicate
+ all head revisions. May also be "head" to indicate a single
+ head revision.
+
+ """
+ with self._catch_revision_errors(start=base, end=head):
+ for rev in self.revision_map.iterate_revisions(
+ head, base, inclusive=True, assert_relative_length=False
+ ):
+ yield cast(Script, rev)
+
+ def get_revisions(self, id_: _GetRevArg) -> Tuple[Script, ...]:
+ """Return the :class:`.Script` instance with the given rev identifier,
+ symbolic name, or sequence of identifiers.
+
+ """
+ with self._catch_revision_errors():
+ return cast(
+ Tuple[Script, ...],
+ self.revision_map.get_revisions(id_),
+ )
+
+ def get_all_current(self, id_: Tuple[str, ...]) -> Set[Script]:
+ with self._catch_revision_errors():
+ return cast(Set[Script], self.revision_map._get_all_current(id_))
+
+ def get_revision(self, id_: str) -> Script:
+ """Return the :class:`.Script` instance with the given rev id.
+
+ .. seealso::
+
+ :meth:`.ScriptDirectory.get_revisions`
+
+ """
+
+ with self._catch_revision_errors():
+ return cast(Script, self.revision_map.get_revision(id_))
+
+ def as_revision_number(
+ self, id_: Optional[str]
+ ) -> Optional[Union[str, Tuple[str, ...]]]:
+ """Convert a symbolic revision, i.e. 'head' or 'base', into
+ an actual revision number."""
+
+ with self._catch_revision_errors():
+ rev, branch_name = self.revision_map._resolve_revision_number(id_)
+
+ if not rev:
+ # convert () to None
+ return None
+ elif id_ == "heads":
+ return rev
+ else:
+ return rev[0]
+
+ def iterate_revisions(
+ self,
+ upper: Union[str, Tuple[str, ...], None],
+ lower: Union[str, Tuple[str, ...], None],
+ **kw: Any,
+ ) -> Iterator[Script]:
+ """Iterate through script revisions, starting at the given
+ upper revision identifier and ending at the lower.
+
+ The traversal uses strictly the `down_revision`
+ marker inside each migration script, so
+ it is a requirement that upper >= lower,
+ else you'll get nothing back.
+
+ The iterator yields :class:`.Script` objects.
+
+ .. seealso::
+
+ :meth:`.RevisionMap.iterate_revisions`
+
+ """
+ return cast(
+ Iterator[Script],
+ self.revision_map.iterate_revisions(upper, lower, **kw),
+ )
+
+ def get_current_head(self) -> Optional[str]:
+ """Return the current head revision.
+
+ If the script directory has multiple heads
+ due to branching, an error is raised;
+ :meth:`.ScriptDirectory.get_heads` should be
+ preferred.
+
+ :return: a string revision number.
+
+ .. seealso::
+
+ :meth:`.ScriptDirectory.get_heads`
+
+ """
+ with self._catch_revision_errors(
+ multiple_heads=(
+ "The script directory has multiple heads (due to branching)."
+ "Please use get_heads(), or merge the branches using "
+ "alembic merge."
+ )
+ ):
+ return self.revision_map.get_current_head()
+
+ def get_heads(self) -> List[str]:
+ """Return all "versioned head" revisions as strings.
+
+ This is normally a list of length one,
+ unless branches are present. The
+ :meth:`.ScriptDirectory.get_current_head()` method
+ can be used normally when a script directory
+ has only one head.
+
+ :return: a tuple of string revision numbers.
+ """
+ return list(self.revision_map.heads)
+
+ def get_base(self) -> Optional[str]:
+ """Return the "base" revision as a string.
+
+ This is the revision number of the script that
+ has a ``down_revision`` of None.
+
+ If the script directory has multiple bases, an error is raised;
+ :meth:`.ScriptDirectory.get_bases` should be
+ preferred.
+
+ """
+ bases = self.get_bases()
+ if len(bases) > 1:
+ raise util.CommandError(
+ "The script directory has multiple bases. "
+ "Please use get_bases()."
+ )
+ elif bases:
+ return bases[0]
+ else:
+ return None
+
+ def get_bases(self) -> List[str]:
+ """return all "base" revisions as strings.
+
+ This is the revision number of all scripts that
+ have a ``down_revision`` of None.
+
+ """
+ return list(self.revision_map.bases)
+
+ def _upgrade_revs(
+ self, destination: str, current_rev: str
+ ) -> List[RevisionStep]:
+ with self._catch_revision_errors(
+ ancestor="Destination %(end)s is not a valid upgrade "
+ "target from current head(s)",
+ end=destination,
+ ):
+ revs = self.iterate_revisions(
+ destination, current_rev, implicit_base=True
+ )
+ return [
+ migration.MigrationStep.upgrade_from_script(
+ self.revision_map, script
+ )
+ for script in reversed(list(revs))
+ ]
+
+ def _downgrade_revs(
+ self, destination: str, current_rev: Optional[str]
+ ) -> List[RevisionStep]:
+ with self._catch_revision_errors(
+ ancestor="Destination %(end)s is not a valid downgrade "
+ "target from current head(s)",
+ end=destination,
+ ):
+ revs = self.iterate_revisions(
+ current_rev, destination, select_for_downgrade=True
+ )
+ return [
+ migration.MigrationStep.downgrade_from_script(
+ self.revision_map, script
+ )
+ for script in revs
+ ]
+
+ def _stamp_revs(
+ self, revision: _RevIdType, heads: _RevIdType
+ ) -> List[StampStep]:
+ with self._catch_revision_errors(
+ multiple_heads="Multiple heads are present; please specify a "
+ "single target revision"
+ ):
+ heads_revs = self.get_revisions(heads)
+
+ steps = []
+
+ if not revision:
+ revision = "base"
+
+ filtered_heads: List[Script] = []
+ for rev in util.to_tuple(revision):
+ if rev:
+ filtered_heads.extend(
+ self.revision_map.filter_for_lineage(
+ cast(Sequence[Script], heads_revs),
+ rev,
+ include_dependencies=True,
+ )
+ )
+ filtered_heads = util.unique_list(filtered_heads)
+
+ dests = self.get_revisions(revision) or [None]
+
+ for dest in dests:
+ if dest is None:
+ # dest is 'base'. Return a "delete branch" migration
+ # for all applicable heads.
+ steps.extend(
+ [
+ migration.StampStep(
+ head.revision,
+ None,
+ False,
+ True,
+ self.revision_map,
+ )
+ for head in filtered_heads
+ ]
+ )
+ continue
+ elif dest in filtered_heads:
+ # the dest is already in the version table, do nothing.
+ continue
+
+ # figure out if the dest is a descendant or an
+ # ancestor of the selected nodes
+ descendants = set(
+ self.revision_map._get_descendant_nodes([dest])
+ )
+ ancestors = set(self.revision_map._get_ancestor_nodes([dest]))
+
+ if descendants.intersection(filtered_heads):
+ # heads are above the target, so this is a downgrade.
+ # we can treat them as a "merge", single step.
+ assert not ancestors.intersection(filtered_heads)
+ todo_heads = [head.revision for head in filtered_heads]
+ step = migration.StampStep(
+ todo_heads,
+ dest.revision,
+ False,
+ False,
+ self.revision_map,
+ )
+ steps.append(step)
+ continue
+ elif ancestors.intersection(filtered_heads):
+ # heads are below the target, so this is an upgrade.
+ # we can treat them as a "merge", single step.
+ todo_heads = [head.revision for head in filtered_heads]
+ step = migration.StampStep(
+ todo_heads,
+ dest.revision,
+ True,
+ False,
+ self.revision_map,
+ )
+ steps.append(step)
+ continue
+ else:
+ # destination is in a branch not represented,
+ # treat it as new branch
+ step = migration.StampStep(
+ (), dest.revision, True, True, self.revision_map
+ )
+ steps.append(step)
+ continue
+
+ return steps
+
+ def run_env(self) -> None:
+ """Run the script environment.
+
+ This basically runs the ``env.py`` script present
+ in the migration environment. It is called exclusively
+ by the command functions in :mod:`alembic.command`.
+
+
+ """
+ util.load_python_file(self.dir, "env.py")
+
+ @property
+ def env_py_location(self) -> str:
+ return os.path.abspath(os.path.join(self.dir, "env.py"))
+
+ def _generate_template(self, src: str, dest: str, **kw: Any) -> None:
+ with util.status(
+ f"Generating {os.path.abspath(dest)}", **self.messaging_opts
+ ):
+ util.template_to_file(src, dest, self.output_encoding, **kw)
+
+ def _copy_file(self, src: str, dest: str) -> None:
+ with util.status(
+ f"Generating {os.path.abspath(dest)}", **self.messaging_opts
+ ):
+ shutil.copy(src, dest)
+
+ def _ensure_directory(self, path: str) -> None:
+ path = os.path.abspath(path)
+ if not os.path.exists(path):
+ with util.status(
+ f"Creating directory {path}", **self.messaging_opts
+ ):
+ os.makedirs(path)
+
+ def _generate_create_date(self) -> datetime.datetime:
+ if self.timezone is not None:
+ if ZoneInfo is None:
+ raise util.CommandError(
+ "Python >= 3.9 is required for timezone support or"
+ "the 'backports.zoneinfo' package must be installed."
+ )
+ # First, assume correct capitalization
+ try:
+ tzinfo = ZoneInfo(self.timezone)
+ except ZoneInfoNotFoundError:
+ tzinfo = None
+ if tzinfo is None:
+ try:
+ tzinfo = ZoneInfo(self.timezone.upper())
+ except ZoneInfoNotFoundError:
+ raise util.CommandError(
+ "Can't locate timezone: %s" % self.timezone
+ ) from None
+ create_date = (
+ datetime.datetime.utcnow()
+ .replace(tzinfo=datetime.timezone.utc)
+ .astimezone(tzinfo)
+ )
+ else:
+ create_date = datetime.datetime.now()
+ return create_date
+
+ def generate_revision(
+ self,
+ revid: str,
+ message: Optional[str],
+ head: Optional[_RevIdType] = None,
+ splice: Optional[bool] = False,
+ branch_labels: Optional[_RevIdType] = None,
+ version_path: Optional[str] = None,
+ depends_on: Optional[_RevIdType] = None,
+ **kw: Any,
+ ) -> Optional[Script]:
+ """Generate a new revision file.
+
+ This runs the ``script.py.mako`` template, given
+ template arguments, and creates a new file.
+
+ :param revid: String revision id. Typically this
+ comes from ``alembic.util.rev_id()``.
+ :param message: the revision message, the one passed
+ by the -m argument to the ``revision`` command.
+ :param head: the head revision to generate against. Defaults
+ to the current "head" if no branches are present, else raises
+ an exception.
+ :param splice: if True, allow the "head" version to not be an
+ actual head; otherwise, the selected head must be a head
+ (e.g. endpoint) revision.
+
+ """
+ if head is None:
+ head = "head"
+
+ try:
+ Script.verify_rev_id(revid)
+ except revision.RevisionError as err:
+ raise util.CommandError(err.args[0]) from err
+
+ with self._catch_revision_errors(
+ multiple_heads=(
+ "Multiple heads are present; please specify the head "
+ "revision on which the new revision should be based, "
+ "or perform a merge."
+ )
+ ):
+ heads = cast(
+ Tuple[Optional["Revision"], ...],
+ self.revision_map.get_revisions(head),
+ )
+ for h in heads:
+ assert h != "base" # type: ignore[comparison-overlap]
+
+ if len(set(heads)) != len(heads):
+ raise util.CommandError("Duplicate head revisions specified")
+
+ create_date = self._generate_create_date()
+
+ if version_path is None:
+ if len(self._version_locations) > 1:
+ for head_ in heads:
+ if head_ is not None:
+ assert isinstance(head_, Script)
+ version_path = os.path.dirname(head_.path)
+ break
+ else:
+ raise util.CommandError(
+ "Multiple version locations present, "
+ "please specify --version-path"
+ )
+ else:
+ version_path = self.versions
+
+ norm_path = os.path.normpath(os.path.abspath(version_path))
+ for vers_path in self._version_locations:
+ if os.path.normpath(vers_path) == norm_path:
+ break
+ else:
+ raise util.CommandError(
+ "Path %s is not represented in current "
+ "version locations" % version_path
+ )
+
+ if self.version_locations:
+ self._ensure_directory(version_path)
+
+ path = self._rev_path(version_path, revid, message, create_date)
+
+ if not splice:
+ for head_ in heads:
+ if head_ is not None and not head_.is_head:
+ raise util.CommandError(
+ "Revision %s is not a head revision; please specify "
+ "--splice to create a new branch from this revision"
+ % head_.revision
+ )
+
+ resolved_depends_on: Optional[List[str]]
+ if depends_on:
+ with self._catch_revision_errors():
+ resolved_depends_on = [
+ dep
+ if dep in rev.branch_labels # maintain branch labels
+ else rev.revision # resolve partial revision identifiers
+ for rev, dep in [
+ (not_none(self.revision_map.get_revision(dep)), dep)
+ for dep in util.to_list(depends_on)
+ ]
+ ]
+ else:
+ resolved_depends_on = None
+
+ self._generate_template(
+ os.path.join(self.dir, "script.py.mako"),
+ path,
+ up_revision=str(revid),
+ down_revision=revision.tuple_rev_as_scalar(
+ tuple(h.revision if h is not None else None for h in heads)
+ ),
+ branch_labels=util.to_tuple(branch_labels),
+ depends_on=revision.tuple_rev_as_scalar(resolved_depends_on),
+ create_date=create_date,
+ comma=util.format_as_comma,
+ message=message if message is not None else ("empty message"),
+ **kw,
+ )
+
+ post_write_hooks = self.hook_config
+ if post_write_hooks:
+ write_hooks._run_hooks(path, post_write_hooks)
+
+ try:
+ script = Script._from_path(self, path)
+ except revision.RevisionError as err:
+ raise util.CommandError(err.args[0]) from err
+ if script is None:
+ return None
+ if branch_labels and not script.branch_labels:
+ raise util.CommandError(
+ "Version %s specified branch_labels %s, however the "
+ "migration file %s does not have them; have you upgraded "
+ "your script.py.mako to include the "
+ "'branch_labels' section?"
+ % (script.revision, branch_labels, script.path)
+ )
+ self.revision_map.add_revision(script)
+ return script
+
+ def _rev_path(
+ self,
+ path: str,
+ rev_id: str,
+ message: Optional[str],
+ create_date: datetime.datetime,
+ ) -> str:
+ epoch = int(create_date.timestamp())
+ slug = "_".join(_slug_re.findall(message or "")).lower()
+ if len(slug) > self.truncate_slug_length:
+ slug = slug[: self.truncate_slug_length].rsplit("_", 1)[0] + "_"
+ filename = "%s.py" % (
+ self.file_template
+ % {
+ "rev": rev_id,
+ "slug": slug,
+ "epoch": epoch,
+ "year": create_date.year,
+ "month": create_date.month,
+ "day": create_date.day,
+ "hour": create_date.hour,
+ "minute": create_date.minute,
+ "second": create_date.second,
+ }
+ )
+ return os.path.join(path, filename)
+
+
+class Script(revision.Revision):
+
+ """Represent a single revision file in a ``versions/`` directory.
+
+ The :class:`.Script` instance is returned by methods
+ such as :meth:`.ScriptDirectory.iterate_revisions`.
+
+ """
+
+ def __init__(self, module: ModuleType, rev_id: str, path: str):
+ self.module = module
+ self.path = path
+ super().__init__(
+ rev_id,
+ module.down_revision,
+ branch_labels=util.to_tuple(
+ getattr(module, "branch_labels", None), default=()
+ ),
+ dependencies=util.to_tuple(
+ getattr(module, "depends_on", None), default=()
+ ),
+ )
+
+ module: ModuleType
+ """The Python module representing the actual script itself."""
+
+ path: str
+ """Filesystem path of the script."""
+
+ _db_current_indicator: Optional[bool] = None
+ """Utility variable which when set will cause string output to indicate
+ this is a "current" version in some database"""
+
+ @property
+ def doc(self) -> str:
+ """Return the docstring given in the script."""
+
+ return re.split("\n\n", self.longdoc)[0]
+
+ @property
+ def longdoc(self) -> str:
+ """Return the docstring given in the script."""
+
+ doc = self.module.__doc__
+ if doc:
+ if hasattr(self.module, "_alembic_source_encoding"):
+ doc = doc.decode( # type: ignore[attr-defined]
+ self.module._alembic_source_encoding
+ )
+ return doc.strip() # type: ignore[union-attr]
+ else:
+ return ""
+
+ @property
+ def log_entry(self) -> str:
+ entry = "Rev: %s%s%s%s%s\n" % (
+ self.revision,
+ " (head)" if self.is_head else "",
+ " (branchpoint)" if self.is_branch_point else "",
+ " (mergepoint)" if self.is_merge_point else "",
+ " (current)" if self._db_current_indicator else "",
+ )
+ if self.is_merge_point:
+ entry += "Merges: %s\n" % (self._format_down_revision(),)
+ else:
+ entry += "Parent: %s\n" % (self._format_down_revision(),)
+
+ if self.dependencies:
+ entry += "Also depends on: %s\n" % (
+ util.format_as_comma(self.dependencies)
+ )
+
+ if self.is_branch_point:
+ entry += "Branches into: %s\n" % (
+ util.format_as_comma(self.nextrev)
+ )
+
+ if self.branch_labels:
+ entry += "Branch names: %s\n" % (
+ util.format_as_comma(self.branch_labels),
+ )
+
+ entry += "Path: %s\n" % (self.path,)
+
+ entry += "\n%s\n" % (
+ "\n".join(" %s" % para for para in self.longdoc.splitlines())
+ )
+ return entry
+
+ def __str__(self) -> str:
+ return "%s -> %s%s%s%s, %s" % (
+ self._format_down_revision(),
+ self.revision,
+ " (head)" if self.is_head else "",
+ " (branchpoint)" if self.is_branch_point else "",
+ " (mergepoint)" if self.is_merge_point else "",
+ self.doc,
+ )
+
+ def _head_only(
+ self,
+ include_branches: bool = False,
+ include_doc: bool = False,
+ include_parents: bool = False,
+ tree_indicators: bool = True,
+ head_indicators: bool = True,
+ ) -> str:
+ text = self.revision
+ if include_parents:
+ if self.dependencies:
+ text = "%s (%s) -> %s" % (
+ self._format_down_revision(),
+ util.format_as_comma(self.dependencies),
+ text,
+ )
+ else:
+ text = "%s -> %s" % (self._format_down_revision(), text)
+ assert text is not None
+ if include_branches and self.branch_labels:
+ text += " (%s)" % util.format_as_comma(self.branch_labels)
+ if head_indicators or tree_indicators:
+ text += "%s%s%s" % (
+ " (head)" if self._is_real_head else "",
+ " (effective head)"
+ if self.is_head and not self._is_real_head
+ else "",
+ " (current)" if self._db_current_indicator else "",
+ )
+ if tree_indicators:
+ text += "%s%s" % (
+ " (branchpoint)" if self.is_branch_point else "",
+ " (mergepoint)" if self.is_merge_point else "",
+ )
+ if include_doc:
+ text += ", %s" % self.doc
+ return text
+
+ def cmd_format(
+ self,
+ verbose: bool,
+ include_branches: bool = False,
+ include_doc: bool = False,
+ include_parents: bool = False,
+ tree_indicators: bool = True,
+ ) -> str:
+ if verbose:
+ return self.log_entry
+ else:
+ return self._head_only(
+ include_branches, include_doc, include_parents, tree_indicators
+ )
+
+ def _format_down_revision(self) -> str:
+ if not self.down_revision:
+ return ""
+ else:
+ return util.format_as_comma(self._versioned_down_revisions)
+
+ @classmethod
+ def _from_path(
+ cls, scriptdir: ScriptDirectory, path: str
+ ) -> Optional[Script]:
+ dir_, filename = os.path.split(path)
+ return cls._from_filename(scriptdir, dir_, filename)
+
+ @classmethod
+ def _list_py_dir(cls, scriptdir: ScriptDirectory, path: str) -> List[str]:
+ paths = []
+ for root, dirs, files in os.walk(path, topdown=True):
+ if root.endswith("__pycache__"):
+ # a special case - we may include these files
+ # if a `sourceless` option is specified
+ continue
+
+ for filename in sorted(files):
+ paths.append(os.path.join(root, filename))
+
+ if scriptdir.sourceless:
+ # look for __pycache__
+ py_cache_path = os.path.join(root, "__pycache__")
+ if os.path.exists(py_cache_path):
+ # add all files from __pycache__ whose filename is not
+ # already in the names we got from the version directory.
+ # add as relative paths including __pycache__ token
+ names = {filename.split(".")[0] for filename in files}
+ paths.extend(
+ os.path.join(py_cache_path, pyc)
+ for pyc in os.listdir(py_cache_path)
+ if pyc.split(".")[0] not in names
+ )
+
+ if not scriptdir.recursive_version_locations:
+ break
+
+ # the real script order is defined by revision,
+ # but it may be undefined if there are many files with a same
+ # `down_revision`, for a better user experience (ex. debugging),
+ # we use a deterministic order
+ dirs.sort()
+
+ return paths
+
+ @classmethod
+ def _from_filename(
+ cls, scriptdir: ScriptDirectory, dir_: str, filename: str
+ ) -> Optional[Script]:
+ if scriptdir.sourceless:
+ py_match = _sourceless_rev_file.match(filename)
+ else:
+ py_match = _only_source_rev_file.match(filename)
+
+ if not py_match:
+ return None
+
+ py_filename = py_match.group(1)
+
+ if scriptdir.sourceless:
+ is_c = py_match.group(2) == "c"
+ is_o = py_match.group(2) == "o"
+ else:
+ is_c = is_o = False
+
+ if is_o or is_c:
+ py_exists = os.path.exists(os.path.join(dir_, py_filename))
+ pyc_exists = os.path.exists(os.path.join(dir_, py_filename + "c"))
+
+ # prefer .py over .pyc because we'd like to get the
+ # source encoding; prefer .pyc over .pyo because we'd like to
+ # have the docstrings which a -OO file would not have
+ if py_exists or is_o and pyc_exists:
+ return None
+
+ module = util.load_python_file(dir_, filename)
+
+ if not hasattr(module, "revision"):
+ # attempt to get the revision id from the script name,
+ # this for legacy only
+ m = _legacy_rev.match(filename)
+ if not m:
+ raise util.CommandError(
+ "Could not determine revision id from filename %s. "
+ "Be sure the 'revision' variable is "
+ "declared inside the script (please see 'Upgrading "
+ "from Alembic 0.1 to 0.2' in the documentation)."
+ % filename
+ )
+ else:
+ revision = m.group(1)
+ else:
+ revision = module.revision
+ return Script(module, revision, os.path.join(dir_, filename))
diff --git a/venv/lib/python3.12/site-packages/alembic/script/revision.py b/venv/lib/python3.12/site-packages/alembic/script/revision.py
new file mode 100644
index 0000000..77a802c
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/script/revision.py
@@ -0,0 +1,1721 @@
+from __future__ import annotations
+
+import collections
+import re
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Collection
+from typing import Deque
+from typing import Dict
+from typing import FrozenSet
+from typing import Iterable
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import overload
+from typing import Protocol
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy import util as sqlautil
+
+from .. import util
+from ..util import not_none
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+_RevIdType = Union[str, List[str], Tuple[str, ...]]
+_GetRevArg = Union[
+ str,
+ Iterable[Optional[str]],
+ Iterable[str],
+]
+_RevisionIdentifierType = Union[str, Tuple[str, ...], None]
+_RevisionOrStr = Union["Revision", str]
+_RevisionOrBase = Union["Revision", "Literal['base']"]
+_InterimRevisionMapType = Dict[str, "Revision"]
+_RevisionMapType = Dict[Union[None, str, Tuple[()]], Optional["Revision"]]
+_T = TypeVar("_T")
+_TR = TypeVar("_TR", bound=Optional[_RevisionOrStr])
+
+_relative_destination = re.compile(r"(?:(.+?)@)?(\w+)?((?:\+|-)\d+)")
+_revision_illegal_chars = ["@", "-", "+"]
+
+
+class _CollectRevisionsProtocol(Protocol):
+ def __call__(
+ self,
+ upper: _RevisionIdentifierType,
+ lower: _RevisionIdentifierType,
+ inclusive: bool,
+ implicit_base: bool,
+ assert_relative_length: bool,
+ ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]:
+ ...
+
+
+class RevisionError(Exception):
+ pass
+
+
+class RangeNotAncestorError(RevisionError):
+ def __init__(
+ self, lower: _RevisionIdentifierType, upper: _RevisionIdentifierType
+ ) -> None:
+ self.lower = lower
+ self.upper = upper
+ super().__init__(
+ "Revision %s is not an ancestor of revision %s"
+ % (lower or "base", upper or "base")
+ )
+
+
+class MultipleHeads(RevisionError):
+ def __init__(self, heads: Sequence[str], argument: Optional[str]) -> None:
+ self.heads = heads
+ self.argument = argument
+ super().__init__(
+ "Multiple heads are present for given argument '%s'; "
+ "%s" % (argument, ", ".join(heads))
+ )
+
+
+class ResolutionError(RevisionError):
+ def __init__(self, message: str, argument: str) -> None:
+ super().__init__(message)
+ self.argument = argument
+
+
+class CycleDetected(RevisionError):
+ kind = "Cycle"
+
+ def __init__(self, revisions: Sequence[str]) -> None:
+ self.revisions = revisions
+ super().__init__(
+ "%s is detected in revisions (%s)"
+ % (self.kind, ", ".join(revisions))
+ )
+
+
+class DependencyCycleDetected(CycleDetected):
+ kind = "Dependency cycle"
+
+ def __init__(self, revisions: Sequence[str]) -> None:
+ super().__init__(revisions)
+
+
+class LoopDetected(CycleDetected):
+ kind = "Self-loop"
+
+ def __init__(self, revision: str) -> None:
+ super().__init__([revision])
+
+
+class DependencyLoopDetected(DependencyCycleDetected, LoopDetected):
+ kind = "Dependency self-loop"
+
+ def __init__(self, revision: Sequence[str]) -> None:
+ super().__init__(revision)
+
+
+class RevisionMap:
+ """Maintains a map of :class:`.Revision` objects.
+
+ :class:`.RevisionMap` is used by :class:`.ScriptDirectory` to maintain
+ and traverse the collection of :class:`.Script` objects, which are
+ themselves instances of :class:`.Revision`.
+
+ """
+
+ def __init__(self, generator: Callable[[], Iterable[Revision]]) -> None:
+ """Construct a new :class:`.RevisionMap`.
+
+ :param generator: a zero-arg callable that will generate an iterable
+ of :class:`.Revision` instances to be used. These are typically
+ :class:`.Script` subclasses within regular Alembic use.
+
+ """
+ self._generator = generator
+
+ @util.memoized_property
+ def heads(self) -> Tuple[str, ...]:
+ """All "head" revisions as strings.
+
+ This is normally a tuple of length one,
+ unless unmerged branches are present.
+
+ :return: a tuple of string revision numbers.
+
+ """
+ self._revision_map
+ return self.heads
+
+ @util.memoized_property
+ def bases(self) -> Tuple[str, ...]:
+ """All "base" revisions as strings.
+
+ These are revisions that have a ``down_revision`` of None,
+ or empty tuple.
+
+ :return: a tuple of string revision numbers.
+
+ """
+ self._revision_map
+ return self.bases
+
+ @util.memoized_property
+ def _real_heads(self) -> Tuple[str, ...]:
+ """All "real" head revisions as strings.
+
+ :return: a tuple of string revision numbers.
+
+ """
+ self._revision_map
+ return self._real_heads
+
+ @util.memoized_property
+ def _real_bases(self) -> Tuple[str, ...]:
+ """All "real" base revisions as strings.
+
+ :return: a tuple of string revision numbers.
+
+ """
+ self._revision_map
+ return self._real_bases
+
+ @util.memoized_property
+ def _revision_map(self) -> _RevisionMapType:
+ """memoized attribute, initializes the revision map from the
+ initial collection.
+
+ """
+ # Ordering required for some tests to pass (but not required in
+ # general)
+ map_: _InterimRevisionMapType = sqlautil.OrderedDict()
+
+ heads: Set[Revision] = sqlautil.OrderedSet()
+ _real_heads: Set[Revision] = sqlautil.OrderedSet()
+ bases: Tuple[Revision, ...] = ()
+ _real_bases: Tuple[Revision, ...] = ()
+
+ has_branch_labels = set()
+ all_revisions = set()
+
+ for revision in self._generator():
+ all_revisions.add(revision)
+
+ if revision.revision in map_:
+ util.warn(
+ "Revision %s is present more than once" % revision.revision
+ )
+ map_[revision.revision] = revision
+ if revision.branch_labels:
+ has_branch_labels.add(revision)
+
+ heads.add(revision)
+ _real_heads.add(revision)
+ if revision.is_base:
+ bases += (revision,)
+ if revision._is_real_base:
+ _real_bases += (revision,)
+
+ # add the branch_labels to the map_. We'll need these
+ # to resolve the dependencies.
+ rev_map = map_.copy()
+ self._map_branch_labels(
+ has_branch_labels, cast(_RevisionMapType, map_)
+ )
+
+ # resolve dependency names from branch labels and symbolic
+ # names
+ self._add_depends_on(all_revisions, cast(_RevisionMapType, map_))
+
+ for rev in map_.values():
+ for downrev in rev._all_down_revisions:
+ if downrev not in map_:
+ util.warn(
+ "Revision %s referenced from %s is not present"
+ % (downrev, rev)
+ )
+ down_revision = map_[downrev]
+ down_revision.add_nextrev(rev)
+ if downrev in rev._versioned_down_revisions:
+ heads.discard(down_revision)
+ _real_heads.discard(down_revision)
+
+ # once the map has downrevisions populated, the dependencies
+ # can be further refined to include only those which are not
+ # already ancestors
+ self._normalize_depends_on(all_revisions, cast(_RevisionMapType, map_))
+ self._detect_cycles(rev_map, heads, bases, _real_heads, _real_bases)
+
+ revision_map: _RevisionMapType = dict(map_.items())
+ revision_map[None] = revision_map[()] = None
+ self.heads = tuple(rev.revision for rev in heads)
+ self._real_heads = tuple(rev.revision for rev in _real_heads)
+ self.bases = tuple(rev.revision for rev in bases)
+ self._real_bases = tuple(rev.revision for rev in _real_bases)
+
+ self._add_branches(has_branch_labels, revision_map)
+ return revision_map
+
+ def _detect_cycles(
+ self,
+ rev_map: _InterimRevisionMapType,
+ heads: Set[Revision],
+ bases: Tuple[Revision, ...],
+ _real_heads: Set[Revision],
+ _real_bases: Tuple[Revision, ...],
+ ) -> None:
+ if not rev_map:
+ return
+ if not heads or not bases:
+ raise CycleDetected(list(rev_map))
+ total_space = {
+ rev.revision
+ for rev in self._iterate_related_revisions(
+ lambda r: r._versioned_down_revisions,
+ heads,
+ map_=cast(_RevisionMapType, rev_map),
+ )
+ }.intersection(
+ rev.revision
+ for rev in self._iterate_related_revisions(
+ lambda r: r.nextrev,
+ bases,
+ map_=cast(_RevisionMapType, rev_map),
+ )
+ )
+ deleted_revs = set(rev_map.keys()) - total_space
+ if deleted_revs:
+ raise CycleDetected(sorted(deleted_revs))
+
+ if not _real_heads or not _real_bases:
+ raise DependencyCycleDetected(list(rev_map))
+ total_space = {
+ rev.revision
+ for rev in self._iterate_related_revisions(
+ lambda r: r._all_down_revisions,
+ _real_heads,
+ map_=cast(_RevisionMapType, rev_map),
+ )
+ }.intersection(
+ rev.revision
+ for rev in self._iterate_related_revisions(
+ lambda r: r._all_nextrev,
+ _real_bases,
+ map_=cast(_RevisionMapType, rev_map),
+ )
+ )
+ deleted_revs = set(rev_map.keys()) - total_space
+ if deleted_revs:
+ raise DependencyCycleDetected(sorted(deleted_revs))
+
+ def _map_branch_labels(
+ self, revisions: Collection[Revision], map_: _RevisionMapType
+ ) -> None:
+ for revision in revisions:
+ if revision.branch_labels:
+ assert revision._orig_branch_labels is not None
+ for branch_label in revision._orig_branch_labels:
+ if branch_label in map_:
+ map_rev = map_[branch_label]
+ assert map_rev is not None
+ raise RevisionError(
+ "Branch name '%s' in revision %s already "
+ "used by revision %s"
+ % (
+ branch_label,
+ revision.revision,
+ map_rev.revision,
+ )
+ )
+ map_[branch_label] = revision
+
+ def _add_branches(
+ self, revisions: Collection[Revision], map_: _RevisionMapType
+ ) -> None:
+ for revision in revisions:
+ if revision.branch_labels:
+ revision.branch_labels.update(revision.branch_labels)
+ for node in self._get_descendant_nodes(
+ [revision], map_, include_dependencies=False
+ ):
+ node.branch_labels.update(revision.branch_labels)
+
+ parent = node
+ while (
+ parent
+ and not parent._is_real_branch_point
+ and not parent.is_merge_point
+ ):
+ parent.branch_labels.update(revision.branch_labels)
+ if parent.down_revision:
+ parent = map_[parent.down_revision]
+ else:
+ break
+
+ def _add_depends_on(
+ self, revisions: Collection[Revision], map_: _RevisionMapType
+ ) -> None:
+ """Resolve the 'dependencies' for each revision in a collection
+ in terms of actual revision ids, as opposed to branch labels or other
+ symbolic names.
+
+ The collection is then assigned to the _resolved_dependencies
+ attribute on each revision object.
+
+ """
+
+ for revision in revisions:
+ if revision.dependencies:
+ deps = [
+ map_[dep] for dep in util.to_tuple(revision.dependencies)
+ ]
+ revision._resolved_dependencies = tuple(
+ [d.revision for d in deps if d is not None]
+ )
+ else:
+ revision._resolved_dependencies = ()
+
+ def _normalize_depends_on(
+ self, revisions: Collection[Revision], map_: _RevisionMapType
+ ) -> None:
+ """Create a collection of "dependencies" that omits dependencies
+ that are already ancestor nodes for each revision in a given
+ collection.
+
+ This builds upon the _resolved_dependencies collection created in the
+ _add_depends_on() method, looking in the fully populated revision map
+ for ancestors, and omitting them as the _resolved_dependencies
+ collection as it is copied to a new collection. The new collection is
+ then assigned to the _normalized_resolved_dependencies attribute on
+ each revision object.
+
+ The collection is then used to determine the immediate "down revision"
+ identifiers for this revision.
+
+ """
+
+ for revision in revisions:
+ if revision._resolved_dependencies:
+ normalized_resolved = set(revision._resolved_dependencies)
+ for rev in self._get_ancestor_nodes(
+ [revision],
+ include_dependencies=False,
+ map_=map_,
+ ):
+ if rev is revision:
+ continue
+ elif rev._resolved_dependencies:
+ normalized_resolved.difference_update(
+ rev._resolved_dependencies
+ )
+
+ revision._normalized_resolved_dependencies = tuple(
+ normalized_resolved
+ )
+ else:
+ revision._normalized_resolved_dependencies = ()
+
+ def add_revision(self, revision: Revision, _replace: bool = False) -> None:
+ """add a single revision to an existing map.
+
+ This method is for single-revision use cases, it's not
+ appropriate for fully populating an entire revision map.
+
+ """
+ map_ = self._revision_map
+ if not _replace and revision.revision in map_:
+ util.warn(
+ "Revision %s is present more than once" % revision.revision
+ )
+ elif _replace and revision.revision not in map_:
+ raise Exception("revision %s not in map" % revision.revision)
+
+ map_[revision.revision] = revision
+
+ revisions = [revision]
+ self._add_branches(revisions, map_)
+ self._map_branch_labels(revisions, map_)
+ self._add_depends_on(revisions, map_)
+
+ if revision.is_base:
+ self.bases += (revision.revision,)
+ if revision._is_real_base:
+ self._real_bases += (revision.revision,)
+
+ for downrev in revision._all_down_revisions:
+ if downrev not in map_:
+ util.warn(
+ "Revision %s referenced from %s is not present"
+ % (downrev, revision)
+ )
+ not_none(map_[downrev]).add_nextrev(revision)
+
+ self._normalize_depends_on(revisions, map_)
+
+ if revision._is_real_head:
+ self._real_heads = tuple(
+ head
+ for head in self._real_heads
+ if head
+ not in set(revision._all_down_revisions).union(
+ [revision.revision]
+ )
+ ) + (revision.revision,)
+ if revision.is_head:
+ self.heads = tuple(
+ head
+ for head in self.heads
+ if head
+ not in set(revision._versioned_down_revisions).union(
+ [revision.revision]
+ )
+ ) + (revision.revision,)
+
+ def get_current_head(
+ self, branch_label: Optional[str] = None
+ ) -> Optional[str]:
+ """Return the current head revision.
+
+ If the script directory has multiple heads
+ due to branching, an error is raised;
+ :meth:`.ScriptDirectory.get_heads` should be
+ preferred.
+
+ :param branch_label: optional branch name which will limit the
+ heads considered to those which include that branch_label.
+
+ :return: a string revision number.
+
+ .. seealso::
+
+ :meth:`.ScriptDirectory.get_heads`
+
+ """
+ current_heads: Sequence[str] = self.heads
+ if branch_label:
+ current_heads = self.filter_for_lineage(
+ current_heads, branch_label
+ )
+ if len(current_heads) > 1:
+ raise MultipleHeads(
+ current_heads,
+ "%s@head" % branch_label if branch_label else "head",
+ )
+
+ if current_heads:
+ return current_heads[0]
+ else:
+ return None
+
+ def _get_base_revisions(self, identifier: str) -> Tuple[str, ...]:
+ return self.filter_for_lineage(self.bases, identifier)
+
+ def get_revisions(
+ self, id_: Optional[_GetRevArg]
+ ) -> Tuple[Optional[_RevisionOrBase], ...]:
+ """Return the :class:`.Revision` instances with the given rev id
+ or identifiers.
+
+ May be given a single identifier, a sequence of identifiers, or the
+ special symbols "head" or "base". The result is a tuple of one
+ or more identifiers, or an empty tuple in the case of "base".
+
+ In the cases where 'head', 'heads' is requested and the
+ revision map is empty, returns an empty tuple.
+
+ Supports partial identifiers, where the given identifier
+ is matched against all identifiers that start with the given
+ characters; if there is exactly one match, that determines the
+ full revision.
+
+ """
+
+ if isinstance(id_, (list, tuple, set, frozenset)):
+ return sum([self.get_revisions(id_elem) for id_elem in id_], ())
+ else:
+ resolved_id, branch_label = self._resolve_revision_number(id_)
+ if len(resolved_id) == 1:
+ try:
+ rint = int(resolved_id[0])
+ if rint < 0:
+ # branch@-n -> walk down from heads
+ select_heads = self.get_revisions("heads")
+ if branch_label is not None:
+ select_heads = tuple(
+ head
+ for head in select_heads
+ if branch_label
+ in is_revision(head).branch_labels
+ )
+ return tuple(
+ self._walk(head, steps=rint)
+ for head in select_heads
+ )
+ except ValueError:
+ # couldn't resolve as integer
+ pass
+ return tuple(
+ self._revision_for_ident(rev_id, branch_label)
+ for rev_id in resolved_id
+ )
+
+ def get_revision(self, id_: Optional[str]) -> Optional[Revision]:
+ """Return the :class:`.Revision` instance with the given rev id.
+
+ If a symbolic name such as "head" or "base" is given, resolves
+ the identifier into the current head or base revision. If the symbolic
+ name refers to multiples, :class:`.MultipleHeads` is raised.
+
+ Supports partial identifiers, where the given identifier
+ is matched against all identifiers that start with the given
+ characters; if there is exactly one match, that determines the
+ full revision.
+
+ """
+
+ resolved_id, branch_label = self._resolve_revision_number(id_)
+ if len(resolved_id) > 1:
+ raise MultipleHeads(resolved_id, id_)
+
+ resolved: Union[str, Tuple[()]] = resolved_id[0] if resolved_id else ()
+ return self._revision_for_ident(resolved, branch_label)
+
+ def _resolve_branch(self, branch_label: str) -> Optional[Revision]:
+ try:
+ branch_rev = self._revision_map[branch_label]
+ except KeyError:
+ try:
+ nonbranch_rev = self._revision_for_ident(branch_label)
+ except ResolutionError as re:
+ raise ResolutionError(
+ "No such branch: '%s'" % branch_label, branch_label
+ ) from re
+
+ else:
+ return nonbranch_rev
+ else:
+ return branch_rev
+
+ def _revision_for_ident(
+ self,
+ resolved_id: Union[str, Tuple[()], None],
+ check_branch: Optional[str] = None,
+ ) -> Optional[Revision]:
+ branch_rev: Optional[Revision]
+ if check_branch:
+ branch_rev = self._resolve_branch(check_branch)
+ else:
+ branch_rev = None
+
+ revision: Union[Optional[Revision], Literal[False]]
+ try:
+ revision = self._revision_map[resolved_id]
+ except KeyError:
+ # break out to avoid misleading py3k stack traces
+ revision = False
+ revs: Sequence[str]
+ if revision is False:
+ assert resolved_id
+ # do a partial lookup
+ revs = [
+ x
+ for x in self._revision_map
+ if x and len(x) > 3 and x.startswith(resolved_id)
+ ]
+
+ if branch_rev:
+ revs = self.filter_for_lineage(revs, check_branch)
+ if not revs:
+ raise ResolutionError(
+ "No such revision or branch '%s'%s"
+ % (
+ resolved_id,
+ (
+ "; please ensure at least four characters are "
+ "present for partial revision identifier matches"
+ if len(resolved_id) < 4
+ else ""
+ ),
+ ),
+ resolved_id,
+ )
+ elif len(revs) > 1:
+ raise ResolutionError(
+ "Multiple revisions start "
+ "with '%s': %s..."
+ % (resolved_id, ", ".join("'%s'" % r for r in revs[0:3])),
+ resolved_id,
+ )
+ else:
+ revision = self._revision_map[revs[0]]
+
+ if check_branch and revision is not None:
+ assert branch_rev is not None
+ assert resolved_id
+ if not self._shares_lineage(
+ revision.revision, branch_rev.revision
+ ):
+ raise ResolutionError(
+ "Revision %s is not a member of branch '%s'"
+ % (revision.revision, check_branch),
+ resolved_id,
+ )
+ return revision
+
+ def _filter_into_branch_heads(
+ self, targets: Iterable[Optional[_RevisionOrBase]]
+ ) -> Set[Optional[_RevisionOrBase]]:
+ targets = set(targets)
+
+ for rev in list(targets):
+ assert rev
+ if targets.intersection(
+ self._get_descendant_nodes([rev], include_dependencies=False)
+ ).difference([rev]):
+ targets.discard(rev)
+ return targets
+
+ def filter_for_lineage(
+ self,
+ targets: Iterable[_TR],
+ check_against: Optional[str],
+ include_dependencies: bool = False,
+ ) -> Tuple[_TR, ...]:
+ id_, branch_label = self._resolve_revision_number(check_against)
+
+ shares = []
+ if branch_label:
+ shares.append(branch_label)
+ if id_:
+ shares.extend(id_)
+
+ return tuple(
+ tg
+ for tg in targets
+ if self._shares_lineage(
+ tg, shares, include_dependencies=include_dependencies
+ )
+ )
+
+ def _shares_lineage(
+ self,
+ target: Optional[_RevisionOrStr],
+ test_against_revs: Sequence[_RevisionOrStr],
+ include_dependencies: bool = False,
+ ) -> bool:
+ if not test_against_revs:
+ return True
+ if not isinstance(target, Revision):
+ resolved_target = not_none(self._revision_for_ident(target))
+ else:
+ resolved_target = target
+
+ resolved_test_against_revs = [
+ self._revision_for_ident(test_against_rev)
+ if not isinstance(test_against_rev, Revision)
+ else test_against_rev
+ for test_against_rev in util.to_tuple(
+ test_against_revs, default=()
+ )
+ ]
+
+ return bool(
+ set(
+ self._get_descendant_nodes(
+ [resolved_target],
+ include_dependencies=include_dependencies,
+ )
+ )
+ .union(
+ self._get_ancestor_nodes(
+ [resolved_target],
+ include_dependencies=include_dependencies,
+ )
+ )
+ .intersection(resolved_test_against_revs)
+ )
+
+ def _resolve_revision_number(
+ self, id_: Optional[_GetRevArg]
+ ) -> Tuple[Tuple[str, ...], Optional[str]]:
+ branch_label: Optional[str]
+ if isinstance(id_, str) and "@" in id_:
+ branch_label, id_ = id_.split("@", 1)
+
+ elif id_ is not None and (
+ (isinstance(id_, tuple) and id_ and not isinstance(id_[0], str))
+ or not isinstance(id_, (str, tuple))
+ ):
+ raise RevisionError(
+ "revision identifier %r is not a string; ensure database "
+ "driver settings are correct" % (id_,)
+ )
+
+ else:
+ branch_label = None
+
+ # ensure map is loaded
+ self._revision_map
+ if id_ == "heads":
+ if branch_label:
+ return (
+ self.filter_for_lineage(self.heads, branch_label),
+ branch_label,
+ )
+ else:
+ return self._real_heads, branch_label
+ elif id_ == "head":
+ current_head = self.get_current_head(branch_label)
+ if current_head:
+ return (current_head,), branch_label
+ else:
+ return (), branch_label
+ elif id_ == "base" or id_ is None:
+ return (), branch_label
+ else:
+ return util.to_tuple(id_, default=None), branch_label
+
+ def iterate_revisions(
+ self,
+ upper: _RevisionIdentifierType,
+ lower: _RevisionIdentifierType,
+ implicit_base: bool = False,
+ inclusive: bool = False,
+ assert_relative_length: bool = True,
+ select_for_downgrade: bool = False,
+ ) -> Iterator[Revision]:
+ """Iterate through script revisions, starting at the given
+ upper revision identifier and ending at the lower.
+
+ The traversal uses strictly the `down_revision`
+ marker inside each migration script, so
+ it is a requirement that upper >= lower,
+ else you'll get nothing back.
+
+ The iterator yields :class:`.Revision` objects.
+
+ """
+ fn: _CollectRevisionsProtocol
+ if select_for_downgrade:
+ fn = self._collect_downgrade_revisions
+ else:
+ fn = self._collect_upgrade_revisions
+
+ revisions, heads = fn(
+ upper,
+ lower,
+ inclusive=inclusive,
+ implicit_base=implicit_base,
+ assert_relative_length=assert_relative_length,
+ )
+
+ for node in self._topological_sort(revisions, heads):
+ yield not_none(self.get_revision(node))
+
+ def _get_descendant_nodes(
+ self,
+ targets: Collection[Optional[_RevisionOrBase]],
+ map_: Optional[_RevisionMapType] = None,
+ check: bool = False,
+ omit_immediate_dependencies: bool = False,
+ include_dependencies: bool = True,
+ ) -> Iterator[Any]:
+ if omit_immediate_dependencies:
+
+ def fn(rev: Revision) -> Iterable[str]:
+ if rev not in targets:
+ return rev._all_nextrev
+ else:
+ return rev.nextrev
+
+ elif include_dependencies:
+
+ def fn(rev: Revision) -> Iterable[str]:
+ return rev._all_nextrev
+
+ else:
+
+ def fn(rev: Revision) -> Iterable[str]:
+ return rev.nextrev
+
+ return self._iterate_related_revisions(
+ fn, targets, map_=map_, check=check
+ )
+
+ def _get_ancestor_nodes(
+ self,
+ targets: Collection[Optional[_RevisionOrBase]],
+ map_: Optional[_RevisionMapType] = None,
+ check: bool = False,
+ include_dependencies: bool = True,
+ ) -> Iterator[Revision]:
+ if include_dependencies:
+
+ def fn(rev: Revision) -> Iterable[str]:
+ return rev._normalized_down_revisions
+
+ else:
+
+ def fn(rev: Revision) -> Iterable[str]:
+ return rev._versioned_down_revisions
+
+ return self._iterate_related_revisions(
+ fn, targets, map_=map_, check=check
+ )
+
+ def _iterate_related_revisions(
+ self,
+ fn: Callable[[Revision], Iterable[str]],
+ targets: Collection[Optional[_RevisionOrBase]],
+ map_: Optional[_RevisionMapType],
+ check: bool = False,
+ ) -> Iterator[Revision]:
+ if map_ is None:
+ map_ = self._revision_map
+
+ seen = set()
+ todo: Deque[Revision] = collections.deque()
+ for target_for in targets:
+ target = is_revision(target_for)
+ todo.append(target)
+ if check:
+ per_target = set()
+
+ while todo:
+ rev = todo.pop()
+ if check:
+ per_target.add(rev)
+
+ if rev in seen:
+ continue
+ seen.add(rev)
+ # Check for map errors before collecting.
+ for rev_id in fn(rev):
+ next_rev = map_[rev_id]
+ assert next_rev is not None
+ if next_rev.revision != rev_id:
+ raise RevisionError(
+ "Dependency resolution failed; broken map"
+ )
+ todo.append(next_rev)
+ yield rev
+ if check:
+ overlaps = per_target.intersection(targets).difference(
+ [target]
+ )
+ if overlaps:
+ raise RevisionError(
+ "Requested revision %s overlaps with "
+ "other requested revisions %s"
+ % (
+ target.revision,
+ ", ".join(r.revision for r in overlaps),
+ )
+ )
+
+ def _topological_sort(
+ self,
+ revisions: Collection[Revision],
+ heads: Any,
+ ) -> List[str]:
+ """Yield revision ids of a collection of Revision objects in
+ topological sorted order (i.e. revisions always come after their
+ down_revisions and dependencies). Uses the order of keys in
+ _revision_map to sort.
+
+ """
+
+ id_to_rev = self._revision_map
+
+ def get_ancestors(rev_id: str) -> Set[str]:
+ return {
+ r.revision
+ for r in self._get_ancestor_nodes([id_to_rev[rev_id]])
+ }
+
+ todo = {d.revision for d in revisions}
+
+ # Use revision map (ordered dict) key order to pre-sort.
+ inserted_order = list(self._revision_map)
+
+ current_heads = list(
+ sorted(
+ {d.revision for d in heads if d.revision in todo},
+ key=inserted_order.index,
+ )
+ )
+ ancestors_by_idx = [get_ancestors(rev_id) for rev_id in current_heads]
+
+ output = []
+
+ current_candidate_idx = 0
+ while current_heads:
+ candidate = current_heads[current_candidate_idx]
+
+ for check_head_index, ancestors in enumerate(ancestors_by_idx):
+ # scan all the heads. see if we can continue walking
+ # down the current branch indicated by current_candidate_idx.
+ if (
+ check_head_index != current_candidate_idx
+ and candidate in ancestors
+ ):
+ current_candidate_idx = check_head_index
+ # nope, another head is dependent on us, they have
+ # to be traversed first
+ break
+ else:
+ # yup, we can emit
+ if candidate in todo:
+ output.append(candidate)
+ todo.remove(candidate)
+
+ # now update the heads with our ancestors.
+
+ candidate_rev = id_to_rev[candidate]
+ assert candidate_rev is not None
+
+ heads_to_add = [
+ r
+ for r in candidate_rev._normalized_down_revisions
+ if r in todo and r not in current_heads
+ ]
+
+ if not heads_to_add:
+ # no ancestors, so remove this head from the list
+ del current_heads[current_candidate_idx]
+ del ancestors_by_idx[current_candidate_idx]
+ current_candidate_idx = max(current_candidate_idx - 1, 0)
+ else:
+ if (
+ not candidate_rev._normalized_resolved_dependencies
+ and len(candidate_rev._versioned_down_revisions) == 1
+ ):
+ current_heads[current_candidate_idx] = heads_to_add[0]
+
+ # for plain movement down a revision line without
+ # any mergepoints, branchpoints, or deps, we
+ # can update the ancestors collection directly
+ # by popping out the candidate we just emitted
+ ancestors_by_idx[current_candidate_idx].discard(
+ candidate
+ )
+
+ else:
+ # otherwise recalculate it again, things get
+ # complicated otherwise. This can possibly be
+ # improved to not run the whole ancestor thing
+ # each time but it was getting complicated
+ current_heads[current_candidate_idx] = heads_to_add[0]
+ current_heads.extend(heads_to_add[1:])
+ ancestors_by_idx[
+ current_candidate_idx
+ ] = get_ancestors(heads_to_add[0])
+ ancestors_by_idx.extend(
+ get_ancestors(head) for head in heads_to_add[1:]
+ )
+
+ assert not todo
+ return output
+
+ def _walk(
+ self,
+ start: Optional[Union[str, Revision]],
+ steps: int,
+ branch_label: Optional[str] = None,
+ no_overwalk: bool = True,
+ ) -> Optional[_RevisionOrBase]:
+ """
+ Walk the requested number of :steps up (steps > 0) or down (steps < 0)
+ the revision tree.
+
+ :branch_label is used to select branches only when walking up.
+
+ If the walk goes past the boundaries of the tree and :no_overwalk is
+ True, None is returned, otherwise the walk terminates early.
+
+ A RevisionError is raised if there is no unambiguous revision to
+ walk to.
+ """
+ initial: Optional[_RevisionOrBase]
+ if isinstance(start, str):
+ initial = self.get_revision(start)
+ else:
+ initial = start
+
+ children: Sequence[Optional[_RevisionOrBase]]
+ for _ in range(abs(steps)):
+ if steps > 0:
+ assert initial != "base" # type: ignore[comparison-overlap]
+ # Walk up
+ walk_up = [
+ is_revision(rev)
+ for rev in self.get_revisions(
+ self.bases if initial is None else initial.nextrev
+ )
+ ]
+ if branch_label:
+ children = self.filter_for_lineage(walk_up, branch_label)
+ else:
+ children = walk_up
+ else:
+ # Walk down
+ if initial == "base": # type: ignore[comparison-overlap]
+ children = ()
+ else:
+ children = self.get_revisions(
+ self.heads
+ if initial is None
+ else initial.down_revision
+ )
+ if not children:
+ children = ("base",)
+ if not children:
+ # This will return an invalid result if no_overwalk, otherwise
+ # further steps will stay where we are.
+ ret = None if no_overwalk else initial
+ return ret
+ elif len(children) > 1:
+ raise RevisionError("Ambiguous walk")
+ initial = children[0]
+
+ return initial
+
+ def _parse_downgrade_target(
+ self,
+ current_revisions: _RevisionIdentifierType,
+ target: _RevisionIdentifierType,
+ assert_relative_length: bool,
+ ) -> Tuple[Optional[str], Optional[_RevisionOrBase]]:
+ """
+ Parse downgrade command syntax :target to retrieve the target revision
+ and branch label (if any) given the :current_revisions stamp of the
+ database.
+
+ Returns a tuple (branch_label, target_revision) where branch_label
+ is a string from the command specifying the branch to consider (or
+ None if no branch given), and target_revision is a Revision object
+ which the command refers to. target_revisions is None if the command
+ refers to 'base'. The target may be specified in absolute form, or
+ relative to :current_revisions.
+ """
+ if target is None:
+ return None, None
+ assert isinstance(
+ target, str
+ ), "Expected downgrade target in string form"
+ match = _relative_destination.match(target)
+ if match:
+ branch_label, symbol, relative = match.groups()
+ rel_int = int(relative)
+ if rel_int >= 0:
+ if symbol is None:
+ # Downgrading to current + n is not valid.
+ raise RevisionError(
+ "Relative revision %s didn't "
+ "produce %d migrations" % (relative, abs(rel_int))
+ )
+ # Find target revision relative to given symbol.
+ rev = self._walk(
+ symbol,
+ rel_int,
+ branch_label,
+ no_overwalk=assert_relative_length,
+ )
+ if rev is None:
+ raise RevisionError("Walked too far")
+ return branch_label, rev
+ else:
+ relative_revision = symbol is None
+ if relative_revision:
+ # Find target revision relative to current state.
+ if branch_label:
+ cr_tuple = util.to_tuple(current_revisions)
+ symbol_list: Sequence[str]
+ symbol_list = self.filter_for_lineage(
+ cr_tuple, branch_label
+ )
+ if not symbol_list:
+ # check the case where there are multiple branches
+ # but there is currently a single heads, since all
+ # other branch heads are dependent of the current
+ # single heads.
+ all_current = cast(
+ Set[Revision], self._get_all_current(cr_tuple)
+ )
+ sl_all_current = self.filter_for_lineage(
+ all_current, branch_label
+ )
+ symbol_list = [
+ r.revision if r else r # type: ignore[misc]
+ for r in sl_all_current
+ ]
+
+ assert len(symbol_list) == 1
+ symbol = symbol_list[0]
+ else:
+ current_revisions = util.to_tuple(current_revisions)
+ if not current_revisions:
+ raise RevisionError(
+ "Relative revision %s didn't "
+ "produce %d migrations"
+ % (relative, abs(rel_int))
+ )
+ # Have to check uniques here for duplicate rows test.
+ if len(set(current_revisions)) > 1:
+ util.warn(
+ "downgrade -1 from multiple heads is "
+ "ambiguous; "
+ "this usage will be disallowed in a future "
+ "release."
+ )
+ symbol = current_revisions[0]
+ # Restrict iteration to just the selected branch when
+ # ambiguous branches are involved.
+ branch_label = symbol
+ # Walk down the tree to find downgrade target.
+ rev = self._walk(
+ start=self.get_revision(symbol)
+ if branch_label is None
+ else self.get_revision("%s@%s" % (branch_label, symbol)),
+ steps=rel_int,
+ no_overwalk=assert_relative_length,
+ )
+ if rev is None:
+ if relative_revision:
+ raise RevisionError(
+ "Relative revision %s didn't "
+ "produce %d migrations" % (relative, abs(rel_int))
+ )
+ else:
+ raise RevisionError("Walked too far")
+ return branch_label, rev
+
+ # No relative destination given, revision specified is absolute.
+ branch_label, _, symbol = target.rpartition("@")
+ if not branch_label:
+ branch_label = None
+ return branch_label, self.get_revision(symbol)
+
+ def _parse_upgrade_target(
+ self,
+ current_revisions: _RevisionIdentifierType,
+ target: _RevisionIdentifierType,
+ assert_relative_length: bool,
+ ) -> Tuple[Optional[_RevisionOrBase], ...]:
+ """
+ Parse upgrade command syntax :target to retrieve the target revision
+ and given the :current_revisions stamp of the database.
+
+ Returns a tuple of Revision objects which should be iterated/upgraded
+ to. The target may be specified in absolute form, or relative to
+ :current_revisions.
+ """
+ if isinstance(target, str):
+ match = _relative_destination.match(target)
+ else:
+ match = None
+
+ if not match:
+ # No relative destination, target is absolute.
+ return self.get_revisions(target)
+
+ current_revisions_tup: Union[str, Tuple[Optional[str], ...], None]
+ current_revisions_tup = util.to_tuple(current_revisions)
+
+ branch_label, symbol, relative_str = match.groups()
+ relative = int(relative_str)
+ if relative > 0:
+ if symbol is None:
+ if not current_revisions_tup:
+ current_revisions_tup = (None,)
+ # Try to filter to a single target (avoid ambiguous branches).
+ start_revs = current_revisions_tup
+ if branch_label:
+ start_revs = self.filter_for_lineage(
+ self.get_revisions(current_revisions_tup), # type: ignore[arg-type] # noqa: E501
+ branch_label,
+ )
+ if not start_revs:
+ # The requested branch is not a head, so we need to
+ # backtrack to find a branchpoint.
+ active_on_branch = self.filter_for_lineage(
+ self._get_ancestor_nodes(
+ self.get_revisions(current_revisions_tup)
+ ),
+ branch_label,
+ )
+ # Find the tips of this set of revisions (revisions
+ # without children within the set).
+ start_revs = tuple(
+ {rev.revision for rev in active_on_branch}
+ - {
+ down
+ for rev in active_on_branch
+ for down in rev._normalized_down_revisions
+ }
+ )
+ if not start_revs:
+ # We must need to go right back to base to find
+ # a starting point for this branch.
+ start_revs = (None,)
+ if len(start_revs) > 1:
+ raise RevisionError(
+ "Ambiguous upgrade from multiple current revisions"
+ )
+ # Walk up from unique target revision.
+ rev = self._walk(
+ start=start_revs[0],
+ steps=relative,
+ branch_label=branch_label,
+ no_overwalk=assert_relative_length,
+ )
+ if rev is None:
+ raise RevisionError(
+ "Relative revision %s didn't "
+ "produce %d migrations" % (relative_str, abs(relative))
+ )
+ return (rev,)
+ else:
+ # Walk is relative to a given revision, not the current state.
+ return (
+ self._walk(
+ start=self.get_revision(symbol),
+ steps=relative,
+ branch_label=branch_label,
+ no_overwalk=assert_relative_length,
+ ),
+ )
+ else:
+ if symbol is None:
+ # Upgrading to current - n is not valid.
+ raise RevisionError(
+ "Relative revision %s didn't "
+ "produce %d migrations" % (relative, abs(relative))
+ )
+ return (
+ self._walk(
+ start=self.get_revision(symbol)
+ if branch_label is None
+ else self.get_revision("%s@%s" % (branch_label, symbol)),
+ steps=relative,
+ no_overwalk=assert_relative_length,
+ ),
+ )
+
+ def _collect_downgrade_revisions(
+ self,
+ upper: _RevisionIdentifierType,
+ lower: _RevisionIdentifierType,
+ inclusive: bool,
+ implicit_base: bool,
+ assert_relative_length: bool,
+ ) -> Tuple[Set[Revision], Tuple[Optional[_RevisionOrBase], ...]]:
+ """
+ Compute the set of current revisions specified by :upper, and the
+ downgrade target specified by :target. Return all dependents of target
+ which are currently active.
+
+ :inclusive=True includes the target revision in the set
+ """
+
+ branch_label, target_revision = self._parse_downgrade_target(
+ current_revisions=upper,
+ target=lower,
+ assert_relative_length=assert_relative_length,
+ )
+ if target_revision == "base":
+ target_revision = None
+ assert target_revision is None or isinstance(target_revision, Revision)
+
+ roots: List[Revision]
+ # Find candidates to drop.
+ if target_revision is None:
+ # Downgrading back to base: find all tree roots.
+ roots = [
+ rev
+ for rev in self._revision_map.values()
+ if rev is not None and rev.down_revision is None
+ ]
+ elif inclusive:
+ # inclusive implies target revision should also be dropped
+ roots = [target_revision]
+ else:
+ # Downgrading to fixed target: find all direct children.
+ roots = [
+ is_revision(rev)
+ for rev in self.get_revisions(target_revision.nextrev)
+ ]
+
+ if branch_label and len(roots) > 1:
+ # Need to filter roots.
+ ancestors = {
+ rev.revision
+ for rev in self._get_ancestor_nodes(
+ [self._resolve_branch(branch_label)],
+ include_dependencies=False,
+ )
+ }
+ # Intersection gives the root revisions we are trying to
+ # rollback with the downgrade.
+ roots = [
+ is_revision(rev)
+ for rev in self.get_revisions(
+ {rev.revision for rev in roots}.intersection(ancestors)
+ )
+ ]
+
+ # Ensure we didn't throw everything away when filtering branches.
+ if len(roots) == 0:
+ raise RevisionError(
+ "Not a valid downgrade target from current heads"
+ )
+
+ heads = self.get_revisions(upper)
+
+ # Aim is to drop :branch_revision; to do so we also need to drop its
+ # descendents and anything dependent on it.
+ downgrade_revisions = set(
+ self._get_descendant_nodes(
+ roots,
+ include_dependencies=True,
+ omit_immediate_dependencies=False,
+ )
+ )
+ active_revisions = set(
+ self._get_ancestor_nodes(heads, include_dependencies=True)
+ )
+
+ # Emit revisions to drop in reverse topological sorted order.
+ downgrade_revisions.intersection_update(active_revisions)
+
+ if implicit_base:
+ # Wind other branches back to base.
+ downgrade_revisions.update(
+ active_revisions.difference(self._get_ancestor_nodes(roots))
+ )
+
+ if (
+ target_revision is not None
+ and not downgrade_revisions
+ and target_revision not in heads
+ ):
+ # Empty intersection: target revs are not present.
+
+ raise RangeNotAncestorError("Nothing to drop", upper)
+
+ return downgrade_revisions, heads
+
+ def _collect_upgrade_revisions(
+ self,
+ upper: _RevisionIdentifierType,
+ lower: _RevisionIdentifierType,
+ inclusive: bool,
+ implicit_base: bool,
+ assert_relative_length: bool,
+ ) -> Tuple[Set[Revision], Tuple[Revision, ...]]:
+ """
+ Compute the set of required revisions specified by :upper, and the
+ current set of active revisions specified by :lower. Find the
+ difference between the two to compute the required upgrades.
+
+ :inclusive=True includes the current/lower revisions in the set
+
+ :implicit_base=False only returns revisions which are downstream
+ of the current/lower revisions. Dependencies from branches with
+ different bases will not be included.
+ """
+ targets: Collection[Revision] = [
+ is_revision(rev)
+ for rev in self._parse_upgrade_target(
+ current_revisions=lower,
+ target=upper,
+ assert_relative_length=assert_relative_length,
+ )
+ ]
+
+ # assert type(targets) is tuple, "targets should be a tuple"
+
+ # Handled named bases (e.g. branch@... -> heads should only produce
+ # targets on the given branch)
+ if isinstance(lower, str) and "@" in lower:
+ branch, _, _ = lower.partition("@")
+ branch_rev = self.get_revision(branch)
+ if branch_rev is not None and branch_rev.revision == branch:
+ # A revision was used as a label; get its branch instead
+ assert len(branch_rev.branch_labels) == 1
+ branch = next(iter(branch_rev.branch_labels))
+ targets = {
+ need for need in targets if branch in need.branch_labels
+ }
+
+ required_node_set = set(
+ self._get_ancestor_nodes(
+ targets, check=True, include_dependencies=True
+ )
+ ).union(targets)
+
+ current_revisions = self.get_revisions(lower)
+ if not implicit_base and any(
+ rev not in required_node_set
+ for rev in current_revisions
+ if rev is not None
+ ):
+ raise RangeNotAncestorError(lower, upper)
+ assert (
+ type(current_revisions) is tuple
+ ), "current_revisions should be a tuple"
+
+ # Special case where lower = a relative value (get_revisions can't
+ # find it)
+ if current_revisions and current_revisions[0] is None:
+ _, rev = self._parse_downgrade_target(
+ current_revisions=upper,
+ target=lower,
+ assert_relative_length=assert_relative_length,
+ )
+ assert rev
+ if rev == "base":
+ current_revisions = tuple()
+ lower = None
+ else:
+ current_revisions = (rev,)
+ lower = rev.revision
+
+ current_node_set = set(
+ self._get_ancestor_nodes(
+ current_revisions, check=True, include_dependencies=True
+ )
+ ).union(current_revisions)
+
+ needs = required_node_set.difference(current_node_set)
+
+ # Include the lower revision (=current_revisions?) in the iteration
+ if inclusive:
+ needs.update(is_revision(rev) for rev in self.get_revisions(lower))
+ # By default, base is implicit as we want all dependencies returned.
+ # Base is also implicit if lower = base
+ # implicit_base=False -> only return direct downstreams of
+ # current_revisions
+ if current_revisions and not implicit_base:
+ lower_descendents = self._get_descendant_nodes(
+ [is_revision(rev) for rev in current_revisions],
+ check=True,
+ include_dependencies=False,
+ )
+ needs.intersection_update(lower_descendents)
+
+ return needs, tuple(targets)
+
+ def _get_all_current(
+ self, id_: Tuple[str, ...]
+ ) -> Set[Optional[_RevisionOrBase]]:
+ top_revs: Set[Optional[_RevisionOrBase]]
+ top_revs = set(self.get_revisions(id_))
+ top_revs.update(
+ self._get_ancestor_nodes(list(top_revs), include_dependencies=True)
+ )
+ return self._filter_into_branch_heads(top_revs)
+
+
+class Revision:
+ """Base class for revisioned objects.
+
+ The :class:`.Revision` class is the base of the more public-facing
+ :class:`.Script` object, which represents a migration script.
+ The mechanics of revision management and traversal are encapsulated
+ within :class:`.Revision`, while :class:`.Script` applies this logic
+ to Python files in a version directory.
+
+ """
+
+ nextrev: FrozenSet[str] = frozenset()
+ """following revisions, based on down_revision only."""
+
+ _all_nextrev: FrozenSet[str] = frozenset()
+
+ revision: str = None # type: ignore[assignment]
+ """The string revision number."""
+
+ down_revision: Optional[_RevIdType] = None
+ """The ``down_revision`` identifier(s) within the migration script.
+
+ Note that the total set of "down" revisions is
+ down_revision + dependencies.
+
+ """
+
+ dependencies: Optional[_RevIdType] = None
+ """Additional revisions which this revision is dependent on.
+
+ From a migration standpoint, these dependencies are added to the
+ down_revision to form the full iteration. However, the separation
+ of down_revision from "dependencies" is to assist in navigating
+ a history that contains many branches, typically a multi-root scenario.
+
+ """
+
+ branch_labels: Set[str] = None # type: ignore[assignment]
+ """Optional string/tuple of symbolic names to apply to this
+ revision's branch"""
+
+ _resolved_dependencies: Tuple[str, ...]
+ _normalized_resolved_dependencies: Tuple[str, ...]
+
+ @classmethod
+ def verify_rev_id(cls, revision: str) -> None:
+ illegal_chars = set(revision).intersection(_revision_illegal_chars)
+ if illegal_chars:
+ raise RevisionError(
+ "Character(s) '%s' not allowed in revision identifier '%s'"
+ % (", ".join(sorted(illegal_chars)), revision)
+ )
+
+ def __init__(
+ self,
+ revision: str,
+ down_revision: Optional[Union[str, Tuple[str, ...]]],
+ dependencies: Optional[Union[str, Tuple[str, ...]]] = None,
+ branch_labels: Optional[Union[str, Tuple[str, ...]]] = None,
+ ) -> None:
+ if down_revision and revision in util.to_tuple(down_revision):
+ raise LoopDetected(revision)
+ elif dependencies is not None and revision in util.to_tuple(
+ dependencies
+ ):
+ raise DependencyLoopDetected(revision)
+
+ self.verify_rev_id(revision)
+ self.revision = revision
+ self.down_revision = tuple_rev_as_scalar(util.to_tuple(down_revision))
+ self.dependencies = tuple_rev_as_scalar(util.to_tuple(dependencies))
+ self._orig_branch_labels = util.to_tuple(branch_labels, default=())
+ self.branch_labels = set(self._orig_branch_labels)
+
+ def __repr__(self) -> str:
+ args = [repr(self.revision), repr(self.down_revision)]
+ if self.dependencies:
+ args.append("dependencies=%r" % (self.dependencies,))
+ if self.branch_labels:
+ args.append("branch_labels=%r" % (self.branch_labels,))
+ return "%s(%s)" % (self.__class__.__name__, ", ".join(args))
+
+ def add_nextrev(self, revision: Revision) -> None:
+ self._all_nextrev = self._all_nextrev.union([revision.revision])
+ if self.revision in revision._versioned_down_revisions:
+ self.nextrev = self.nextrev.union([revision.revision])
+
+ @property
+ def _all_down_revisions(self) -> Tuple[str, ...]:
+ return util.dedupe_tuple(
+ util.to_tuple(self.down_revision, default=())
+ + self._resolved_dependencies
+ )
+
+ @property
+ def _normalized_down_revisions(self) -> Tuple[str, ...]:
+ """return immediate down revisions for a rev, omitting dependencies
+ that are still dependencies of ancestors.
+
+ """
+ return util.dedupe_tuple(
+ util.to_tuple(self.down_revision, default=())
+ + self._normalized_resolved_dependencies
+ )
+
+ @property
+ def _versioned_down_revisions(self) -> Tuple[str, ...]:
+ return util.to_tuple(self.down_revision, default=())
+
+ @property
+ def is_head(self) -> bool:
+ """Return True if this :class:`.Revision` is a 'head' revision.
+
+ This is determined based on whether any other :class:`.Script`
+ within the :class:`.ScriptDirectory` refers to this
+ :class:`.Script`. Multiple heads can be present.
+
+ """
+ return not bool(self.nextrev)
+
+ @property
+ def _is_real_head(self) -> bool:
+ return not bool(self._all_nextrev)
+
+ @property
+ def is_base(self) -> bool:
+ """Return True if this :class:`.Revision` is a 'base' revision."""
+
+ return self.down_revision is None
+
+ @property
+ def _is_real_base(self) -> bool:
+ """Return True if this :class:`.Revision` is a "real" base revision,
+ e.g. that it has no dependencies either."""
+
+ # we use self.dependencies here because this is called up
+ # in initialization where _real_dependencies isn't set up
+ # yet
+ return self.down_revision is None and self.dependencies is None
+
+ @property
+ def is_branch_point(self) -> bool:
+ """Return True if this :class:`.Script` is a branch point.
+
+ A branchpoint is defined as a :class:`.Script` which is referred
+ to by more than one succeeding :class:`.Script`, that is more
+ than one :class:`.Script` has a `down_revision` identifier pointing
+ here.
+
+ """
+ return len(self.nextrev) > 1
+
+ @property
+ def _is_real_branch_point(self) -> bool:
+ """Return True if this :class:`.Script` is a 'real' branch point,
+ taking into account dependencies as well.
+
+ """
+ return len(self._all_nextrev) > 1
+
+ @property
+ def is_merge_point(self) -> bool:
+ """Return True if this :class:`.Script` is a merge point."""
+
+ return len(self._versioned_down_revisions) > 1
+
+
+@overload
+def tuple_rev_as_scalar(rev: None) -> None:
+ ...
+
+
+@overload
+def tuple_rev_as_scalar(
+ rev: Union[Tuple[_T, ...], List[_T]]
+) -> Union[_T, Tuple[_T, ...], List[_T]]:
+ ...
+
+
+def tuple_rev_as_scalar(
+ rev: Optional[Sequence[_T]],
+) -> Union[_T, Sequence[_T], None]:
+ if not rev:
+ return None
+ elif len(rev) == 1:
+ return rev[0]
+ else:
+ return rev
+
+
+def is_revision(rev: Any) -> Revision:
+ assert isinstance(rev, Revision)
+ return rev
diff --git a/venv/lib/python3.12/site-packages/alembic/script/write_hooks.py b/venv/lib/python3.12/site-packages/alembic/script/write_hooks.py
new file mode 100644
index 0000000..9977147
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/script/write_hooks.py
@@ -0,0 +1,179 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import shlex
+import subprocess
+import sys
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Mapping
+from typing import Optional
+from typing import Union
+
+from .. import util
+from ..util import compat
+
+
+REVISION_SCRIPT_TOKEN = "REVISION_SCRIPT_FILENAME"
+
+_registry: dict = {}
+
+
+def register(name: str) -> Callable:
+ """A function decorator that will register that function as a write hook.
+
+ See the documentation linked below for an example.
+
+ .. seealso::
+
+ :ref:`post_write_hooks_custom`
+
+
+ """
+
+ def decorate(fn):
+ _registry[name] = fn
+ return fn
+
+ return decorate
+
+
+def _invoke(
+ name: str, revision: str, options: Mapping[str, Union[str, int]]
+) -> Any:
+ """Invokes the formatter registered for the given name.
+
+ :param name: The name of a formatter in the registry
+ :param revision: A :class:`.MigrationRevision` instance
+ :param options: A dict containing kwargs passed to the
+ specified formatter.
+ :raises: :class:`alembic.util.CommandError`
+ """
+ try:
+ hook = _registry[name]
+ except KeyError as ke:
+ raise util.CommandError(
+ f"No formatter with name '{name}' registered"
+ ) from ke
+ else:
+ return hook(revision, options)
+
+
+def _run_hooks(path: str, hook_config: Mapping[str, str]) -> None:
+ """Invoke hooks for a generated revision."""
+
+ from .base import _split_on_space_comma
+
+ names = _split_on_space_comma.split(hook_config.get("hooks", ""))
+
+ for name in names:
+ if not name:
+ continue
+ opts = {
+ key[len(name) + 1 :]: hook_config[key]
+ for key in hook_config
+ if key.startswith(name + ".")
+ }
+ opts["_hook_name"] = name
+ try:
+ type_ = opts["type"]
+ except KeyError as ke:
+ raise util.CommandError(
+ f"Key {name}.type is required for post write hook {name!r}"
+ ) from ke
+ else:
+ with util.status(
+ f"Running post write hook {name!r}", newline=True
+ ):
+ _invoke(type_, path, opts)
+
+
+def _parse_cmdline_options(cmdline_options_str: str, path: str) -> List[str]:
+ """Parse options from a string into a list.
+
+ Also substitutes the revision script token with the actual filename of
+ the revision script.
+
+ If the revision script token doesn't occur in the options string, it is
+ automatically prepended.
+ """
+ if REVISION_SCRIPT_TOKEN not in cmdline_options_str:
+ cmdline_options_str = REVISION_SCRIPT_TOKEN + " " + cmdline_options_str
+ cmdline_options_list = shlex.split(
+ cmdline_options_str, posix=compat.is_posix
+ )
+ cmdline_options_list = [
+ option.replace(REVISION_SCRIPT_TOKEN, path)
+ for option in cmdline_options_list
+ ]
+ return cmdline_options_list
+
+
+@register("console_scripts")
+def console_scripts(
+ path: str, options: dict, ignore_output: bool = False
+) -> None:
+ try:
+ entrypoint_name = options["entrypoint"]
+ except KeyError as ke:
+ raise util.CommandError(
+ f"Key {options['_hook_name']}.entrypoint is required for post "
+ f"write hook {options['_hook_name']!r}"
+ ) from ke
+ for entry in compat.importlib_metadata_get("console_scripts"):
+ if entry.name == entrypoint_name:
+ impl: Any = entry
+ break
+ else:
+ raise util.CommandError(
+ f"Could not find entrypoint console_scripts.{entrypoint_name}"
+ )
+ cwd: Optional[str] = options.get("cwd", None)
+ cmdline_options_str = options.get("options", "")
+ cmdline_options_list = _parse_cmdline_options(cmdline_options_str, path)
+
+ kw: Dict[str, Any] = {}
+ if ignore_output:
+ kw["stdout"] = kw["stderr"] = subprocess.DEVNULL
+
+ subprocess.run(
+ [
+ sys.executable,
+ "-c",
+ f"import {impl.module}; {impl.module}.{impl.attr}()",
+ ]
+ + cmdline_options_list,
+ cwd=cwd,
+ **kw,
+ )
+
+
+@register("exec")
+def exec_(path: str, options: dict, ignore_output: bool = False) -> None:
+ try:
+ executable = options["executable"]
+ except KeyError as ke:
+ raise util.CommandError(
+ f"Key {options['_hook_name']}.executable is required for post "
+ f"write hook {options['_hook_name']!r}"
+ ) from ke
+ cwd: Optional[str] = options.get("cwd", None)
+ cmdline_options_str = options.get("options", "")
+ cmdline_options_list = _parse_cmdline_options(cmdline_options_str, path)
+
+ kw: Dict[str, Any] = {}
+ if ignore_output:
+ kw["stdout"] = kw["stderr"] = subprocess.DEVNULL
+
+ subprocess.run(
+ [
+ executable,
+ *cmdline_options_list,
+ ],
+ cwd=cwd,
+ **kw,
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/async/README b/venv/lib/python3.12/site-packages/alembic/templates/async/README
new file mode 100644
index 0000000..e0d0858
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/async/README
@@ -0,0 +1 @@
+Generic single-database configuration with an async dbapi.
\ No newline at end of file
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/async/__pycache__/env.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/templates/async/__pycache__/env.cpython-312.pyc
new file mode 100644
index 0000000..3572a00
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/templates/async/__pycache__/env.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/async/alembic.ini.mako b/venv/lib/python3.12/site-packages/alembic/templates/async/alembic.ini.mako
new file mode 100644
index 0000000..0e5f43f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/async/alembic.ini.mako
@@ -0,0 +1,114 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = ${script_location}
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to ${script_location}/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = %(here)s/.venv/bin/ruff
+# ruff.options = --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/async/env.py b/venv/lib/python3.12/site-packages/alembic/templates/async/env.py
new file mode 100644
index 0000000..9f2d519
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/async/env.py
@@ -0,0 +1,89 @@
+import asyncio
+from logging.config import fileConfig
+
+from sqlalchemy import pool
+from sqlalchemy.engine import Connection
+from sqlalchemy.ext.asyncio import async_engine_from_config
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def do_run_migrations(connection: Connection) -> None:
+ context.configure(connection=connection, target_metadata=target_metadata)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+async def run_async_migrations() -> None:
+ """In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ connectable = async_engine_from_config(
+ config.get_section(config.config_ini_section, {}),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ async with connectable.connect() as connection:
+ await connection.run_sync(do_run_migrations)
+
+ await connectable.dispose()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode."""
+
+ asyncio.run(run_async_migrations())
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/async/script.py.mako b/venv/lib/python3.12/site-packages/alembic/templates/async/script.py.mako
new file mode 100644
index 0000000..fbc4b07
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/async/script.py.mako
@@ -0,0 +1,26 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/generic/README b/venv/lib/python3.12/site-packages/alembic/templates/generic/README
new file mode 100644
index 0000000..98e4f9c
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/generic/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/generic/__pycache__/env.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/templates/generic/__pycache__/env.cpython-312.pyc
new file mode 100644
index 0000000..b803396
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/templates/generic/__pycache__/env.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/generic/alembic.ini.mako b/venv/lib/python3.12/site-packages/alembic/templates/generic/alembic.ini.mako
new file mode 100644
index 0000000..29245dd
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/generic/alembic.ini.mako
@@ -0,0 +1,116 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = ${script_location}
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to ${script_location}/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = %(here)s/.venv/bin/ruff
+# ruff.options = --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/generic/env.py b/venv/lib/python3.12/site-packages/alembic/templates/generic/env.py
new file mode 100644
index 0000000..36112a3
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/generic/env.py
@@ -0,0 +1,78 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section, {}),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection, target_metadata=target_metadata
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/generic/script.py.mako b/venv/lib/python3.12/site-packages/alembic/templates/generic/script.py.mako
new file mode 100644
index 0000000..fbc4b07
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/generic/script.py.mako
@@ -0,0 +1,26 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+ ${downgrades if downgrades else "pass"}
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/multidb/README b/venv/lib/python3.12/site-packages/alembic/templates/multidb/README
new file mode 100644
index 0000000..f046ec9
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/multidb/README
@@ -0,0 +1,12 @@
+Rudimentary multi-database configuration.
+
+Multi-DB isn't vastly different from generic. The primary difference is that it
+will run the migrations N times (depending on how many databases you have
+configured), providing one engine name and associated context for each run.
+
+That engine name will then allow the migration to restrict what runs within it to
+just the appropriate migrations for that engine. You can see this behavior within
+the mako template.
+
+In the provided configuration, you'll need to have `databases` provided in
+alembic's config, and an `sqlalchemy.url` provided for each engine name.
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/multidb/__pycache__/env.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/templates/multidb/__pycache__/env.cpython-312.pyc
new file mode 100644
index 0000000..c9852ca
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/templates/multidb/__pycache__/env.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/multidb/alembic.ini.mako b/venv/lib/python3.12/site-packages/alembic/templates/multidb/alembic.ini.mako
new file mode 100644
index 0000000..c7fbe48
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/multidb/alembic.ini.mako
@@ -0,0 +1,121 @@
+# a multi-database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = ${script_location}
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
+# for all available tokens
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to ${script_location}/versions. When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+databases = engine1, engine2
+
+[engine1]
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+[engine2]
+sqlalchemy.url = driver://user:pass@localhost/dbname2
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = %(here)s/.venv/bin/ruff
+# ruff.options = --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/multidb/env.py b/venv/lib/python3.12/site-packages/alembic/templates/multidb/env.py
new file mode 100644
index 0000000..e937b64
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/multidb/env.py
@@ -0,0 +1,140 @@
+import logging
+from logging.config import fileConfig
+import re
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+USE_TWOPHASE = False
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+ fileConfig(config.config_file_name)
+logger = logging.getLogger("alembic.env")
+
+# gather section names referring to different
+# databases. These are named "engine1", "engine2"
+# in the sample .ini file.
+db_names = config.get_main_option("databases", "")
+
+# add your model's MetaData objects here
+# for 'autogenerate' support. These must be set
+# up to hold just those tables targeting a
+# particular database. table.tometadata() may be
+# helpful here in case a "copy" of
+# a MetaData is needed.
+# from myapp import mymodel
+# target_metadata = {
+# 'engine1':mymodel.metadata1,
+# 'engine2':mymodel.metadata2
+# }
+target_metadata = {}
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline() -> None:
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ # for the --sql use case, run migrations for each URL into
+ # individual files.
+
+ engines = {}
+ for name in re.split(r",\s*", db_names):
+ engines[name] = rec = {}
+ rec["url"] = context.config.get_section_option(name, "sqlalchemy.url")
+
+ for name, rec in engines.items():
+ logger.info("Migrating database %s" % name)
+ file_ = "%s.sql" % name
+ logger.info("Writing output to %s" % file_)
+ with open(file_, "w") as buffer:
+ context.configure(
+ url=rec["url"],
+ output_buffer=buffer,
+ target_metadata=target_metadata.get(name),
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+ with context.begin_transaction():
+ context.run_migrations(engine_name=name)
+
+
+def run_migrations_online() -> None:
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # for the direct-to-DB use case, start a transaction on all
+ # engines, then run all migrations, then commit all transactions.
+
+ engines = {}
+ for name in re.split(r",\s*", db_names):
+ engines[name] = rec = {}
+ rec["engine"] = engine_from_config(
+ context.config.get_section(name, {}),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ for name, rec in engines.items():
+ engine = rec["engine"]
+ rec["connection"] = conn = engine.connect()
+
+ if USE_TWOPHASE:
+ rec["transaction"] = conn.begin_twophase()
+ else:
+ rec["transaction"] = conn.begin()
+
+ try:
+ for name, rec in engines.items():
+ logger.info("Migrating database %s" % name)
+ context.configure(
+ connection=rec["connection"],
+ upgrade_token="%s_upgrades" % name,
+ downgrade_token="%s_downgrades" % name,
+ target_metadata=target_metadata.get(name),
+ )
+ context.run_migrations(engine_name=name)
+
+ if USE_TWOPHASE:
+ for rec in engines.values():
+ rec["transaction"].prepare()
+
+ for rec in engines.values():
+ rec["transaction"].commit()
+ except:
+ for rec in engines.values():
+ rec["transaction"].rollback()
+ raise
+ finally:
+ for rec in engines.values():
+ rec["connection"].close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/venv/lib/python3.12/site-packages/alembic/templates/multidb/script.py.mako b/venv/lib/python3.12/site-packages/alembic/templates/multidb/script.py.mako
new file mode 100644
index 0000000..6108b8a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/templates/multidb/script.py.mako
@@ -0,0 +1,47 @@
+<%!
+import re
+
+%>"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade(engine_name: str) -> None:
+ globals()["upgrade_%s" % engine_name]()
+
+
+def downgrade(engine_name: str) -> None:
+ globals()["downgrade_%s" % engine_name]()
+
+<%
+ db_names = config.get_main_option("databases")
+%>
+
+## generate an "upgrade_() / downgrade_()" function
+## for each database name in the ini file.
+
+% for db_name in re.split(r',\s*', db_names):
+
+def upgrade_${db_name}() -> None:
+ ${context.get("%s_upgrades" % db_name, "pass")}
+
+
+def downgrade_${db_name}() -> None:
+ ${context.get("%s_downgrades" % db_name, "pass")}
+
+% endfor
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__init__.py b/venv/lib/python3.12/site-packages/alembic/testing/__init__.py
new file mode 100644
index 0000000..0407adf
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/__init__.py
@@ -0,0 +1,29 @@
+from sqlalchemy.testing import config
+from sqlalchemy.testing import emits_warning
+from sqlalchemy.testing import engines
+from sqlalchemy.testing import exclusions
+from sqlalchemy.testing import mock
+from sqlalchemy.testing import provide_metadata
+from sqlalchemy.testing import skip_if
+from sqlalchemy.testing import uses_deprecated
+from sqlalchemy.testing.config import combinations
+from sqlalchemy.testing.config import fixture
+from sqlalchemy.testing.config import requirements as requires
+
+from .assertions import assert_raises
+from .assertions import assert_raises_message
+from .assertions import emits_python_deprecation_warning
+from .assertions import eq_
+from .assertions import eq_ignore_whitespace
+from .assertions import expect_raises
+from .assertions import expect_raises_message
+from .assertions import expect_sqlalchemy_deprecated
+from .assertions import expect_sqlalchemy_deprecated_20
+from .assertions import expect_warnings
+from .assertions import is_
+from .assertions import is_false
+from .assertions import is_not_
+from .assertions import is_true
+from .assertions import ne_
+from .fixtures import TestBase
+from .util import resolve_lambda
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..6ce60d3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/assertions.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/assertions.cpython-312.pyc
new file mode 100644
index 0000000..a5707c1
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/assertions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/env.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/env.cpython-312.pyc
new file mode 100644
index 0000000..645883a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/env.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/fixtures.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/fixtures.cpython-312.pyc
new file mode 100644
index 0000000..019c264
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/fixtures.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/requirements.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/requirements.cpython-312.pyc
new file mode 100644
index 0000000..ab6701d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/requirements.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/schemacompare.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/schemacompare.cpython-312.pyc
new file mode 100644
index 0000000..e7b7f07
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/schemacompare.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/util.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/util.cpython-312.pyc
new file mode 100644
index 0000000..cd9baee
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/util.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/warnings.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/warnings.cpython-312.pyc
new file mode 100644
index 0000000..2f36c4b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/__pycache__/warnings.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/assertions.py b/venv/lib/python3.12/site-packages/alembic/testing/assertions.py
new file mode 100644
index 0000000..ec9593b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/assertions.py
@@ -0,0 +1,167 @@
+from __future__ import annotations
+
+import contextlib
+import re
+import sys
+from typing import Any
+from typing import Dict
+
+from sqlalchemy import exc as sa_exc
+from sqlalchemy.engine import default
+from sqlalchemy.testing.assertions import _expect_warnings
+from sqlalchemy.testing.assertions import eq_ # noqa
+from sqlalchemy.testing.assertions import is_ # noqa
+from sqlalchemy.testing.assertions import is_false # noqa
+from sqlalchemy.testing.assertions import is_not_ # noqa
+from sqlalchemy.testing.assertions import is_true # noqa
+from sqlalchemy.testing.assertions import ne_ # noqa
+from sqlalchemy.util import decorator
+
+from ..util import sqla_compat
+
+
+def _assert_proper_exception_context(exception):
+ """assert that any exception we're catching does not have a __context__
+ without a __cause__, and that __suppress_context__ is never set.
+
+ Python 3 will report nested as exceptions as "during the handling of
+ error X, error Y occurred". That's not what we want to do. we want
+ these exceptions in a cause chain.
+
+ """
+
+ if (
+ exception.__context__ is not exception.__cause__
+ and not exception.__suppress_context__
+ ):
+ assert False, (
+ "Exception %r was correctly raised but did not set a cause, "
+ "within context %r as its cause."
+ % (exception, exception.__context__)
+ )
+
+
+def assert_raises(except_cls, callable_, *args, **kw):
+ return _assert_raises(except_cls, callable_, args, kw, check_context=True)
+
+
+def assert_raises_context_ok(except_cls, callable_, *args, **kw):
+ return _assert_raises(except_cls, callable_, args, kw)
+
+
+def assert_raises_message(except_cls, msg, callable_, *args, **kwargs):
+ return _assert_raises(
+ except_cls, callable_, args, kwargs, msg=msg, check_context=True
+ )
+
+
+def assert_raises_message_context_ok(
+ except_cls, msg, callable_, *args, **kwargs
+):
+ return _assert_raises(except_cls, callable_, args, kwargs, msg=msg)
+
+
+def _assert_raises(
+ except_cls, callable_, args, kwargs, msg=None, check_context=False
+):
+ with _expect_raises(except_cls, msg, check_context) as ec:
+ callable_(*args, **kwargs)
+ return ec.error
+
+
+class _ErrorContainer:
+ error: Any = None
+
+
+@contextlib.contextmanager
+def _expect_raises(except_cls, msg=None, check_context=False):
+ ec = _ErrorContainer()
+ if check_context:
+ are_we_already_in_a_traceback = sys.exc_info()[0]
+ try:
+ yield ec
+ success = False
+ except except_cls as err:
+ ec.error = err
+ success = True
+ if msg is not None:
+ assert re.search(msg, str(err), re.UNICODE), f"{msg} !~ {err}"
+ if check_context and not are_we_already_in_a_traceback:
+ _assert_proper_exception_context(err)
+ print(str(err).encode("utf-8"))
+
+ # assert outside the block so it works for AssertionError too !
+ assert success, "Callable did not raise an exception"
+
+
+def expect_raises(except_cls, check_context=True):
+ return _expect_raises(except_cls, check_context=check_context)
+
+
+def expect_raises_message(except_cls, msg, check_context=True):
+ return _expect_raises(except_cls, msg=msg, check_context=check_context)
+
+
+def eq_ignore_whitespace(a, b, msg=None):
+ a = re.sub(r"^\s+?|\n", "", a)
+ a = re.sub(r" {2,}", " ", a)
+ b = re.sub(r"^\s+?|\n", "", b)
+ b = re.sub(r" {2,}", " ", b)
+
+ assert a == b, msg or "%r != %r" % (a, b)
+
+
+_dialect_mods: Dict[Any, Any] = {}
+
+
+def _get_dialect(name):
+ if name is None or name == "default":
+ return default.DefaultDialect()
+ else:
+ d = sqla_compat._create_url(name).get_dialect()()
+
+ if name == "postgresql":
+ d.implicit_returning = True
+ elif name == "mssql":
+ d.legacy_schema_aliasing = False
+ return d
+
+
+def expect_warnings(*messages, **kw):
+ """Context manager which expects one or more warnings.
+
+ With no arguments, squelches all SAWarnings emitted via
+ sqlalchemy.util.warn and sqlalchemy.util.warn_limited. Otherwise
+ pass string expressions that will match selected warnings via regex;
+ all non-matching warnings are sent through.
+
+ The expect version **asserts** that the warnings were in fact seen.
+
+ Note that the test suite sets SAWarning warnings to raise exceptions.
+
+ """
+ return _expect_warnings(Warning, messages, **kw)
+
+
+def emits_python_deprecation_warning(*messages):
+ """Decorator form of expect_warnings().
+
+ Note that emits_warning does **not** assert that the warnings
+ were in fact seen.
+
+ """
+
+ @decorator
+ def decorate(fn, *args, **kw):
+ with _expect_warnings(DeprecationWarning, assert_=False, *messages):
+ return fn(*args, **kw)
+
+ return decorate
+
+
+def expect_sqlalchemy_deprecated(*messages, **kw):
+ return _expect_warnings(sa_exc.SADeprecationWarning, messages, **kw)
+
+
+def expect_sqlalchemy_deprecated_20(*messages, **kw):
+ return _expect_warnings(sa_exc.RemovedIn20Warning, messages, **kw)
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/env.py b/venv/lib/python3.12/site-packages/alembic/testing/env.py
new file mode 100644
index 0000000..5df7ef8
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/env.py
@@ -0,0 +1,518 @@
+import importlib.machinery
+import os
+import shutil
+import textwrap
+
+from sqlalchemy.testing import config
+from sqlalchemy.testing import provision
+
+from . import util as testing_util
+from .. import command
+from .. import script
+from .. import util
+from ..script import Script
+from ..script import ScriptDirectory
+
+
+def _get_staging_directory():
+ if provision.FOLLOWER_IDENT:
+ return "scratch_%s" % provision.FOLLOWER_IDENT
+ else:
+ return "scratch"
+
+
+def staging_env(create=True, template="generic", sourceless=False):
+ cfg = _testing_config()
+ if create:
+ path = os.path.join(_get_staging_directory(), "scripts")
+ assert not os.path.exists(path), (
+ "staging directory %s already exists; poor cleanup?" % path
+ )
+
+ command.init(cfg, path, template=template)
+ if sourceless:
+ try:
+ # do an import so that a .pyc/.pyo is generated.
+ util.load_python_file(path, "env.py")
+ except AttributeError:
+ # we don't have the migration context set up yet
+ # so running the .env py throws this exception.
+ # theoretically we could be using py_compiler here to
+ # generate .pyc/.pyo without importing but not really
+ # worth it.
+ pass
+ assert sourceless in (
+ "pep3147_envonly",
+ "simple",
+ "pep3147_everything",
+ ), sourceless
+ make_sourceless(
+ os.path.join(path, "env.py"),
+ "pep3147" if "pep3147" in sourceless else "simple",
+ )
+
+ sc = script.ScriptDirectory.from_config(cfg)
+ return sc
+
+
+def clear_staging_env():
+ from sqlalchemy.testing import engines
+
+ engines.testing_reaper.close_all()
+ shutil.rmtree(_get_staging_directory(), True)
+
+
+def script_file_fixture(txt):
+ dir_ = os.path.join(_get_staging_directory(), "scripts")
+ path = os.path.join(dir_, "script.py.mako")
+ with open(path, "w") as f:
+ f.write(txt)
+
+
+def env_file_fixture(txt):
+ dir_ = os.path.join(_get_staging_directory(), "scripts")
+ txt = (
+ """
+from alembic import context
+
+config = context.config
+"""
+ + txt
+ )
+
+ path = os.path.join(dir_, "env.py")
+ pyc_path = util.pyc_file_from_path(path)
+ if pyc_path:
+ os.unlink(pyc_path)
+
+ with open(path, "w") as f:
+ f.write(txt)
+
+
+def _sqlite_file_db(tempname="foo.db", future=False, scope=None, **options):
+ dir_ = os.path.join(_get_staging_directory(), "scripts")
+ url = "sqlite:///%s/%s" % (dir_, tempname)
+ if scope and util.sqla_14:
+ options["scope"] = scope
+ return testing_util.testing_engine(url=url, future=future, options=options)
+
+
+def _sqlite_testing_config(sourceless=False, future=False):
+ dir_ = os.path.join(_get_staging_directory(), "scripts")
+ url = "sqlite:///%s/foo.db" % dir_
+
+ sqlalchemy_future = future or ("future" in config.db.__class__.__module__)
+
+ return _write_config_file(
+ """
+[alembic]
+script_location = %s
+sqlalchemy.url = %s
+sourceless = %s
+%s
+
+[loggers]
+keys = root,sqlalchemy
+
+[handlers]
+keys = console
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = DEBUG
+handlers =
+qualname = sqlalchemy.engine
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatters]
+keys = generic
+
+[formatter_generic]
+format = %%(levelname)-5.5s [%%(name)s] %%(message)s
+datefmt = %%H:%%M:%%S
+ """
+ % (
+ dir_,
+ url,
+ "true" if sourceless else "false",
+ "sqlalchemy.future = true" if sqlalchemy_future else "",
+ )
+ )
+
+
+def _multi_dir_testing_config(sourceless=False, extra_version_location=""):
+ dir_ = os.path.join(_get_staging_directory(), "scripts")
+ sqlalchemy_future = "future" in config.db.__class__.__module__
+
+ url = "sqlite:///%s/foo.db" % dir_
+
+ return _write_config_file(
+ """
+[alembic]
+script_location = %s
+sqlalchemy.url = %s
+sqlalchemy.future = %s
+sourceless = %s
+version_locations = %%(here)s/model1/ %%(here)s/model2/ %%(here)s/model3/ %s
+
+[loggers]
+keys = root
+
+[handlers]
+keys = console
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatters]
+keys = generic
+
+[formatter_generic]
+format = %%(levelname)-5.5s [%%(name)s] %%(message)s
+datefmt = %%H:%%M:%%S
+ """
+ % (
+ dir_,
+ url,
+ "true" if sqlalchemy_future else "false",
+ "true" if sourceless else "false",
+ extra_version_location,
+ )
+ )
+
+
+def _no_sql_testing_config(dialect="postgresql", directives=""):
+ """use a postgresql url with no host so that
+ connections guaranteed to fail"""
+ dir_ = os.path.join(_get_staging_directory(), "scripts")
+ return _write_config_file(
+ """
+[alembic]
+script_location = %s
+sqlalchemy.url = %s://
+%s
+
+[loggers]
+keys = root
+
+[handlers]
+keys = console
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatters]
+keys = generic
+
+[formatter_generic]
+format = %%(levelname)-5.5s [%%(name)s] %%(message)s
+datefmt = %%H:%%M:%%S
+
+"""
+ % (dir_, dialect, directives)
+ )
+
+
+def _write_config_file(text):
+ cfg = _testing_config()
+ with open(cfg.config_file_name, "w") as f:
+ f.write(text)
+ return cfg
+
+
+def _testing_config():
+ from alembic.config import Config
+
+ if not os.access(_get_staging_directory(), os.F_OK):
+ os.mkdir(_get_staging_directory())
+ return Config(os.path.join(_get_staging_directory(), "test_alembic.ini"))
+
+
+def write_script(
+ scriptdir, rev_id, content, encoding="ascii", sourceless=False
+):
+ old = scriptdir.revision_map.get_revision(rev_id)
+ path = old.path
+
+ content = textwrap.dedent(content)
+ if encoding:
+ content = content.encode(encoding)
+ with open(path, "wb") as fp:
+ fp.write(content)
+ pyc_path = util.pyc_file_from_path(path)
+ if pyc_path:
+ os.unlink(pyc_path)
+ script = Script._from_path(scriptdir, path)
+ old = scriptdir.revision_map.get_revision(script.revision)
+ if old.down_revision != script.down_revision:
+ raise Exception(
+ "Can't change down_revision " "on a refresh operation."
+ )
+ scriptdir.revision_map.add_revision(script, _replace=True)
+
+ if sourceless:
+ make_sourceless(
+ path, "pep3147" if sourceless == "pep3147_everything" else "simple"
+ )
+
+
+def make_sourceless(path, style):
+ import py_compile
+
+ py_compile.compile(path)
+
+ if style == "simple":
+ pyc_path = util.pyc_file_from_path(path)
+ suffix = importlib.machinery.BYTECODE_SUFFIXES[0]
+ filepath, ext = os.path.splitext(path)
+ simple_pyc_path = filepath + suffix
+ shutil.move(pyc_path, simple_pyc_path)
+ pyc_path = simple_pyc_path
+ else:
+ assert style in ("pep3147", "simple")
+ pyc_path = util.pyc_file_from_path(path)
+
+ assert os.access(pyc_path, os.F_OK)
+
+ os.unlink(path)
+
+
+def three_rev_fixture(cfg):
+ a = util.rev_id()
+ b = util.rev_id()
+ c = util.rev_id()
+
+ script = ScriptDirectory.from_config(cfg)
+ script.generate_revision(a, "revision a", refresh=True, head="base")
+ write_script(
+ script,
+ a,
+ """\
+"Rev A"
+revision = '%s'
+down_revision = None
+
+from alembic import op
+
+
+def upgrade():
+ op.execute("CREATE STEP 1")
+
+
+def downgrade():
+ op.execute("DROP STEP 1")
+
+"""
+ % a,
+ )
+
+ script.generate_revision(b, "revision b", refresh=True, head=a)
+ write_script(
+ script,
+ b,
+ f"""# coding: utf-8
+"Rev B, méil, %3"
+revision = '{b}'
+down_revision = '{a}'
+
+from alembic import op
+
+
+def upgrade():
+ op.execute("CREATE STEP 2")
+
+
+def downgrade():
+ op.execute("DROP STEP 2")
+
+""",
+ encoding="utf-8",
+ )
+
+ script.generate_revision(c, "revision c", refresh=True, head=b)
+ write_script(
+ script,
+ c,
+ """\
+"Rev C"
+revision = '%s'
+down_revision = '%s'
+
+from alembic import op
+
+
+def upgrade():
+ op.execute("CREATE STEP 3")
+
+
+def downgrade():
+ op.execute("DROP STEP 3")
+
+"""
+ % (c, b),
+ )
+ return a, b, c
+
+
+def multi_heads_fixture(cfg, a, b, c):
+ """Create a multiple head fixture from the three-revs fixture"""
+
+ # a->b->c
+ # -> d -> e
+ # -> f
+ d = util.rev_id()
+ e = util.rev_id()
+ f = util.rev_id()
+
+ script = ScriptDirectory.from_config(cfg)
+ script.generate_revision(
+ d, "revision d from b", head=b, splice=True, refresh=True
+ )
+ write_script(
+ script,
+ d,
+ """\
+"Rev D"
+revision = '%s'
+down_revision = '%s'
+
+from alembic import op
+
+
+def upgrade():
+ op.execute("CREATE STEP 4")
+
+
+def downgrade():
+ op.execute("DROP STEP 4")
+
+"""
+ % (d, b),
+ )
+
+ script.generate_revision(
+ e, "revision e from d", head=d, splice=True, refresh=True
+ )
+ write_script(
+ script,
+ e,
+ """\
+"Rev E"
+revision = '%s'
+down_revision = '%s'
+
+from alembic import op
+
+
+def upgrade():
+ op.execute("CREATE STEP 5")
+
+
+def downgrade():
+ op.execute("DROP STEP 5")
+
+"""
+ % (e, d),
+ )
+
+ script.generate_revision(
+ f, "revision f from b", head=b, splice=True, refresh=True
+ )
+ write_script(
+ script,
+ f,
+ """\
+"Rev F"
+revision = '%s'
+down_revision = '%s'
+
+from alembic import op
+
+
+def upgrade():
+ op.execute("CREATE STEP 6")
+
+
+def downgrade():
+ op.execute("DROP STEP 6")
+
+"""
+ % (f, b),
+ )
+
+ return d, e, f
+
+
+def _multidb_testing_config(engines):
+ """alembic.ini fixture to work exactly with the 'multidb' template"""
+
+ dir_ = os.path.join(_get_staging_directory(), "scripts")
+
+ sqlalchemy_future = "future" in config.db.__class__.__module__
+
+ databases = ", ".join(engines.keys())
+ engines = "\n\n".join(
+ "[%s]\n" "sqlalchemy.url = %s" % (key, value.url)
+ for key, value in engines.items()
+ )
+
+ return _write_config_file(
+ """
+[alembic]
+script_location = %s
+sourceless = false
+sqlalchemy.future = %s
+databases = %s
+
+%s
+[loggers]
+keys = root
+
+[handlers]
+keys = console
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatters]
+keys = generic
+
+[formatter_generic]
+format = %%(levelname)-5.5s [%%(name)s] %%(message)s
+datefmt = %%H:%%M:%%S
+ """
+ % (dir_, "true" if sqlalchemy_future else "false", databases, engines)
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/fixtures.py b/venv/lib/python3.12/site-packages/alembic/testing/fixtures.py
new file mode 100644
index 0000000..4b83a74
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/fixtures.py
@@ -0,0 +1,306 @@
+from __future__ import annotations
+
+import configparser
+from contextlib import contextmanager
+import io
+import re
+from typing import Any
+from typing import Dict
+
+from sqlalchemy import Column
+from sqlalchemy import inspect
+from sqlalchemy import MetaData
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy import testing
+from sqlalchemy import text
+from sqlalchemy.testing import config
+from sqlalchemy.testing import mock
+from sqlalchemy.testing.assertions import eq_
+from sqlalchemy.testing.fixtures import TablesTest as SQLAlchemyTablesTest
+from sqlalchemy.testing.fixtures import TestBase as SQLAlchemyTestBase
+
+import alembic
+from .assertions import _get_dialect
+from ..environment import EnvironmentContext
+from ..migration import MigrationContext
+from ..operations import Operations
+from ..util import sqla_compat
+from ..util.sqla_compat import create_mock_engine
+from ..util.sqla_compat import sqla_14
+from ..util.sqla_compat import sqla_2
+
+
+testing_config = configparser.ConfigParser()
+testing_config.read(["test.cfg"])
+
+
+class TestBase(SQLAlchemyTestBase):
+ is_sqlalchemy_future = sqla_2
+
+ @testing.fixture()
+ def ops_context(self, migration_context):
+ with migration_context.begin_transaction(_per_migration=True):
+ yield Operations(migration_context)
+
+ @testing.fixture
+ def migration_context(self, connection):
+ return MigrationContext.configure(
+ connection, opts=dict(transaction_per_migration=True)
+ )
+
+ @testing.fixture
+ def connection(self):
+ with config.db.connect() as conn:
+ yield conn
+
+
+class TablesTest(TestBase, SQLAlchemyTablesTest):
+ pass
+
+
+if sqla_14:
+ from sqlalchemy.testing.fixtures import FutureEngineMixin
+else:
+
+ class FutureEngineMixin: # type:ignore[no-redef]
+ __requires__ = ("sqlalchemy_14",)
+
+
+FutureEngineMixin.is_sqlalchemy_future = True
+
+
+def capture_db(dialect="postgresql://"):
+ buf = []
+
+ def dump(sql, *multiparams, **params):
+ buf.append(str(sql.compile(dialect=engine.dialect)))
+
+ engine = create_mock_engine(dialect, dump)
+ return engine, buf
+
+
+_engs: Dict[Any, Any] = {}
+
+
+@contextmanager
+def capture_context_buffer(**kw):
+ if kw.pop("bytes_io", False):
+ buf = io.BytesIO()
+ else:
+ buf = io.StringIO()
+
+ kw.update({"dialect_name": "sqlite", "output_buffer": buf})
+ conf = EnvironmentContext.configure
+
+ def configure(*arg, **opt):
+ opt.update(**kw)
+ return conf(*arg, **opt)
+
+ with mock.patch.object(EnvironmentContext, "configure", configure):
+ yield buf
+
+
+@contextmanager
+def capture_engine_context_buffer(**kw):
+ from .env import _sqlite_file_db
+ from sqlalchemy import event
+
+ buf = io.StringIO()
+
+ eng = _sqlite_file_db()
+
+ conn = eng.connect()
+
+ @event.listens_for(conn, "before_cursor_execute")
+ def bce(conn, cursor, statement, parameters, context, executemany):
+ buf.write(statement + "\n")
+
+ kw.update({"connection": conn})
+ conf = EnvironmentContext.configure
+
+ def configure(*arg, **opt):
+ opt.update(**kw)
+ return conf(*arg, **opt)
+
+ with mock.patch.object(EnvironmentContext, "configure", configure):
+ yield buf
+
+
+def op_fixture(
+ dialect="default",
+ as_sql=False,
+ naming_convention=None,
+ literal_binds=False,
+ native_boolean=None,
+):
+ opts = {}
+ if naming_convention:
+ opts["target_metadata"] = MetaData(naming_convention=naming_convention)
+
+ class buffer_:
+ def __init__(self):
+ self.lines = []
+
+ def write(self, msg):
+ msg = msg.strip()
+ msg = re.sub(r"[\n\t]", "", msg)
+ if as_sql:
+ # the impl produces soft tabs,
+ # so search for blocks of 4 spaces
+ msg = re.sub(r" ", "", msg)
+ msg = re.sub(r"\;\n*$", "", msg)
+
+ self.lines.append(msg)
+
+ def flush(self):
+ pass
+
+ buf = buffer_()
+
+ class ctx(MigrationContext):
+ def get_buf(self):
+ return buf
+
+ def clear_assertions(self):
+ buf.lines[:] = []
+
+ def assert_(self, *sql):
+ # TODO: make this more flexible about
+ # whitespace and such
+ eq_(buf.lines, [re.sub(r"[\n\t]", "", s) for s in sql])
+
+ def assert_contains(self, sql):
+ for stmt in buf.lines:
+ if re.sub(r"[\n\t]", "", sql) in stmt:
+ return
+ else:
+ assert False, "Could not locate fragment %r in %r" % (
+ sql,
+ buf.lines,
+ )
+
+ if as_sql:
+ opts["as_sql"] = as_sql
+ if literal_binds:
+ opts["literal_binds"] = literal_binds
+ if not sqla_14 and dialect == "mariadb":
+ ctx_dialect = _get_dialect("mysql")
+ ctx_dialect.server_version_info = (10, 4, 0, "MariaDB")
+
+ else:
+ ctx_dialect = _get_dialect(dialect)
+ if native_boolean is not None:
+ ctx_dialect.supports_native_boolean = native_boolean
+ # this is new as of SQLAlchemy 1.2.7 and is used by SQL Server,
+ # which breaks assumptions in the alembic test suite
+ ctx_dialect.non_native_boolean_check_constraint = True
+ if not as_sql:
+
+ def execute(stmt, *multiparam, **param):
+ if isinstance(stmt, str):
+ stmt = text(stmt)
+ assert stmt.supports_execution
+ sql = str(stmt.compile(dialect=ctx_dialect))
+
+ buf.write(sql)
+
+ connection = mock.Mock(dialect=ctx_dialect, execute=execute)
+ else:
+ opts["output_buffer"] = buf
+ connection = None
+ context = ctx(ctx_dialect, connection, opts)
+
+ alembic.op._proxy = Operations(context)
+ return context
+
+
+class AlterColRoundTripFixture:
+ # since these tests are about syntax, use more recent SQLAlchemy as some of
+ # the type / server default compare logic might not work on older
+ # SQLAlchemy versions as seems to be the case for SQLAlchemy 1.1 on Oracle
+
+ __requires__ = ("alter_column",)
+
+ def setUp(self):
+ self.conn = config.db.connect()
+ self.ctx = MigrationContext.configure(self.conn)
+ self.op = Operations(self.ctx)
+ self.metadata = MetaData()
+
+ def _compare_type(self, t1, t2):
+ c1 = Column("q", t1)
+ c2 = Column("q", t2)
+ assert not self.ctx.impl.compare_type(
+ c1, c2
+ ), "Type objects %r and %r didn't compare as equivalent" % (t1, t2)
+
+ def _compare_server_default(self, t1, s1, t2, s2):
+ c1 = Column("q", t1, server_default=s1)
+ c2 = Column("q", t2, server_default=s2)
+ assert not self.ctx.impl.compare_server_default(
+ c1, c2, s2, s1
+ ), "server defaults %r and %r didn't compare as equivalent" % (s1, s2)
+
+ def tearDown(self):
+ sqla_compat._safe_rollback_connection_transaction(self.conn)
+ with self.conn.begin():
+ self.metadata.drop_all(self.conn)
+ self.conn.close()
+
+ def _run_alter_col(self, from_, to_, compare=None):
+ column = Column(
+ from_.get("name", "colname"),
+ from_.get("type", String(10)),
+ nullable=from_.get("nullable", True),
+ server_default=from_.get("server_default", None),
+ # comment=from_.get("comment", None)
+ )
+ t = Table("x", self.metadata, column)
+
+ with sqla_compat._ensure_scope_for_ddl(self.conn):
+ t.create(self.conn)
+ insp = inspect(self.conn)
+ old_col = insp.get_columns("x")[0]
+
+ # TODO: conditional comment support
+ self.op.alter_column(
+ "x",
+ column.name,
+ existing_type=column.type,
+ existing_server_default=column.server_default
+ if column.server_default is not None
+ else False,
+ existing_nullable=True if column.nullable else False,
+ # existing_comment=column.comment,
+ nullable=to_.get("nullable", None),
+ # modify_comment=False,
+ server_default=to_.get("server_default", False),
+ new_column_name=to_.get("name", None),
+ type_=to_.get("type", None),
+ )
+
+ insp = inspect(self.conn)
+ new_col = insp.get_columns("x")[0]
+
+ if compare is None:
+ compare = to_
+
+ eq_(
+ new_col["name"],
+ compare["name"] if "name" in compare else column.name,
+ )
+ self._compare_type(
+ new_col["type"], compare.get("type", old_col["type"])
+ )
+ eq_(new_col["nullable"], compare.get("nullable", column.nullable))
+ self._compare_server_default(
+ new_col["type"],
+ new_col.get("default", None),
+ compare.get("type", old_col["type"]),
+ compare["server_default"].text
+ if "server_default" in compare
+ else column.server_default.arg.text
+ if column.server_default is not None
+ else None,
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/plugin/__init__.py b/venv/lib/python3.12/site-packages/alembic/testing/plugin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/plugin/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/plugin/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..782f841
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/plugin/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/plugin/__pycache__/bootstrap.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/plugin/__pycache__/bootstrap.cpython-312.pyc
new file mode 100644
index 0000000..908912d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/plugin/__pycache__/bootstrap.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/plugin/bootstrap.py b/venv/lib/python3.12/site-packages/alembic/testing/plugin/bootstrap.py
new file mode 100644
index 0000000..d4a2c55
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/plugin/bootstrap.py
@@ -0,0 +1,4 @@
+"""
+Bootstrapper for test framework plugins.
+
+"""
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/requirements.py b/venv/lib/python3.12/site-packages/alembic/testing/requirements.py
new file mode 100644
index 0000000..6e07e28
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/requirements.py
@@ -0,0 +1,210 @@
+from sqlalchemy.testing.requirements import Requirements
+
+from alembic import util
+from alembic.util import sqla_compat
+from ..testing import exclusions
+
+
+class SuiteRequirements(Requirements):
+ @property
+ def schemas(self):
+ """Target database must support external schemas, and have one
+ named 'test_schema'."""
+
+ return exclusions.open()
+
+ @property
+ def autocommit_isolation(self):
+ """target database should support 'AUTOCOMMIT' isolation level"""
+
+ return exclusions.closed()
+
+ @property
+ def materialized_views(self):
+ """needed for sqlalchemy compat"""
+ return exclusions.closed()
+
+ @property
+ def unique_constraint_reflection(self):
+ def doesnt_have_check_uq_constraints(config):
+ from sqlalchemy import inspect
+
+ insp = inspect(config.db)
+ try:
+ insp.get_unique_constraints("x")
+ except NotImplementedError:
+ return True
+ except TypeError:
+ return True
+ except Exception:
+ pass
+ return False
+
+ return exclusions.skip_if(doesnt_have_check_uq_constraints)
+
+ @property
+ def sequences(self):
+ """Target database must support SEQUENCEs."""
+
+ return exclusions.only_if(
+ [lambda config: config.db.dialect.supports_sequences],
+ "no sequence support",
+ )
+
+ @property
+ def foreign_key_match(self):
+ return exclusions.open()
+
+ @property
+ def foreign_key_constraint_reflection(self):
+ return exclusions.open()
+
+ @property
+ def check_constraints_w_enforcement(self):
+ """Target database must support check constraints
+ and also enforce them."""
+
+ return exclusions.open()
+
+ @property
+ def reflects_pk_names(self):
+ return exclusions.closed()
+
+ @property
+ def reflects_fk_options(self):
+ return exclusions.closed()
+
+ @property
+ def sqlalchemy_14(self):
+ return exclusions.skip_if(
+ lambda config: not util.sqla_14,
+ "SQLAlchemy 1.4 or greater required",
+ )
+
+ @property
+ def sqlalchemy_1x(self):
+ return exclusions.skip_if(
+ lambda config: util.sqla_2,
+ "SQLAlchemy 1.x test",
+ )
+
+ @property
+ def sqlalchemy_2(self):
+ return exclusions.skip_if(
+ lambda config: not util.sqla_2,
+ "SQLAlchemy 2.x test",
+ )
+
+ @property
+ def asyncio(self):
+ def go(config):
+ try:
+ import greenlet # noqa: F401
+ except ImportError:
+ return False
+ else:
+ return True
+
+ return self.sqlalchemy_14 + exclusions.only_if(go)
+
+ @property
+ def comments(self):
+ return exclusions.only_if(
+ lambda config: config.db.dialect.supports_comments
+ )
+
+ @property
+ def alter_column(self):
+ return exclusions.open()
+
+ @property
+ def computed_columns(self):
+ return exclusions.closed()
+
+ @property
+ def computed_columns_api(self):
+ return exclusions.only_if(
+ exclusions.BooleanPredicate(sqla_compat.has_computed)
+ )
+
+ @property
+ def computed_reflects_normally(self):
+ return exclusions.only_if(
+ exclusions.BooleanPredicate(sqla_compat.has_computed_reflection)
+ )
+
+ @property
+ def computed_reflects_as_server_default(self):
+ return exclusions.closed()
+
+ @property
+ def computed_doesnt_reflect_as_server_default(self):
+ return exclusions.closed()
+
+ @property
+ def autoincrement_on_composite_pk(self):
+ return exclusions.closed()
+
+ @property
+ def fk_ondelete_is_reflected(self):
+ return exclusions.closed()
+
+ @property
+ def fk_onupdate_is_reflected(self):
+ return exclusions.closed()
+
+ @property
+ def fk_onupdate(self):
+ return exclusions.open()
+
+ @property
+ def fk_ondelete_restrict(self):
+ return exclusions.open()
+
+ @property
+ def fk_onupdate_restrict(self):
+ return exclusions.open()
+
+ @property
+ def fk_ondelete_noaction(self):
+ return exclusions.open()
+
+ @property
+ def fk_initially(self):
+ return exclusions.closed()
+
+ @property
+ def fk_deferrable(self):
+ return exclusions.closed()
+
+ @property
+ def fk_deferrable_is_reflected(self):
+ return exclusions.closed()
+
+ @property
+ def fk_names(self):
+ return exclusions.open()
+
+ @property
+ def integer_subtype_comparisons(self):
+ return exclusions.open()
+
+ @property
+ def no_name_normalize(self):
+ return exclusions.skip_if(
+ lambda config: config.db.dialect.requires_name_normalize
+ )
+
+ @property
+ def identity_columns(self):
+ return exclusions.closed()
+
+ @property
+ def identity_columns_alter(self):
+ return exclusions.closed()
+
+ @property
+ def identity_columns_api(self):
+ return exclusions.only_if(
+ exclusions.BooleanPredicate(sqla_compat.has_identity)
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/schemacompare.py b/venv/lib/python3.12/site-packages/alembic/testing/schemacompare.py
new file mode 100644
index 0000000..204cc4d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/schemacompare.py
@@ -0,0 +1,169 @@
+from itertools import zip_longest
+
+from sqlalchemy import schema
+from sqlalchemy.sql.elements import ClauseList
+
+
+class CompareTable:
+ def __init__(self, table):
+ self.table = table
+
+ def __eq__(self, other):
+ if self.table.name != other.name or self.table.schema != other.schema:
+ return False
+
+ for c1, c2 in zip_longest(self.table.c, other.c):
+ if (c1 is None and c2 is not None) or (
+ c2 is None and c1 is not None
+ ):
+ return False
+ if CompareColumn(c1) != c2:
+ return False
+
+ return True
+
+ # TODO: compare constraints, indexes
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class CompareColumn:
+ def __init__(self, column):
+ self.column = column
+
+ def __eq__(self, other):
+ return (
+ self.column.name == other.name
+ and self.column.nullable == other.nullable
+ )
+ # TODO: datatypes etc
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class CompareIndex:
+ def __init__(self, index, name_only=False):
+ self.index = index
+ self.name_only = name_only
+
+ def __eq__(self, other):
+ if self.name_only:
+ return self.index.name == other.name
+ else:
+ return (
+ str(schema.CreateIndex(self.index))
+ == str(schema.CreateIndex(other))
+ and self.index.dialect_kwargs == other.dialect_kwargs
+ )
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ expr = ClauseList(*self.index.expressions)
+ try:
+ expr_str = expr.compile().string
+ except Exception:
+ expr_str = str(expr)
+ return f""
+
+
+class CompareCheckConstraint:
+ def __init__(self, constraint):
+ self.constraint = constraint
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, schema.CheckConstraint)
+ and self.constraint.name == other.name
+ and (str(self.constraint.sqltext) == str(other.sqltext))
+ and (other.table.name == self.constraint.table.name)
+ and other.table.schema == self.constraint.table.schema
+ )
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class CompareForeignKey:
+ def __init__(self, constraint):
+ self.constraint = constraint
+
+ def __eq__(self, other):
+ r1 = (
+ isinstance(other, schema.ForeignKeyConstraint)
+ and self.constraint.name == other.name
+ and (other.table.name == self.constraint.table.name)
+ and other.table.schema == self.constraint.table.schema
+ )
+ if not r1:
+ return False
+ for c1, c2 in zip_longest(self.constraint.columns, other.columns):
+ if (c1 is None and c2 is not None) or (
+ c2 is None and c1 is not None
+ ):
+ return False
+ if CompareColumn(c1) != c2:
+ return False
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class ComparePrimaryKey:
+ def __init__(self, constraint):
+ self.constraint = constraint
+
+ def __eq__(self, other):
+ r1 = (
+ isinstance(other, schema.PrimaryKeyConstraint)
+ and self.constraint.name == other.name
+ and (other.table.name == self.constraint.table.name)
+ and other.table.schema == self.constraint.table.schema
+ )
+ if not r1:
+ return False
+
+ for c1, c2 in zip_longest(self.constraint.columns, other.columns):
+ if (c1 is None and c2 is not None) or (
+ c2 is None and c1 is not None
+ ):
+ return False
+ if CompareColumn(c1) != c2:
+ return False
+
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class CompareUniqueConstraint:
+ def __init__(self, constraint):
+ self.constraint = constraint
+
+ def __eq__(self, other):
+ r1 = (
+ isinstance(other, schema.UniqueConstraint)
+ and self.constraint.name == other.name
+ and (other.table.name == self.constraint.table.name)
+ and other.table.schema == self.constraint.table.schema
+ )
+ if not r1:
+ return False
+
+ for c1, c2 in zip_longest(self.constraint.columns, other.columns):
+ if (c1 is None and c2 is not None) or (
+ c2 is None and c1 is not None
+ ):
+ return False
+ if CompareColumn(c1) != c2:
+ return False
+
+ return True
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__init__.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/__init__.py
new file mode 100644
index 0000000..3da498d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/__init__.py
@@ -0,0 +1,7 @@
+from .test_autogen_comments import * # noqa
+from .test_autogen_computed import * # noqa
+from .test_autogen_diffs import * # noqa
+from .test_autogen_fks import * # noqa
+from .test_autogen_identity import * # noqa
+from .test_environment import * # noqa
+from .test_op import * # noqa
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..b90cb34
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/_autogen_fixtures.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/_autogen_fixtures.cpython-312.pyc
new file mode 100644
index 0000000..5ab91e3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/_autogen_fixtures.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_comments.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_comments.cpython-312.pyc
new file mode 100644
index 0000000..ae220c6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_comments.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_computed.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_computed.cpython-312.pyc
new file mode 100644
index 0000000..698b3a3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_computed.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_diffs.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_diffs.cpython-312.pyc
new file mode 100644
index 0000000..45e5bca
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_diffs.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_fks.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_fks.cpython-312.pyc
new file mode 100644
index 0000000..a2338f5
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_fks.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_identity.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_identity.cpython-312.pyc
new file mode 100644
index 0000000..7108c30
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_autogen_identity.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_environment.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_environment.cpython-312.pyc
new file mode 100644
index 0000000..b4c1f36
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_environment.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_op.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_op.cpython-312.pyc
new file mode 100644
index 0000000..f9e7e92
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/testing/suite/__pycache__/test_op.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/_autogen_fixtures.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/_autogen_fixtures.py
new file mode 100644
index 0000000..d838ebe
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/_autogen_fixtures.py
@@ -0,0 +1,335 @@
+from __future__ import annotations
+
+from typing import Any
+from typing import Dict
+from typing import Set
+
+from sqlalchemy import CHAR
+from sqlalchemy import CheckConstraint
+from sqlalchemy import Column
+from sqlalchemy import event
+from sqlalchemy import ForeignKey
+from sqlalchemy import Index
+from sqlalchemy import inspect
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import Numeric
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy import Text
+from sqlalchemy import text
+from sqlalchemy import UniqueConstraint
+
+from ... import autogenerate
+from ... import util
+from ...autogenerate import api
+from ...ddl.base import _fk_spec
+from ...migration import MigrationContext
+from ...operations import ops
+from ...testing import config
+from ...testing import eq_
+from ...testing.env import clear_staging_env
+from ...testing.env import staging_env
+
+names_in_this_test: Set[Any] = set()
+
+
+@event.listens_for(Table, "after_parent_attach")
+def new_table(table, parent):
+ names_in_this_test.add(table.name)
+
+
+def _default_include_object(obj, name, type_, reflected, compare_to):
+ if type_ == "table":
+ return name in names_in_this_test
+ else:
+ return True
+
+
+_default_object_filters: Any = _default_include_object
+
+_default_name_filters: Any = None
+
+
+class ModelOne:
+ __requires__ = ("unique_constraint_reflection",)
+
+ schema: Any = None
+
+ @classmethod
+ def _get_db_schema(cls):
+ schema = cls.schema
+
+ m = MetaData(schema=schema)
+
+ Table(
+ "user",
+ m,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50)),
+ Column("a1", Text),
+ Column("pw", String(50)),
+ Index("pw_idx", "pw"),
+ )
+
+ Table(
+ "address",
+ m,
+ Column("id", Integer, primary_key=True),
+ Column("email_address", String(100), nullable=False),
+ )
+
+ Table(
+ "order",
+ m,
+ Column("order_id", Integer, primary_key=True),
+ Column(
+ "amount",
+ Numeric(8, 2),
+ nullable=False,
+ server_default=text("0"),
+ ),
+ CheckConstraint("amount >= 0", name="ck_order_amount"),
+ )
+
+ Table(
+ "extra",
+ m,
+ Column("x", CHAR),
+ Column("uid", Integer, ForeignKey("user.id")),
+ )
+
+ return m
+
+ @classmethod
+ def _get_model_schema(cls):
+ schema = cls.schema
+
+ m = MetaData(schema=schema)
+
+ Table(
+ "user",
+ m,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", Text, server_default="x"),
+ )
+
+ Table(
+ "address",
+ m,
+ Column("id", Integer, primary_key=True),
+ Column("email_address", String(100), nullable=False),
+ Column("street", String(50)),
+ UniqueConstraint("email_address", name="uq_email"),
+ )
+
+ Table(
+ "order",
+ m,
+ Column("order_id", Integer, primary_key=True),
+ Column(
+ "amount",
+ Numeric(10, 2),
+ nullable=True,
+ server_default=text("0"),
+ ),
+ Column("user_id", Integer, ForeignKey("user.id")),
+ CheckConstraint("amount > -1", name="ck_order_amount"),
+ )
+
+ Table(
+ "item",
+ m,
+ Column("id", Integer, primary_key=True),
+ Column("description", String(100)),
+ Column("order_id", Integer, ForeignKey("order.order_id")),
+ CheckConstraint("len(description) > 5"),
+ )
+ return m
+
+
+class _ComparesFKs:
+ def _assert_fk_diff(
+ self,
+ diff,
+ type_,
+ source_table,
+ source_columns,
+ target_table,
+ target_columns,
+ name=None,
+ conditional_name=None,
+ source_schema=None,
+ onupdate=None,
+ ondelete=None,
+ initially=None,
+ deferrable=None,
+ ):
+ # the public API for ForeignKeyConstraint was not very rich
+ # in 0.7, 0.8, so here we use the well-known but slightly
+ # private API to get at its elements
+ (
+ fk_source_schema,
+ fk_source_table,
+ fk_source_columns,
+ fk_target_schema,
+ fk_target_table,
+ fk_target_columns,
+ fk_onupdate,
+ fk_ondelete,
+ fk_deferrable,
+ fk_initially,
+ ) = _fk_spec(diff[1])
+
+ eq_(diff[0], type_)
+ eq_(fk_source_table, source_table)
+ eq_(fk_source_columns, source_columns)
+ eq_(fk_target_table, target_table)
+ eq_(fk_source_schema, source_schema)
+ eq_(fk_onupdate, onupdate)
+ eq_(fk_ondelete, ondelete)
+ eq_(fk_initially, initially)
+ eq_(fk_deferrable, deferrable)
+
+ eq_([elem.column.name for elem in diff[1].elements], target_columns)
+ if conditional_name is not None:
+ if conditional_name == "servergenerated":
+ fks = inspect(self.bind).get_foreign_keys(source_table)
+ server_fk_name = fks[0]["name"]
+ eq_(diff[1].name, server_fk_name)
+ else:
+ eq_(diff[1].name, conditional_name)
+ else:
+ eq_(diff[1].name, name)
+
+
+class AutogenTest(_ComparesFKs):
+ def _flatten_diffs(self, diffs):
+ for d in diffs:
+ if isinstance(d, list):
+ yield from self._flatten_diffs(d)
+ else:
+ yield d
+
+ @classmethod
+ def _get_bind(cls):
+ return config.db
+
+ configure_opts: Dict[Any, Any] = {}
+
+ @classmethod
+ def setup_class(cls):
+ staging_env()
+ cls.bind = cls._get_bind()
+ cls.m1 = cls._get_db_schema()
+ cls.m1.create_all(cls.bind)
+ cls.m2 = cls._get_model_schema()
+
+ @classmethod
+ def teardown_class(cls):
+ cls.m1.drop_all(cls.bind)
+ clear_staging_env()
+
+ def setUp(self):
+ self.conn = conn = self.bind.connect()
+ ctx_opts = {
+ "compare_type": True,
+ "compare_server_default": True,
+ "target_metadata": self.m2,
+ "upgrade_token": "upgrades",
+ "downgrade_token": "downgrades",
+ "alembic_module_prefix": "op.",
+ "sqlalchemy_module_prefix": "sa.",
+ "include_object": _default_object_filters,
+ "include_name": _default_name_filters,
+ }
+ if self.configure_opts:
+ ctx_opts.update(self.configure_opts)
+ self.context = context = MigrationContext.configure(
+ connection=conn, opts=ctx_opts
+ )
+
+ self.autogen_context = api.AutogenContext(context, self.m2)
+
+ def tearDown(self):
+ self.conn.close()
+
+ def _update_context(
+ self, object_filters=None, name_filters=None, include_schemas=None
+ ):
+ if include_schemas is not None:
+ self.autogen_context.opts["include_schemas"] = include_schemas
+ if object_filters is not None:
+ self.autogen_context._object_filters = [object_filters]
+ if name_filters is not None:
+ self.autogen_context._name_filters = [name_filters]
+ return self.autogen_context
+
+
+class AutogenFixtureTest(_ComparesFKs):
+ def _fixture(
+ self,
+ m1,
+ m2,
+ include_schemas=False,
+ opts=None,
+ object_filters=_default_object_filters,
+ name_filters=_default_name_filters,
+ return_ops=False,
+ max_identifier_length=None,
+ ):
+ if max_identifier_length:
+ dialect = self.bind.dialect
+ existing_length = dialect.max_identifier_length
+ dialect.max_identifier_length = (
+ dialect._user_defined_max_identifier_length
+ ) = max_identifier_length
+ try:
+ self._alembic_metadata, model_metadata = m1, m2
+ for m in util.to_list(self._alembic_metadata):
+ m.create_all(self.bind)
+
+ with self.bind.connect() as conn:
+ ctx_opts = {
+ "compare_type": True,
+ "compare_server_default": True,
+ "target_metadata": model_metadata,
+ "upgrade_token": "upgrades",
+ "downgrade_token": "downgrades",
+ "alembic_module_prefix": "op.",
+ "sqlalchemy_module_prefix": "sa.",
+ "include_object": object_filters,
+ "include_name": name_filters,
+ "include_schemas": include_schemas,
+ }
+ if opts:
+ ctx_opts.update(opts)
+ self.context = context = MigrationContext.configure(
+ connection=conn, opts=ctx_opts
+ )
+
+ autogen_context = api.AutogenContext(context, model_metadata)
+ uo = ops.UpgradeOps(ops=[])
+ autogenerate._produce_net_changes(autogen_context, uo)
+
+ if return_ops:
+ return uo
+ else:
+ return uo.as_diffs()
+ finally:
+ if max_identifier_length:
+ dialect = self.bind.dialect
+ dialect.max_identifier_length = (
+ dialect._user_defined_max_identifier_length
+ ) = existing_length
+
+ def setUp(self):
+ staging_env()
+ self.bind = config.db
+
+ def tearDown(self):
+ if hasattr(self, "_alembic_metadata"):
+ for m in util.to_list(self._alembic_metadata):
+ m.drop_all(self.bind)
+ clear_staging_env()
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_comments.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_comments.py
new file mode 100644
index 0000000..7ef074f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_comments.py
@@ -0,0 +1,242 @@
+from sqlalchemy import Column
+from sqlalchemy import Float
+from sqlalchemy import MetaData
+from sqlalchemy import String
+from sqlalchemy import Table
+
+from ._autogen_fixtures import AutogenFixtureTest
+from ...testing import eq_
+from ...testing import mock
+from ...testing import TestBase
+
+
+class AutogenerateCommentsTest(AutogenFixtureTest, TestBase):
+ __backend__ = True
+
+ __requires__ = ("comments",)
+
+ def test_existing_table_comment_no_change(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ comment="this is some table",
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ comment="this is some table",
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs, [])
+
+ def test_add_table_comment(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("some_table", m1, Column("test", String(10), primary_key=True))
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ comment="this is some table",
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0], "add_table_comment")
+ eq_(diffs[0][1].comment, "this is some table")
+ eq_(diffs[0][2], None)
+
+ def test_remove_table_comment(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ comment="this is some table",
+ )
+
+ Table("some_table", m2, Column("test", String(10), primary_key=True))
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0], "remove_table_comment")
+ eq_(diffs[0][1].comment, None)
+
+ def test_alter_table_comment(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ comment="this is some table",
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ comment="this is also some table",
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0], "add_table_comment")
+ eq_(diffs[0][1].comment, "this is also some table")
+ eq_(diffs[0][2], "this is some table")
+
+ def test_existing_column_comment_no_change(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float, comment="the amount"),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float, comment="the amount"),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs, [])
+
+ def test_add_column_comment(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float, comment="the amount"),
+ )
+
+ diffs = self._fixture(m1, m2)
+ eq_(
+ diffs,
+ [
+ [
+ (
+ "modify_comment",
+ None,
+ "some_table",
+ "amount",
+ {
+ "existing_nullable": True,
+ "existing_type": mock.ANY,
+ "existing_server_default": False,
+ },
+ None,
+ "the amount",
+ )
+ ]
+ ],
+ )
+
+ def test_remove_column_comment(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float, comment="the amount"),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float),
+ )
+
+ diffs = self._fixture(m1, m2)
+ eq_(
+ diffs,
+ [
+ [
+ (
+ "modify_comment",
+ None,
+ "some_table",
+ "amount",
+ {
+ "existing_nullable": True,
+ "existing_type": mock.ANY,
+ "existing_server_default": False,
+ },
+ "the amount",
+ None,
+ )
+ ]
+ ],
+ )
+
+ def test_alter_column_comment(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float, comment="the amount"),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ Column("amount", Float, comment="the adjusted amount"),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(
+ diffs,
+ [
+ [
+ (
+ "modify_comment",
+ None,
+ "some_table",
+ "amount",
+ {
+ "existing_nullable": True,
+ "existing_type": mock.ANY,
+ "existing_server_default": False,
+ },
+ "the amount",
+ "the adjusted amount",
+ )
+ ]
+ ],
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_computed.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_computed.py
new file mode 100644
index 0000000..01a89a1
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_computed.py
@@ -0,0 +1,203 @@
+import sqlalchemy as sa
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import Table
+
+from ._autogen_fixtures import AutogenFixtureTest
+from ... import testing
+from ...testing import config
+from ...testing import eq_
+from ...testing import exclusions
+from ...testing import is_
+from ...testing import is_true
+from ...testing import mock
+from ...testing import TestBase
+
+
+class AutogenerateComputedTest(AutogenFixtureTest, TestBase):
+ __requires__ = ("computed_columns",)
+ __backend__ = True
+
+ def test_add_computed_column(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("user", m1, Column("id", Integer, primary_key=True))
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("foo", Integer, sa.Computed("5")),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0], "add_column")
+ eq_(diffs[0][2], "user")
+ eq_(diffs[0][3].name, "foo")
+ c = diffs[0][3].computed
+
+ is_true(isinstance(c, sa.Computed))
+ is_(c.persisted, None)
+ eq_(str(c.sqltext), "5")
+
+ def test_remove_computed_column(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("foo", Integer, sa.Computed("5")),
+ )
+
+ Table("user", m2, Column("id", Integer, primary_key=True))
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0], "remove_column")
+ eq_(diffs[0][2], "user")
+ c = diffs[0][3]
+ eq_(c.name, "foo")
+
+ if config.requirements.computed_reflects_normally.enabled:
+ is_true(isinstance(c.computed, sa.Computed))
+ else:
+ is_(c.computed, None)
+
+ if config.requirements.computed_reflects_as_server_default.enabled:
+ is_true(isinstance(c.server_default, sa.DefaultClause))
+ eq_(str(c.server_default.arg.text), "5")
+ elif config.requirements.computed_reflects_normally.enabled:
+ is_true(isinstance(c.computed, sa.Computed))
+ else:
+ is_(c.computed, None)
+
+ @testing.combinations(
+ lambda: (None, sa.Computed("bar*5")),
+ (lambda: (sa.Computed("bar*5"), None)),
+ lambda: (
+ sa.Computed("bar*5"),
+ sa.Computed("bar * 42", persisted=True),
+ ),
+ lambda: (sa.Computed("bar*5"), sa.Computed("bar * 42")),
+ )
+ @config.requirements.computed_reflects_normally
+ def test_cant_change_computed_warning(self, test_case):
+ arg_before, arg_after = testing.resolve_lambda(test_case, **locals())
+ m1 = MetaData()
+ m2 = MetaData()
+
+ arg_before = [] if arg_before is None else [arg_before]
+ arg_after = [] if arg_after is None else [arg_after]
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("bar", Integer),
+ Column("foo", Integer, *arg_before),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("bar", Integer),
+ Column("foo", Integer, *arg_after),
+ )
+
+ with mock.patch("alembic.util.warn") as mock_warn:
+ diffs = self._fixture(m1, m2)
+
+ eq_(
+ mock_warn.mock_calls,
+ [mock.call("Computed default on user.foo cannot be modified")],
+ )
+
+ eq_(list(diffs), [])
+
+ @testing.combinations(
+ lambda: (None, None),
+ lambda: (sa.Computed("5"), sa.Computed("5")),
+ lambda: (sa.Computed("bar*5"), sa.Computed("bar*5")),
+ (
+ lambda: (sa.Computed("bar*5"), None),
+ config.requirements.computed_doesnt_reflect_as_server_default,
+ ),
+ )
+ def test_computed_unchanged(self, test_case):
+ arg_before, arg_after = testing.resolve_lambda(test_case, **locals())
+ m1 = MetaData()
+ m2 = MetaData()
+
+ arg_before = [] if arg_before is None else [arg_before]
+ arg_after = [] if arg_after is None else [arg_after]
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("bar", Integer),
+ Column("foo", Integer, *arg_before),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("bar", Integer),
+ Column("foo", Integer, *arg_after),
+ )
+
+ with mock.patch("alembic.util.warn") as mock_warn:
+ diffs = self._fixture(m1, m2)
+ eq_(mock_warn.mock_calls, [])
+
+ eq_(list(diffs), [])
+
+ @config.requirements.computed_reflects_as_server_default
+ def test_remove_computed_default_on_computed(self):
+ """Asserts the current behavior which is that on PG and Oracle,
+ the GENERATED ALWAYS AS is reflected as a server default which we can't
+ tell is actually "computed", so these come out as a modification to
+ the server default.
+
+ """
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("bar", Integer),
+ Column("foo", Integer, sa.Computed("bar + 42")),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("bar", Integer),
+ Column("foo", Integer),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0][0], "modify_default")
+ eq_(diffs[0][0][2], "user")
+ eq_(diffs[0][0][3], "foo")
+ old = diffs[0][0][-2]
+ new = diffs[0][0][-1]
+
+ is_(new, None)
+ is_true(isinstance(old, sa.DefaultClause))
+
+ if exclusions.against(config, "postgresql"):
+ eq_(str(old.arg.text), "(bar + 42)")
+ elif exclusions.against(config, "oracle"):
+ eq_(str(old.arg.text), '"BAR"+42')
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_diffs.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_diffs.py
new file mode 100644
index 0000000..75bcd37
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_diffs.py
@@ -0,0 +1,273 @@
+from sqlalchemy import BigInteger
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import Table
+from sqlalchemy.testing import in_
+
+from ._autogen_fixtures import AutogenFixtureTest
+from ... import testing
+from ...testing import config
+from ...testing import eq_
+from ...testing import is_
+from ...testing import TestBase
+
+
+class AlterColumnTest(AutogenFixtureTest, TestBase):
+ __backend__ = True
+
+ @testing.combinations((True,), (False,))
+ @config.requirements.comments
+ def test_all_existings_filled(self, pk):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("a", m1, Column("x", Integer, primary_key=pk))
+ Table("a", m2, Column("x", Integer, comment="x", primary_key=pk))
+
+ alter_col = self._assert_alter_col(m1, m2, pk)
+ eq_(alter_col.modify_comment, "x")
+
+ @testing.combinations((True,), (False,))
+ @config.requirements.comments
+ def test_all_existings_filled_in_notnull(self, pk):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("a", m1, Column("x", Integer, nullable=False, primary_key=pk))
+ Table(
+ "a",
+ m2,
+ Column("x", Integer, nullable=False, comment="x", primary_key=pk),
+ )
+
+ self._assert_alter_col(m1, m2, pk, nullable=False)
+
+ @testing.combinations((True,), (False,))
+ @config.requirements.comments
+ def test_all_existings_filled_in_comment(self, pk):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("a", m1, Column("x", Integer, comment="old", primary_key=pk))
+ Table("a", m2, Column("x", Integer, comment="new", primary_key=pk))
+
+ alter_col = self._assert_alter_col(m1, m2, pk)
+ eq_(alter_col.existing_comment, "old")
+
+ @testing.combinations((True,), (False,))
+ @config.requirements.comments
+ def test_all_existings_filled_in_server_default(self, pk):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a", m1, Column("x", Integer, server_default="5", primary_key=pk)
+ )
+ Table(
+ "a",
+ m2,
+ Column(
+ "x", Integer, server_default="5", comment="new", primary_key=pk
+ ),
+ )
+
+ alter_col = self._assert_alter_col(m1, m2, pk)
+ in_("5", alter_col.existing_server_default.arg.text)
+
+ def _assert_alter_col(self, m1, m2, pk, nullable=None):
+ ops = self._fixture(m1, m2, return_ops=True)
+ modify_table = ops.ops[-1]
+ alter_col = modify_table.ops[0]
+
+ if nullable is None:
+ eq_(alter_col.existing_nullable, not pk)
+ else:
+ eq_(alter_col.existing_nullable, nullable)
+ assert alter_col.existing_type._compare_type_affinity(Integer())
+ return alter_col
+
+
+class AutoincrementTest(AutogenFixtureTest, TestBase):
+ __backend__ = True
+ __requires__ = ("integer_subtype_comparisons",)
+
+ def test_alter_column_autoincrement_none(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("a", m1, Column("x", Integer, nullable=False))
+ Table("a", m2, Column("x", Integer, nullable=True))
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ assert "autoincrement" not in ops.ops[0].ops[0].kw
+
+ def test_alter_column_autoincrement_pk_false(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a",
+ m1,
+ Column("x", Integer, primary_key=True, autoincrement=False),
+ )
+ Table(
+ "a",
+ m2,
+ Column("x", BigInteger, primary_key=True, autoincrement=False),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ is_(ops.ops[0].ops[0].kw["autoincrement"], False)
+
+ def test_alter_column_autoincrement_pk_implicit_true(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("a", m1, Column("x", Integer, primary_key=True))
+ Table("a", m2, Column("x", BigInteger, primary_key=True))
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ is_(ops.ops[0].ops[0].kw["autoincrement"], True)
+
+ def test_alter_column_autoincrement_pk_explicit_true(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a", m1, Column("x", Integer, primary_key=True, autoincrement=True)
+ )
+ Table(
+ "a",
+ m2,
+ Column("x", BigInteger, primary_key=True, autoincrement=True),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ is_(ops.ops[0].ops[0].kw["autoincrement"], True)
+
+ def test_alter_column_autoincrement_nonpk_false(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("x", Integer, autoincrement=False),
+ )
+ Table(
+ "a",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("x", BigInteger, autoincrement=False),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ is_(ops.ops[0].ops[0].kw["autoincrement"], False)
+
+ def test_alter_column_autoincrement_nonpk_implicit_false(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("x", Integer),
+ )
+ Table(
+ "a",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("x", BigInteger),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ assert "autoincrement" not in ops.ops[0].ops[0].kw
+
+ def test_alter_column_autoincrement_nonpk_explicit_true(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a",
+ m1,
+ Column("id", Integer, primary_key=True, autoincrement=False),
+ Column("x", Integer, autoincrement=True),
+ )
+ Table(
+ "a",
+ m2,
+ Column("id", Integer, primary_key=True, autoincrement=False),
+ Column("x", BigInteger, autoincrement=True),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ is_(ops.ops[0].ops[0].kw["autoincrement"], True)
+
+ def test_alter_column_autoincrement_compositepk_false(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("x", Integer, primary_key=True, autoincrement=False),
+ )
+ Table(
+ "a",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("x", BigInteger, primary_key=True, autoincrement=False),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ is_(ops.ops[0].ops[0].kw["autoincrement"], False)
+
+ def test_alter_column_autoincrement_compositepk_implicit_false(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("x", Integer, primary_key=True),
+ )
+ Table(
+ "a",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("x", BigInteger, primary_key=True),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ assert "autoincrement" not in ops.ops[0].ops[0].kw
+
+ @config.requirements.autoincrement_on_composite_pk
+ def test_alter_column_autoincrement_compositepk_explicit_true(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "a",
+ m1,
+ Column("id", Integer, primary_key=True, autoincrement=False),
+ Column("x", Integer, primary_key=True, autoincrement=True),
+ # on SQLA 1.0 and earlier, this being present
+ # trips the "add KEY for the primary key" so that the
+ # AUTO_INCREMENT keyword is accepted by MySQL. SQLA 1.1 and
+ # greater the columns are just reorganized.
+ mysql_engine="InnoDB",
+ )
+ Table(
+ "a",
+ m2,
+ Column("id", Integer, primary_key=True, autoincrement=False),
+ Column("x", BigInteger, primary_key=True, autoincrement=True),
+ )
+
+ ops = self._fixture(m1, m2, return_ops=True)
+ is_(ops.ops[0].ops[0].kw["autoincrement"], True)
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_fks.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_fks.py
new file mode 100644
index 0000000..0240b98
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_fks.py
@@ -0,0 +1,1190 @@
+from sqlalchemy import Column
+from sqlalchemy import ForeignKeyConstraint
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import String
+from sqlalchemy import Table
+
+from ._autogen_fixtures import AutogenFixtureTest
+from ...testing import combinations
+from ...testing import config
+from ...testing import eq_
+from ...testing import mock
+from ...testing import TestBase
+
+
+class AutogenerateForeignKeysTest(AutogenFixtureTest, TestBase):
+ __backend__ = True
+ __requires__ = ("foreign_key_constraint_reflection",)
+
+ def test_remove_fk(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", String(10)),
+ ForeignKeyConstraint(["test2"], ["some_table.test"]),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", String(10)),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["test2"],
+ "some_table",
+ ["test"],
+ conditional_name="servergenerated",
+ )
+
+ def test_add_fk(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("test", String(10)),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", String(10)),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("test", String(10)),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", String(10)),
+ ForeignKeyConstraint(["test2"], ["some_table.test"]),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ self._assert_fk_diff(
+ diffs[0], "add_fk", "user", ["test2"], "some_table", ["test"]
+ )
+
+ def test_no_change(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("test", String(10)),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", Integer),
+ ForeignKeyConstraint(["test2"], ["some_table.id"]),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("test", String(10)),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", Integer),
+ ForeignKeyConstraint(["test2"], ["some_table.id"]),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs, [])
+
+ def test_no_change_composite_fk(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ ForeignKeyConstraint(
+ ["other_id_1", "other_id_2"],
+ ["some_table.id_1", "some_table.id_2"],
+ ),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ ForeignKeyConstraint(
+ ["other_id_1", "other_id_2"],
+ ["some_table.id_1", "some_table.id_2"],
+ ),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs, [])
+
+ def test_casing_convention_changed_so_put_drops_first(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("test", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", String(10)),
+ ForeignKeyConstraint(["test2"], ["some_table.test"], name="MyFK"),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("test", String(10), primary_key=True),
+ )
+
+ # foreign key autogen currently does not take "name" into account,
+ # so change the def just for the purposes of testing the
+ # add/drop order for now.
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("test2", String(10)),
+ ForeignKeyConstraint(["a1"], ["some_table.test"], name="myfk"),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["test2"],
+ "some_table",
+ ["test"],
+ name="MyFK" if config.requirements.fk_names.enabled else None,
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["a1"],
+ "some_table",
+ ["test"],
+ name="myfk",
+ )
+
+ def test_add_composite_fk_with_name(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ ForeignKeyConstraint(
+ ["other_id_1", "other_id_2"],
+ ["some_table.id_1", "some_table.id_2"],
+ name="fk_test_name",
+ ),
+ )
+
+ diffs = self._fixture(m1, m2)
+ self._assert_fk_diff(
+ diffs[0],
+ "add_fk",
+ "user",
+ ["other_id_1", "other_id_2"],
+ "some_table",
+ ["id_1", "id_2"],
+ name="fk_test_name",
+ )
+
+ @config.requirements.no_name_normalize
+ def test_remove_composite_fk(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ ForeignKeyConstraint(
+ ["other_id_1", "other_id_2"],
+ ["some_table.id_1", "some_table.id_2"],
+ name="fk_test_name",
+ ),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("a1", String(10), server_default="x"),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["other_id_1", "other_id_2"],
+ "some_table",
+ ["id_1", "id_2"],
+ conditional_name="fk_test_name",
+ )
+
+ def test_add_fk_colkeys(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id_1", String(10), key="tid1", primary_key=True),
+ Column("id_2", String(10), key="tid2", primary_key=True),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("other_id_1", String(10), key="oid1"),
+ Column("other_id_2", String(10), key="oid2"),
+ ForeignKeyConstraint(
+ ["oid1", "oid2"],
+ ["some_table.tid1", "some_table.tid2"],
+ name="fk_test_name",
+ ),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ self._assert_fk_diff(
+ diffs[0],
+ "add_fk",
+ "user",
+ ["other_id_1", "other_id_2"],
+ "some_table",
+ ["id_1", "id_2"],
+ name="fk_test_name",
+ )
+
+ def test_no_change_colkeys(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id_1", String(10), primary_key=True),
+ Column("id_2", String(10), primary_key=True),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("other_id_1", String(10)),
+ Column("other_id_2", String(10)),
+ ForeignKeyConstraint(
+ ["other_id_1", "other_id_2"],
+ ["some_table.id_1", "some_table.id_2"],
+ ),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id_1", String(10), key="tid1", primary_key=True),
+ Column("id_2", String(10), key="tid2", primary_key=True),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("other_id_1", String(10), key="oid1"),
+ Column("other_id_2", String(10), key="oid2"),
+ ForeignKeyConstraint(
+ ["oid1", "oid2"], ["some_table.tid1", "some_table.tid2"]
+ ),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs, [])
+
+
+class IncludeHooksTest(AutogenFixtureTest, TestBase):
+ __backend__ = True
+ __requires__ = ("fk_names",)
+
+ @combinations(("object",), ("name",))
+ @config.requirements.no_name_normalize
+ def test_remove_connection_fk(self, hook_type):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ ref = Table(
+ "ref",
+ m1,
+ Column("id", Integer, primary_key=True),
+ )
+ t1 = Table(
+ "t",
+ m1,
+ Column("x", Integer),
+ Column("y", Integer),
+ )
+ t1.append_constraint(
+ ForeignKeyConstraint([t1.c.x], [ref.c.id], name="fk1")
+ )
+ t1.append_constraint(
+ ForeignKeyConstraint([t1.c.y], [ref.c.id], name="fk2")
+ )
+
+ ref = Table(
+ "ref",
+ m2,
+ Column("id", Integer, primary_key=True),
+ )
+ Table(
+ "t",
+ m2,
+ Column("x", Integer),
+ Column("y", Integer),
+ )
+
+ if hook_type == "object":
+
+ def include_object(object_, name, type_, reflected, compare_to):
+ return not (
+ isinstance(object_, ForeignKeyConstraint)
+ and type_ == "foreign_key_constraint"
+ and reflected
+ and name == "fk1"
+ )
+
+ diffs = self._fixture(m1, m2, object_filters=include_object)
+ elif hook_type == "name":
+
+ def include_name(name, type_, parent_names):
+ if name == "fk1":
+ if type_ == "index": # MariaDB thing
+ return True
+ eq_(type_, "foreign_key_constraint")
+ eq_(
+ parent_names,
+ {
+ "schema_name": None,
+ "table_name": "t",
+ "schema_qualified_table_name": "t",
+ },
+ )
+ return False
+ else:
+ return True
+
+ diffs = self._fixture(m1, m2, name_filters=include_name)
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "t",
+ ["y"],
+ "ref",
+ ["id"],
+ conditional_name="fk2",
+ )
+ eq_(len(diffs), 1)
+
+ def test_add_metadata_fk(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "ref",
+ m1,
+ Column("id", Integer, primary_key=True),
+ )
+ Table(
+ "t",
+ m1,
+ Column("x", Integer),
+ Column("y", Integer),
+ )
+
+ ref = Table(
+ "ref",
+ m2,
+ Column("id", Integer, primary_key=True),
+ )
+ t2 = Table(
+ "t",
+ m2,
+ Column("x", Integer),
+ Column("y", Integer),
+ )
+ t2.append_constraint(
+ ForeignKeyConstraint([t2.c.x], [ref.c.id], name="fk1")
+ )
+ t2.append_constraint(
+ ForeignKeyConstraint([t2.c.y], [ref.c.id], name="fk2")
+ )
+
+ def include_object(object_, name, type_, reflected, compare_to):
+ return not (
+ isinstance(object_, ForeignKeyConstraint)
+ and type_ == "foreign_key_constraint"
+ and not reflected
+ and name == "fk1"
+ )
+
+ diffs = self._fixture(m1, m2, object_filters=include_object)
+
+ self._assert_fk_diff(
+ diffs[0], "add_fk", "t", ["y"], "ref", ["id"], name="fk2"
+ )
+ eq_(len(diffs), 1)
+
+ @combinations(("object",), ("name",))
+ @config.requirements.no_name_normalize
+ def test_change_fk(self, hook_type):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ r1a = Table(
+ "ref_a",
+ m1,
+ Column("a", Integer, primary_key=True),
+ )
+ Table(
+ "ref_b",
+ m1,
+ Column("a", Integer, primary_key=True),
+ Column("b", Integer, primary_key=True),
+ )
+ t1 = Table(
+ "t",
+ m1,
+ Column("x", Integer),
+ Column("y", Integer),
+ Column("z", Integer),
+ )
+ t1.append_constraint(
+ ForeignKeyConstraint([t1.c.x], [r1a.c.a], name="fk1")
+ )
+ t1.append_constraint(
+ ForeignKeyConstraint([t1.c.y], [r1a.c.a], name="fk2")
+ )
+
+ Table(
+ "ref_a",
+ m2,
+ Column("a", Integer, primary_key=True),
+ )
+ r2b = Table(
+ "ref_b",
+ m2,
+ Column("a", Integer, primary_key=True),
+ Column("b", Integer, primary_key=True),
+ )
+ t2 = Table(
+ "t",
+ m2,
+ Column("x", Integer),
+ Column("y", Integer),
+ Column("z", Integer),
+ )
+ t2.append_constraint(
+ ForeignKeyConstraint(
+ [t2.c.x, t2.c.z], [r2b.c.a, r2b.c.b], name="fk1"
+ )
+ )
+ t2.append_constraint(
+ ForeignKeyConstraint(
+ [t2.c.y, t2.c.z], [r2b.c.a, r2b.c.b], name="fk2"
+ )
+ )
+
+ if hook_type == "object":
+
+ def include_object(object_, name, type_, reflected, compare_to):
+ return not (
+ isinstance(object_, ForeignKeyConstraint)
+ and type_ == "foreign_key_constraint"
+ and name == "fk1"
+ )
+
+ diffs = self._fixture(m1, m2, object_filters=include_object)
+ elif hook_type == "name":
+
+ def include_name(name, type_, parent_names):
+ if type_ == "index":
+ return True # MariaDB thing
+
+ if name == "fk1":
+ eq_(type_, "foreign_key_constraint")
+ eq_(
+ parent_names,
+ {
+ "schema_name": None,
+ "table_name": "t",
+ "schema_qualified_table_name": "t",
+ },
+ )
+ return False
+ else:
+ return True
+
+ diffs = self._fixture(m1, m2, name_filters=include_name)
+
+ if hook_type == "object":
+ self._assert_fk_diff(
+ diffs[0], "remove_fk", "t", ["y"], "ref_a", ["a"], name="fk2"
+ )
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "t",
+ ["y", "z"],
+ "ref_b",
+ ["a", "b"],
+ name="fk2",
+ )
+ eq_(len(diffs), 2)
+ elif hook_type == "name":
+ eq_(
+ {(d[0], d[1].name) for d in diffs},
+ {("add_fk", "fk2"), ("add_fk", "fk1"), ("remove_fk", "fk2")},
+ )
+
+
+class AutogenerateFKOptionsTest(AutogenFixtureTest, TestBase):
+ __backend__ = True
+
+ def _fk_opts_fixture(self, old_opts, new_opts):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "some_table",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("test", String(10)),
+ )
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("tid", Integer),
+ ForeignKeyConstraint(["tid"], ["some_table.id"], **old_opts),
+ )
+
+ Table(
+ "some_table",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("test", String(10)),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50), nullable=False),
+ Column("tid", Integer),
+ ForeignKeyConstraint(["tid"], ["some_table.id"], **new_opts),
+ )
+
+ return self._fixture(m1, m2)
+
+ @config.requirements.fk_ondelete_is_reflected
+ def test_add_ondelete(self):
+ diffs = self._fk_opts_fixture({}, {"ondelete": "cascade"})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ ondelete=None,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ ondelete="cascade",
+ )
+
+ @config.requirements.fk_ondelete_is_reflected
+ def test_remove_ondelete(self):
+ diffs = self._fk_opts_fixture({"ondelete": "CASCADE"}, {})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ ondelete="CASCADE",
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ ondelete=None,
+ )
+
+ def test_nochange_ondelete(self):
+ """test case sensitivity"""
+ diffs = self._fk_opts_fixture(
+ {"ondelete": "caSCAde"}, {"ondelete": "CasCade"}
+ )
+ eq_(diffs, [])
+
+ @config.requirements.fk_onupdate_is_reflected
+ def test_add_onupdate(self):
+ diffs = self._fk_opts_fixture({}, {"onupdate": "cascade"})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate=None,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate="cascade",
+ )
+
+ @config.requirements.fk_onupdate_is_reflected
+ def test_remove_onupdate(self):
+ diffs = self._fk_opts_fixture({"onupdate": "CASCADE"}, {})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate="CASCADE",
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate=None,
+ )
+
+ @config.requirements.fk_onupdate
+ def test_nochange_onupdate(self):
+ """test case sensitivity"""
+ diffs = self._fk_opts_fixture(
+ {"onupdate": "caSCAde"}, {"onupdate": "CasCade"}
+ )
+ eq_(diffs, [])
+
+ @config.requirements.fk_ondelete_restrict
+ def test_nochange_ondelete_restrict(self):
+ """test the RESTRICT option which MySQL doesn't report on"""
+
+ diffs = self._fk_opts_fixture(
+ {"ondelete": "restrict"}, {"ondelete": "restrict"}
+ )
+ eq_(diffs, [])
+
+ @config.requirements.fk_onupdate_restrict
+ def test_nochange_onupdate_restrict(self):
+ """test the RESTRICT option which MySQL doesn't report on"""
+
+ diffs = self._fk_opts_fixture(
+ {"onupdate": "restrict"}, {"onupdate": "restrict"}
+ )
+ eq_(diffs, [])
+
+ @config.requirements.fk_ondelete_noaction
+ def test_nochange_ondelete_noaction(self):
+ """test the NO ACTION option which generally comes back as None"""
+
+ diffs = self._fk_opts_fixture(
+ {"ondelete": "no action"}, {"ondelete": "no action"}
+ )
+ eq_(diffs, [])
+
+ @config.requirements.fk_onupdate
+ def test_nochange_onupdate_noaction(self):
+ """test the NO ACTION option which generally comes back as None"""
+
+ diffs = self._fk_opts_fixture(
+ {"onupdate": "no action"}, {"onupdate": "no action"}
+ )
+ eq_(diffs, [])
+
+ @config.requirements.fk_ondelete_restrict
+ def test_change_ondelete_from_restrict(self):
+ """test the RESTRICT option which MySQL doesn't report on"""
+
+ # note that this is impossible to detect if we change
+ # from RESTRICT to NO ACTION on MySQL.
+ diffs = self._fk_opts_fixture(
+ {"ondelete": "restrict"}, {"ondelete": "cascade"}
+ )
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate=None,
+ ondelete=mock.ANY, # MySQL reports None, PG reports RESTRICT
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate=None,
+ ondelete="cascade",
+ )
+
+ @config.requirements.fk_ondelete_restrict
+ def test_change_onupdate_from_restrict(self):
+ """test the RESTRICT option which MySQL doesn't report on"""
+
+ # note that this is impossible to detect if we change
+ # from RESTRICT to NO ACTION on MySQL.
+ diffs = self._fk_opts_fixture(
+ {"onupdate": "restrict"}, {"onupdate": "cascade"}
+ )
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate=mock.ANY, # MySQL reports None, PG reports RESTRICT
+ ondelete=None,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate="cascade",
+ ondelete=None,
+ )
+
+ @config.requirements.fk_ondelete_is_reflected
+ @config.requirements.fk_onupdate_is_reflected
+ def test_ondelete_onupdate_combo(self):
+ diffs = self._fk_opts_fixture(
+ {"onupdate": "CASCADE", "ondelete": "SET NULL"},
+ {"onupdate": "RESTRICT", "ondelete": "RESTRICT"},
+ )
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate="CASCADE",
+ ondelete="SET NULL",
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ onupdate="RESTRICT",
+ ondelete="RESTRICT",
+ )
+
+ @config.requirements.fk_initially
+ def test_add_initially_deferred(self):
+ diffs = self._fk_opts_fixture({}, {"initially": "deferred"})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially=None,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially="deferred",
+ )
+
+ @config.requirements.fk_initially
+ def test_remove_initially_deferred(self):
+ diffs = self._fk_opts_fixture({"initially": "deferred"}, {})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially="DEFERRED",
+ deferrable=True,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially=None,
+ )
+
+ @config.requirements.fk_deferrable
+ @config.requirements.fk_initially
+ def test_add_initially_immediate_plus_deferrable(self):
+ diffs = self._fk_opts_fixture(
+ {}, {"initially": "immediate", "deferrable": True}
+ )
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially=None,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially="immediate",
+ deferrable=True,
+ )
+
+ @config.requirements.fk_deferrable
+ @config.requirements.fk_initially
+ def test_remove_initially_immediate_plus_deferrable(self):
+ diffs = self._fk_opts_fixture(
+ {"initially": "immediate", "deferrable": True}, {}
+ )
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially=None, # immediate is the default
+ deferrable=True,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ initially=None,
+ deferrable=None,
+ )
+
+ @config.requirements.fk_initially
+ @config.requirements.fk_deferrable
+ def test_add_initially_deferrable_nochange_one(self):
+ diffs = self._fk_opts_fixture(
+ {"deferrable": True, "initially": "immediate"},
+ {"deferrable": True, "initially": "immediate"},
+ )
+
+ eq_(diffs, [])
+
+ @config.requirements.fk_initially
+ @config.requirements.fk_deferrable
+ def test_add_initially_deferrable_nochange_two(self):
+ diffs = self._fk_opts_fixture(
+ {"deferrable": True, "initially": "deferred"},
+ {"deferrable": True, "initially": "deferred"},
+ )
+
+ eq_(diffs, [])
+
+ @config.requirements.fk_initially
+ @config.requirements.fk_deferrable
+ def test_add_initially_deferrable_nochange_three(self):
+ diffs = self._fk_opts_fixture(
+ {"deferrable": None, "initially": "deferred"},
+ {"deferrable": None, "initially": "deferred"},
+ )
+
+ eq_(diffs, [])
+
+ @config.requirements.fk_deferrable
+ def test_add_deferrable(self):
+ diffs = self._fk_opts_fixture({}, {"deferrable": True})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ deferrable=None,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ deferrable=True,
+ )
+
+ @config.requirements.fk_deferrable_is_reflected
+ def test_remove_deferrable(self):
+ diffs = self._fk_opts_fixture({"deferrable": True}, {})
+
+ self._assert_fk_diff(
+ diffs[0],
+ "remove_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ deferrable=True,
+ conditional_name="servergenerated",
+ )
+
+ self._assert_fk_diff(
+ diffs[1],
+ "add_fk",
+ "user",
+ ["tid"],
+ "some_table",
+ ["id"],
+ deferrable=None,
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_identity.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_identity.py
new file mode 100644
index 0000000..3dee9fc
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_autogen_identity.py
@@ -0,0 +1,226 @@
+import sqlalchemy as sa
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import Table
+
+from alembic.util import sqla_compat
+from ._autogen_fixtures import AutogenFixtureTest
+from ... import testing
+from ...testing import config
+from ...testing import eq_
+from ...testing import is_true
+from ...testing import TestBase
+
+
+class AutogenerateIdentityTest(AutogenFixtureTest, TestBase):
+ __requires__ = ("identity_columns",)
+ __backend__ = True
+
+ def test_add_identity_column(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table("user", m1, Column("other", sa.Text))
+
+ Table(
+ "user",
+ m2,
+ Column("other", sa.Text),
+ Column(
+ "id",
+ Integer,
+ sa.Identity(start=5, increment=7),
+ primary_key=True,
+ ),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0], "add_column")
+ eq_(diffs[0][2], "user")
+ eq_(diffs[0][3].name, "id")
+ i = diffs[0][3].identity
+
+ is_true(isinstance(i, sa.Identity))
+ eq_(i.start, 5)
+ eq_(i.increment, 7)
+
+ def test_remove_identity_column(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "user",
+ m1,
+ Column(
+ "id",
+ Integer,
+ sa.Identity(start=2, increment=3),
+ primary_key=True,
+ ),
+ )
+
+ Table("user", m2)
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs[0][0], "remove_column")
+ eq_(diffs[0][2], "user")
+ c = diffs[0][3]
+ eq_(c.name, "id")
+
+ is_true(isinstance(c.identity, sa.Identity))
+ eq_(c.identity.start, 2)
+ eq_(c.identity.increment, 3)
+
+ def test_no_change_identity_column(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ for m in (m1, m2):
+ id_ = sa.Identity(start=2)
+ Table("user", m, Column("id", Integer, id_))
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(diffs, [])
+
+ def test_dialect_kwargs_changes(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ if sqla_compat.identity_has_dialect_kwargs:
+ args = {"oracle_on_null": True, "oracle_order": True}
+ else:
+ args = {"on_null": True, "order": True}
+
+ Table("user", m1, Column("id", Integer, sa.Identity(start=2)))
+ id_ = sa.Identity(start=2, **args)
+ Table("user", m2, Column("id", Integer, id_))
+
+ diffs = self._fixture(m1, m2)
+ if config.db.name == "oracle":
+ is_true(len(diffs), 1)
+ eq_(diffs[0][0][0], "modify_default")
+ else:
+ eq_(diffs, [])
+
+ @testing.combinations(
+ (None, dict(start=2)),
+ (dict(start=2), None),
+ (dict(start=2), dict(start=2, increment=7)),
+ (dict(always=False), dict(always=True)),
+ (
+ dict(start=1, minvalue=0, maxvalue=100, cycle=True),
+ dict(start=1, minvalue=0, maxvalue=100, cycle=False),
+ ),
+ (
+ dict(start=10, increment=3, maxvalue=9999),
+ dict(start=10, increment=1, maxvalue=3333),
+ ),
+ )
+ @config.requirements.identity_columns_alter
+ def test_change_identity(self, before, after):
+ arg_before = (sa.Identity(**before),) if before else ()
+ arg_after = (sa.Identity(**after),) if after else ()
+
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, *arg_before),
+ Column("other", sa.Text),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, *arg_after),
+ Column("other", sa.Text),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(len(diffs[0]), 1)
+ diffs = diffs[0][0]
+ eq_(diffs[0], "modify_default")
+ eq_(diffs[2], "user")
+ eq_(diffs[3], "id")
+ old = diffs[5]
+ new = diffs[6]
+
+ def check(kw, idt):
+ if kw:
+ is_true(isinstance(idt, sa.Identity))
+ for k, v in kw.items():
+ eq_(getattr(idt, k), v)
+ else:
+ is_true(idt in (None, False))
+
+ check(before, old)
+ check(after, new)
+
+ def test_add_identity_to_column(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer),
+ Column("other", sa.Text),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer, sa.Identity(start=2, maxvalue=1000)),
+ Column("other", sa.Text),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(len(diffs[0]), 1)
+ diffs = diffs[0][0]
+ eq_(diffs[0], "modify_default")
+ eq_(diffs[2], "user")
+ eq_(diffs[3], "id")
+ eq_(diffs[5], None)
+ added = diffs[6]
+
+ is_true(isinstance(added, sa.Identity))
+ eq_(added.start, 2)
+ eq_(added.maxvalue, 1000)
+
+ def test_remove_identity_from_column(self):
+ m1 = MetaData()
+ m2 = MetaData()
+
+ Table(
+ "user",
+ m1,
+ Column("id", Integer, sa.Identity(start=2, maxvalue=1000)),
+ Column("other", sa.Text),
+ )
+
+ Table(
+ "user",
+ m2,
+ Column("id", Integer),
+ Column("other", sa.Text),
+ )
+
+ diffs = self._fixture(m1, m2)
+
+ eq_(len(diffs[0]), 1)
+ diffs = diffs[0][0]
+ eq_(diffs[0], "modify_default")
+ eq_(diffs[2], "user")
+ eq_(diffs[3], "id")
+ eq_(diffs[6], None)
+ removed = diffs[5]
+
+ is_true(isinstance(removed, sa.Identity))
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/test_environment.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_environment.py
new file mode 100644
index 0000000..8c86859
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_environment.py
@@ -0,0 +1,364 @@
+import io
+
+from ...migration import MigrationContext
+from ...testing import assert_raises
+from ...testing import config
+from ...testing import eq_
+from ...testing import is_
+from ...testing import is_false
+from ...testing import is_not_
+from ...testing import is_true
+from ...testing import ne_
+from ...testing.fixtures import TestBase
+
+
+class MigrationTransactionTest(TestBase):
+ __backend__ = True
+
+ conn = None
+
+ def _fixture(self, opts):
+ self.conn = conn = config.db.connect()
+
+ if opts.get("as_sql", False):
+ self.context = MigrationContext.configure(
+ dialect=conn.dialect, opts=opts
+ )
+ self.context.output_buffer = (
+ self.context.impl.output_buffer
+ ) = io.StringIO()
+ else:
+ self.context = MigrationContext.configure(
+ connection=conn, opts=opts
+ )
+ return self.context
+
+ def teardown_method(self):
+ if self.conn:
+ self.conn.close()
+
+ def test_proxy_transaction_rollback(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": True}
+ )
+
+ is_false(self.conn.in_transaction())
+ proxy = context.begin_transaction(_per_migration=True)
+ is_true(self.conn.in_transaction())
+ proxy.rollback()
+ is_false(self.conn.in_transaction())
+
+ def test_proxy_transaction_commit(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": True}
+ )
+ proxy = context.begin_transaction(_per_migration=True)
+ is_true(self.conn.in_transaction())
+ proxy.commit()
+ is_false(self.conn.in_transaction())
+
+ def test_proxy_transaction_contextmanager_commit(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": True}
+ )
+ proxy = context.begin_transaction(_per_migration=True)
+ is_true(self.conn.in_transaction())
+ with proxy:
+ pass
+ is_false(self.conn.in_transaction())
+
+ def test_proxy_transaction_contextmanager_rollback(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": True}
+ )
+ proxy = context.begin_transaction(_per_migration=True)
+ is_true(self.conn.in_transaction())
+
+ def go():
+ with proxy:
+ raise Exception("hi")
+
+ assert_raises(Exception, go)
+ is_false(self.conn.in_transaction())
+
+ def test_proxy_transaction_contextmanager_explicit_rollback(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": True}
+ )
+ proxy = context.begin_transaction(_per_migration=True)
+ is_true(self.conn.in_transaction())
+
+ with proxy:
+ is_true(self.conn.in_transaction())
+ proxy.rollback()
+ is_false(self.conn.in_transaction())
+
+ is_false(self.conn.in_transaction())
+
+ def test_proxy_transaction_contextmanager_explicit_commit(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": True}
+ )
+ proxy = context.begin_transaction(_per_migration=True)
+ is_true(self.conn.in_transaction())
+
+ with proxy:
+ is_true(self.conn.in_transaction())
+ proxy.commit()
+ is_false(self.conn.in_transaction())
+
+ is_false(self.conn.in_transaction())
+
+ def test_transaction_per_migration_transactional_ddl(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": True}
+ )
+
+ is_false(self.conn.in_transaction())
+
+ with context.begin_transaction():
+ is_false(self.conn.in_transaction())
+ with context.begin_transaction(_per_migration=True):
+ is_true(self.conn.in_transaction())
+
+ is_false(self.conn.in_transaction())
+ is_false(self.conn.in_transaction())
+
+ def test_transaction_per_migration_non_transactional_ddl(self):
+ context = self._fixture(
+ {"transaction_per_migration": True, "transactional_ddl": False}
+ )
+
+ is_false(self.conn.in_transaction())
+
+ with context.begin_transaction():
+ is_false(self.conn.in_transaction())
+ with context.begin_transaction(_per_migration=True):
+ is_true(self.conn.in_transaction())
+
+ is_false(self.conn.in_transaction())
+ is_false(self.conn.in_transaction())
+
+ def test_transaction_per_all_transactional_ddl(self):
+ context = self._fixture({"transactional_ddl": True})
+
+ is_false(self.conn.in_transaction())
+
+ with context.begin_transaction():
+ is_true(self.conn.in_transaction())
+ with context.begin_transaction(_per_migration=True):
+ is_true(self.conn.in_transaction())
+
+ is_true(self.conn.in_transaction())
+ is_false(self.conn.in_transaction())
+
+ def test_transaction_per_all_non_transactional_ddl(self):
+ context = self._fixture({"transactional_ddl": False})
+
+ is_false(self.conn.in_transaction())
+
+ with context.begin_transaction():
+ is_false(self.conn.in_transaction())
+ with context.begin_transaction(_per_migration=True):
+ is_true(self.conn.in_transaction())
+
+ is_false(self.conn.in_transaction())
+ is_false(self.conn.in_transaction())
+
+ def test_transaction_per_all_sqlmode(self):
+ context = self._fixture({"as_sql": True})
+
+ context.execute("step 1")
+ with context.begin_transaction():
+ context.execute("step 2")
+ with context.begin_transaction(_per_migration=True):
+ context.execute("step 3")
+
+ context.execute("step 4")
+ context.execute("step 5")
+
+ if context.impl.transactional_ddl:
+ self._assert_impl_steps(
+ "step 1",
+ "BEGIN",
+ "step 2",
+ "step 3",
+ "step 4",
+ "COMMIT",
+ "step 5",
+ )
+ else:
+ self._assert_impl_steps(
+ "step 1", "step 2", "step 3", "step 4", "step 5"
+ )
+
+ def test_transaction_per_migration_sqlmode(self):
+ context = self._fixture(
+ {"as_sql": True, "transaction_per_migration": True}
+ )
+
+ context.execute("step 1")
+ with context.begin_transaction():
+ context.execute("step 2")
+ with context.begin_transaction(_per_migration=True):
+ context.execute("step 3")
+
+ context.execute("step 4")
+ context.execute("step 5")
+
+ if context.impl.transactional_ddl:
+ self._assert_impl_steps(
+ "step 1",
+ "step 2",
+ "BEGIN",
+ "step 3",
+ "COMMIT",
+ "step 4",
+ "step 5",
+ )
+ else:
+ self._assert_impl_steps(
+ "step 1", "step 2", "step 3", "step 4", "step 5"
+ )
+
+ @config.requirements.autocommit_isolation
+ def test_autocommit_block(self):
+ context = self._fixture({"transaction_per_migration": True})
+
+ is_false(self.conn.in_transaction())
+
+ with context.begin_transaction():
+ is_false(self.conn.in_transaction())
+ with context.begin_transaction(_per_migration=True):
+ is_true(self.conn.in_transaction())
+
+ with context.autocommit_block():
+ # in 1.x, self.conn is separate due to the
+ # execution_options call. however for future they are the
+ # same connection and there is a "transaction" block
+ # despite autocommit
+ if self.is_sqlalchemy_future:
+ is_(context.connection, self.conn)
+ else:
+ is_not_(context.connection, self.conn)
+ is_false(self.conn.in_transaction())
+
+ eq_(
+ context.connection._execution_options[
+ "isolation_level"
+ ],
+ "AUTOCOMMIT",
+ )
+
+ ne_(
+ context.connection._execution_options.get(
+ "isolation_level", None
+ ),
+ "AUTOCOMMIT",
+ )
+ is_true(self.conn.in_transaction())
+
+ is_false(self.conn.in_transaction())
+ is_false(self.conn.in_transaction())
+
+ @config.requirements.autocommit_isolation
+ def test_autocommit_block_no_transaction(self):
+ context = self._fixture({"transaction_per_migration": True})
+
+ is_false(self.conn.in_transaction())
+
+ with context.autocommit_block():
+ is_true(context.connection.in_transaction())
+
+ # in 1.x, self.conn is separate due to the execution_options
+ # call. however for future they are the same connection and there
+ # is a "transaction" block despite autocommit
+ if self.is_sqlalchemy_future:
+ is_(context.connection, self.conn)
+ else:
+ is_not_(context.connection, self.conn)
+ is_false(self.conn.in_transaction())
+
+ eq_(
+ context.connection._execution_options["isolation_level"],
+ "AUTOCOMMIT",
+ )
+
+ ne_(
+ context.connection._execution_options.get("isolation_level", None),
+ "AUTOCOMMIT",
+ )
+
+ is_false(self.conn.in_transaction())
+
+ def test_autocommit_block_transactional_ddl_sqlmode(self):
+ context = self._fixture(
+ {
+ "transaction_per_migration": True,
+ "transactional_ddl": True,
+ "as_sql": True,
+ }
+ )
+
+ with context.begin_transaction():
+ context.execute("step 1")
+ with context.begin_transaction(_per_migration=True):
+ context.execute("step 2")
+
+ with context.autocommit_block():
+ context.execute("step 3")
+
+ context.execute("step 4")
+
+ context.execute("step 5")
+
+ self._assert_impl_steps(
+ "step 1",
+ "BEGIN",
+ "step 2",
+ "COMMIT",
+ "step 3",
+ "BEGIN",
+ "step 4",
+ "COMMIT",
+ "step 5",
+ )
+
+ def test_autocommit_block_nontransactional_ddl_sqlmode(self):
+ context = self._fixture(
+ {
+ "transaction_per_migration": True,
+ "transactional_ddl": False,
+ "as_sql": True,
+ }
+ )
+
+ with context.begin_transaction():
+ context.execute("step 1")
+ with context.begin_transaction(_per_migration=True):
+ context.execute("step 2")
+
+ with context.autocommit_block():
+ context.execute("step 3")
+
+ context.execute("step 4")
+
+ context.execute("step 5")
+
+ self._assert_impl_steps(
+ "step 1", "step 2", "step 3", "step 4", "step 5"
+ )
+
+ def _assert_impl_steps(self, *steps):
+ to_check = self.context.output_buffer.getvalue()
+
+ self.context.impl.output_buffer = buf = io.StringIO()
+ for step in steps:
+ if step == "BEGIN":
+ self.context.impl.emit_begin()
+ elif step == "COMMIT":
+ self.context.impl.emit_commit()
+ else:
+ self.context.impl._exec(step)
+
+ eq_(to_check, buf.getvalue())
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/suite/test_op.py b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_op.py
new file mode 100644
index 0000000..a63b3f2
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/suite/test_op.py
@@ -0,0 +1,42 @@
+"""Test against the builders in the op.* module."""
+
+from sqlalchemy import Column
+from sqlalchemy import event
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy.sql import text
+
+from ...testing.fixtures import AlterColRoundTripFixture
+from ...testing.fixtures import TestBase
+
+
+@event.listens_for(Table, "after_parent_attach")
+def _add_cols(table, metadata):
+ if table.name == "tbl_with_auto_appended_column":
+ table.append_column(Column("bat", Integer))
+
+
+class BackendAlterColumnTest(AlterColRoundTripFixture, TestBase):
+ __backend__ = True
+
+ def test_rename_column(self):
+ self._run_alter_col({}, {"name": "newname"})
+
+ def test_modify_type_int_str(self):
+ self._run_alter_col({"type": Integer()}, {"type": String(50)})
+
+ def test_add_server_default_int(self):
+ self._run_alter_col({"type": Integer}, {"server_default": text("5")})
+
+ def test_modify_server_default_int(self):
+ self._run_alter_col(
+ {"type": Integer, "server_default": text("2")},
+ {"server_default": text("5")},
+ )
+
+ def test_modify_nullable_to_non(self):
+ self._run_alter_col({}, {"nullable": False})
+
+ def test_modify_non_nullable_to_nullable(self):
+ self._run_alter_col({"nullable": False}, {"nullable": True})
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/util.py b/venv/lib/python3.12/site-packages/alembic/testing/util.py
new file mode 100644
index 0000000..4517a69
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/util.py
@@ -0,0 +1,126 @@
+# testing/util.py
+# Copyright (C) 2005-2019 the SQLAlchemy authors and contributors
+#
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+from __future__ import annotations
+
+import types
+from typing import Union
+
+from sqlalchemy.util import inspect_getfullargspec
+
+from ..util import sqla_2
+
+
+def flag_combinations(*combinations):
+ """A facade around @testing.combinations() oriented towards boolean
+ keyword-based arguments.
+
+ Basically generates a nice looking identifier based on the keywords
+ and also sets up the argument names.
+
+ E.g.::
+
+ @testing.flag_combinations(
+ dict(lazy=False, passive=False),
+ dict(lazy=True, passive=False),
+ dict(lazy=False, passive=True),
+ dict(lazy=False, passive=True, raiseload=True),
+ )
+
+
+ would result in::
+
+ @testing.combinations(
+ ('', False, False, False),
+ ('lazy', True, False, False),
+ ('lazy_passive', True, True, False),
+ ('lazy_passive', True, True, True),
+ id_='iaaa',
+ argnames='lazy,passive,raiseload'
+ )
+
+ """
+ from sqlalchemy.testing import config
+
+ keys = set()
+
+ for d in combinations:
+ keys.update(d)
+
+ keys = sorted(keys)
+
+ return config.combinations(
+ *[
+ ("_".join(k for k in keys if d.get(k, False)),)
+ + tuple(d.get(k, False) for k in keys)
+ for d in combinations
+ ],
+ id_="i" + ("a" * len(keys)),
+ argnames=",".join(keys),
+ )
+
+
+def resolve_lambda(__fn, **kw):
+ """Given a no-arg lambda and a namespace, return a new lambda that
+ has all the values filled in.
+
+ This is used so that we can have module-level fixtures that
+ refer to instance-level variables using lambdas.
+
+ """
+
+ pos_args = inspect_getfullargspec(__fn)[0]
+ pass_pos_args = {arg: kw.pop(arg) for arg in pos_args}
+ glb = dict(__fn.__globals__)
+ glb.update(kw)
+ new_fn = types.FunctionType(__fn.__code__, glb)
+ return new_fn(**pass_pos_args)
+
+
+def metadata_fixture(ddl="function"):
+ """Provide MetaData for a pytest fixture."""
+
+ from sqlalchemy.testing import config
+ from . import fixture_functions
+
+ def decorate(fn):
+ def run_ddl(self):
+ from sqlalchemy import schema
+
+ metadata = self.metadata = schema.MetaData()
+ try:
+ result = fn(self, metadata)
+ metadata.create_all(config.db)
+ # TODO:
+ # somehow get a per-function dml erase fixture here
+ yield result
+ finally:
+ metadata.drop_all(config.db)
+
+ return fixture_functions.fixture(scope=ddl)(run_ddl)
+
+ return decorate
+
+
+def _safe_int(value: str) -> Union[int, str]:
+ try:
+ return int(value)
+ except:
+ return value
+
+
+def testing_engine(url=None, options=None, future=False):
+ from sqlalchemy.testing import config
+ from sqlalchemy.testing.engines import testing_engine
+
+ if not future:
+ future = getattr(config._current.options, "future_engine", False)
+
+ if not sqla_2:
+ kw = {"future": future} if future else {}
+ else:
+ kw = {}
+ return testing_engine(url, options, **kw)
diff --git a/venv/lib/python3.12/site-packages/alembic/testing/warnings.py b/venv/lib/python3.12/site-packages/alembic/testing/warnings.py
new file mode 100644
index 0000000..e87136b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/testing/warnings.py
@@ -0,0 +1,40 @@
+# testing/warnings.py
+# Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
+#
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+
+import warnings
+
+from sqlalchemy import exc as sa_exc
+
+from ..util import sqla_14
+
+
+def setup_filters():
+ """Set global warning behavior for the test suite."""
+
+ warnings.resetwarnings()
+
+ warnings.filterwarnings("error", category=sa_exc.SADeprecationWarning)
+ warnings.filterwarnings("error", category=sa_exc.SAWarning)
+
+ # some selected deprecations...
+ warnings.filterwarnings("error", category=DeprecationWarning)
+ if not sqla_14:
+ # 1.3 uses pkg_resources in PluginLoader
+ warnings.filterwarnings(
+ "ignore",
+ "pkg_resources is deprecated as an API",
+ DeprecationWarning,
+ )
+ try:
+ import pytest
+ except ImportError:
+ pass
+ else:
+ warnings.filterwarnings(
+ "once", category=pytest.PytestDeprecationWarning
+ )
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__init__.py b/venv/lib/python3.12/site-packages/alembic/util/__init__.py
new file mode 100644
index 0000000..4724e1f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/__init__.py
@@ -0,0 +1,35 @@
+from .editor import open_in_editor as open_in_editor
+from .exc import AutogenerateDiffsDetected as AutogenerateDiffsDetected
+from .exc import CommandError as CommandError
+from .langhelpers import _with_legacy_names as _with_legacy_names
+from .langhelpers import asbool as asbool
+from .langhelpers import dedupe_tuple as dedupe_tuple
+from .langhelpers import Dispatcher as Dispatcher
+from .langhelpers import EMPTY_DICT as EMPTY_DICT
+from .langhelpers import immutabledict as immutabledict
+from .langhelpers import memoized_property as memoized_property
+from .langhelpers import ModuleClsProxy as ModuleClsProxy
+from .langhelpers import not_none as not_none
+from .langhelpers import rev_id as rev_id
+from .langhelpers import to_list as to_list
+from .langhelpers import to_tuple as to_tuple
+from .langhelpers import unique_list as unique_list
+from .messaging import err as err
+from .messaging import format_as_comma as format_as_comma
+from .messaging import msg as msg
+from .messaging import obfuscate_url_pw as obfuscate_url_pw
+from .messaging import status as status
+from .messaging import warn as warn
+from .messaging import write_outstream as write_outstream
+from .pyfiles import coerce_resource_to_filename as coerce_resource_to_filename
+from .pyfiles import load_python_file as load_python_file
+from .pyfiles import pyc_file_from_path as pyc_file_from_path
+from .pyfiles import template_to_file as template_to_file
+from .sqla_compat import has_computed as has_computed
+from .sqla_compat import sqla_13 as sqla_13
+from .sqla_compat import sqla_14 as sqla_14
+from .sqla_compat import sqla_2 as sqla_2
+
+
+if not sqla_13:
+ raise CommandError("SQLAlchemy 1.3.0 or greater is required.")
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..9972f22
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/compat.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/compat.cpython-312.pyc
new file mode 100644
index 0000000..c3e87d4
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/compat.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/editor.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/editor.cpython-312.pyc
new file mode 100644
index 0000000..d915576
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/editor.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/exc.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/exc.cpython-312.pyc
new file mode 100644
index 0000000..1af1096
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/exc.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/langhelpers.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/langhelpers.cpython-312.pyc
new file mode 100644
index 0000000..631af77
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/langhelpers.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/messaging.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/messaging.cpython-312.pyc
new file mode 100644
index 0000000..0a1bb07
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/messaging.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/pyfiles.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/pyfiles.cpython-312.pyc
new file mode 100644
index 0000000..ddf4ebf
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/pyfiles.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/__pycache__/sqla_compat.cpython-312.pyc b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/sqla_compat.cpython-312.pyc
new file mode 100644
index 0000000..2cb4d7a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/alembic/util/__pycache__/sqla_compat.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/alembic/util/compat.py b/venv/lib/python3.12/site-packages/alembic/util/compat.py
new file mode 100644
index 0000000..e185cc4
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/compat.py
@@ -0,0 +1,89 @@
+# mypy: no-warn-unused-ignores
+
+from __future__ import annotations
+
+from configparser import ConfigParser
+import io
+import os
+import sys
+import typing
+from typing import Any
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Union
+
+if True:
+ # zimports hack for too-long names
+ from sqlalchemy.util import ( # noqa: F401
+ inspect_getfullargspec as inspect_getfullargspec,
+ )
+ from sqlalchemy.util.compat import ( # noqa: F401
+ inspect_formatargspec as inspect_formatargspec,
+ )
+
+is_posix = os.name == "posix"
+
+py311 = sys.version_info >= (3, 11)
+py310 = sys.version_info >= (3, 10)
+py39 = sys.version_info >= (3, 9)
+
+
+# produce a wrapper that allows encoded text to stream
+# into a given buffer, but doesn't close it.
+# not sure of a more idiomatic approach to this.
+class EncodedIO(io.TextIOWrapper):
+ def close(self) -> None:
+ pass
+
+
+if py39:
+ from importlib import resources as _resources
+
+ importlib_resources = _resources
+ from importlib import metadata as _metadata
+
+ importlib_metadata = _metadata
+ from importlib.metadata import EntryPoint as EntryPoint
+else:
+ import importlib_resources # type:ignore # noqa
+ import importlib_metadata # type:ignore # noqa
+ from importlib_metadata import EntryPoint # type:ignore # noqa
+
+
+def importlib_metadata_get(group: str) -> Sequence[EntryPoint]:
+ ep = importlib_metadata.entry_points()
+ if hasattr(ep, "select"):
+ return ep.select(group=group)
+ else:
+ return ep.get(group, ()) # type: ignore
+
+
+def formatannotation_fwdref(
+ annotation: Any, base_module: Optional[Any] = None
+) -> str:
+ """vendored from python 3.7"""
+ # copied over _formatannotation from sqlalchemy 2.0
+
+ if isinstance(annotation, str):
+ return annotation
+
+ if getattr(annotation, "__module__", None) == "typing":
+ return repr(annotation).replace("typing.", "").replace("~", "")
+ if isinstance(annotation, type):
+ if annotation.__module__ in ("builtins", base_module):
+ return repr(annotation.__qualname__)
+ return annotation.__module__ + "." + annotation.__qualname__
+ elif isinstance(annotation, typing.TypeVar):
+ return repr(annotation).replace("~", "")
+ return repr(annotation).replace("~", "")
+
+
+def read_config_parser(
+ file_config: ConfigParser,
+ file_argument: Sequence[Union[str, os.PathLike[str]]],
+) -> List[str]:
+ if py310:
+ return file_config.read(file_argument, encoding="locale")
+ else:
+ return file_config.read(file_argument)
diff --git a/venv/lib/python3.12/site-packages/alembic/util/editor.py b/venv/lib/python3.12/site-packages/alembic/util/editor.py
new file mode 100644
index 0000000..f1d1557
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/editor.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+import os
+from os.path import exists
+from os.path import join
+from os.path import splitext
+from subprocess import check_call
+from typing import Dict
+from typing import List
+from typing import Mapping
+from typing import Optional
+
+from .compat import is_posix
+from .exc import CommandError
+
+
+def open_in_editor(
+ filename: str, environ: Optional[Dict[str, str]] = None
+) -> None:
+ """
+ Opens the given file in a text editor. If the environment variable
+ ``EDITOR`` is set, this is taken as preference.
+
+ Otherwise, a list of commonly installed editors is tried.
+
+ If no editor matches, an :py:exc:`OSError` is raised.
+
+ :param filename: The filename to open. Will be passed verbatim to the
+ editor command.
+ :param environ: An optional drop-in replacement for ``os.environ``. Used
+ mainly for testing.
+ """
+ env = os.environ if environ is None else environ
+ try:
+ editor = _find_editor(env)
+ check_call([editor, filename])
+ except Exception as exc:
+ raise CommandError("Error executing editor (%s)" % (exc,)) from exc
+
+
+def _find_editor(environ: Mapping[str, str]) -> str:
+ candidates = _default_editors()
+ for i, var in enumerate(("EDITOR", "VISUAL")):
+ if var in environ:
+ user_choice = environ[var]
+ if exists(user_choice):
+ return user_choice
+ if os.sep not in user_choice:
+ candidates.insert(i, user_choice)
+
+ for candidate in candidates:
+ path = _find_executable(candidate, environ)
+ if path is not None:
+ return path
+ raise OSError(
+ "No suitable editor found. Please set the "
+ '"EDITOR" or "VISUAL" environment variables'
+ )
+
+
+def _find_executable(
+ candidate: str, environ: Mapping[str, str]
+) -> Optional[str]:
+ # Assuming this is on the PATH, we need to determine it's absolute
+ # location. Otherwise, ``check_call`` will fail
+ if not is_posix and splitext(candidate)[1] != ".exe":
+ candidate += ".exe"
+ for path in environ.get("PATH", "").split(os.pathsep):
+ value = join(path, candidate)
+ if exists(value):
+ return value
+ return None
+
+
+def _default_editors() -> List[str]:
+ # Look for an editor. Prefer the user's choice by env-var, fall back to
+ # most commonly installed editor (nano/vim)
+ if is_posix:
+ return ["sensible-editor", "editor", "nano", "vim", "code"]
+ else:
+ return ["code.exe", "notepad++.exe", "notepad.exe"]
diff --git a/venv/lib/python3.12/site-packages/alembic/util/exc.py b/venv/lib/python3.12/site-packages/alembic/util/exc.py
new file mode 100644
index 0000000..0d0496b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/exc.py
@@ -0,0 +1,6 @@
+class CommandError(Exception):
+ pass
+
+
+class AutogenerateDiffsDetected(CommandError):
+ pass
diff --git a/venv/lib/python3.12/site-packages/alembic/util/langhelpers.py b/venv/lib/python3.12/site-packages/alembic/util/langhelpers.py
new file mode 100644
index 0000000..4a5bf09
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/langhelpers.py
@@ -0,0 +1,335 @@
+from __future__ import annotations
+
+import collections
+from collections.abc import Iterable
+import textwrap
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import List
+from typing import Mapping
+from typing import MutableMapping
+from typing import NoReturn
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+import uuid
+import warnings
+
+from sqlalchemy.util import asbool as asbool # noqa: F401
+from sqlalchemy.util import immutabledict as immutabledict # noqa: F401
+from sqlalchemy.util import to_list as to_list # noqa: F401
+from sqlalchemy.util import unique_list as unique_list
+
+from .compat import inspect_getfullargspec
+
+if True:
+ # zimports workaround :(
+ from sqlalchemy.util import ( # noqa: F401
+ memoized_property as memoized_property,
+ )
+
+
+EMPTY_DICT: Mapping[Any, Any] = immutabledict()
+_T = TypeVar("_T", bound=Any)
+
+_C = TypeVar("_C", bound=Callable[..., Any])
+
+
+class _ModuleClsMeta(type):
+ def __setattr__(cls, key: str, value: Callable[..., Any]) -> None:
+ super().__setattr__(key, value)
+ cls._update_module_proxies(key) # type: ignore
+
+
+class ModuleClsProxy(metaclass=_ModuleClsMeta):
+ """Create module level proxy functions for the
+ methods on a given class.
+
+ The functions will have a compatible signature
+ as the methods.
+
+ """
+
+ _setups: Dict[
+ Type[Any],
+ Tuple[
+ Set[str],
+ List[Tuple[MutableMapping[str, Any], MutableMapping[str, Any]]],
+ ],
+ ] = collections.defaultdict(lambda: (set(), []))
+
+ @classmethod
+ def _update_module_proxies(cls, name: str) -> None:
+ attr_names, modules = cls._setups[cls]
+ for globals_, locals_ in modules:
+ cls._add_proxied_attribute(name, globals_, locals_, attr_names)
+
+ def _install_proxy(self) -> None:
+ attr_names, modules = self._setups[self.__class__]
+ for globals_, locals_ in modules:
+ globals_["_proxy"] = self
+ for attr_name in attr_names:
+ globals_[attr_name] = getattr(self, attr_name)
+
+ def _remove_proxy(self) -> None:
+ attr_names, modules = self._setups[self.__class__]
+ for globals_, locals_ in modules:
+ globals_["_proxy"] = None
+ for attr_name in attr_names:
+ del globals_[attr_name]
+
+ @classmethod
+ def create_module_class_proxy(
+ cls,
+ globals_: MutableMapping[str, Any],
+ locals_: MutableMapping[str, Any],
+ ) -> None:
+ attr_names, modules = cls._setups[cls]
+ modules.append((globals_, locals_))
+ cls._setup_proxy(globals_, locals_, attr_names)
+
+ @classmethod
+ def _setup_proxy(
+ cls,
+ globals_: MutableMapping[str, Any],
+ locals_: MutableMapping[str, Any],
+ attr_names: Set[str],
+ ) -> None:
+ for methname in dir(cls):
+ cls._add_proxied_attribute(methname, globals_, locals_, attr_names)
+
+ @classmethod
+ def _add_proxied_attribute(
+ cls,
+ methname: str,
+ globals_: MutableMapping[str, Any],
+ locals_: MutableMapping[str, Any],
+ attr_names: Set[str],
+ ) -> None:
+ if not methname.startswith("_"):
+ meth = getattr(cls, methname)
+ if callable(meth):
+ locals_[methname] = cls._create_method_proxy(
+ methname, globals_, locals_
+ )
+ else:
+ attr_names.add(methname)
+
+ @classmethod
+ def _create_method_proxy(
+ cls,
+ name: str,
+ globals_: MutableMapping[str, Any],
+ locals_: MutableMapping[str, Any],
+ ) -> Callable[..., Any]:
+ fn = getattr(cls, name)
+
+ def _name_error(name: str, from_: Exception) -> NoReturn:
+ raise NameError(
+ "Can't invoke function '%s', as the proxy object has "
+ "not yet been "
+ "established for the Alembic '%s' class. "
+ "Try placing this code inside a callable."
+ % (name, cls.__name__)
+ ) from from_
+
+ globals_["_name_error"] = _name_error
+
+ translations = getattr(fn, "_legacy_translations", [])
+ if translations:
+ spec = inspect_getfullargspec(fn)
+ if spec[0] and spec[0][0] == "self":
+ spec[0].pop(0)
+
+ outer_args = inner_args = "*args, **kw"
+ translate_str = "args, kw = _translate(%r, %r, %r, args, kw)" % (
+ fn.__name__,
+ tuple(spec),
+ translations,
+ )
+
+ def translate(
+ fn_name: str, spec: Any, translations: Any, args: Any, kw: Any
+ ) -> Any:
+ return_kw = {}
+ return_args = []
+
+ for oldname, newname in translations:
+ if oldname in kw:
+ warnings.warn(
+ "Argument %r is now named %r "
+ "for method %s()." % (oldname, newname, fn_name)
+ )
+ return_kw[newname] = kw.pop(oldname)
+ return_kw.update(kw)
+
+ args = list(args)
+ if spec[3]:
+ pos_only = spec[0][: -len(spec[3])]
+ else:
+ pos_only = spec[0]
+ for arg in pos_only:
+ if arg not in return_kw:
+ try:
+ return_args.append(args.pop(0))
+ except IndexError:
+ raise TypeError(
+ "missing required positional argument: %s"
+ % arg
+ )
+ return_args.extend(args)
+
+ return return_args, return_kw
+
+ globals_["_translate"] = translate
+ else:
+ outer_args = "*args, **kw"
+ inner_args = "*args, **kw"
+ translate_str = ""
+
+ func_text = textwrap.dedent(
+ """\
+ def %(name)s(%(args)s):
+ %(doc)r
+ %(translate)s
+ try:
+ p = _proxy
+ except NameError as ne:
+ _name_error('%(name)s', ne)
+ return _proxy.%(name)s(%(apply_kw)s)
+ e
+ """
+ % {
+ "name": name,
+ "translate": translate_str,
+ "args": outer_args,
+ "apply_kw": inner_args,
+ "doc": fn.__doc__,
+ }
+ )
+ lcl: MutableMapping[str, Any] = {}
+
+ exec(func_text, cast("Dict[str, Any]", globals_), lcl)
+ return cast("Callable[..., Any]", lcl[name])
+
+
+def _with_legacy_names(translations: Any) -> Any:
+ def decorate(fn: _C) -> _C:
+ fn._legacy_translations = translations # type: ignore[attr-defined]
+ return fn
+
+ return decorate
+
+
+def rev_id() -> str:
+ return uuid.uuid4().hex[-12:]
+
+
+@overload
+def to_tuple(x: Any, default: Tuple[Any, ...]) -> Tuple[Any, ...]:
+ ...
+
+
+@overload
+def to_tuple(x: None, default: Optional[_T] = ...) -> _T:
+ ...
+
+
+@overload
+def to_tuple(
+ x: Any, default: Optional[Tuple[Any, ...]] = None
+) -> Tuple[Any, ...]:
+ ...
+
+
+def to_tuple(
+ x: Any, default: Optional[Tuple[Any, ...]] = None
+) -> Optional[Tuple[Any, ...]]:
+ if x is None:
+ return default
+ elif isinstance(x, str):
+ return (x,)
+ elif isinstance(x, Iterable):
+ return tuple(x)
+ else:
+ return (x,)
+
+
+def dedupe_tuple(tup: Tuple[str, ...]) -> Tuple[str, ...]:
+ return tuple(unique_list(tup))
+
+
+class Dispatcher:
+ def __init__(self, uselist: bool = False) -> None:
+ self._registry: Dict[Tuple[Any, ...], Any] = {}
+ self.uselist = uselist
+
+ def dispatch_for(
+ self, target: Any, qualifier: str = "default"
+ ) -> Callable[[_C], _C]:
+ def decorate(fn: _C) -> _C:
+ if self.uselist:
+ self._registry.setdefault((target, qualifier), []).append(fn)
+ else:
+ assert (target, qualifier) not in self._registry
+ self._registry[(target, qualifier)] = fn
+ return fn
+
+ return decorate
+
+ def dispatch(self, obj: Any, qualifier: str = "default") -> Any:
+ if isinstance(obj, str):
+ targets: Sequence[Any] = [obj]
+ elif isinstance(obj, type):
+ targets = obj.__mro__
+ else:
+ targets = type(obj).__mro__
+
+ for spcls in targets:
+ if qualifier != "default" and (spcls, qualifier) in self._registry:
+ return self._fn_or_list(self._registry[(spcls, qualifier)])
+ elif (spcls, "default") in self._registry:
+ return self._fn_or_list(self._registry[(spcls, "default")])
+ else:
+ raise ValueError("no dispatch function for object: %s" % obj)
+
+ def _fn_or_list(
+ self, fn_or_list: Union[List[Callable[..., Any]], Callable[..., Any]]
+ ) -> Callable[..., Any]:
+ if self.uselist:
+
+ def go(*arg: Any, **kw: Any) -> None:
+ if TYPE_CHECKING:
+ assert isinstance(fn_or_list, Sequence)
+ for fn in fn_or_list:
+ fn(*arg, **kw)
+
+ return go
+ else:
+ return fn_or_list # type: ignore
+
+ def branch(self) -> Dispatcher:
+ """Return a copy of this dispatcher that is independently
+ writable."""
+
+ d = Dispatcher()
+ if self.uselist:
+ d._registry.update(
+ (k, [fn for fn in self._registry[k]]) for k in self._registry
+ )
+ else:
+ d._registry.update(self._registry)
+ return d
+
+
+def not_none(value: Optional[_T]) -> _T:
+ assert value is not None
+ return value
diff --git a/venv/lib/python3.12/site-packages/alembic/util/messaging.py b/venv/lib/python3.12/site-packages/alembic/util/messaging.py
new file mode 100644
index 0000000..5f14d59
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/messaging.py
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+from collections.abc import Iterable
+from contextlib import contextmanager
+import logging
+import sys
+import textwrap
+from typing import Iterator
+from typing import Optional
+from typing import TextIO
+from typing import Union
+import warnings
+
+from sqlalchemy.engine import url
+
+from . import sqla_compat
+
+log = logging.getLogger(__name__)
+
+# disable "no handler found" errors
+logging.getLogger("alembic").addHandler(logging.NullHandler())
+
+
+try:
+ import fcntl
+ import termios
+ import struct
+
+ ioctl = fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0))
+ _h, TERMWIDTH, _hp, _wp = struct.unpack("HHHH", ioctl)
+ if TERMWIDTH <= 0: # can occur if running in emacs pseudo-tty
+ TERMWIDTH = None
+except (ImportError, OSError):
+ TERMWIDTH = None
+
+
+def write_outstream(
+ stream: TextIO, *text: Union[str, bytes], quiet: bool = False
+) -> None:
+ if quiet:
+ return
+ encoding = getattr(stream, "encoding", "ascii") or "ascii"
+ for t in text:
+ if not isinstance(t, bytes):
+ t = t.encode(encoding, "replace")
+ t = t.decode(encoding)
+ try:
+ stream.write(t)
+ except OSError:
+ # suppress "broken pipe" errors.
+ # no known way to handle this on Python 3 however
+ # as the exception is "ignored" (noisily) in TextIOWrapper.
+ break
+
+
+@contextmanager
+def status(
+ status_msg: str, newline: bool = False, quiet: bool = False
+) -> Iterator[None]:
+ msg(status_msg + " ...", newline, flush=True, quiet=quiet)
+ try:
+ yield
+ except:
+ if not quiet:
+ write_outstream(sys.stdout, " FAILED\n")
+ raise
+ else:
+ if not quiet:
+ write_outstream(sys.stdout, " done\n")
+
+
+def err(message: str, quiet: bool = False) -> None:
+ log.error(message)
+ msg(f"FAILED: {message}", quiet=quiet)
+ sys.exit(-1)
+
+
+def obfuscate_url_pw(input_url: str) -> str:
+ u = url.make_url(input_url)
+ return sqla_compat.url_render_as_string(u, hide_password=True) # type: ignore # noqa: E501
+
+
+def warn(msg: str, stacklevel: int = 2) -> None:
+ warnings.warn(msg, UserWarning, stacklevel=stacklevel)
+
+
+def msg(
+ msg: str, newline: bool = True, flush: bool = False, quiet: bool = False
+) -> None:
+ if quiet:
+ return
+ if TERMWIDTH is None:
+ write_outstream(sys.stdout, msg)
+ if newline:
+ write_outstream(sys.stdout, "\n")
+ else:
+ # left indent output lines
+ lines = textwrap.wrap(msg, TERMWIDTH)
+ if len(lines) > 1:
+ for line in lines[0:-1]:
+ write_outstream(sys.stdout, " ", line, "\n")
+ write_outstream(sys.stdout, " ", lines[-1], ("\n" if newline else ""))
+ if flush:
+ sys.stdout.flush()
+
+
+def format_as_comma(value: Optional[Union[str, Iterable[str]]]) -> str:
+ if value is None:
+ return ""
+ elif isinstance(value, str):
+ return value
+ elif isinstance(value, Iterable):
+ return ", ".join(value)
+ else:
+ raise ValueError("Don't know how to comma-format %r" % value)
diff --git a/venv/lib/python3.12/site-packages/alembic/util/pyfiles.py b/venv/lib/python3.12/site-packages/alembic/util/pyfiles.py
new file mode 100644
index 0000000..973bd45
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/pyfiles.py
@@ -0,0 +1,114 @@
+from __future__ import annotations
+
+import atexit
+from contextlib import ExitStack
+import importlib
+import importlib.machinery
+import importlib.util
+import os
+import re
+import tempfile
+from types import ModuleType
+from typing import Any
+from typing import Optional
+
+from mako import exceptions
+from mako.template import Template
+
+from . import compat
+from .exc import CommandError
+
+
+def template_to_file(
+ template_file: str, dest: str, output_encoding: str, **kw: Any
+) -> None:
+ template = Template(filename=template_file)
+ try:
+ output = template.render_unicode(**kw).encode(output_encoding)
+ except:
+ with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as ntf:
+ ntf.write(
+ exceptions.text_error_template()
+ .render_unicode()
+ .encode(output_encoding)
+ )
+ fname = ntf.name
+ raise CommandError(
+ "Template rendering failed; see %s for a "
+ "template-oriented traceback." % fname
+ )
+ else:
+ with open(dest, "wb") as f:
+ f.write(output)
+
+
+def coerce_resource_to_filename(fname: str) -> str:
+ """Interpret a filename as either a filesystem location or as a package
+ resource.
+
+ Names that are non absolute paths and contain a colon
+ are interpreted as resources and coerced to a file location.
+
+ """
+ if not os.path.isabs(fname) and ":" in fname:
+ tokens = fname.split(":")
+
+ # from https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-filename # noqa E501
+
+ file_manager = ExitStack()
+ atexit.register(file_manager.close)
+
+ ref = compat.importlib_resources.files(tokens[0])
+ for tok in tokens[1:]:
+ ref = ref / tok
+ fname = file_manager.enter_context( # type: ignore[assignment]
+ compat.importlib_resources.as_file(ref)
+ )
+ return fname
+
+
+def pyc_file_from_path(path: str) -> Optional[str]:
+ """Given a python source path, locate the .pyc."""
+
+ candidate = importlib.util.cache_from_source(path)
+ if os.path.exists(candidate):
+ return candidate
+
+ # even for pep3147, fall back to the old way of finding .pyc files,
+ # to support sourceless operation
+ filepath, ext = os.path.splitext(path)
+ for ext in importlib.machinery.BYTECODE_SUFFIXES:
+ if os.path.exists(filepath + ext):
+ return filepath + ext
+ else:
+ return None
+
+
+def load_python_file(dir_: str, filename: str) -> ModuleType:
+ """Load a file from the given path as a Python module."""
+
+ module_id = re.sub(r"\W", "_", filename)
+ path = os.path.join(dir_, filename)
+ _, ext = os.path.splitext(filename)
+ if ext == ".py":
+ if os.path.exists(path):
+ module = load_module_py(module_id, path)
+ else:
+ pyc_path = pyc_file_from_path(path)
+ if pyc_path is None:
+ raise ImportError("Can't find Python file %s" % path)
+ else:
+ module = load_module_py(module_id, pyc_path)
+ elif ext in (".pyc", ".pyo"):
+ module = load_module_py(module_id, path)
+ else:
+ assert False
+ return module
+
+
+def load_module_py(module_id: str, path: str) -> ModuleType:
+ spec = importlib.util.spec_from_file_location(module_id, path)
+ assert spec
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module) # type: ignore
+ return module
diff --git a/venv/lib/python3.12/site-packages/alembic/util/sqla_compat.py b/venv/lib/python3.12/site-packages/alembic/util/sqla_compat.py
new file mode 100644
index 0000000..8489c19
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/alembic/util/sqla_compat.py
@@ -0,0 +1,665 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import contextlib
+import re
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Iterable
+from typing import Iterator
+from typing import Mapping
+from typing import Optional
+from typing import Protocol
+from typing import Set
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy import __version__
+from sqlalchemy import inspect
+from sqlalchemy import schema
+from sqlalchemy import sql
+from sqlalchemy import types as sqltypes
+from sqlalchemy.engine import url
+from sqlalchemy.schema import CheckConstraint
+from sqlalchemy.schema import Column
+from sqlalchemy.schema import ForeignKeyConstraint
+from sqlalchemy.sql import visitors
+from sqlalchemy.sql.base import DialectKWArgs
+from sqlalchemy.sql.elements import BindParameter
+from sqlalchemy.sql.elements import ColumnClause
+from sqlalchemy.sql.elements import quoted_name
+from sqlalchemy.sql.elements import TextClause
+from sqlalchemy.sql.elements import UnaryExpression
+from sqlalchemy.sql.visitors import traverse
+from typing_extensions import TypeGuard
+
+if TYPE_CHECKING:
+ from sqlalchemy import ClauseElement
+ from sqlalchemy import Index
+ from sqlalchemy import Table
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.engine import Dialect
+ from sqlalchemy.engine import Transaction
+ from sqlalchemy.engine.reflection import Inspector
+ from sqlalchemy.sql.base import ColumnCollection
+ from sqlalchemy.sql.compiler import SQLCompiler
+ from sqlalchemy.sql.dml import Insert
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.selectable import Select
+ from sqlalchemy.sql.selectable import TableClause
+
+_CE = TypeVar("_CE", bound=Union["ColumnElement[Any]", "SchemaItem"])
+
+
+class _CompilerProtocol(Protocol):
+ def __call__(self, element: Any, compiler: Any, **kw: Any) -> str:
+ ...
+
+
+def _safe_int(value: str) -> Union[int, str]:
+ try:
+ return int(value)
+ except:
+ return value
+
+
+_vers = tuple(
+ [_safe_int(x) for x in re.findall(r"(\d+|[abc]\d)", __version__)]
+)
+sqla_13 = _vers >= (1, 3)
+sqla_14 = _vers >= (1, 4)
+# https://docs.sqlalchemy.org/en/latest/changelog/changelog_14.html#change-0c6e0cc67dfe6fac5164720e57ef307d
+sqla_14_18 = _vers >= (1, 4, 18)
+sqla_14_26 = _vers >= (1, 4, 26)
+sqla_2 = _vers >= (2,)
+sqlalchemy_version = __version__
+
+try:
+ from sqlalchemy.sql.naming import _NONE_NAME as _NONE_NAME # type: ignore[attr-defined] # noqa: E501
+except ImportError:
+ from sqlalchemy.sql.elements import _NONE_NAME as _NONE_NAME # type: ignore # noqa: E501
+
+
+class _Unsupported:
+ "Placeholder for unsupported SQLAlchemy classes"
+
+
+if TYPE_CHECKING:
+
+ def compiles(
+ element: Type[ClauseElement], *dialects: str
+ ) -> Callable[[_CompilerProtocol], _CompilerProtocol]:
+ ...
+
+else:
+ from sqlalchemy.ext.compiler import compiles
+
+try:
+ from sqlalchemy import Computed as Computed
+except ImportError:
+ if not TYPE_CHECKING:
+
+ class Computed(_Unsupported):
+ pass
+
+ has_computed = False
+ has_computed_reflection = False
+else:
+ has_computed = True
+ has_computed_reflection = _vers >= (1, 3, 16)
+
+try:
+ from sqlalchemy import Identity as Identity
+except ImportError:
+ if not TYPE_CHECKING:
+
+ class Identity(_Unsupported):
+ pass
+
+ has_identity = False
+else:
+ identity_has_dialect_kwargs = issubclass(Identity, DialectKWArgs)
+
+ def _get_identity_options_dict(
+ identity: Union[Identity, schema.Sequence, None],
+ dialect_kwargs: bool = False,
+ ) -> Dict[str, Any]:
+ if identity is None:
+ return {}
+ elif identity_has_dialect_kwargs:
+ as_dict = identity._as_dict() # type: ignore
+ if dialect_kwargs:
+ assert isinstance(identity, DialectKWArgs)
+ as_dict.update(identity.dialect_kwargs)
+ else:
+ as_dict = {}
+ if isinstance(identity, Identity):
+ # always=None means something different than always=False
+ as_dict["always"] = identity.always
+ if identity.on_null is not None:
+ as_dict["on_null"] = identity.on_null
+ # attributes common to Identity and Sequence
+ attrs = (
+ "start",
+ "increment",
+ "minvalue",
+ "maxvalue",
+ "nominvalue",
+ "nomaxvalue",
+ "cycle",
+ "cache",
+ "order",
+ )
+ as_dict.update(
+ {
+ key: getattr(identity, key, None)
+ for key in attrs
+ if getattr(identity, key, None) is not None
+ }
+ )
+ return as_dict
+
+ has_identity = True
+
+if sqla_2:
+ from sqlalchemy.sql.base import _NoneName
+else:
+ from sqlalchemy.util import symbol as _NoneName # type: ignore[assignment]
+
+
+_ConstraintName = Union[None, str, _NoneName]
+
+_ConstraintNameDefined = Union[str, _NoneName]
+
+
+def constraint_name_defined(
+ name: _ConstraintName,
+) -> TypeGuard[_ConstraintNameDefined]:
+ return name is _NONE_NAME or isinstance(name, (str, _NoneName))
+
+
+def constraint_name_string(
+ name: _ConstraintName,
+) -> TypeGuard[str]:
+ return isinstance(name, str)
+
+
+def constraint_name_or_none(
+ name: _ConstraintName,
+) -> Optional[str]:
+ return name if constraint_name_string(name) else None
+
+
+AUTOINCREMENT_DEFAULT = "auto"
+
+
+@contextlib.contextmanager
+def _ensure_scope_for_ddl(
+ connection: Optional[Connection],
+) -> Iterator[None]:
+ try:
+ in_transaction = connection.in_transaction # type: ignore[union-attr]
+ except AttributeError:
+ # catch for MockConnection, None
+ in_transaction = None
+ pass
+
+ # yield outside the catch
+ if in_transaction is None:
+ yield
+ else:
+ if not in_transaction():
+ assert connection is not None
+ with connection.begin():
+ yield
+ else:
+ yield
+
+
+def url_render_as_string(url, hide_password=True):
+ if sqla_14:
+ return url.render_as_string(hide_password=hide_password)
+ else:
+ return url.__to_string__(hide_password=hide_password)
+
+
+def _safe_begin_connection_transaction(
+ connection: Connection,
+) -> Transaction:
+ transaction = _get_connection_transaction(connection)
+ if transaction:
+ return transaction
+ else:
+ return connection.begin()
+
+
+def _safe_commit_connection_transaction(
+ connection: Connection,
+) -> None:
+ transaction = _get_connection_transaction(connection)
+ if transaction:
+ transaction.commit()
+
+
+def _safe_rollback_connection_transaction(
+ connection: Connection,
+) -> None:
+ transaction = _get_connection_transaction(connection)
+ if transaction:
+ transaction.rollback()
+
+
+def _get_connection_in_transaction(connection: Optional[Connection]) -> bool:
+ try:
+ in_transaction = connection.in_transaction # type: ignore
+ except AttributeError:
+ # catch for MockConnection
+ return False
+ else:
+ return in_transaction()
+
+
+def _idx_table_bound_expressions(idx: Index) -> Iterable[ColumnElement[Any]]:
+ return idx.expressions # type: ignore
+
+
+def _copy(schema_item: _CE, **kw) -> _CE:
+ if hasattr(schema_item, "_copy"):
+ return schema_item._copy(**kw)
+ else:
+ return schema_item.copy(**kw) # type: ignore[union-attr]
+
+
+def _get_connection_transaction(
+ connection: Connection,
+) -> Optional[Transaction]:
+ if sqla_14:
+ return connection.get_transaction()
+ else:
+ r = connection._root # type: ignore[attr-defined]
+ return r._Connection__transaction
+
+
+def _create_url(*arg, **kw) -> url.URL:
+ if hasattr(url.URL, "create"):
+ return url.URL.create(*arg, **kw)
+ else:
+ return url.URL(*arg, **kw)
+
+
+def _connectable_has_table(
+ connectable: Connection, tablename: str, schemaname: Union[str, None]
+) -> bool:
+ if sqla_14:
+ return inspect(connectable).has_table(tablename, schemaname)
+ else:
+ return connectable.dialect.has_table(
+ connectable, tablename, schemaname
+ )
+
+
+def _exec_on_inspector(inspector, statement, **params):
+ if sqla_14:
+ with inspector._operation_context() as conn:
+ return conn.execute(statement, params)
+ else:
+ return inspector.bind.execute(statement, params)
+
+
+def _nullability_might_be_unset(metadata_column):
+ if not sqla_14:
+ return metadata_column.nullable
+ else:
+ from sqlalchemy.sql import schema
+
+ return (
+ metadata_column._user_defined_nullable is schema.NULL_UNSPECIFIED
+ )
+
+
+def _server_default_is_computed(*server_default) -> bool:
+ if not has_computed:
+ return False
+ else:
+ return any(isinstance(sd, Computed) for sd in server_default)
+
+
+def _server_default_is_identity(*server_default) -> bool:
+ if not sqla_14:
+ return False
+ else:
+ return any(isinstance(sd, Identity) for sd in server_default)
+
+
+def _table_for_constraint(constraint: Constraint) -> Table:
+ if isinstance(constraint, ForeignKeyConstraint):
+ table = constraint.parent
+ assert table is not None
+ return table # type: ignore[return-value]
+ else:
+ return constraint.table
+
+
+def _columns_for_constraint(constraint):
+ if isinstance(constraint, ForeignKeyConstraint):
+ return [fk.parent for fk in constraint.elements]
+ elif isinstance(constraint, CheckConstraint):
+ return _find_columns(constraint.sqltext)
+ else:
+ return list(constraint.columns)
+
+
+def _reflect_table(inspector: Inspector, table: Table) -> None:
+ if sqla_14:
+ return inspector.reflect_table(table, None)
+ else:
+ return inspector.reflecttable( # type: ignore[attr-defined]
+ table, None
+ )
+
+
+def _resolve_for_variant(type_, dialect):
+ if _type_has_variants(type_):
+ base_type, mapping = _get_variant_mapping(type_)
+ return mapping.get(dialect.name, base_type)
+ else:
+ return type_
+
+
+if hasattr(sqltypes.TypeEngine, "_variant_mapping"):
+
+ def _type_has_variants(type_):
+ return bool(type_._variant_mapping)
+
+ def _get_variant_mapping(type_):
+ return type_, type_._variant_mapping
+
+else:
+
+ def _type_has_variants(type_):
+ return type(type_) is sqltypes.Variant
+
+ def _get_variant_mapping(type_):
+ return type_.impl, type_.mapping
+
+
+def _fk_spec(constraint: ForeignKeyConstraint) -> Any:
+ if TYPE_CHECKING:
+ assert constraint.columns is not None
+ assert constraint.elements is not None
+ assert isinstance(constraint.parent, Table)
+
+ source_columns = [
+ constraint.columns[key].name for key in constraint.column_keys
+ ]
+
+ source_table = constraint.parent.name
+ source_schema = constraint.parent.schema
+ target_schema = constraint.elements[0].column.table.schema
+ target_table = constraint.elements[0].column.table.name
+ target_columns = [element.column.name for element in constraint.elements]
+ ondelete = constraint.ondelete
+ onupdate = constraint.onupdate
+ deferrable = constraint.deferrable
+ initially = constraint.initially
+ return (
+ source_schema,
+ source_table,
+ source_columns,
+ target_schema,
+ target_table,
+ target_columns,
+ onupdate,
+ ondelete,
+ deferrable,
+ initially,
+ )
+
+
+def _fk_is_self_referential(constraint: ForeignKeyConstraint) -> bool:
+ spec = constraint.elements[0]._get_colspec()
+ tokens = spec.split(".")
+ tokens.pop(-1) # colname
+ tablekey = ".".join(tokens)
+ assert constraint.parent is not None
+ return tablekey == constraint.parent.key
+
+
+def _is_type_bound(constraint: Constraint) -> bool:
+ # this deals with SQLAlchemy #3260, don't copy CHECK constraints
+ # that will be generated by the type.
+ # new feature added for #3260
+ return constraint._type_bound
+
+
+def _find_columns(clause):
+ """locate Column objects within the given expression."""
+
+ cols: Set[ColumnElement[Any]] = set()
+ traverse(clause, {}, {"column": cols.add})
+ return cols
+
+
+def _remove_column_from_collection(
+ collection: ColumnCollection, column: Union[Column[Any], ColumnClause[Any]]
+) -> None:
+ """remove a column from a ColumnCollection."""
+
+ # workaround for older SQLAlchemy, remove the
+ # same object that's present
+ assert column.key is not None
+ to_remove = collection[column.key]
+
+ # SQLAlchemy 2.0 will use more ReadOnlyColumnCollection
+ # (renamed from ImmutableColumnCollection)
+ if hasattr(collection, "_immutable") or hasattr(collection, "_readonly"):
+ collection._parent.remove(to_remove)
+ else:
+ collection.remove(to_remove)
+
+
+def _textual_index_column(
+ table: Table, text_: Union[str, TextClause, ColumnElement[Any]]
+) -> Union[ColumnElement[Any], Column[Any]]:
+ """a workaround for the Index construct's severe lack of flexibility"""
+ if isinstance(text_, str):
+ c = Column(text_, sqltypes.NULLTYPE)
+ table.append_column(c)
+ return c
+ elif isinstance(text_, TextClause):
+ return _textual_index_element(table, text_)
+ elif isinstance(text_, _textual_index_element):
+ return _textual_index_column(table, text_.text)
+ elif isinstance(text_, sql.ColumnElement):
+ return _copy_expression(text_, table)
+ else:
+ raise ValueError("String or text() construct expected")
+
+
+def _copy_expression(expression: _CE, target_table: Table) -> _CE:
+ def replace(col):
+ if (
+ isinstance(col, Column)
+ and col.table is not None
+ and col.table is not target_table
+ ):
+ if col.name in target_table.c:
+ return target_table.c[col.name]
+ else:
+ c = _copy(col)
+ target_table.append_column(c)
+ return c
+ else:
+ return None
+
+ return visitors.replacement_traverse( # type: ignore[call-overload]
+ expression, {}, replace
+ )
+
+
+class _textual_index_element(sql.ColumnElement):
+ """Wrap around a sqlalchemy text() construct in such a way that
+ we appear like a column-oriented SQL expression to an Index
+ construct.
+
+ The issue here is that currently the Postgresql dialect, the biggest
+ recipient of functional indexes, keys all the index expressions to
+ the corresponding column expressions when rendering CREATE INDEX,
+ so the Index we create here needs to have a .columns collection that
+ is the same length as the .expressions collection. Ultimately
+ SQLAlchemy should support text() expressions in indexes.
+
+ See SQLAlchemy issue 3174.
+
+ """
+
+ __visit_name__ = "_textual_idx_element"
+
+ def __init__(self, table: Table, text: TextClause) -> None:
+ self.table = table
+ self.text = text
+ self.key = text.text
+ self.fake_column = schema.Column(self.text.text, sqltypes.NULLTYPE)
+ table.append_column(self.fake_column)
+
+ def get_children(self):
+ return [self.fake_column]
+
+
+@compiles(_textual_index_element)
+def _render_textual_index_column(
+ element: _textual_index_element, compiler: SQLCompiler, **kw
+) -> str:
+ return compiler.process(element.text, **kw)
+
+
+class _literal_bindparam(BindParameter):
+ pass
+
+
+@compiles(_literal_bindparam)
+def _render_literal_bindparam(
+ element: _literal_bindparam, compiler: SQLCompiler, **kw
+) -> str:
+ return compiler.render_literal_bindparam(element, **kw)
+
+
+def _column_kwargs(col: Column) -> Mapping:
+ if sqla_13:
+ return col.kwargs
+ else:
+ return {}
+
+
+def _get_constraint_final_name(
+ constraint: Union[Index, Constraint], dialect: Optional[Dialect]
+) -> Optional[str]:
+ if constraint.name is None:
+ return None
+ assert dialect is not None
+ if sqla_14:
+ # for SQLAlchemy 1.4 we would like to have the option to expand
+ # the use of "deferred" names for constraints as well as to have
+ # some flexibility with "None" name and similar; make use of new
+ # SQLAlchemy API to return what would be the final compiled form of
+ # the name for this dialect.
+ return dialect.identifier_preparer.format_constraint(
+ constraint, _alembic_quote=False
+ )
+ else:
+ # prior to SQLAlchemy 1.4, work around quoting logic to get at the
+ # final compiled name without quotes.
+ if hasattr(constraint.name, "quote"):
+ # might be quoted_name, might be truncated_name, keep it the
+ # same
+ quoted_name_cls: type = type(constraint.name)
+ else:
+ quoted_name_cls = quoted_name
+
+ new_name = quoted_name_cls(str(constraint.name), quote=False)
+ constraint = constraint.__class__(name=new_name)
+
+ if isinstance(constraint, schema.Index):
+ # name should not be quoted.
+ d = dialect.ddl_compiler(dialect, None) # type: ignore[arg-type]
+ return d._prepared_index_name(constraint)
+ else:
+ # name should not be quoted.
+ return dialect.identifier_preparer.format_constraint(constraint)
+
+
+def _constraint_is_named(
+ constraint: Union[Constraint, Index], dialect: Optional[Dialect]
+) -> bool:
+ if sqla_14:
+ if constraint.name is None:
+ return False
+ assert dialect is not None
+ name = dialect.identifier_preparer.format_constraint(
+ constraint, _alembic_quote=False
+ )
+ return name is not None
+ else:
+ return constraint.name is not None
+
+
+def _is_mariadb(mysql_dialect: Dialect) -> bool:
+ if sqla_14:
+ return mysql_dialect.is_mariadb # type: ignore[attr-defined]
+ else:
+ return bool(
+ mysql_dialect.server_version_info
+ and mysql_dialect._is_mariadb # type: ignore[attr-defined]
+ )
+
+
+def _mariadb_normalized_version_info(mysql_dialect):
+ return mysql_dialect._mariadb_normalized_version_info
+
+
+def _insert_inline(table: Union[TableClause, Table]) -> Insert:
+ if sqla_14:
+ return table.insert().inline()
+ else:
+ return table.insert(inline=True) # type: ignore[call-arg]
+
+
+if sqla_14:
+ from sqlalchemy import create_mock_engine
+
+ # weird mypy workaround
+ from sqlalchemy import select as _sa_select
+
+ _select = _sa_select
+else:
+ from sqlalchemy import create_engine
+
+ def create_mock_engine(url, executor, **kw): # type: ignore[misc]
+ return create_engine(
+ "postgresql://", strategy="mock", executor=executor
+ )
+
+ def _select(*columns, **kw) -> Select:
+ return sql.select(list(columns), **kw) # type: ignore[call-overload]
+
+
+def is_expression_index(index: Index) -> bool:
+ for expr in index.expressions:
+ if is_expression(expr):
+ return True
+ return False
+
+
+def is_expression(expr: Any) -> bool:
+ while isinstance(expr, UnaryExpression):
+ expr = expr.element
+ if not isinstance(expr, ColumnClause) or expr.is_literal:
+ return True
+ return False
diff --git a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA
new file mode 100644
index 0000000..3ac05cf
--- /dev/null
+++ b/venv/lib/python3.12/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
+
+[](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
+[](https://pypi.python.org/pypi/annotated-types)
+[](https://github.com/annotated-types/annotated-types)
+[](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.12/site-packages/annotated_types-0.7.0.dist-info/RECORD b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/RECORD
new file mode 100644
index 0000000..a66e278
--- /dev/null
+++ b/venv/lib/python3.12/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-312.pyc,,
+annotated_types/__pycache__/test_cases.cpython-312.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.12/site-packages/annotated_types-0.7.0.dist-info/WHEEL b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/WHEEL
new file mode 100644
index 0000000..516596c
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE
new file mode 100644
index 0000000..d99323a
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/annotated_types/__init__.py b/venv/lib/python3.12/site-packages/annotated_types/__init__.py
new file mode 100644
index 0000000..74e0dee
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..81ec349
Binary files /dev/null and b/venv/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc b/venv/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc
new file mode 100644
index 0000000..58107c9
Binary files /dev/null and b/venv/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/annotated_types/py.typed b/venv/lib/python3.12/site-packages/annotated_types/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/annotated_types/test_cases.py b/venv/lib/python3.12/site-packages/annotated_types/test_cases.py
new file mode 100644
index 0000000..d9164d6
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio-4.12.1.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/METADATA b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/METADATA
new file mode 100644
index 0000000..dbeb198
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/METADATA
@@ -0,0 +1,96 @@
+Metadata-Version: 2.4
+Name: anyio
+Version: 4.12.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.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.9
+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; python_version >= "3.10" and extra == "trio"
+Requires-Dist: trio>=0.31.0; python_version < "3.10" and 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
+
+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 version of the functools_ module
+
+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
+.. _Hypothesis: https://hypothesis.works/
diff --git a/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/RECORD b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/RECORD
new file mode 100644
index 0000000..4b3b57c
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/RECORD
@@ -0,0 +1,92 @@
+anyio-4.12.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+anyio-4.12.1.dist-info/METADATA,sha256=DfiDab9Tmmcfy802lOLTMEHJQShkOSbopCwqCYbLuJk,4277
+anyio-4.12.1.dist-info/RECORD,,
+anyio-4.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
+anyio-4.12.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
+anyio-4.12.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
+anyio-4.12.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
+anyio/__init__.py,sha256=7iDVqMUprUuKNY91FuoKqayAhR-OY136YDPI6P78HHk,6170
+anyio/__pycache__/__init__.cpython-312.pyc,,
+anyio/__pycache__/from_thread.cpython-312.pyc,,
+anyio/__pycache__/functools.cpython-312.pyc,,
+anyio/__pycache__/lowlevel.cpython-312.pyc,,
+anyio/__pycache__/pytest_plugin.cpython-312.pyc,,
+anyio/__pycache__/to_interpreter.cpython-312.pyc,,
+anyio/__pycache__/to_process.cpython-312.pyc,,
+anyio/__pycache__/to_thread.cpython-312.pyc,,
+anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/_backends/__pycache__/__init__.cpython-312.pyc,,
+anyio/_backends/__pycache__/_asyncio.cpython-312.pyc,,
+anyio/_backends/__pycache__/_trio.cpython-312.pyc,,
+anyio/_backends/_asyncio.py,sha256=xG6qv60mgGnL0mK82dxjH2b8hlkMlJ-x2BqIq3qv70Y,98863
+anyio/_backends/_trio.py,sha256=30Rctb7lm8g63ZHljVPVnj5aH-uK6oQvphjwUBoAzuI,41456
+anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/_core/__pycache__/__init__.cpython-312.pyc,,
+anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc,,
+anyio/_core/__pycache__/_contextmanagers.cpython-312.pyc,,
+anyio/_core/__pycache__/_eventloop.cpython-312.pyc,,
+anyio/_core/__pycache__/_exceptions.cpython-312.pyc,,
+anyio/_core/__pycache__/_fileio.cpython-312.pyc,,
+anyio/_core/__pycache__/_resources.cpython-312.pyc,,
+anyio/_core/__pycache__/_signals.cpython-312.pyc,,
+anyio/_core/__pycache__/_sockets.cpython-312.pyc,,
+anyio/_core/__pycache__/_streams.cpython-312.pyc,,
+anyio/_core/__pycache__/_subprocesses.cpython-312.pyc,,
+anyio/_core/__pycache__/_synchronization.cpython-312.pyc,,
+anyio/_core/__pycache__/_tasks.cpython-312.pyc,,
+anyio/_core/__pycache__/_tempfile.cpython-312.pyc,,
+anyio/_core/__pycache__/_testing.cpython-312.pyc,,
+anyio/_core/__pycache__/_typedattr.cpython-312.pyc,,
+anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626
+anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215
+anyio/_core/_eventloop.py,sha256=c2EdcBX-xnKwxPcC4Pjn3_qG9I-x4IWFO2R9RqCGjM4,6448
+anyio/_core/_exceptions.py,sha256=Y3aq-Wxd7Q2HqwSg7nZPvRsHEuGazv_qeet6gqEBdPk,4407
+anyio/_core/_fileio.py,sha256=uc7t10Vb-If7GbdWM_zFf-ajUe6uek63fSt7IBLlZW0,25731
+anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435
+anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016
+anyio/_core/_sockets.py,sha256=RBXHcUqZt5gg_-OOfgHVv8uq2FSKk1uVUzTdpjBoI1o,34977
+anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806
+anyio/_core/_subprocesses.py,sha256=EXm5igL7dj55iYkPlbYVAqtbqxJxjU-6OndSTIx9SRg,8047
+anyio/_core/_synchronization.py,sha256=MgVVqFzvt580tHC31LiOcq1G6aryut--xRG4Ff8KwxQ,20869
+anyio/_core/_tasks.py,sha256=pVB7K6AAulzUM8YgXAeqNZG44nSyZ1bYJjH8GznC00I,5435
+anyio/_core/_tempfile.py,sha256=lHb7CW4FyIlpkf5ADAf4VmLHCKwEHF9nxqNyBCFFUiA,19697
+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-312.pyc,,
+anyio/abc/__pycache__/_eventloop.cpython-312.pyc,,
+anyio/abc/__pycache__/_resources.cpython-312.pyc,,
+anyio/abc/__pycache__/_sockets.cpython-312.pyc,,
+anyio/abc/__pycache__/_streams.cpython-312.pyc,,
+anyio/abc/__pycache__/_subprocesses.cpython-312.pyc,,
+anyio/abc/__pycache__/_tasks.cpython-312.pyc,,
+anyio/abc/__pycache__/_testing.cpython-312.pyc,,
+anyio/abc/_eventloop.py,sha256=GlzgB3UJGgG6Kr7olpjOZ-o00PghecXuofVDQ_5611Q,10749
+anyio/abc/_resources.py,sha256=DrYvkNN1hH6Uvv5_5uKySvDsnknGVDe8FCKfko0VtN8,783
+anyio/abc/_sockets.py,sha256=ECTY0jLEF18gryANHR3vFzXzGdZ-xPwELq1QdgOb0Jo,13258
+anyio/abc/_streams.py,sha256=005GKSCXGprxnhucILboSqc2JFovECZk9m3p-qqxXVc,7640
+anyio/abc/_subprocesses.py,sha256=cumAPJTktOQtw63IqG0lDpyZqu_l1EElvQHMiwJgL08,2067
+anyio/abc/_tasks.py,sha256=KC7wrciE48AINOI-AhPutnFhe1ewfP7QnamFlDzqesQ,3721
+anyio/abc/_testing.py,sha256=tBJUzkSfOXJw23fe8qSJ03kJlShOYjjaEyFB6k6MYT8,1821
+anyio/from_thread.py,sha256=L-0w1HxJ6BSb-KuVi57k5Tkc3yzQrx3QK5tAxMPcY-0,19141
+anyio/functools.py,sha256=HWj7GBEmc0Z-mZg3uok7Z7ZJn0rEC_0Pzbt0nYUDaTQ,10973
+anyio/lowlevel.py,sha256=AyKLVK3LaWSoK39LkCKxE4_GDMLKZBNqTrLUgk63y80,5158
+anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/pytest_plugin.py,sha256=3jAFQn0jv_pyoWE2GBBlHaj9sqXj4e8vob0_hgrsXE8,10244
+anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/streams/__pycache__/__init__.cpython-312.pyc,,
+anyio/streams/__pycache__/buffered.cpython-312.pyc,,
+anyio/streams/__pycache__/file.cpython-312.pyc,,
+anyio/streams/__pycache__/memory.cpython-312.pyc,,
+anyio/streams/__pycache__/stapled.cpython-312.pyc,,
+anyio/streams/__pycache__/text.cpython-312.pyc,,
+anyio/streams/__pycache__/tls.cpython-312.pyc,,
+anyio/streams/buffered.py,sha256=2R3PeJhe4EXrdYqz44Y6-Eg9R6DrmlsYrP36Ir43-po,6263
+anyio/streams/file.py,sha256=4WZ7XGz5WNu39FQHvqbe__TQ0HDP9OOhgO1mk9iVpVU,4470
+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=Jpxy0Mfbcp1BxHCwE-YjSSFaLnIBbnnwur-excYThs4,15368
+anyio/to_interpreter.py,sha256=_mLngrMy97TMR6VbW4Y6YzDUk9ZuPcQMPlkuyRh3C9k,7100
+anyio/to_process.py,sha256=J7gAA_YOuoHqnpDAf5fm1Qu6kOmTzdFbiDNvnV755vk,9798
+anyio/to_thread.py,sha256=menEgXYmUV7Fjg_9WqCV95P9MAtQS8BzPGGcWB_QnfQ,2687
diff --git a/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/WHEEL b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/WHEEL
new file mode 100644
index 0000000..e7fa31b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio-4.12.1.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.12/site-packages/anyio-4.12.1.dist-info/entry_points.txt b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/entry_points.txt
new file mode 100644
index 0000000..44dd9bd
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[pytest11]
+anyio = anyio.pytest_plugin
diff --git a/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/licenses/LICENSE b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/licenses/LICENSE
new file mode 100644
index 0000000..104eebf
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio-4.12.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.12/site-packages/anyio-4.12.1.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/top_level.txt
new file mode 100644
index 0000000..c77c069
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio-4.12.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+anyio
diff --git a/venv/lib/python3.12/site-packages/anyio/__init__.py b/venv/lib/python3.12/site-packages/anyio/__init__.py
new file mode 100644
index 0000000..d23c5a5
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/__init__.py
@@ -0,0 +1,111 @@
+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 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 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.12/site-packages/anyio/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..21461e4
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..c1bff24
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..1690027
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc
new file mode 100644
index 0000000..660350e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/functools.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/functools.cpython-312.pyc
new file mode 100644
index 0000000..a63f442
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/functools.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..07cabdc
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc
new file mode 100644
index 0000000..9ad9851
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..8e2b764
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc
new file mode 100644
index 0000000..87c9060
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_interpreter.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_interpreter.cpython-312.pyc
new file mode 100644
index 0000000..11a251f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_interpreter.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_process.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_process.cpython-312.pyc
new file mode 100644
index 0000000..9ec5588
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_process.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..209c994
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc
new file mode 100644
index 0000000..2ca5c3f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__init__.py b/venv/lib/python3.12/site-packages/anyio/_backends/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..24d1d9d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..24d1d9d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..9badafd
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc
new file mode 100644
index 0000000..2af70d0
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..71335d1
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc
new file mode 100644
index 0000000..c7cc6ad
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py b/venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py
new file mode 100644
index 0000000..8ff009e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py
@@ -0,0 +1,2980 @@
+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, suppress
+from contextvars import Context, copy_context
+from dataclasses import dataclass, field
+from functools import partial, wraps
+from inspect import (
+ CORO_RUNNING,
+ CORO_SUSPENDED,
+ getcoroutinestate,
+ iscoroutine,
+)
+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,
+ Optional,
+ 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,
+ iterate_exceptions,
+)
+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 ..abc import (
+ AsyncBackend,
+ IPSockAddrType,
+ SocketListener,
+ UDPPacketType,
+ UNIXDatagramPacketType,
+)
+from ..abc._eventloop import StrOrBytesPath
+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, 10):
+ from typing import ParamSpec
+else:
+ from typing_extensions import ParamSpec
+
+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_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
+
+
+def get_callable_name(func: Callable) -> str:
+ module = getattr(func, "__module__", None)
+ qualname = getattr(func, "__qualname__", None)
+ return ".".join([x for x in (module, qualname) if x])
+
+
+#
+# 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
+ try:
+ return getcoroutinestate(coro) in (CORO_RUNNING, CORO_SUSPENDED)
+ except AttributeError:
+ # task coro is async_genenerator_asend https://bugs.python.org/issue37771
+ raise Exception(f"Cannot determine if task {task} has started or not") from None
+
+
+#
+# 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):
+ 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
+ cannot_swallow_exc_val = False
+ if exc_val is not None:
+ for exc in iterate_exceptions(exc_val):
+ if isinstance(exc, CancelledError) and is_anyio_cancellation(
+ exc
+ ):
+ self._cancelled_caught = True
+ else:
+ cannot_swallow_exc_val = True
+
+ return self._cancelled_caught and not cannot_swallow_exc_val
+ 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._active = 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:
+ self.cancel_scope.__enter__()
+ self._active = True
+ 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()
+
+ self._active = False
+ 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,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
+ args: tuple[Unpack[PosArgsT]],
+ name: object,
+ task_status_future: asyncio.Future | None = None,
+ ) -> asyncio.Task:
+ 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 not self._active:
+ raise RuntimeError(
+ "This task group is not active; no new tasks can be started."
+ )
+
+ kwargs = {}
+ if task_status_future:
+ parent_id = id(current_task())
+ kwargs["task_status"] = _AsyncioTaskStatus(
+ task_status_future, id(self.cancel_scope._host_task)
+ )
+ else:
+ parent_id = id(self.cancel_scope._host_task)
+
+ coro = func(*args, **kwargs)
+ if not iscoroutine(coro):
+ 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"
+ )
+
+ name = get_callable_name(func) if name is None else str(name)
+ loop = asyncio.get_running_loop()
+ 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(coro, loop=loop, name=name)
+ else:
+ task = create_task(coro, name=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 task
+
+ def start_soon(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ ) -> None:
+ self._spawn(func, args, name)
+
+ async def start(
+ self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None
+ ) -> Any:
+ future: asyncio.Future = asyncio.Future()
+ task = self._spawn(func, args, 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:
+ return await future
+ except CancelledError:
+ # Cancel the task and wait for it to exit before returning
+ task.cancel()
+ with CancelScope(shield=True), suppress(CancelledError):
+ await task
+
+ raise
+
+
+#
+# Threads
+#
+
+_Retval_Queue_Type = tuple[Optional[T_Retval], Optional[BaseException]]
+
+
+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 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 = BrokenResourceError()
+ self.exception.__cause__ = 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
+ 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.write_event.set()
+
+ def connection_lost(self, exc: Exception | None) -> None:
+ self.read_event.set()
+ self.write_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 self._protocol.exception from None
+ 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 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()
+
+ 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()
+
+ 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):
+ 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):
+ 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:
+ self._waiters.remove(item)
+ if self._owner_task is task:
+ self.release()
+
+ raise
+
+ self._waiters.remove(item)
+
+ 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")
+
+ for task, fut in self._waiters:
+ if not fut.cancelled():
+ 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):
+ 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:
+ try:
+ self._waiters.remove(fut)
+ except ValueError:
+ 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")
+
+ for fut in self._waiters:
+ if not fut.cancelled():
+ fut.set_result(None)
+ self._waiters.remove(fut)
+ 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):
+ _total_tokens: float = 0
+
+ def __new__(cls, total_tokens: float) -> CapacityLimiter:
+ return object.__new__(cls)
+
+ def __init__(self, total_tokens: float):
+ 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 _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:
+ try:
+ self.get_loop().run_until_complete(
+ self._call_in_runner_task(test_func, **kwargs)
+ )
+ except Exception as exc:
+ self._exceptions.append(exc)
+
+ 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 by cancel scope {id(scope):x}")
+
+ if scope.shield:
+ return
+
+ scope = scope._parent_scope
+
+ @classmethod
+ def run_async_from_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_Retval:
+ async def task_wrapper() -> T_Retval:
+ __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_Retval] = 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:
+ 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.12/site-packages/anyio/_backends/_trio.py b/venv/lib/python3.12/site-packages/anyio/_backends/_trio.py
new file mode 100644
index 0000000..f460a7f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_backends/_trio.py
@@ -0,0 +1,1346 @@
+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 dataclasses import dataclass
+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,
+ NoReturn,
+ 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 ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType
+from ..abc._eventloop import AsyncBackend, StrOrBytesPath
+from ..streams.memory import MemoryObjectSendStream
+
+if TYPE_CHECKING:
+ from _typeshed import FileDescriptorLike
+
+if sys.version_info >= (3, 10):
+ from typing import ParamSpec
+else:
+ from typing_extensions import ParamSpec
+
+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_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType)
+PosArgsT = TypeVarTuple("PosArgsT")
+P = ParamSpec("P")
+
+
+#
+# Event loop
+#
+
+RunVar = trio.lowlevel.RunVar
+
+
+#
+# Timeouts and cancellation
+#
+
+
+class CancelScope(BaseCancelScope):
+ 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
+#
+
+
+class TaskGroup(abc.TaskGroup):
+ def __init__(self) -> None:
+ 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:
+ 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 start_soon(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ ) -> None:
+ if not self._active:
+ raise RuntimeError(
+ "This task group is not active; no new tasks can be started."
+ )
+
+ self._nursery.start_soon(func, *args, name=name)
+
+ async def start(
+ self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None
+ ) -> Any:
+ if not self._active:
+ raise RuntimeError(
+ "This task group is not active; no new tasks can be started."
+ )
+
+ return await self._nursery.start(func, *args, name=name)
+
+
+#
+# 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):
+ 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):
+ 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):
+ 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):
+ 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 | 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()()
+
+ async def _run_tests_and_fixtures(self) -> None:
+ self._send_stream, receive_stream = create_memory_object_stream(1)
+ with receive_stream:
+ async for coro, outcome_holder in receive_stream:
+ try:
+ retval = await coro
+ 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:
+ return trio.run(func, *args)
+
+ @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]], Awaitable[T_Retval]],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_Retval:
+ 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:
+ 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.12/site-packages/anyio/_core/__init__.py b/venv/lib/python3.12/site-packages/anyio/_core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..71c06ae
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..71c06ae
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc
new file mode 100644
index 0000000..e5e7e4b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..a78f67b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-312.pyc
new file mode 100644
index 0000000..8c37d64
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..8d1a18f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc
new file mode 100644
index 0000000..ec48fe4
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..f505cd9
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc
new file mode 100644
index 0000000..2fe6ff6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..c48a6ef
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312.pyc
new file mode 100644
index 0000000..4cff048
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..710a77e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312.pyc
new file mode 100644
index 0000000..7aeb728
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..8a5c609
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc
new file mode 100644
index 0000000..785250a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..8cd37cd
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312.pyc
new file mode 100644
index 0000000..e32e042
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..0ca315f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc
new file mode 100644
index 0000000..6942571
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..55ba4a3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc
new file mode 100644
index 0000000..a212b88
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..7b159cc
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312.pyc
new file mode 100644
index 0000000..d0b8c02
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..a1ca3f0
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc
new file mode 100644
index 0000000..ff4fa4b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..c802056
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312.pyc
new file mode 100644
index 0000000..10d2a0d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..4b3801e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc
new file mode 100644
index 0000000..9803e07
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..e0d8adf
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312.pyc
new file mode 100644
index 0000000..460b8f6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py b/venv/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py
new file mode 100644
index 0000000..9f35bae
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/_core/_contextmanagers.py b/venv/lib/python3.12/site-packages/anyio/_core/_contextmanagers.py
new file mode 100644
index 0000000..302f32b
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/_core/_eventloop.py b/venv/lib/python3.12/site-packages/anyio/_core/_eventloop.py
new file mode 100644
index 0000000..59a69cc
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_eventloop.py
@@ -0,0 +1,234 @@
+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:
+ 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.12/site-packages/anyio/_core/_exceptions.py b/venv/lib/python3.12/site-packages/anyio/_core/_exceptions.py
new file mode 100644
index 0000000..3776bed
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_exceptions.py
@@ -0,0 +1,156 @@
+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"
+ )
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_fileio.py b/venv/lib/python3.12/site-packages/anyio/_core/_fileio.py
new file mode 100644
index 0000000..061f0d7
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_fileio.py
@@ -0,0 +1,797 @@
+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,
+ overload,
+)
+
+from .. import to_thread
+from ..abc import AsyncResource
+
+if TYPE_CHECKING:
+ from types import ModuleType
+
+ from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer
+else:
+ ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object
+
+
+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]) -> None:
+ self._fp: Any = fp
+
+ def __getattr__(self, name: str) -> object:
+ return getattr(self._fp, name)
+
+ @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)
+
+ async def read(self, size: int = -1) -> AnyStr:
+ return await to_thread.run_sync(self._fp.read, size)
+
+ async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes:
+ return await to_thread.run_sync(self._fp.read1, size)
+
+ async def readline(self) -> AnyStr:
+ return await to_thread.run_sync(self._fp.readline)
+
+ async def readlines(self) -> list[AnyStr]:
+ return await to_thread.run_sync(self._fp.readlines)
+
+ async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int:
+ return await to_thread.run_sync(self._fp.readinto, b)
+
+ async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int:
+ return await to_thread.run_sync(self._fp.readinto1, b)
+
+ @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)
+
+ @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)
+
+ async def truncate(self, size: int | None = None) -> int:
+ return await to_thread.run_sync(self._fp.truncate, size)
+
+ async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int:
+ return await to_thread.run_sync(self._fp.seek, offset, whence)
+
+ async def tell(self) -> int:
+ return await to_thread.run_sync(self._fp.tell)
+
+ async def flush(self) -> None:
+ return await to_thread.run_sync(self._fp.flush)
+
+
+@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 = ...,
+) -> 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 = ...,
+) -> 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,
+) -> AsyncFile[Any]:
+ """
+ Open a file asynchronously.
+
+ The arguments are exactly the same as for the builtin :func:`open`.
+
+ :return: an asynchronous file object
+
+ """
+ fp = await to_thread.run_sync(
+ open, file, mode, buffering, encoding, errors, newline, closefd, opener
+ )
+ return AsyncFile(fp)
+
+
+def wrap_file(file: IO[AnyStr]) -> AsyncFile[AnyStr]:
+ """
+ Wrap an existing file as an asynchronous file.
+
+ :param file: an existing file-like object
+ :return: an asynchronous file object
+
+ """
+ return AsyncFile(file)
+
+
+@dataclass(eq=False)
+class _PathIterator(AsyncIterator["Path"]):
+ iterator: Iterator[PathLike[str]]
+
+ async def __anext__(self) -> Path:
+ nextval = await to_thread.run_sync(
+ next, self.iterator, None, abandon_on_cancel=True
+ )
+ if nextval is None:
+ raise StopAsyncIteration from None
+
+ return Path(nextval)
+
+
+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`
+ """
+
+ __slots__ = "_path", "__weakref__"
+
+ __weakref__: Any
+
+ def __init__(self, *args: str | PathLike[str]) -> None:
+ self._path: Final[pathlib.Path] = pathlib.Path(*args)
+
+ def __fspath__(self) -> str:
+ return self._path.__fspath__()
+
+ 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]) -> Path:
+ return Path(self._path / other)
+
+ def __rtruediv__(self, other: str | PathLike[str]) -> Path:
+ return Path(other) / self
+
+ @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[Path]:
+ return tuple(Path(p) for p in self._path.parents)
+
+ @property
+ def parent(self) -> Path:
+ return Path(self._path.parent)
+
+ @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) -> Path:
+ path = await to_thread.run_sync(self._path.absolute)
+ return Path(path)
+
+ 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) -> Path:
+ return Path(pathlib.Path.from_uri(uri))
+
+ 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) -> Any: # TODO: add return type annotation when Typeshed gets it
+ return self._path.info
+
+ async def copy(
+ self,
+ target: str | os.PathLike[str],
+ *,
+ follow_symlinks: bool = True,
+ preserve_metadata: bool = False,
+ ) -> Path:
+ func = partial(
+ self._path.copy,
+ follow_symlinks=follow_symlinks,
+ preserve_metadata=preserve_metadata,
+ )
+ return Path(await to_thread.run_sync(func, pathlib.Path(target)))
+
+ async def copy_into(
+ self,
+ target_dir: str | os.PathLike[str],
+ *,
+ follow_symlinks: bool = True,
+ preserve_metadata: bool = False,
+ ) -> Path:
+ func = partial(
+ self._path.copy_into,
+ follow_symlinks=follow_symlinks,
+ preserve_metadata=preserve_metadata,
+ )
+ return Path(await to_thread.run_sync(func, pathlib.Path(target_dir)))
+
+ async def move(self, target: str | os.PathLike[str]) -> Path:
+ # Upstream does not handle anyio.Path properly as a PathLike
+ target = pathlib.Path(target)
+ return Path(await to_thread.run_sync(self._path.move, target))
+
+ async def move_into(
+ self,
+ target_dir: str | os.PathLike[str],
+ ) -> Path:
+ return Path(await to_thread.run_sync(self._path.move_into, target_dir))
+
+ 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)
+
+ @classmethod
+ async def cwd(cls) -> Path:
+ path = await to_thread.run_sync(pathlib.Path.cwd)
+ return cls(path)
+
+ async def exists(self) -> bool:
+ return await to_thread.run_sync(self._path.exists, abandon_on_cancel=True)
+
+ async def expanduser(self) -> Path:
+ return Path(
+ await to_thread.run_sync(self._path.expanduser, abandon_on_cancel=True)
+ )
+
+ if sys.version_info < (3, 12):
+ # Python 3.11 and earlier
+ def glob(self, pattern: str) -> AsyncIterator[Path]:
+ gen = self._path.glob(pattern)
+ return _PathIterator(gen)
+ 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[Path]:
+ gen = self._path.glob(pattern, case_sensitive=case_sensitive)
+ return _PathIterator(gen)
+ 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[Path]:
+ gen = self._path.glob(
+ pattern, # type: ignore[arg-type]
+ case_sensitive=case_sensitive,
+ recurse_symlinks=recurse_symlinks,
+ )
+ return _PathIterator(gen)
+
+ async def group(self) -> str:
+ return await to_thread.run_sync(self._path.group, abandon_on_cancel=True)
+
+ 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)
+
+ @classmethod
+ async def home(cls) -> Path:
+ home_path = await to_thread.run_sync(pathlib.Path.home)
+ return cls(home_path)
+
+ 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
+ )
+
+ async def is_char_device(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_char_device, abandon_on_cancel=True
+ )
+
+ async def is_dir(self) -> bool:
+ return await to_thread.run_sync(self._path.is_dir, abandon_on_cancel=True)
+
+ async def is_fifo(self) -> bool:
+ return await to_thread.run_sync(self._path.is_fifo, abandon_on_cancel=True)
+
+ async def is_file(self) -> bool:
+ return await to_thread.run_sync(self._path.is_file, abandon_on_cancel=True)
+
+ if sys.version_info >= (3, 12):
+
+ async def is_junction(self) -> bool:
+ return await to_thread.run_sync(self._path.is_junction)
+
+ async def is_mount(self) -> bool:
+ return await to_thread.run_sync(
+ os.path.ismount, self._path, abandon_on_cancel=True
+ )
+
+ 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)
+
+ async def is_symlink(self) -> bool:
+ return await to_thread.run_sync(self._path.is_symlink, abandon_on_cancel=True)
+
+ async def iterdir(self) -> AsyncIterator[Path]:
+ gen = (
+ self._path.iterdir()
+ if sys.version_info < (3, 13)
+ else await to_thread.run_sync(self._path.iterdir, abandon_on_cancel=True)
+ )
+ async for path in _PathIterator(gen):
+ yield path
+
+ def joinpath(self, *args: str | PathLike[str]) -> Path:
+ return Path(self._path.joinpath(*args))
+
+ async def lchmod(self, mode: int) -> None:
+ await to_thread.run_sync(self._path.lchmod, mode)
+
+ async def lstat(self) -> os.stat_result:
+ return await to_thread.run_sync(self._path.lstat, abandon_on_cancel=True)
+
+ 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)
+
+ @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
+ )
+ return AsyncFile(fp)
+
+ async def owner(self) -> str:
+ return await to_thread.run_sync(self._path.owner, abandon_on_cancel=True)
+
+ async def read_bytes(self) -> bytes:
+ return await to_thread.run_sync(self._path.read_bytes)
+
+ 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)
+
+ if sys.version_info >= (3, 12):
+
+ def relative_to(
+ self, *other: str | PathLike[str], walk_up: bool = False
+ ) -> Path:
+ # relative_to() should work with any PathLike but it doesn't
+ others = [pathlib.Path(other) for other in other]
+ return Path(self._path.relative_to(*others, walk_up=walk_up))
+
+ else:
+
+ def relative_to(self, *other: str | PathLike[str]) -> Path:
+ return Path(self._path.relative_to(*other))
+
+ async def readlink(self) -> Path:
+ target = await to_thread.run_sync(os.readlink, self._path)
+ return Path(target)
+
+ async def rename(self, target: str | pathlib.PurePath | Path) -> Path:
+ if isinstance(target, Path):
+ target = target._path
+
+ await to_thread.run_sync(self._path.rename, target)
+ return Path(target)
+
+ async def replace(self, target: str | pathlib.PurePath | Path) -> Path:
+ if isinstance(target, Path):
+ target = target._path
+
+ await to_thread.run_sync(self._path.replace, target)
+ return Path(target)
+
+ async def resolve(self, strict: bool = False) -> Path:
+ func = partial(self._path.resolve, strict=strict)
+ return Path(await to_thread.run_sync(func, abandon_on_cancel=True))
+
+ if sys.version_info < (3, 12):
+ # Pre Python 3.12
+ def rglob(self, pattern: str) -> AsyncIterator[Path]:
+ gen = self._path.rglob(pattern)
+ return _PathIterator(gen)
+ 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[Path]:
+ gen = self._path.rglob(pattern, case_sensitive=case_sensitive)
+ return _PathIterator(gen)
+ 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[Path]:
+ gen = self._path.rglob(
+ pattern, # type: ignore[arg-type]
+ case_sensitive=case_sensitive,
+ recurse_symlinks=recurse_symlinks,
+ )
+ return _PathIterator(gen)
+
+ async def rmdir(self) -> None:
+ await to_thread.run_sync(self._path.rmdir)
+
+ 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
+ )
+
+ 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)
+
+ 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)
+
+ async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
+ await to_thread.run_sync(self._path.touch, mode, exist_ok)
+
+ async def unlink(self, missing_ok: bool = False) -> None:
+ try:
+ await to_thread.run_sync(self._path.unlink)
+ 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[Path, 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)
+ if value is None:
+ return
+
+ root, dirs, paths = value
+ yield Path(root), dirs, paths
+
+ def with_name(self, name: str) -> Path:
+ return Path(self._path.with_name(name))
+
+ def with_stem(self, stem: str) -> Path:
+ return Path(self._path.with_name(stem + self._path.suffix))
+
+ def with_suffix(self, suffix: str) -> Path:
+ return Path(self._path.with_suffix(suffix))
+
+ def with_segments(self, *pathsegments: str | PathLike[str]) -> Path:
+ return Path(*pathsegments)
+
+ async def write_bytes(self, data: bytes) -> int:
+ return await to_thread.run_sync(self._path.write_bytes, data)
+
+ async def write_text(
+ self,
+ data: str,
+ encoding: str | None = None,
+ errors: str | None = None,
+ newline: str | None = None,
+ ) -> int:
+ # Path.write_text() does not support the "newline" parameter before Python 3.10
+ def sync_write_text() -> int:
+ with self._path.open(
+ "w", encoding=encoding, errors=errors, newline=newline
+ ) as fp:
+ return fp.write(data)
+
+ return await to_thread.run_sync(sync_write_text)
+
+
+PathLike.register(Path)
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_resources.py b/venv/lib/python3.12/site-packages/anyio/_core/_resources.py
new file mode 100644
index 0000000..b9a5344
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/_core/_signals.py b/venv/lib/python3.12/site-packages/anyio/_core/_signals.py
new file mode 100644
index 0000000..e24c79e
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/_core/_sockets.py b/venv/lib/python3.12/site-packages/anyio/_core/_sockets.py
new file mode 100644
index 0000000..6c99b3a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_sockets.py
@@ -0,0 +1,1003 @@
+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 = ...,
+ 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 = ...,
+ 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 = ...,
+ 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 = ...,
+ 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 = ...,
+ happy_eyeballs_delay: float = ...,
+) -> SocketStream: ...
+
+
+async def connect_tcp(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ local_host: IPAddressType | 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 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), None)
+ 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.12/site-packages/anyio/_core/_streams.py b/venv/lib/python3.12/site-packages/anyio/_core/_streams.py
new file mode 100644
index 0000000..2b9c7df
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/_core/_subprocesses.py b/venv/lib/python3.12/site-packages/anyio/_core/_subprocesses.py
new file mode 100644
index 0000000..36d9b30
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_subprocesses.py
@@ -0,0 +1,202 @@
+from __future__ import annotations
+
+import sys
+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, Union, cast
+
+from ..abc import Process
+from ._eventloop import get_async_backend
+from ._tasks import create_task_group
+
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+else:
+ from typing_extensions import TypeAlias
+
+StrOrBytesPath: TypeAlias = Union[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.12/site-packages/anyio/_core/_synchronization.py b/venv/lib/python3.12/site-packages/anyio/_core/_synchronization.py
new file mode 100644
index 0000000..c0ef27a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_synchronization.py
@@ -0,0 +1,753 @@
+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:
+ 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):
+ _internal_event: Event | None = None
+ _is_set: bool = False
+
+ def __new__(cls) -> EventAdapter:
+ return object.__new__(cls)
+
+ @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:
+ 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):
+ _internal_lock: Lock | None = None
+
+ def __new__(cls, *, fast_acquire: bool = False) -> LockAdapter:
+ return object.__new__(cls)
+
+ def __init__(self, *, fast_acquire: bool = False):
+ 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:
+ _owner_task: TaskInfo | None = None
+
+ def __init__(self, lock: Lock | 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)
+
+ 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:
+ 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):
+ _internal_semaphore: Semaphore | None = None
+
+ 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._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:
+ 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):
+ _internal_limiter: CapacityLimiter | None = None
+
+ def __new__(cls, total_tokens: float) -> CapacityLimiterAdapter:
+ return object.__new__(cls)
+
+ def __init__(self, total_tokens: float) -> 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 < 1:
+ raise ValueError("total_tokens must be >= 1")
+
+ 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__ = "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.12/site-packages/anyio/_core/_tasks.py b/venv/lib/python3.12/site-packages/anyio/_core/_tasks.py
new file mode 100644
index 0000000..0688bfe
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_tasks.py
@@ -0,0 +1,173 @@
+from __future__ import annotations
+
+import math
+from collections.abc import Generator
+from contextlib import contextmanager
+from types import TracebackType
+
+from ..abc._tasks import TaskGroup, TaskStatus
+from ._eventloop import get_async_backend
+
+
+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
+ """
+
+ 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()
diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_tempfile.py b/venv/lib/python3.12/site-packages/anyio/_core/_tempfile.py
new file mode 100644
index 0000000..fbb6b14
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/_core/_tempfile.py
@@ -0,0 +1,616 @@
+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
+ (Python 3.10+).
+ :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,
+ }
+ if sys.version_info >= (3, 10):
+ params["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.12/site-packages/anyio/_core/_testing.py b/venv/lib/python3.12/site-packages/anyio/_core/_testing.py
new file mode 100644
index 0000000..369e65c
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/_core/_typedattr.py b/venv/lib/python3.12/site-packages/anyio/_core/_typedattr.py
new file mode 100644
index 0000000..f358a44
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/abc/__init__.py b/venv/lib/python3.12/site-packages/anyio/abc/__init__.py
new file mode 100644
index 0000000..d560ce3
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..5584cf8
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..1a910b7
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..4f658d7
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312.pyc
new file mode 100644
index 0000000..5f145e1
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..32161af
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc
new file mode 100644
index 0000000..9cd4811
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..71b80e5
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc
new file mode 100644
index 0000000..cea4998
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..6bb6d22
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc
new file mode 100644
index 0000000..e9ddd91
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..a26157b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc
new file mode 100644
index 0000000..bace610
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..fd0cf9e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312.pyc
new file mode 100644
index 0000000..f9b9802
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..a7554b3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312.pyc
new file mode 100644
index 0000000..2b12b5a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py b/venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py
new file mode 100644
index 0000000..b1bd085
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py
@@ -0,0 +1,414 @@
+from __future__ import annotations
+
+import math
+import sys
+from abc import ABCMeta, abstractmethod
+from collections.abc import AsyncIterator, Awaitable, Callable, 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,
+ TypeVar,
+ Union,
+ overload,
+)
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+else:
+ from typing_extensions import TypeAlias
+
+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")
+PosArgsT = TypeVarTuple("PosArgsT")
+StrOrBytesPath: TypeAlias = Union[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]], Awaitable[T_Retval]],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_Retval:
+ 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.12/site-packages/anyio/abc/_resources.py b/venv/lib/python3.12/site-packages/anyio/abc/_resources.py
new file mode 100644
index 0000000..10df115
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/abc/_sockets.py b/venv/lib/python3.12/site-packages/anyio/abc/_sockets.py
new file mode 100644
index 0000000..3ff60d4
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/abc/_sockets.py
@@ -0,0 +1,405 @@
+from __future__ import annotations
+
+import errno
+import socket
+import sys
+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, TypeVar, Union
+
+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
+
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+else:
+ from typing_extensions import TypeAlias
+
+IPAddressType: TypeAlias = Union[str, IPv4Address, IPv6Address]
+IPSockAddrType: TypeAlias = tuple[str, int]
+SockAddrType: TypeAlias = Union[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.12/site-packages/anyio/abc/_streams.py b/venv/lib/python3.12/site-packages/anyio/abc/_streams.py
new file mode 100644
index 0000000..369df3f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/abc/_streams.py
@@ -0,0 +1,239 @@
+from __future__ import annotations
+
+import sys
+from abc import ABCMeta, abstractmethod
+from collections.abc import Callable
+from typing import Any, Generic, TypeVar, Union
+
+from .._core._exceptions import EndOfStream
+from .._core._typedattr import TypedAttributeProvider
+from ._resources import AsyncResource
+from ._tasks import TaskGroup
+
+if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+else:
+ from typing_extensions import TypeAlias
+
+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 = Union[
+ UnreliableObjectReceiveStream[bytes], ByteReceiveStream
+]
+#: Type alias for all unreliable bytes-oriented send streams.
+AnyUnreliableByteSendStream: TypeAlias = Union[
+ UnreliableObjectSendStream[bytes], ByteSendStream
+]
+#: Type alias for all unreliable bytes-oriented streams.
+AnyUnreliableByteStream: TypeAlias = Union[UnreliableObjectStream[bytes], ByteStream]
+#: Type alias for all bytes-oriented receive streams.
+AnyByteReceiveStream: TypeAlias = Union[ObjectReceiveStream[bytes], ByteReceiveStream]
+#: Type alias for all bytes-oriented send streams.
+AnyByteSendStream: TypeAlias = Union[ObjectSendStream[bytes], ByteSendStream]
+#: Type alias for all bytes-oriented streams.
+AnyByteStream: TypeAlias = Union[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 = Union[
+ ObjectStreamConnectable[bytes], ByteStreamConnectable
+]
diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_subprocesses.py b/venv/lib/python3.12/site-packages/anyio/abc/_subprocesses.py
new file mode 100644
index 0000000..ce0564c
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/abc/_tasks.py b/venv/lib/python3.12/site-packages/anyio/abc/_tasks.py
new file mode 100644
index 0000000..516b3ec
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/abc/_tasks.py
@@ -0,0 +1,117 @@
+from __future__ import annotations
+
+import sys
+from abc import ABCMeta, abstractmethod
+from collections.abc import Awaitable, Callable
+from types import TracebackType
+from typing import TYPE_CHECKING, Any, Protocol, 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
+
+T_Retval = TypeVar("T_Retval")
+T_contra = TypeVar("T_contra", contravariant=True, default=None)
+PosArgsT = TypeVarTuple("PosArgsT")
+
+
+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
+
+ @abstractmethod
+ def start_soon(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ ) -> None:
+ """
+ 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
+
+ .. versionadded:: 3.0
+ """
+
+ @abstractmethod
+ async def start(
+ self,
+ func: Callable[..., Awaitable[Any]],
+ *args: object,
+ name: object = None,
+ ) -> 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
+ :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.12/site-packages/anyio/abc/_testing.py b/venv/lib/python3.12/site-packages/anyio/abc/_testing.py
new file mode 100644
index 0000000..7c50ed7
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/abc/_testing.py
@@ -0,0 +1,65 @@
+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
+ """
diff --git a/venv/lib/python3.12/site-packages/anyio/from_thread.py b/venv/lib/python3.12/site-packages/anyio/from_thread.py
new file mode 100644
index 0000000..837de5e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/from_thread.py
@@ -0,0 +1,578 @@
+from __future__ import annotations
+
+__all__ = (
+ "BlockingPortal",
+ "BlockingPortalProvider",
+ "check_cancelled",
+ "run",
+ "run_sync",
+ "start_blocking_portal",
+)
+
+import sys
+from collections.abc import Awaitable, Callable, 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]], Awaitable[T_Retval]],
+ *args: Unpack[PosArgsT],
+ token: EventLoopToken | None = None,
+) -> T_Retval:
+ """
+ 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:
+ def callback(f: Future[T_Retval]) -> None:
+ if f.cancelled():
+ if self._event_loop_thread_id == get_ident():
+ scope.cancel("the future was cancelled")
+ elif self._event_loop_thread_id is not None:
+ self.call(scope.cancel, "the future was cancelled")
+
+ 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.12/site-packages/anyio/functools.py b/venv/lib/python3.12/site-packages/anyio/functools.py
new file mode 100644
index 0000000..b80afe6
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/functools.py
@@ -0,0 +1,375 @@
+from __future__ import annotations
+
+__all__ = (
+ "AsyncCacheInfo",
+ "AsyncCacheParameters",
+ "AsyncLRUCacheWrapper",
+ "cache",
+ "lru_cache",
+ "reduce",
+)
+
+import functools
+import sys
+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,
+ TypedDict,
+ TypeVar,
+ cast,
+ final,
+ overload,
+)
+from weakref import WeakKeyDictionary
+
+from ._core._synchronization import Lock
+from .lowlevel import RunVar, checkpoint
+
+if sys.version_info >= (3, 11):
+ from typing import ParamSpec
+else:
+ from typing_extensions import ParamSpec
+
+T = TypeVar("T")
+S = TypeVar("S")
+P = ParamSpec("P")
+lru_cache_items: RunVar[
+ WeakKeyDictionary[
+ AsyncLRUCacheWrapper[Any, Any],
+ OrderedDict[Hashable, tuple[_InitialMissingType, Lock] | tuple[Any, None]],
+ ]
+] = RunVar("lru_cache_items")
+
+
+class _InitialMissingType:
+ pass
+
+
+initial_missing: _InitialMissingType = _InitialMissingType()
+
+
+class AsyncCacheInfo(NamedTuple):
+ hits: int
+ misses: int
+ maxsize: int | None
+ currsize: int
+
+
+class AsyncCacheParameters(TypedDict):
+ maxsize: int | None
+ typed: bool
+ always_checkpoint: bool
+
+
+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,
+ ):
+ 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
+ update_wrapper(self, func)
+
+ def cache_info(self) -> AsyncCacheInfo:
+ return AsyncCacheInfo(self._hits, self._misses, self._maxsize, self._currsize)
+
+ def cache_parameters(self) -> AsyncCacheParameters:
+ return {
+ "maxsize": self._maxsize,
+ "typed": self._typed,
+ "always_checkpoint": self._always_checkpoint,
+ }
+
+ 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 = cache_entry[key]
+ except KeyError:
+ # We're the first task to call this function
+ cached_value, lock = (
+ initial_missing,
+ Lock(fast_acquire=not self._always_checkpoint),
+ )
+ cache_entry[key] = cached_value, lock
+
+ if lock is None:
+ # 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)
+ cache_entry[key] = value, None
+ 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(Generic[T]):
+ def __init__(self, maxsize: int | None, typed: bool, always_checkpoint: bool):
+ self._maxsize = maxsize
+ self._typed = typed
+ self._always_checkpoint = always_checkpoint
+
+ @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
+ )
+
+ 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[..., T] | Callable[P, Coroutine[Any, Any, T]], /
+) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]:
+ """
+ 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 = ...
+) -> _LRUCacheWrapper[Any]: ...
+
+
+@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[P, Coroutine[Any, Any, T]] | Callable[..., T] | None = None,
+ /,
+ *,
+ maxsize: int | None = 128,
+ typed: bool = False,
+ always_checkpoint: bool = False,
+) -> (
+ AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T] | _LRUCacheWrapper[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
+
+ .. note:: Caches and locks are managed on a per-event loop basis.
+
+ """
+ if func is None:
+ return _LRUCacheWrapper[Any](maxsize, typed, always_checkpoint)
+
+ if not callable(func):
+ raise TypeError("the first argument must be callable")
+
+ return _LRUCacheWrapper[T](maxsize, typed, always_checkpoint)(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.12/site-packages/anyio/lowlevel.py b/venv/lib/python3.12/site-packages/anyio/lowlevel.py
new file mode 100644
index 0000000..ffbb75a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/lowlevel.py
@@ -0,0 +1,196 @@
+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]):
+ __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:
+ 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]:
+ 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:
+ 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.12/site-packages/anyio/py.typed b/venv/lib/python3.12/site-packages/anyio/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/anyio/pytest_plugin.py b/venv/lib/python3.12/site-packages/anyio/pytest_plugin.py
new file mode 100644
index 0000000..4222816
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/pytest_plugin.py
@@ -0,0 +1,302 @@
+from __future__ import annotations
+
+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 SubRequest
+from _pytest.outcomes import Exit
+
+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:
+ 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)
+
+
+@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.12/site-packages/anyio/streams/__init__.py b/venv/lib/python3.12/site-packages/anyio/streams/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..1a745c3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..1a745c3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc
new file mode 100644
index 0000000..a9bd507
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc
new file mode 100644
index 0000000..c221b51
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..c4242cc
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc
new file mode 100644
index 0000000..a5e6bbf
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..1c7723e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312.pyc
new file mode 100644
index 0000000..56f3370
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/text.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/text.cpython-312.pyc
new file mode 100644
index 0000000..3522552
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/text.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312-pytest-7.4.4.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312-pytest-7.4.4.pyc
new file mode 100644
index 0000000..ad51a7b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312-pytest-7.4.4.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc
new file mode 100644
index 0000000..4a3fe8c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/anyio/streams/buffered.py b/venv/lib/python3.12/site-packages/anyio/streams/buffered.py
new file mode 100644
index 0000000..57c7cd7
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/streams/buffered.py
@@ -0,0 +1,188 @@
+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):
+ 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.12/site-packages/anyio/streams/file.py b/venv/lib/python3.12/site-packages/anyio/streams/file.py
new file mode 100644
index 0000000..82d2da8
--- /dev/null
+++ b/venv/lib/python3.12/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 Any, BinaryIO, cast
+
+from .. import (
+ BrokenResourceError,
+ ClosedResourceError,
+ EndOfStream,
+ TypedAttributeSet,
+ to_thread,
+ typed_attribute,
+)
+from ..abc import ByteReceiveStream, ByteSendStream
+
+
+class FileStreamAttribute(TypedAttributeSet):
+ #: the open file descriptor
+ file: BinaryIO = 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: BinaryIO):
+ 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(cast(BinaryIO, 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(cast(BinaryIO, 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.12/site-packages/anyio/streams/memory.py b/venv/lib/python3.12/site-packages/anyio/streams/memory.py
new file mode 100644
index 0000000..a3fa0c3
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/streams/stapled.py b/venv/lib/python3.12/site-packages/anyio/streams/stapled.py
new file mode 100644
index 0000000..9248b68
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/streams/text.py b/venv/lib/python3.12/site-packages/anyio/streams/text.py
new file mode 100644
index 0000000..296cd25
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/streams/tls.py b/venv/lib/python3.12/site-packages/anyio/streams/tls.py
new file mode 100644
index 0000000..b507488
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/streams/tls.py
@@ -0,0 +1,424 @@
+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, 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, 10):
+ from typing import TypeAlias
+else:
+ from typing_extensions import TypeAlias
+
+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.12/site-packages/anyio/to_interpreter.py b/venv/lib/python3.12/site-packages/anyio/to_interpreter.py
new file mode 100644
index 0000000..694dbe7
--- /dev/null
+++ b/venv/lib/python3.12/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.12/site-packages/anyio/to_process.py b/venv/lib/python3.12/site-packages/anyio/to_process.py
new file mode 100644
index 0000000..b289234
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/anyio/to_process.py
@@ -0,0 +1,266 @@
+from __future__ import annotations
+
+__all__ = (
+ "current_default_process_limiter",
+ "process_worker",
+ "run_sync",
+)
+
+import os
+import pickle
+import subprocess
+import sys
+from collections import deque
+from collections.abc import Callable
+from importlib.util import module_from_spec, spec_from_file_location
+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:
+ spec = spec_from_file_location("__mp_main__", main_module_path)
+ if spec and spec.loader:
+ main = module_from_spec(spec)
+ spec.loader.exec_module(main)
+ sys.modules["__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.12/site-packages/anyio/to_thread.py b/venv/lib/python3.12/site-packages/anyio/to_thread.py
new file mode 100644
index 0000000..4be5b71
--- /dev/null
+++ b/venv/lib/python3.12/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 ``cancellable`` 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.12/site-packages/boto3-1.34.0.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/LICENSE b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/LICENSE
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/LICENSE
@@ -0,0 +1,177 @@
+
+ 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
diff --git a/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/METADATA b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/METADATA
new file mode 100644
index 0000000..57cddf8
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/METADATA
@@ -0,0 +1,183 @@
+Metadata-Version: 2.1
+Name: boto3
+Version: 1.34.0
+Summary: The AWS SDK for Python
+Home-page: https://github.com/boto/boto3
+Author: Amazon Web Services
+License: Apache License 2.0
+Project-URL: Documentation, https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
+Project-URL: Source, https://github.com/boto/boto3
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Natural Language :: English
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Requires-Python: >= 3.8
+License-File: LICENSE
+License-File: NOTICE
+Requires-Dist: botocore (<1.35.0,>=1.34.0)
+Requires-Dist: jmespath (<2.0.0,>=0.7.1)
+Requires-Dist: s3transfer (<0.10.0,>=0.9.0)
+Provides-Extra: crt
+Requires-Dist: botocore[crt] (<2.0a0,>=1.21.0) ; extra == 'crt'
+
+===============================
+Boto3 - The AWS SDK for Python
+===============================
+
+|Version| |Python| |License|
+
+Boto3 is the Amazon Web Services (AWS) Software Development Kit (SDK) for
+Python, which allows Python developers to write software that makes use
+of services like Amazon S3 and Amazon EC2. You can find the latest, most
+up to date, documentation at our `doc site`_, including a list of
+services that are supported.
+
+Boto3 is maintained and published by `Amazon Web Services`_.
+
+Boto (pronounced boh-toh) was named after the fresh water dolphin native to the Amazon river. The name was chosen by the author of the original Boto library, Mitch Garnaat, as a reference to the company.
+
+Notices
+-------
+
+On 2023-12-13, support for Python 3.7 ended for Boto3. This follows the
+Python Software Foundation `end of support `__
+for the runtime which occurred on 2023-06-27.
+For more information, see this `blog post `__.
+
+.. _boto: https://docs.pythonboto.org/
+.. _`doc site`: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
+.. _`Amazon Web Services`: https://aws.amazon.com/what-is-aws/
+.. |Python| image:: https://img.shields.io/pypi/pyversions/boto3.svg?style=flat
+ :target: https://pypi.python.org/pypi/boto3/
+ :alt: Python Versions
+.. |Version| image:: http://img.shields.io/pypi/v/boto3.svg?style=flat
+ :target: https://pypi.python.org/pypi/boto3/
+ :alt: Package Version
+.. |License| image:: http://img.shields.io/pypi/l/boto3.svg?style=flat
+ :target: https://github.com/boto/boto3/blob/develop/LICENSE
+ :alt: License
+
+Getting Started
+---------------
+Assuming that you have a supported version of Python installed, you can first
+set up your environment with:
+
+.. code-block:: sh
+
+ $ python -m venv .venv
+ ...
+ $ . .venv/bin/activate
+
+Then, you can install boto3 from PyPI with:
+
+.. code-block:: sh
+
+ $ python -m pip install boto3
+
+or install from source with:
+
+.. code-block:: sh
+
+ $ git clone https://github.com/boto/boto3.git
+ $ cd boto3
+ $ python -m pip install -r requirements.txt
+ $ python -m pip install -e .
+
+
+Using Boto3
+~~~~~~~~~~~~~~
+After installing boto3
+
+Next, set up credentials (in e.g. ``~/.aws/credentials``):
+
+.. code-block:: ini
+
+ [default]
+ aws_access_key_id = YOUR_KEY
+ aws_secret_access_key = YOUR_SECRET
+
+Then, set up a default region (in e.g. ``~/.aws/config``):
+
+.. code-block:: ini
+
+ [default]
+ region=us-east-1
+
+Other credential configuration methods can be found `here `__
+
+Then, from a Python interpreter:
+
+.. code-block:: python
+
+ >>> import boto3
+ >>> s3 = boto3.resource('s3')
+ >>> for bucket in s3.buckets.all():
+ print(bucket.name)
+
+Running Tests
+~~~~~~~~~~~~~
+You can run tests in all supported Python versions using ``tox``. By default,
+it will run all of the unit and functional tests, but you can also specify your own
+``pytest`` options. Note that this requires that you have all supported
+versions of Python installed, otherwise you must pass ``-e`` or run the
+``pytest`` command directly:
+
+.. code-block:: sh
+
+ $ tox
+ $ tox -- unit/test_session.py
+ $ tox -e py26,py33 -- integration/
+
+You can also run individual tests with your default Python version:
+
+.. code-block:: sh
+
+ $ pytest tests/unit
+
+
+Getting Help
+------------
+
+We use GitHub issues for tracking bugs and feature requests and have limited
+bandwidth to address them. Please use these community resources for getting
+help:
+
+* Ask a question on `Stack Overflow `__ and tag it with `boto3 `__
+* Open a support ticket with `AWS Support `__
+* If it turns out that you may have found a bug, please `open an issue `__
+
+
+Contributing
+------------
+
+We value feedback and contributions from our community. Whether it's a bug report, new feature, correction, or additional documentation, we welcome your issues and pull requests. Please read through this `CONTRIBUTING `__ document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your contribution.
+
+
+Maintenance and Support for SDK Major Versions
+----------------------------------------------
+
+Boto3 was made generally available on 06/22/2015 and is currently in the full support phase of the availability life cycle.
+
+For information about maintenance and support for SDK major versions and their underlying dependencies, see the following in the AWS SDKs and Tools Shared Configuration and Credentials Reference Guide:
+
+* `AWS SDKs and Tools Maintenance Policy `__
+* `AWS SDKs and Tools Version Support Matrix `__
+
+
+More Resources
+--------------
+
+* `NOTICE `__
+* `Changelog `__
+* `License `__
+
+
diff --git a/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/NOTICE b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/NOTICE
new file mode 100644
index 0000000..eff609f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/NOTICE
@@ -0,0 +1,2 @@
+boto3
+Copyright 2013-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
diff --git a/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/RECORD b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/RECORD
new file mode 100644
index 0000000..00ba687
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/RECORD
@@ -0,0 +1,104 @@
+boto3-1.34.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+boto3-1.34.0.dist-info/LICENSE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
+boto3-1.34.0.dist-info/METADATA,sha256=ySOLlmadeITEKMtx0V74KY81Y_xHGHnsZX5diVDypag,6617
+boto3-1.34.0.dist-info/NOTICE,sha256=BPseYUhKeBDxugm7QrwByljJrzOSfXxaIVVuTE0cf6Q,83
+boto3-1.34.0.dist-info/RECORD,,
+boto3-1.34.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+boto3-1.34.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
+boto3-1.34.0.dist-info/top_level.txt,sha256=MP6_SI1GcPseXodd3Ykt5F_mCBsrUksiziLxjEZKGUU,6
+boto3/__init__.py,sha256=5YDbH95cvK_pLCvDyiz9bu-2xftmvyPpCfyG6YeRtrs,3419
+boto3/__pycache__/__init__.cpython-312.pyc,,
+boto3/__pycache__/compat.cpython-312.pyc,,
+boto3/__pycache__/crt.cpython-312.pyc,,
+boto3/__pycache__/exceptions.cpython-312.pyc,,
+boto3/__pycache__/session.cpython-312.pyc,,
+boto3/__pycache__/utils.cpython-312.pyc,,
+boto3/compat.py,sha256=1T-LvBYqd0z-L9hI-soU7O7v-678tyWWR2pztF055u0,2887
+boto3/crt.py,sha256=VFstUtHMZrZ6eHJJ-YdXb4vqfIOcHbv1l51fdeY5cS0,5407
+boto3/data/cloudformation/2010-05-15/resources-1.json,sha256=5mFVKJVtbVoHyPdHSyNfZ5mpkgCAws5PhnveSu4qzdI,5110
+boto3/data/cloudwatch/2010-08-01/resources-1.json,sha256=q4AgE8F4pbscd-2U3NYSGAzK55zpMyOQGr83JUxbZXI,11690
+boto3/data/dynamodb/2012-08-10/resources-1.json,sha256=hBLa1Jt7bdT557U9A7UcSi8SCpONKzdbtDRTzjM1-Y0,3849
+boto3/data/ec2/2014-10-01/resources-1.json,sha256=tMG1AMYP2ksnPWY6-3l8DB-EhKsSNtAO9YHhvHqBKu0,68469
+boto3/data/ec2/2015-03-01/resources-1.json,sha256=tMG1AMYP2ksnPWY6-3l8DB-EhKsSNtAO9YHhvHqBKu0,68469
+boto3/data/ec2/2015-04-15/resources-1.json,sha256=tMG1AMYP2ksnPWY6-3l8DB-EhKsSNtAO9YHhvHqBKu0,68469
+boto3/data/ec2/2015-10-01/resources-1.json,sha256=SOfYX2c1KgvnxMO2FCdJpV42rJWNMwVhlFAXhvUPTzA,76564
+boto3/data/ec2/2016-04-01/resources-1.json,sha256=SOfYX2c1KgvnxMO2FCdJpV42rJWNMwVhlFAXhvUPTzA,76564
+boto3/data/ec2/2016-09-15/resources-1.json,sha256=SOfYX2c1KgvnxMO2FCdJpV42rJWNMwVhlFAXhvUPTzA,76564
+boto3/data/ec2/2016-11-15/resources-1.json,sha256=vx7YiL-sUvBFeo4SZ81G7Qa2Hy-y6xY4z2YlSx7_wEw,76922
+boto3/data/glacier/2012-06-01/resources-1.json,sha256=GT5qWQLGeXtrHgTDNG23Mrpyweg6O0Udgd139BuNTVs,19940
+boto3/data/iam/2010-05-08/resources-1.json,sha256=PsOT9yBqSJtluBFHCVRsg6k6Ly2VkSYODnYxSl0DVOc,50357
+boto3/data/opsworks/2013-02-18/resources-1.json,sha256=Y6ygEyegsbYA1gGZn-Ad2yuDd3jUCOt2UKrW_b2YBeM,4136
+boto3/data/s3/2006-03-01/resources-1.json,sha256=VeKALhMRqv7fyDHMLOM5_RzXUEuDdg_n6OIRi3sdB-o,37204
+boto3/data/sns/2010-03-31/resources-1.json,sha256=7zmKQhafgsRDu4U1yiw3NXHz-zJhHKrOmtuoYlxQP-s,9091
+boto3/data/sqs/2012-11-05/resources-1.json,sha256=LRIIr5BId3UDeuBfLn-vRiWsSZCM9_ynqdxF8uzHgy8,6545
+boto3/docs/__init__.py,sha256=ncXQfWgitU2kFSghqy2lezeeW1RneKZ-3wcsvEddsr0,1845
+boto3/docs/__pycache__/__init__.cpython-312.pyc,,
+boto3/docs/__pycache__/action.cpython-312.pyc,,
+boto3/docs/__pycache__/attr.cpython-312.pyc,,
+boto3/docs/__pycache__/base.cpython-312.pyc,,
+boto3/docs/__pycache__/client.cpython-312.pyc,,
+boto3/docs/__pycache__/collection.cpython-312.pyc,,
+boto3/docs/__pycache__/docstring.cpython-312.pyc,,
+boto3/docs/__pycache__/method.cpython-312.pyc,,
+boto3/docs/__pycache__/resource.cpython-312.pyc,,
+boto3/docs/__pycache__/service.cpython-312.pyc,,
+boto3/docs/__pycache__/subresource.cpython-312.pyc,,
+boto3/docs/__pycache__/utils.cpython-312.pyc,,
+boto3/docs/__pycache__/waiter.cpython-312.pyc,,
+boto3/docs/action.py,sha256=5ZQ2C9vIZdk8grFlnej-cwpVoNz0drcMiirKzqHczck,8178
+boto3/docs/attr.py,sha256=BnG3tR1KKQvvY58aeJiWQ5W5DiMnJ_9jUjmG6tDbFiU,2500
+boto3/docs/base.py,sha256=nOrQSCeUSIZPkn-I59o7CfjEthgdkpCt_rXtE9zQnXc,2103
+boto3/docs/client.py,sha256=RpCngTolE4OtGIPvvJvkw8FJCqh5-7b-Q0QN5mVE7to,1078
+boto3/docs/collection.py,sha256=pWO9I9LTMyhyYyCT_alrO4hZpqNI1228IwABotYTqBU,11679
+boto3/docs/docstring.py,sha256=oPugaubdAXY6aNa-kXGI51lP1xE2s4AnfTsLhibf7-E,2511
+boto3/docs/method.py,sha256=kJ3UJS2JBSt6SB_3TsEf3lxcjda5TAAfmocrLmxtSLc,2733
+boto3/docs/resource.py,sha256=HBFq7c-tio19Da2Wb60j99EcW-I9M5-_C-9IjTuWrok,15376
+boto3/docs/service.py,sha256=bCd2LPfZOeTkDOKggTyXJYXXPkuYUy91x5KYyqPPQnE,8544
+boto3/docs/subresource.py,sha256=W19brjJjeW55ssyYhCnFaZICfp2LjOoC4BP_jW2ViC8,5864
+boto3/docs/utils.py,sha256=H0UeVvmVbYBZ6F-CVEUxVggLMBOIoA5q8y8hxBFnRKE,5436
+boto3/docs/waiter.py,sha256=xfnXtbMTOCyNG9vTNZW7Alsy77ZXuJCFcQcq0sNtg8Q,5175
+boto3/dynamodb/__init__.py,sha256=GkSq-WxXWfVHu1SEcMrlJbzkfw9ACgF3UdCL6fPpTmY,562
+boto3/dynamodb/__pycache__/__init__.cpython-312.pyc,,
+boto3/dynamodb/__pycache__/conditions.cpython-312.pyc,,
+boto3/dynamodb/__pycache__/table.cpython-312.pyc,,
+boto3/dynamodb/__pycache__/transform.cpython-312.pyc,,
+boto3/dynamodb/__pycache__/types.cpython-312.pyc,,
+boto3/dynamodb/conditions.py,sha256=sjkd0kIqFP_h8aUvysZQel0zts5HF22ogqKiv0t0KRw,15045
+boto3/dynamodb/table.py,sha256=us79dxZSQSno8gsUoAdQyzc2oBJL2riUpN6RGc8vQk8,6343
+boto3/dynamodb/transform.py,sha256=JnW5ZzPIfxEcDszSvXKUZmp_1rw445tsddS3FG--JwA,12909
+boto3/dynamodb/types.py,sha256=ch0vIKaAYexjL42S_OJWyvjWMcb0UbNrmkKGcz76O3c,9541
+boto3/ec2/__init__.py,sha256=GkSq-WxXWfVHu1SEcMrlJbzkfw9ACgF3UdCL6fPpTmY,562
+boto3/ec2/__pycache__/__init__.cpython-312.pyc,,
+boto3/ec2/__pycache__/createtags.cpython-312.pyc,,
+boto3/ec2/__pycache__/deletetags.cpython-312.pyc,,
+boto3/ec2/createtags.py,sha256=pUPJOYn7m0Jcch9UL-DEVGgbQHoyAemECPBhzyBx28c,1577
+boto3/ec2/deletetags.py,sha256=KaYcqSt8FFM_TW0g0pZ14qDjVnmRCPV0sMe6DprEtvo,1217
+boto3/examples/cloudfront.rst,sha256=K-sBWZxoLjABCZHrqAZs57cYefwPmDir03pm6PE_mh4,1390
+boto3/examples/s3.rst,sha256=jCfgEDfpw08nFtCizCN2OGg15zQRkx3DiJXZUfqhE2s,5486
+boto3/exceptions.py,sha256=i13QpGxoFizxAGCzA2qmF9ldbI5IfBpn37DH75ddRF8,4127
+boto3/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+boto3/resources/__pycache__/__init__.cpython-312.pyc,,
+boto3/resources/__pycache__/action.cpython-312.pyc,,
+boto3/resources/__pycache__/base.cpython-312.pyc,,
+boto3/resources/__pycache__/collection.cpython-312.pyc,,
+boto3/resources/__pycache__/factory.cpython-312.pyc,,
+boto3/resources/__pycache__/model.cpython-312.pyc,,
+boto3/resources/__pycache__/params.cpython-312.pyc,,
+boto3/resources/__pycache__/response.cpython-312.pyc,,
+boto3/resources/action.py,sha256=vPfVHVgXiGqhwpgRSCC7lSsY3vGjgsSiYhXa14CMAqw,9600
+boto3/resources/base.py,sha256=Nf5Anssquo3urPDyWLAN8di379z5oafjwzl3gD9WbsI,5044
+boto3/resources/collection.py,sha256=bSV0353zcTRLEPws2qqMFd2Xure8I8LgU-IDR-TM3sI,19242
+boto3/resources/factory.py,sha256=iXV5l7UZePNIfkkUMgUNC0tIdJhxr_65m9KYdwIOfKA,22708
+boto3/resources/model.py,sha256=3mCNSvnmCKPzjK-hW4yEv0PjKYb0hxBsAE9nopY-3bU,20394
+boto3/resources/params.py,sha256=i6KAjOzjzou7ouViYbRZCz0CwqB6fA_6gOJFDIruTV8,6112
+boto3/resources/response.py,sha256=aC1AZuO08qtb1psJtbrc5Na32AQ9WI-Il4DpVxsUtXs,11694
+boto3/s3/__init__.py,sha256=GkSq-WxXWfVHu1SEcMrlJbzkfw9ACgF3UdCL6fPpTmY,562
+boto3/s3/__pycache__/__init__.cpython-312.pyc,,
+boto3/s3/__pycache__/constants.cpython-312.pyc,,
+boto3/s3/__pycache__/inject.cpython-312.pyc,,
+boto3/s3/__pycache__/transfer.cpython-312.pyc,,
+boto3/s3/constants.py,sha256=ZaYknNwqGwsJEGkL92GXaBs9kjfRbyCDFt89wei8t7E,690
+boto3/s3/inject.py,sha256=t1XiGqUIB_BNtJSiKgqu1hfzhfCntWzmUSxWgCPv4bU,28205
+boto3/s3/transfer.py,sha256=830bC0zLuZZBsUZ-WxVNMMfcM7HxEhWt4bqSAw9xQoQ,15928
+boto3/session.py,sha256=ITqrFauYJ74IfZrpPS411rLaKNNVQKgCe388fpnUV-0,20758
+boto3/utils.py,sha256=dBw0Eu23TOhDsP1Lkrp4uOVMn5DS8s0kRGwVRiCD_KM,3141
diff --git a/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/REQUESTED b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/WHEEL b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/WHEEL
new file mode 100644
index 0000000..5bad85f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/top_level.txt
new file mode 100644
index 0000000..30ddf82
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3-1.34.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+boto3
diff --git a/venv/lib/python3.12/site-packages/boto3/__init__.py b/venv/lib/python3.12/site-packages/boto3/__init__.py
new file mode 100644
index 0000000..a167e9f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/__init__.py
@@ -0,0 +1,111 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import logging
+
+from boto3.compat import _warn_deprecated_python
+from boto3.session import Session
+
+__author__ = 'Amazon Web Services'
+__version__ = '1.34.0'
+
+
+# The default Boto3 session; autoloaded when needed.
+DEFAULT_SESSION = None
+
+
+def setup_default_session(**kwargs):
+ """
+ Set up a default session, passing through any parameters to the session
+ constructor. There is no need to call this unless you wish to pass custom
+ parameters, because a default session will be created for you.
+ """
+ global DEFAULT_SESSION
+ DEFAULT_SESSION = Session(**kwargs)
+
+
+def set_stream_logger(name='boto3', level=logging.DEBUG, format_string=None):
+ """
+ Add a stream handler for the given name and level to the logging module.
+ By default, this logs all boto3 messages to ``stdout``.
+
+ >>> import boto3
+ >>> boto3.set_stream_logger('boto3.resources', logging.INFO)
+
+ For debugging purposes a good choice is to set the stream logger to ``''``
+ which is equivalent to saying "log everything".
+
+ .. WARNING::
+ Be aware that when logging anything from ``'botocore'`` the full wire
+ trace will appear in your logs. If your payloads contain sensitive data
+ this should not be used in production.
+
+ :type name: string
+ :param name: Log name
+ :type level: int
+ :param level: Logging level, e.g. ``logging.INFO``
+ :type format_string: str
+ :param format_string: Log message format
+ """
+ if format_string is None:
+ format_string = "%(asctime)s %(name)s [%(levelname)s] %(message)s"
+
+ logger = logging.getLogger(name)
+ logger.setLevel(level)
+ handler = logging.StreamHandler()
+ handler.setLevel(level)
+ formatter = logging.Formatter(format_string)
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+
+
+def _get_default_session():
+ """
+ Get the default session, creating one if needed.
+
+ :rtype: :py:class:`~boto3.session.Session`
+ :return: The default session
+ """
+ if DEFAULT_SESSION is None:
+ setup_default_session()
+ _warn_deprecated_python()
+
+ return DEFAULT_SESSION
+
+
+def client(*args, **kwargs):
+ """
+ Create a low-level service client by name using the default session.
+
+ See :py:meth:`boto3.session.Session.client`.
+ """
+ return _get_default_session().client(*args, **kwargs)
+
+
+def resource(*args, **kwargs):
+ """
+ Create a resource service client by name using the default session.
+
+ See :py:meth:`boto3.session.Session.resource`.
+ """
+ return _get_default_session().resource(*args, **kwargs)
+
+
+# Set up logging to ``/dev/null`` like a library is supposed to.
+# https://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library
+class NullHandler(logging.Handler):
+ def emit(self, record):
+ pass
+
+
+logging.getLogger('boto3').addHandler(NullHandler())
diff --git a/venv/lib/python3.12/site-packages/boto3/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..9476c11
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/__pycache__/compat.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/__pycache__/compat.cpython-312.pyc
new file mode 100644
index 0000000..7bcbd25
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/__pycache__/compat.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/__pycache__/crt.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/__pycache__/crt.cpython-312.pyc
new file mode 100644
index 0000000..b030392
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/__pycache__/crt.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/__pycache__/exceptions.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/__pycache__/exceptions.cpython-312.pyc
new file mode 100644
index 0000000..46c792e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/__pycache__/exceptions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/__pycache__/session.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/__pycache__/session.cpython-312.pyc
new file mode 100644
index 0000000..79180c6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/__pycache__/session.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/__pycache__/utils.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/__pycache__/utils.cpython-312.pyc
new file mode 100644
index 0000000..43499d2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/__pycache__/utils.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/compat.py b/venv/lib/python3.12/site-packages/boto3/compat.py
new file mode 100644
index 0000000..ec53a99
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/compat.py
@@ -0,0 +1,82 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import sys
+import os
+import errno
+import socket
+import warnings
+
+from boto3.exceptions import PythonDeprecationWarning
+
+# In python3, socket.error is OSError, which is too general
+# for what we want (i.e FileNotFoundError is a subclass of OSError).
+# In py3 all the socket related errors are in a newly created
+# ConnectionError
+SOCKET_ERROR = ConnectionError
+
+import collections.abc as collections_abc
+
+
+if sys.platform.startswith('win'):
+ def rename_file(current_filename, new_filename):
+ try:
+ os.remove(new_filename)
+ except OSError as e:
+ if not e.errno == errno.ENOENT:
+ # We only want to a ignore trying to remove
+ # a file that does not exist. If it fails
+ # for any other reason we should be propagating
+ # that exception.
+ raise
+ os.rename(current_filename, new_filename)
+else:
+ rename_file = os.rename
+
+
+def filter_python_deprecation_warnings():
+ """
+ Invoking this filter acknowledges your runtime will soon be deprecated
+ at which time you will stop receiving all updates to your client.
+ """
+ warnings.filterwarnings(
+ 'ignore',
+ message=".*Boto3 will no longer support Python.*",
+ category=PythonDeprecationWarning,
+ module=r".*boto3\.compat"
+ )
+
+
+def _warn_deprecated_python():
+ """Use this template for future deprecation campaigns as needed."""
+ py_37_params = {
+ 'date': 'December 13, 2023',
+ 'blog_link': (
+ 'https://aws.amazon.com/blogs/developer/'
+ 'python-support-policy-updates-for-aws-sdks-and-tools/'
+ )
+ }
+ deprecated_versions = {
+ # Example template for future deprecations
+ (3, 7): py_37_params,
+ }
+ py_version = sys.version_info[:2]
+
+ if py_version in deprecated_versions:
+ params = deprecated_versions[py_version]
+ warning = (
+ "Boto3 will no longer support Python {}.{} "
+ "starting {}. To continue receiving service updates, "
+ "bug fixes, and security updates please upgrade to Python 3.8 or "
+ "later. More information can be found here: {}"
+ ).format(py_version[0], py_version[1], params['date'], params['blog_link'])
+ warnings.warn(warning, PythonDeprecationWarning)
diff --git a/venv/lib/python3.12/site-packages/boto3/crt.py b/venv/lib/python3.12/site-packages/boto3/crt.py
new file mode 100644
index 0000000..4b8df31
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/crt.py
@@ -0,0 +1,167 @@
+# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+"""
+This file contains private functionality for interacting with the AWS
+Common Runtime library (awscrt) in boto3.
+
+All code contained within this file is for internal usage within this
+project and is not intended for external consumption. All interfaces
+contained within are subject to abrupt breaking changes.
+"""
+
+import threading
+
+import botocore.exceptions
+from botocore.session import Session
+from s3transfer.crt import (
+ BotocoreCRTCredentialsWrapper,
+ BotocoreCRTRequestSerializer,
+ CRTTransferManager,
+ acquire_crt_s3_process_lock,
+ create_s3_crt_client,
+)
+
+# Singletons for CRT-backed transfers
+CRT_S3_CLIENT = None
+BOTOCORE_CRT_SERIALIZER = None
+
+CLIENT_CREATION_LOCK = threading.Lock()
+PROCESS_LOCK_NAME = 'boto3'
+
+
+def _create_crt_client(session, config, region_name, cred_provider):
+ """Create a CRT S3 Client for file transfer.
+
+ Instantiating many of these may lead to degraded performance or
+ system resource exhaustion.
+ """
+ create_crt_client_kwargs = {
+ 'region': region_name,
+ 'use_ssl': True,
+ 'crt_credentials_provider': cred_provider,
+ }
+ return create_s3_crt_client(**create_crt_client_kwargs)
+
+
+def _create_crt_request_serializer(session, region_name):
+ return BotocoreCRTRequestSerializer(
+ session, {'region_name': region_name, 'endpoint_url': None}
+ )
+
+
+def _create_crt_s3_client(
+ session, config, region_name, credentials, lock, **kwargs
+):
+ """Create boto3 wrapper class to manage crt lock reference and S3 client."""
+ cred_wrapper = BotocoreCRTCredentialsWrapper(credentials)
+ cred_provider = cred_wrapper.to_crt_credentials_provider()
+ return CRTS3Client(
+ _create_crt_client(session, config, region_name, cred_provider),
+ lock,
+ region_name,
+ cred_wrapper,
+ )
+
+
+def _initialize_crt_transfer_primatives(client, config):
+ lock = acquire_crt_s3_process_lock(PROCESS_LOCK_NAME)
+ if lock is None:
+ # If we're unable to acquire the lock, we cannot
+ # use the CRT in this process and should default to
+ # the classic s3transfer manager.
+ return None, None
+
+ session = Session()
+ region_name = client.meta.region_name
+ credentials = client._get_credentials()
+
+ serializer = _create_crt_request_serializer(session, region_name)
+ s3_client = _create_crt_s3_client(
+ session, config, region_name, credentials, lock
+ )
+ return serializer, s3_client
+
+
+def get_crt_s3_client(client, config):
+ global CRT_S3_CLIENT
+ global BOTOCORE_CRT_SERIALIZER
+
+ with CLIENT_CREATION_LOCK:
+ if CRT_S3_CLIENT is None:
+ serializer, s3_client = _initialize_crt_transfer_primatives(
+ client, config
+ )
+ BOTOCORE_CRT_SERIALIZER = serializer
+ CRT_S3_CLIENT = s3_client
+
+ return CRT_S3_CLIENT
+
+
+class CRTS3Client:
+ """
+ This wrapper keeps track of our underlying CRT client, the lock used to
+ acquire it and the region we've used to instantiate the client.
+
+ Due to limitations in the existing CRT interfaces, we can only make calls
+ in a single region and does not support redirects. We track the region to
+ ensure we don't use the CRT client when a successful request cannot be made.
+ """
+
+ def __init__(self, crt_client, process_lock, region, cred_provider):
+ self.crt_client = crt_client
+ self.process_lock = process_lock
+ self.region = region
+ self.cred_provider = cred_provider
+
+
+def is_crt_compatible_request(client, crt_s3_client):
+ """
+ Boto3 client must use same signing region and credentials
+ as the CRT_S3_CLIENT singleton. Otherwise fallback to classic.
+ """
+ if crt_s3_client is None:
+ return False
+
+ boto3_creds = client._get_credentials()
+ if boto3_creds is None:
+ return False
+
+ is_same_identity = compare_identity(
+ boto3_creds.get_frozen_credentials(), crt_s3_client.cred_provider
+ )
+ is_same_region = client.meta.region_name == crt_s3_client.region
+ return is_same_region and is_same_identity
+
+
+def compare_identity(boto3_creds, crt_s3_creds):
+ try:
+ crt_creds = crt_s3_creds()
+ except botocore.exceptions.NoCredentialsError:
+ return False
+
+ is_matching_identity = (
+ boto3_creds.access_key == crt_creds.access_key_id
+ and boto3_creds.secret_key == crt_creds.secret_access_key
+ and boto3_creds.token == crt_creds.session_token
+ )
+ return is_matching_identity
+
+
+def create_crt_transfer_manager(client, config):
+ """Create a CRTTransferManager for optimized data transfer."""
+ crt_s3_client = get_crt_s3_client(client, config)
+ if is_crt_compatible_request(client, crt_s3_client):
+ return CRTTransferManager(
+ crt_s3_client.crt_client, BOTOCORE_CRT_SERIALIZER
+ )
+ return None
diff --git a/venv/lib/python3.12/site-packages/boto3/data/cloudformation/2010-05-15/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/cloudformation/2010-05-15/resources-1.json
new file mode 100644
index 0000000..fd43937
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/cloudformation/2010-05-15/resources-1.json
@@ -0,0 +1,195 @@
+{
+ "service": {
+ "actions": {
+ "CreateStack": {
+ "request": { "operation": "CreateStack" },
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "StackName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Event": {
+ "resource": {
+ "type": "Event",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Stack": {
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Stacks": {
+ "request": { "operation": "DescribeStacks" },
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "Stacks[].StackName" }
+ ],
+ "path": "Stacks[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "Event": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "EventId"
+ }
+ ],
+ "shape": "StackEvent"
+ },
+ "Stack": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "StackName"
+ }
+ ],
+ "shape": "Stack",
+ "load": {
+ "request": {
+ "operation": "DescribeStacks",
+ "params": [
+ { "target": "StackName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "Stacks[0]"
+ },
+ "actions": {
+ "CancelUpdate": {
+ "request": {
+ "operation": "CancelUpdateStack",
+ "params": [
+ { "target": "StackName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteStack",
+ "params": [
+ { "target": "StackName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Update": {
+ "request": {
+ "operation": "UpdateStack",
+ "params": [
+ { "target": "StackName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Resource": {
+ "resource": {
+ "type": "StackResource",
+ "identifiers": [
+ { "target": "StackName", "source": "identifier", "name": "Name" },
+ { "target": "LogicalId", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Events": {
+ "request": {
+ "operation": "DescribeStackEvents",
+ "params": [
+ { "target": "StackName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Event",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "StackEvents[].EventId" }
+ ],
+ "path": "StackEvents[]"
+ }
+ },
+ "ResourceSummaries": {
+ "request": {
+ "operation": "ListStackResources",
+ "params": [
+ { "target": "StackName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "StackResourceSummary",
+ "identifiers": [
+ { "target": "LogicalId", "source": "response", "path": "StackResourceSummaries[].LogicalResourceId" },
+ { "target": "StackName", "source": "requestParameter", "path": "StackName" }
+ ],
+ "path": "StackResourceSummaries[]"
+ }
+ }
+ }
+ },
+ "StackResource": {
+ "identifiers": [
+ { "name": "StackName" },
+ {
+ "name": "LogicalId",
+ "memberName": "LogicalResourceId"
+ }
+ ],
+ "shape": "StackResourceDetail",
+ "load": {
+ "request": {
+ "operation": "DescribeStackResource",
+ "params": [
+ { "target": "LogicalResourceId", "source": "identifier", "name": "LogicalId" },
+ { "target": "StackName", "source": "identifier", "name": "StackName" }
+ ]
+ },
+ "path": "StackResourceDetail"
+ },
+ "has": {
+ "Stack": {
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "StackName" }
+ ]
+ }
+ }
+ }
+ },
+ "StackResourceSummary": {
+ "identifiers": [
+ { "name": "StackName" },
+ {
+ "name": "LogicalId",
+ "memberName": "LogicalResourceId"
+ }
+ ],
+ "shape": "StackResourceSummary",
+ "has": {
+ "Resource": {
+ "resource": {
+ "type": "StackResource",
+ "identifiers": [
+ { "target": "LogicalId", "source": "identifier", "name": "LogicalId" },
+ { "target": "StackName", "source": "identifier", "name": "StackName" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/cloudwatch/2010-08-01/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/cloudwatch/2010-08-01/resources-1.json
new file mode 100644
index 0000000..e0746d0
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/cloudwatch/2010-08-01/resources-1.json
@@ -0,0 +1,334 @@
+{
+ "service": {
+ "has": {
+ "Alarm": {
+ "resource": {
+ "type": "Alarm",
+ "identifiers": [
+ {
+ "target": "Name",
+ "source": "input"
+ }
+ ]
+ }
+ },
+ "Metric": {
+ "resource": {
+ "type": "Metric",
+ "identifiers": [
+ {
+ "target": "Namespace",
+ "source": "input"
+ },
+ {
+ "target": "Name",
+ "source": "input"
+ }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Alarms": {
+ "request": { "operation": "DescribeAlarms" },
+ "resource": {
+ "type": "Alarm",
+ "identifiers": [
+ {
+ "target": "Name",
+ "source": "response",
+ "path": "MetricAlarms[].AlarmName"
+ }
+ ],
+ "path": "MetricAlarms[]"
+ }
+ },
+ "Metrics": {
+ "request": { "operation": "ListMetrics" },
+ "resource": {
+ "type": "Metric",
+ "identifiers": [
+ {
+ "target": "Namespace",
+ "source": "response",
+ "path": "Metrics[].Namespace"
+ },
+ {
+ "target": "Name",
+ "source": "response",
+ "path": "Metrics[].MetricName"
+ }
+ ],
+ "path": "Metrics[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "Alarm": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "AlarmName"
+ }
+ ],
+ "shape": "MetricAlarm",
+ "load": {
+ "request": {
+ "operation": "DescribeAlarms",
+ "params": [
+ {
+ "target": "AlarmNames[0]",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ },
+ "path": "MetricAlarms[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteAlarms",
+ "params": [
+ {
+ "target": "AlarmNames[0]",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "DescribeHistory": {
+ "request": {
+ "operation": "DescribeAlarmHistory",
+ "params": [
+ {
+ "target": "AlarmName",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "DisableActions": {
+ "request": {
+ "operation": "DisableAlarmActions",
+ "params": [
+ {
+ "target": "AlarmNames[0]",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "EnableActions": {
+ "request": {
+ "operation": "EnableAlarmActions",
+ "params": [
+ {
+ "target": "AlarmNames[0]",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "SetState": {
+ "request": {
+ "operation": "SetAlarmState",
+ "params": [
+ {
+ "target": "AlarmName",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteAlarms",
+ "params": [
+ {
+ "target": "AlarmNames[]",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "DisableActions": {
+ "request": {
+ "operation": "DisableAlarmActions",
+ "params": [
+ {
+ "target": "AlarmNames[]",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "EnableActions": {
+ "request": {
+ "operation": "EnableAlarmActions",
+ "params": [
+ {
+ "target": "AlarmNames[]",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Metric": {
+ "resource": {
+ "type": "Metric",
+ "identifiers": [
+ {
+ "target": "Namespace",
+ "source": "data",
+ "path": "Namespace"
+ },
+ {
+ "target": "Name",
+ "source": "data",
+ "path": "MetricName"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "Metric": {
+ "identifiers": [
+ {
+ "name": "Namespace",
+ "memberName": "Namespace"
+ },
+ {
+ "name": "Name",
+ "memberName": "MetricName"
+ }
+ ],
+ "shape": "Metric",
+ "load": {
+ "request": {
+ "operation": "ListMetrics",
+ "params": [
+ {
+ "target": "MetricName",
+ "source": "identifier",
+ "name": "Name"
+ },
+ {
+ "target": "Namespace",
+ "source": "identifier",
+ "name": "Namespace"
+ }
+ ]
+ },
+ "path": "Metrics[0]"
+ },
+ "actions": {
+ "GetStatistics": {
+ "request": {
+ "operation": "GetMetricStatistics",
+ "params": [
+ {
+ "target": "Namespace",
+ "source": "identifier",
+ "name": "Namespace"
+ },
+ {
+ "target": "MetricName",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "PutAlarm": {
+ "request": {
+ "operation": "PutMetricAlarm",
+ "params": [
+ {
+ "target": "Namespace",
+ "source": "identifier",
+ "name": "Namespace"
+ },
+ {
+ "target": "MetricName",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ },
+ "resource": {
+ "type": "Alarm",
+ "identifiers": [
+ {
+ "target": "Name",
+ "source": "requestParameter",
+ "path": "AlarmName"
+ }
+ ]
+ }
+ },
+ "PutData": {
+ "request": {
+ "operation": "PutMetricData",
+ "params": [
+ {
+ "target": "Namespace",
+ "source": "identifier",
+ "name": "Namespace"
+ },
+ {
+ "target": "MetricData[].MetricName",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Alarms": {
+ "request": {
+ "operation": "DescribeAlarmsForMetric",
+ "params": [
+ {
+ "target": "Namespace",
+ "source": "identifier",
+ "name": "Namespace"
+ },
+ {
+ "target": "MetricName",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ },
+ "resource": {
+ "type": "Alarm",
+ "identifiers": [
+ {
+ "target": "Name",
+ "source": "response",
+ "path": "MetricAlarms[].AlarmName"
+ }
+ ],
+ "path": "MetricAlarms[]"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/dynamodb/2012-08-10/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/dynamodb/2012-08-10/resources-1.json
new file mode 100644
index 0000000..b79994e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/dynamodb/2012-08-10/resources-1.json
@@ -0,0 +1,150 @@
+{
+ "service": {
+ "actions": {
+ "BatchGetItem": {
+ "request": { "operation": "BatchGetItem" }
+ },
+ "BatchWriteItem": {
+ "request": { "operation": "BatchWriteItem" }
+ },
+ "CreateTable": {
+ "request": { "operation": "CreateTable" },
+ "resource": {
+ "type": "Table",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "TableDescription.TableName" }
+ ],
+ "path": "TableDescription"
+ }
+ }
+ },
+ "has": {
+ "Table": {
+ "resource": {
+ "type": "Table",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Tables": {
+ "request": { "operation": "ListTables" },
+ "resource": {
+ "type": "Table",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "TableNames[]" }
+ ]
+ }
+ }
+ }
+ },
+ "resources": {
+ "Table": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "TableName"
+ }
+ ],
+ "shape": "TableDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTable",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "Table"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTable",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "DeleteItem": {
+ "request": {
+ "operation": "DeleteItem",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "GetItem": {
+ "request": {
+ "operation": "GetItem",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "PutItem": {
+ "request": {
+ "operation": "PutItem",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Query": {
+ "request": {
+ "operation": "Query",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Scan": {
+ "request": {
+ "operation": "Scan",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Update": {
+ "request": {
+ "operation": "UpdateTable",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Table",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "Name" }
+ ],
+ "path": "TableDescription"
+ }
+ },
+ "UpdateItem": {
+ "request": {
+ "operation": "UpdateItem",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "waiters":{
+ "Exists": {
+ "waiterName": "TableExists",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "NotExists": {
+ "waiterName": "TableNotExists",
+ "params": [
+ { "target": "TableName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/ec2/2014-10-01/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/ec2/2014-10-01/resources-1.json
new file mode 100644
index 0000000..8ccf160
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/ec2/2014-10-01/resources-1.json
@@ -0,0 +1,2289 @@
+{
+ "service": {
+ "actions": {
+ "CreateDhcpOptions": {
+ "request": { "operation": "CreateDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions.DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions"
+ }
+ },
+ "CreateInstances": {
+ "request": { "operation": "RunInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateInternetGateway": {
+ "request": { "operation": "CreateInternetGateway" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateway.InternetGatewayId" }
+ ],
+ "path": "InternetGateway"
+ }
+ },
+ "CreateKeyPair": {
+ "request": { "operation": "CreateKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": { "operation": "CreateNetworkAcl" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": { "operation": "CreateNetworkInterface" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreatePlacementGroup": {
+ "request": { "operation": "CreatePlacementGroup" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ]
+ }
+ },
+ "CreateRouteTable": {
+ "request": { "operation": "CreateRouteTable" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": { "operation": "CreateSecurityGroup" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": { "operation": "CreateSnapshot" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateSubnet": {
+ "request": { "operation": "CreateSubnet" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": { "operation": "CreateTags" },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "requestParameter", "path": "Resources[]" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "CreateVolume": {
+ "request": { "operation": "CreateVolume" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VolumeId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateVpc": {
+ "request": { "operation": "CreateVpc" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpc.VpcId" }
+ ],
+ "path": "Vpc"
+ }
+ },
+ "CreateVpcPeeringConnection": {
+ "request": { "operation": "CreateVpcPeeringConnection" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ },
+ "DisassociateRouteTable": {
+ "request": { "operation": "DisassociateRouteTable" }
+ },
+ "ImportKeyPair": {
+ "request": { "operation": "ImportKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "RegisterImage": {
+ "request": { "operation": "RegisterImage" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Instance": {
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "InternetGateway": {
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "NetworkAcl": {
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "NetworkInterface": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "RouteTableAssociation": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "SecurityGroup": {
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Snapshot": {
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "VpcPeeringConnection": {
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "DhcpOptionsSets": {
+ "request": { "operation": "DescribeDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions[].DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions[]"
+ }
+ },
+ "Images": {
+ "request": { "operation": "DescribeImages" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Images[].ImageId" }
+ ],
+ "path": "Images[]"
+ }
+ },
+ "Instances": {
+ "request": { "operation": "DescribeInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": { "operation": "DescribeInternetGateways" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "KeyPairs": {
+ "request": { "operation": "DescribeKeyPairs" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyPairs[].KeyName" }
+ ],
+ "path": "KeyPairs[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": { "operation": "DescribeNetworkAcls" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": { "operation": "DescribeNetworkInterfaces" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroups": {
+ "request": { "operation": "DescribePlacementGroups" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PlacementGroups[].GroupName" }
+ ],
+ "path": "PlacementGroups[]"
+ }
+ },
+ "RouteTables": {
+ "request": { "operation": "DescribeRouteTables" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": { "operation": "DescribeSecurityGroups" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Snapshots": {
+ "request": { "operation": "DescribeSnapshots" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "Subnets": {
+ "request": { "operation": "DescribeSubnets" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ },
+ "Volumes": {
+ "request": { "operation": "DescribeVolumes" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcPeeringConnections": {
+ "request": { "operation": "DescribeVpcPeeringConnections" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Vpcs": {
+ "request": { "operation": "DescribeVpcs" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpcs[].VpcId" }
+ ],
+ "path": "Vpcs[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "DhcpOptions": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "DhcpOptionsId"
+ }
+ ],
+ "shape": "DhcpOptions",
+ "load": {
+ "request": {
+ "operation": "DescribeDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "DhcpOptions[0]"
+ },
+ "actions": {
+ "AssociateWithVpc": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Image": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "ImageId"
+ }
+ ],
+ "shape": "Image",
+ "load": {
+ "request": {
+ "operation": "DescribeImages",
+ "params": [
+ { "target": "ImageIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Images[0]"
+ },
+ "actions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Deregister": {
+ "request": {
+ "operation": "DeregisterImage",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Instance": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InstanceId"
+ }
+ ],
+ "shape": "Instance",
+ "load": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Reservations[0].Instances[0]"
+ },
+ "actions": {
+ "AttachClassicLinkVpc": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachVolume": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ConsoleOutput": {
+ "request": {
+ "operation": "GetConsoleOutput",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateImage": {
+ "request": {
+ "operation": "CreateImage",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkVpc": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachVolume": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "PasswordData": {
+ "request": {
+ "operation": "GetPasswordData",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReportStatus": {
+ "request": {
+ "operation": "ReportInstanceStatus",
+ "params": [
+ { "target": "Instances[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetKernel": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "kernel" }
+ ]
+ }
+ },
+ "ResetRamdisk": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "ramdisk" }
+ ]
+ }
+ },
+ "ResetSourceDestCheck": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "sourceDestCheck" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "InstanceExists",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Running": {
+ "waiterName": "InstanceRunning",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Stopped": {
+ "waiterName": "InstanceStopped",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Terminated": {
+ "waiterName": "InstanceTerminated",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ }
+ },
+ "has": {
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "ImageId" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "KeyName" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Placement.GroupName" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Volumes": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ }
+ }
+ },
+ "InternetGateway": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InternetGatewayId"
+ }
+ ],
+ "shape": "InternetGateway",
+ "load": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "InternetGatewayIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "InternetGateways[0]"
+ },
+ "actions": {
+ "AttachToVpc": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromVpc": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPair": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPairInfo",
+ "load": {
+ "request": {
+ "operation": "DescribeKeyPairs",
+ "params": [
+ { "target": "KeyNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "KeyPairs[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkAcl": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkAclId"
+ }
+ ],
+ "shape": "NetworkAcl",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "NetworkAclIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkAcls[0]"
+ },
+ "actions": {
+ "CreateEntry": {
+ "request": {
+ "operation": "CreateNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkAcl",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DeleteEntry": {
+ "request": {
+ "operation": "DeleteNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceAssociation": {
+ "request": {
+ "operation": "ReplaceNetworkAclAssociation",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceEntry": {
+ "request": {
+ "operation": "ReplaceNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterface": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkInterfaceId"
+ }
+ ],
+ "shape": "NetworkInterface",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "NetworkInterfaceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0]"
+ },
+ "actions": {
+ "AssignPrivateIpAddresses": {
+ "request": {
+ "operation": "AssignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Attach": {
+ "request": {
+ "operation": "AttachNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Detach": {
+ "request": {
+ "operation": "DetachNetworkInterface",
+ "params": [
+ { "target": "AttachmentId", "source": "data", "path": "Attachment.AttachmentId" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UnassignPrivateIpAddresses": {
+ "request": {
+ "operation": "UnassignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "PlacementGroup": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "PlacementGroup",
+ "load": {
+ "request": {
+ "operation": "DescribePlacementGroups",
+ "params": [
+ { "target": "GroupNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "PlacementGroups[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePlacementGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "placement-group-name" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ }
+ }
+ },
+ "RouteTable": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableId"
+ }
+ ],
+ "shape": "RouteTable",
+ "load": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "RouteTables[0]"
+ },
+ "actions": {
+ "AssociateWithSubnet": {
+ "request": {
+ "operation": "AssociateRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "AssociationId" }
+ ]
+ }
+ },
+ "CreateRoute": {
+ "request": {
+ "operation": "CreateRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Associations": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[0].Associations[].RouteTableAssociationId" }
+ ],
+ "path": "RouteTables[0].Associations[]"
+ }
+ }
+ }
+ },
+ "RouteTableAssociation": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableAssociationId"
+ }
+ ],
+ "shape": "RouteTableAssociation",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateRouteTable",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceSubnet": {
+ "request": {
+ "operation": "ReplaceRouteTableAssociation",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NewAssociationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RouteTableId" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ }
+ }
+ },
+ "SecurityGroup": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "GroupId"
+ }
+ ],
+ "shape": "SecurityGroup",
+ "load": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "GroupIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "SecurityGroups[0]"
+ },
+ "actions": {
+ "AuthorizeEgress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AuthorizeIngress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSecurityGroup",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeEgress": {
+ "request": {
+ "operation": "RevokeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeIngress": {
+ "request": {
+ "operation": "RevokeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Snapshot": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SnapshotId"
+ }
+ ],
+ "shape": "Snapshot",
+ "load": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "SnapshotIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Snapshots[0]"
+ },
+ "actions": {
+ "Copy": {
+ "request": {
+ "operation": "CopySnapshot",
+ "params": [
+ { "target": "SourceSnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSnapshot",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifySnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Completed": {
+ "waiterName": "SnapshotCompleted",
+ "params": [
+ { "target": "SnapshotIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "has": {
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VolumeId" }
+ ]
+ }
+ }
+ }
+ },
+ "Subnet": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SubnetId"
+ }
+ ],
+ "shape": "Subnet",
+ "load": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "SubnetIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Subnets[0]"
+ },
+ "actions": {
+ "CreateInstances": {
+ "request": {
+ "operation": "RunInstances",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": {
+ "operation": "CreateNetworkInterface",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSubnet",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ }
+ }
+ },
+ "Tag": {
+ "identifiers": [
+ {
+ "name": "ResourceId",
+ "memberName": "ResourceId"
+ },
+ {
+ "name": "Key",
+ "memberName": "Key"
+ },
+ {
+ "name": "Value",
+ "memberName": "Value"
+ }
+ ],
+ "shape": "TagDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTags",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "key" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Key" },
+ { "target": "Filters[1].Name", "source": "string", "value": "value" },
+ { "target": "Filters[1].Values[0]", "source": "identifier", "name": "Value" }
+ ]
+ },
+ "path": "Tags[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[0].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[0].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[*].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[*].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ }
+ },
+ "Volume": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VolumeId"
+ }
+ ],
+ "shape": "Volume",
+ "load": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Volumes[0]"
+ },
+ "actions": {
+ "AttachToInstance": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": {
+ "operation": "CreateSnapshot",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeStatus": {
+ "request": {
+ "operation": "DescribeVolumeStatus",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromInstance": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableIo": {
+ "request": {
+ "operation": "EnableVolumeIO",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Snapshots": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "volume-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ }
+ }
+ },
+ "Vpc": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcId"
+ }
+ ],
+ "shape": "Vpc",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcs",
+ "params": [
+ { "target": "VpcIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Vpcs[0]"
+ },
+ "actions": {
+ "AssociateDhcpOptions": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachClassicLinkInstance": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachInternetGateway": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": {
+ "operation": "CreateNetworkAcl",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateRouteTable": {
+ "request": {
+ "operation": "CreateRouteTable",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": {
+ "operation": "CreateSecurityGroup",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSubnet": {
+ "request": {
+ "operation": "CreateSubnet",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkInstance": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachInternetGateway": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DisableClassicLink": {
+ "request": {
+ "operation": "DisableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableClassicLink": {
+ "request": {
+ "operation": "EnableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RequestVpcPeeringConnection": {
+ "request": {
+ "operation": "CreateVpcPeeringConnection",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "DhcpOptionsId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AcceptedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "accepter-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "RequestedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "requester-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "RouteTables": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Subnets": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ }
+ }
+ },
+ "VpcPeeringConnection": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcPeeringConnectionId"
+ }
+ ],
+ "shape": "VpcPeeringConnection",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "VpcPeeringConnections[0]"
+ },
+ "actions": {
+ "Accept": {
+ "request": {
+ "operation": "AcceptVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reject": {
+ "request": {
+ "operation": "RejectVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "AccepterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AccepterVpcInfo.VpcId" }
+ ]
+ }
+ },
+ "RequesterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RequesterVpcInfo.VpcId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-03-01/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-03-01/resources-1.json
new file mode 100644
index 0000000..8ccf160
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-03-01/resources-1.json
@@ -0,0 +1,2289 @@
+{
+ "service": {
+ "actions": {
+ "CreateDhcpOptions": {
+ "request": { "operation": "CreateDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions.DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions"
+ }
+ },
+ "CreateInstances": {
+ "request": { "operation": "RunInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateInternetGateway": {
+ "request": { "operation": "CreateInternetGateway" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateway.InternetGatewayId" }
+ ],
+ "path": "InternetGateway"
+ }
+ },
+ "CreateKeyPair": {
+ "request": { "operation": "CreateKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": { "operation": "CreateNetworkAcl" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": { "operation": "CreateNetworkInterface" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreatePlacementGroup": {
+ "request": { "operation": "CreatePlacementGroup" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ]
+ }
+ },
+ "CreateRouteTable": {
+ "request": { "operation": "CreateRouteTable" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": { "operation": "CreateSecurityGroup" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": { "operation": "CreateSnapshot" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateSubnet": {
+ "request": { "operation": "CreateSubnet" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": { "operation": "CreateTags" },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "requestParameter", "path": "Resources[]" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "CreateVolume": {
+ "request": { "operation": "CreateVolume" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VolumeId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateVpc": {
+ "request": { "operation": "CreateVpc" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpc.VpcId" }
+ ],
+ "path": "Vpc"
+ }
+ },
+ "CreateVpcPeeringConnection": {
+ "request": { "operation": "CreateVpcPeeringConnection" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ },
+ "DisassociateRouteTable": {
+ "request": { "operation": "DisassociateRouteTable" }
+ },
+ "ImportKeyPair": {
+ "request": { "operation": "ImportKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "RegisterImage": {
+ "request": { "operation": "RegisterImage" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Instance": {
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "InternetGateway": {
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "NetworkAcl": {
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "NetworkInterface": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "RouteTableAssociation": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "SecurityGroup": {
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Snapshot": {
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "VpcPeeringConnection": {
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "DhcpOptionsSets": {
+ "request": { "operation": "DescribeDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions[].DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions[]"
+ }
+ },
+ "Images": {
+ "request": { "operation": "DescribeImages" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Images[].ImageId" }
+ ],
+ "path": "Images[]"
+ }
+ },
+ "Instances": {
+ "request": { "operation": "DescribeInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": { "operation": "DescribeInternetGateways" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "KeyPairs": {
+ "request": { "operation": "DescribeKeyPairs" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyPairs[].KeyName" }
+ ],
+ "path": "KeyPairs[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": { "operation": "DescribeNetworkAcls" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": { "operation": "DescribeNetworkInterfaces" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroups": {
+ "request": { "operation": "DescribePlacementGroups" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PlacementGroups[].GroupName" }
+ ],
+ "path": "PlacementGroups[]"
+ }
+ },
+ "RouteTables": {
+ "request": { "operation": "DescribeRouteTables" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": { "operation": "DescribeSecurityGroups" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Snapshots": {
+ "request": { "operation": "DescribeSnapshots" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "Subnets": {
+ "request": { "operation": "DescribeSubnets" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ },
+ "Volumes": {
+ "request": { "operation": "DescribeVolumes" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcPeeringConnections": {
+ "request": { "operation": "DescribeVpcPeeringConnections" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Vpcs": {
+ "request": { "operation": "DescribeVpcs" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpcs[].VpcId" }
+ ],
+ "path": "Vpcs[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "DhcpOptions": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "DhcpOptionsId"
+ }
+ ],
+ "shape": "DhcpOptions",
+ "load": {
+ "request": {
+ "operation": "DescribeDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "DhcpOptions[0]"
+ },
+ "actions": {
+ "AssociateWithVpc": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Image": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "ImageId"
+ }
+ ],
+ "shape": "Image",
+ "load": {
+ "request": {
+ "operation": "DescribeImages",
+ "params": [
+ { "target": "ImageIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Images[0]"
+ },
+ "actions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Deregister": {
+ "request": {
+ "operation": "DeregisterImage",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Instance": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InstanceId"
+ }
+ ],
+ "shape": "Instance",
+ "load": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Reservations[0].Instances[0]"
+ },
+ "actions": {
+ "AttachClassicLinkVpc": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachVolume": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ConsoleOutput": {
+ "request": {
+ "operation": "GetConsoleOutput",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateImage": {
+ "request": {
+ "operation": "CreateImage",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkVpc": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachVolume": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "PasswordData": {
+ "request": {
+ "operation": "GetPasswordData",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReportStatus": {
+ "request": {
+ "operation": "ReportInstanceStatus",
+ "params": [
+ { "target": "Instances[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetKernel": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "kernel" }
+ ]
+ }
+ },
+ "ResetRamdisk": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "ramdisk" }
+ ]
+ }
+ },
+ "ResetSourceDestCheck": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "sourceDestCheck" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "InstanceExists",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Running": {
+ "waiterName": "InstanceRunning",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Stopped": {
+ "waiterName": "InstanceStopped",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Terminated": {
+ "waiterName": "InstanceTerminated",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ }
+ },
+ "has": {
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "ImageId" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "KeyName" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Placement.GroupName" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Volumes": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ }
+ }
+ },
+ "InternetGateway": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InternetGatewayId"
+ }
+ ],
+ "shape": "InternetGateway",
+ "load": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "InternetGatewayIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "InternetGateways[0]"
+ },
+ "actions": {
+ "AttachToVpc": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromVpc": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPair": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPairInfo",
+ "load": {
+ "request": {
+ "operation": "DescribeKeyPairs",
+ "params": [
+ { "target": "KeyNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "KeyPairs[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkAcl": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkAclId"
+ }
+ ],
+ "shape": "NetworkAcl",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "NetworkAclIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkAcls[0]"
+ },
+ "actions": {
+ "CreateEntry": {
+ "request": {
+ "operation": "CreateNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkAcl",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DeleteEntry": {
+ "request": {
+ "operation": "DeleteNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceAssociation": {
+ "request": {
+ "operation": "ReplaceNetworkAclAssociation",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceEntry": {
+ "request": {
+ "operation": "ReplaceNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterface": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkInterfaceId"
+ }
+ ],
+ "shape": "NetworkInterface",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "NetworkInterfaceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0]"
+ },
+ "actions": {
+ "AssignPrivateIpAddresses": {
+ "request": {
+ "operation": "AssignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Attach": {
+ "request": {
+ "operation": "AttachNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Detach": {
+ "request": {
+ "operation": "DetachNetworkInterface",
+ "params": [
+ { "target": "AttachmentId", "source": "data", "path": "Attachment.AttachmentId" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UnassignPrivateIpAddresses": {
+ "request": {
+ "operation": "UnassignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "PlacementGroup": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "PlacementGroup",
+ "load": {
+ "request": {
+ "operation": "DescribePlacementGroups",
+ "params": [
+ { "target": "GroupNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "PlacementGroups[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePlacementGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "placement-group-name" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ }
+ }
+ },
+ "RouteTable": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableId"
+ }
+ ],
+ "shape": "RouteTable",
+ "load": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "RouteTables[0]"
+ },
+ "actions": {
+ "AssociateWithSubnet": {
+ "request": {
+ "operation": "AssociateRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "AssociationId" }
+ ]
+ }
+ },
+ "CreateRoute": {
+ "request": {
+ "operation": "CreateRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Associations": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[0].Associations[].RouteTableAssociationId" }
+ ],
+ "path": "RouteTables[0].Associations[]"
+ }
+ }
+ }
+ },
+ "RouteTableAssociation": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableAssociationId"
+ }
+ ],
+ "shape": "RouteTableAssociation",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateRouteTable",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceSubnet": {
+ "request": {
+ "operation": "ReplaceRouteTableAssociation",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NewAssociationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RouteTableId" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ }
+ }
+ },
+ "SecurityGroup": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "GroupId"
+ }
+ ],
+ "shape": "SecurityGroup",
+ "load": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "GroupIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "SecurityGroups[0]"
+ },
+ "actions": {
+ "AuthorizeEgress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AuthorizeIngress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSecurityGroup",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeEgress": {
+ "request": {
+ "operation": "RevokeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeIngress": {
+ "request": {
+ "operation": "RevokeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Snapshot": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SnapshotId"
+ }
+ ],
+ "shape": "Snapshot",
+ "load": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "SnapshotIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Snapshots[0]"
+ },
+ "actions": {
+ "Copy": {
+ "request": {
+ "operation": "CopySnapshot",
+ "params": [
+ { "target": "SourceSnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSnapshot",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifySnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Completed": {
+ "waiterName": "SnapshotCompleted",
+ "params": [
+ { "target": "SnapshotIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "has": {
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VolumeId" }
+ ]
+ }
+ }
+ }
+ },
+ "Subnet": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SubnetId"
+ }
+ ],
+ "shape": "Subnet",
+ "load": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "SubnetIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Subnets[0]"
+ },
+ "actions": {
+ "CreateInstances": {
+ "request": {
+ "operation": "RunInstances",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": {
+ "operation": "CreateNetworkInterface",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSubnet",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ }
+ }
+ },
+ "Tag": {
+ "identifiers": [
+ {
+ "name": "ResourceId",
+ "memberName": "ResourceId"
+ },
+ {
+ "name": "Key",
+ "memberName": "Key"
+ },
+ {
+ "name": "Value",
+ "memberName": "Value"
+ }
+ ],
+ "shape": "TagDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTags",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "key" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Key" },
+ { "target": "Filters[1].Name", "source": "string", "value": "value" },
+ { "target": "Filters[1].Values[0]", "source": "identifier", "name": "Value" }
+ ]
+ },
+ "path": "Tags[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[0].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[0].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[*].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[*].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ }
+ },
+ "Volume": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VolumeId"
+ }
+ ],
+ "shape": "Volume",
+ "load": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Volumes[0]"
+ },
+ "actions": {
+ "AttachToInstance": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": {
+ "operation": "CreateSnapshot",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeStatus": {
+ "request": {
+ "operation": "DescribeVolumeStatus",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromInstance": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableIo": {
+ "request": {
+ "operation": "EnableVolumeIO",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Snapshots": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "volume-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ }
+ }
+ },
+ "Vpc": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcId"
+ }
+ ],
+ "shape": "Vpc",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcs",
+ "params": [
+ { "target": "VpcIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Vpcs[0]"
+ },
+ "actions": {
+ "AssociateDhcpOptions": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachClassicLinkInstance": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachInternetGateway": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": {
+ "operation": "CreateNetworkAcl",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateRouteTable": {
+ "request": {
+ "operation": "CreateRouteTable",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": {
+ "operation": "CreateSecurityGroup",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSubnet": {
+ "request": {
+ "operation": "CreateSubnet",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkInstance": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachInternetGateway": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DisableClassicLink": {
+ "request": {
+ "operation": "DisableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableClassicLink": {
+ "request": {
+ "operation": "EnableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RequestVpcPeeringConnection": {
+ "request": {
+ "operation": "CreateVpcPeeringConnection",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "DhcpOptionsId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AcceptedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "accepter-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "RequestedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "requester-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "RouteTables": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Subnets": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ }
+ }
+ },
+ "VpcPeeringConnection": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcPeeringConnectionId"
+ }
+ ],
+ "shape": "VpcPeeringConnection",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "VpcPeeringConnections[0]"
+ },
+ "actions": {
+ "Accept": {
+ "request": {
+ "operation": "AcceptVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reject": {
+ "request": {
+ "operation": "RejectVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "AccepterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AccepterVpcInfo.VpcId" }
+ ]
+ }
+ },
+ "RequesterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RequesterVpcInfo.VpcId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-04-15/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-04-15/resources-1.json
new file mode 100644
index 0000000..8ccf160
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-04-15/resources-1.json
@@ -0,0 +1,2289 @@
+{
+ "service": {
+ "actions": {
+ "CreateDhcpOptions": {
+ "request": { "operation": "CreateDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions.DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions"
+ }
+ },
+ "CreateInstances": {
+ "request": { "operation": "RunInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateInternetGateway": {
+ "request": { "operation": "CreateInternetGateway" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateway.InternetGatewayId" }
+ ],
+ "path": "InternetGateway"
+ }
+ },
+ "CreateKeyPair": {
+ "request": { "operation": "CreateKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": { "operation": "CreateNetworkAcl" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": { "operation": "CreateNetworkInterface" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreatePlacementGroup": {
+ "request": { "operation": "CreatePlacementGroup" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ]
+ }
+ },
+ "CreateRouteTable": {
+ "request": { "operation": "CreateRouteTable" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": { "operation": "CreateSecurityGroup" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": { "operation": "CreateSnapshot" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateSubnet": {
+ "request": { "operation": "CreateSubnet" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": { "operation": "CreateTags" },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "requestParameter", "path": "Resources[]" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "CreateVolume": {
+ "request": { "operation": "CreateVolume" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VolumeId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateVpc": {
+ "request": { "operation": "CreateVpc" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpc.VpcId" }
+ ],
+ "path": "Vpc"
+ }
+ },
+ "CreateVpcPeeringConnection": {
+ "request": { "operation": "CreateVpcPeeringConnection" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ },
+ "DisassociateRouteTable": {
+ "request": { "operation": "DisassociateRouteTable" }
+ },
+ "ImportKeyPair": {
+ "request": { "operation": "ImportKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "RegisterImage": {
+ "request": { "operation": "RegisterImage" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Instance": {
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "InternetGateway": {
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "NetworkAcl": {
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "NetworkInterface": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "RouteTableAssociation": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "SecurityGroup": {
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Snapshot": {
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "VpcPeeringConnection": {
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "DhcpOptionsSets": {
+ "request": { "operation": "DescribeDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions[].DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions[]"
+ }
+ },
+ "Images": {
+ "request": { "operation": "DescribeImages" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Images[].ImageId" }
+ ],
+ "path": "Images[]"
+ }
+ },
+ "Instances": {
+ "request": { "operation": "DescribeInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": { "operation": "DescribeInternetGateways" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "KeyPairs": {
+ "request": { "operation": "DescribeKeyPairs" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyPairs[].KeyName" }
+ ],
+ "path": "KeyPairs[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": { "operation": "DescribeNetworkAcls" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": { "operation": "DescribeNetworkInterfaces" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroups": {
+ "request": { "operation": "DescribePlacementGroups" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PlacementGroups[].GroupName" }
+ ],
+ "path": "PlacementGroups[]"
+ }
+ },
+ "RouteTables": {
+ "request": { "operation": "DescribeRouteTables" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": { "operation": "DescribeSecurityGroups" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Snapshots": {
+ "request": { "operation": "DescribeSnapshots" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "Subnets": {
+ "request": { "operation": "DescribeSubnets" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ },
+ "Volumes": {
+ "request": { "operation": "DescribeVolumes" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcPeeringConnections": {
+ "request": { "operation": "DescribeVpcPeeringConnections" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Vpcs": {
+ "request": { "operation": "DescribeVpcs" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpcs[].VpcId" }
+ ],
+ "path": "Vpcs[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "DhcpOptions": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "DhcpOptionsId"
+ }
+ ],
+ "shape": "DhcpOptions",
+ "load": {
+ "request": {
+ "operation": "DescribeDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "DhcpOptions[0]"
+ },
+ "actions": {
+ "AssociateWithVpc": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Image": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "ImageId"
+ }
+ ],
+ "shape": "Image",
+ "load": {
+ "request": {
+ "operation": "DescribeImages",
+ "params": [
+ { "target": "ImageIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Images[0]"
+ },
+ "actions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Deregister": {
+ "request": {
+ "operation": "DeregisterImage",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Instance": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InstanceId"
+ }
+ ],
+ "shape": "Instance",
+ "load": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Reservations[0].Instances[0]"
+ },
+ "actions": {
+ "AttachClassicLinkVpc": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachVolume": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ConsoleOutput": {
+ "request": {
+ "operation": "GetConsoleOutput",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateImage": {
+ "request": {
+ "operation": "CreateImage",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkVpc": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachVolume": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "PasswordData": {
+ "request": {
+ "operation": "GetPasswordData",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReportStatus": {
+ "request": {
+ "operation": "ReportInstanceStatus",
+ "params": [
+ { "target": "Instances[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetKernel": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "kernel" }
+ ]
+ }
+ },
+ "ResetRamdisk": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "ramdisk" }
+ ]
+ }
+ },
+ "ResetSourceDestCheck": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "sourceDestCheck" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "InstanceExists",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Running": {
+ "waiterName": "InstanceRunning",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Stopped": {
+ "waiterName": "InstanceStopped",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Terminated": {
+ "waiterName": "InstanceTerminated",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ }
+ },
+ "has": {
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "ImageId" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "KeyName" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Placement.GroupName" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Volumes": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ }
+ }
+ },
+ "InternetGateway": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InternetGatewayId"
+ }
+ ],
+ "shape": "InternetGateway",
+ "load": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "InternetGatewayIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "InternetGateways[0]"
+ },
+ "actions": {
+ "AttachToVpc": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromVpc": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPair": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPairInfo",
+ "load": {
+ "request": {
+ "operation": "DescribeKeyPairs",
+ "params": [
+ { "target": "KeyNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "KeyPairs[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkAcl": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkAclId"
+ }
+ ],
+ "shape": "NetworkAcl",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "NetworkAclIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkAcls[0]"
+ },
+ "actions": {
+ "CreateEntry": {
+ "request": {
+ "operation": "CreateNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkAcl",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DeleteEntry": {
+ "request": {
+ "operation": "DeleteNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceAssociation": {
+ "request": {
+ "operation": "ReplaceNetworkAclAssociation",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceEntry": {
+ "request": {
+ "operation": "ReplaceNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterface": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkInterfaceId"
+ }
+ ],
+ "shape": "NetworkInterface",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "NetworkInterfaceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0]"
+ },
+ "actions": {
+ "AssignPrivateIpAddresses": {
+ "request": {
+ "operation": "AssignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Attach": {
+ "request": {
+ "operation": "AttachNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Detach": {
+ "request": {
+ "operation": "DetachNetworkInterface",
+ "params": [
+ { "target": "AttachmentId", "source": "data", "path": "Attachment.AttachmentId" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UnassignPrivateIpAddresses": {
+ "request": {
+ "operation": "UnassignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "PlacementGroup": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "PlacementGroup",
+ "load": {
+ "request": {
+ "operation": "DescribePlacementGroups",
+ "params": [
+ { "target": "GroupNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "PlacementGroups[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePlacementGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "placement-group-name" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ }
+ }
+ },
+ "RouteTable": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableId"
+ }
+ ],
+ "shape": "RouteTable",
+ "load": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "RouteTables[0]"
+ },
+ "actions": {
+ "AssociateWithSubnet": {
+ "request": {
+ "operation": "AssociateRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "AssociationId" }
+ ]
+ }
+ },
+ "CreateRoute": {
+ "request": {
+ "operation": "CreateRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Associations": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[0].Associations[].RouteTableAssociationId" }
+ ],
+ "path": "RouteTables[0].Associations[]"
+ }
+ }
+ }
+ },
+ "RouteTableAssociation": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableAssociationId"
+ }
+ ],
+ "shape": "RouteTableAssociation",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateRouteTable",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceSubnet": {
+ "request": {
+ "operation": "ReplaceRouteTableAssociation",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NewAssociationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RouteTableId" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ }
+ }
+ },
+ "SecurityGroup": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "GroupId"
+ }
+ ],
+ "shape": "SecurityGroup",
+ "load": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "GroupIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "SecurityGroups[0]"
+ },
+ "actions": {
+ "AuthorizeEgress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AuthorizeIngress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSecurityGroup",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeEgress": {
+ "request": {
+ "operation": "RevokeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeIngress": {
+ "request": {
+ "operation": "RevokeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Snapshot": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SnapshotId"
+ }
+ ],
+ "shape": "Snapshot",
+ "load": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "SnapshotIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Snapshots[0]"
+ },
+ "actions": {
+ "Copy": {
+ "request": {
+ "operation": "CopySnapshot",
+ "params": [
+ { "target": "SourceSnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSnapshot",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifySnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Completed": {
+ "waiterName": "SnapshotCompleted",
+ "params": [
+ { "target": "SnapshotIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "has": {
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VolumeId" }
+ ]
+ }
+ }
+ }
+ },
+ "Subnet": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SubnetId"
+ }
+ ],
+ "shape": "Subnet",
+ "load": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "SubnetIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Subnets[0]"
+ },
+ "actions": {
+ "CreateInstances": {
+ "request": {
+ "operation": "RunInstances",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": {
+ "operation": "CreateNetworkInterface",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSubnet",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ }
+ }
+ },
+ "Tag": {
+ "identifiers": [
+ {
+ "name": "ResourceId",
+ "memberName": "ResourceId"
+ },
+ {
+ "name": "Key",
+ "memberName": "Key"
+ },
+ {
+ "name": "Value",
+ "memberName": "Value"
+ }
+ ],
+ "shape": "TagDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTags",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "key" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Key" },
+ { "target": "Filters[1].Name", "source": "string", "value": "value" },
+ { "target": "Filters[1].Values[0]", "source": "identifier", "name": "Value" }
+ ]
+ },
+ "path": "Tags[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[0].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[0].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[*].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[*].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ }
+ },
+ "Volume": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VolumeId"
+ }
+ ],
+ "shape": "Volume",
+ "load": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Volumes[0]"
+ },
+ "actions": {
+ "AttachToInstance": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": {
+ "operation": "CreateSnapshot",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeStatus": {
+ "request": {
+ "operation": "DescribeVolumeStatus",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromInstance": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableIo": {
+ "request": {
+ "operation": "EnableVolumeIO",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Snapshots": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "volume-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ }
+ }
+ },
+ "Vpc": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcId"
+ }
+ ],
+ "shape": "Vpc",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcs",
+ "params": [
+ { "target": "VpcIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Vpcs[0]"
+ },
+ "actions": {
+ "AssociateDhcpOptions": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachClassicLinkInstance": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachInternetGateway": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": {
+ "operation": "CreateNetworkAcl",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateRouteTable": {
+ "request": {
+ "operation": "CreateRouteTable",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": {
+ "operation": "CreateSecurityGroup",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSubnet": {
+ "request": {
+ "operation": "CreateSubnet",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkInstance": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachInternetGateway": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DisableClassicLink": {
+ "request": {
+ "operation": "DisableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableClassicLink": {
+ "request": {
+ "operation": "EnableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RequestVpcPeeringConnection": {
+ "request": {
+ "operation": "CreateVpcPeeringConnection",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "DhcpOptionsId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AcceptedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "accepter-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "RequestedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "requester-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "RouteTables": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Subnets": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ }
+ }
+ },
+ "VpcPeeringConnection": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcPeeringConnectionId"
+ }
+ ],
+ "shape": "VpcPeeringConnection",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "VpcPeeringConnections[0]"
+ },
+ "actions": {
+ "Accept": {
+ "request": {
+ "operation": "AcceptVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reject": {
+ "request": {
+ "operation": "RejectVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "AccepterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AccepterVpcInfo.VpcId" }
+ ]
+ }
+ },
+ "RequesterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RequesterVpcInfo.VpcId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-10-01/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-10-01/resources-1.json
new file mode 100644
index 0000000..4831a36
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/ec2/2015-10-01/resources-1.json
@@ -0,0 +1,2567 @@
+{
+ "service": {
+ "actions": {
+ "CreateDhcpOptions": {
+ "request": { "operation": "CreateDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions.DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions"
+ }
+ },
+ "CreateInstances": {
+ "request": { "operation": "RunInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateInternetGateway": {
+ "request": { "operation": "CreateInternetGateway" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateway.InternetGatewayId" }
+ ],
+ "path": "InternetGateway"
+ }
+ },
+ "CreateKeyPair": {
+ "request": { "operation": "CreateKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": { "operation": "CreateNetworkAcl" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": { "operation": "CreateNetworkInterface" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreatePlacementGroup": {
+ "request": { "operation": "CreatePlacementGroup" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ]
+ }
+ },
+ "CreateRouteTable": {
+ "request": { "operation": "CreateRouteTable" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": { "operation": "CreateSecurityGroup" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": { "operation": "CreateSnapshot" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateSubnet": {
+ "request": { "operation": "CreateSubnet" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": { "operation": "CreateTags" }
+ },
+ "CreateVolume": {
+ "request": { "operation": "CreateVolume" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VolumeId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateVpc": {
+ "request": { "operation": "CreateVpc" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpc.VpcId" }
+ ],
+ "path": "Vpc"
+ }
+ },
+ "CreateVpcPeeringConnection": {
+ "request": { "operation": "CreateVpcPeeringConnection" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ },
+ "DisassociateRouteTable": {
+ "request": { "operation": "DisassociateRouteTable" }
+ },
+ "ImportKeyPair": {
+ "request": { "operation": "ImportKeyPair" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "RegisterImage": {
+ "request": { "operation": "RegisterImage" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Instance": {
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "InternetGateway": {
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "NetworkAcl": {
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "NetworkInterface": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "RouteTableAssociation": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "SecurityGroup": {
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Snapshot": {
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "VpcPeeringConnection": {
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "ClassicAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "standard" }
+ ]
+ },
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "response", "path": "Addresses[].PublicIp" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "DhcpOptionsSets": {
+ "request": { "operation": "DescribeDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions[].DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions[]"
+ }
+ },
+ "Images": {
+ "request": { "operation": "DescribeImages" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Images[].ImageId" }
+ ],
+ "path": "Images[]"
+ }
+ },
+ "Instances": {
+ "request": { "operation": "DescribeInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": { "operation": "DescribeInternetGateways" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "KeyPairs": {
+ "request": { "operation": "DescribeKeyPairs" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyPairs[].KeyName" }
+ ],
+ "path": "KeyPairs[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": { "operation": "DescribeNetworkAcls" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": { "operation": "DescribeNetworkInterfaces" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroups": {
+ "request": { "operation": "DescribePlacementGroups" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PlacementGroups[].GroupName" }
+ ],
+ "path": "PlacementGroups[]"
+ }
+ },
+ "RouteTables": {
+ "request": { "operation": "DescribeRouteTables" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": { "operation": "DescribeSecurityGroups" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Snapshots": {
+ "request": { "operation": "DescribeSnapshots" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "Subnets": {
+ "request": { "operation": "DescribeSubnets" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ },
+ "Volumes": {
+ "request": { "operation": "DescribeVolumes" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "vpc" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "VpcPeeringConnections": {
+ "request": { "operation": "DescribeVpcPeeringConnections" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Vpcs": {
+ "request": { "operation": "DescribeVpcs" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpcs[].VpcId" }
+ ],
+ "path": "Vpcs[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "ClassicAddress": {
+ "identifiers": [
+ {
+ "name": "PublicIp"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "PublicIps[]", "source": "identifier", "name": "PublicIp" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "identifier", "name": "PublicIp" }
+ ]
+ }
+ },
+ "Disassociate": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ }
+ }
+ },
+ "DhcpOptions": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "DhcpOptionsId"
+ }
+ ],
+ "shape": "DhcpOptions",
+ "load": {
+ "request": {
+ "operation": "DescribeDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "DhcpOptions[0]"
+ },
+ "actions": {
+ "AssociateWithVpc": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Image": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "ImageId"
+ }
+ ],
+ "shape": "Image",
+ "load": {
+ "request": {
+ "operation": "DescribeImages",
+ "params": [
+ { "target": "ImageIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Images[0]"
+ },
+ "actions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Deregister": {
+ "request": {
+ "operation": "DeregisterImage",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Instance": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InstanceId"
+ }
+ ],
+ "shape": "Instance",
+ "load": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Reservations[0].Instances[0]"
+ },
+ "actions": {
+ "AttachClassicLinkVpc": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachVolume": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ConsoleOutput": {
+ "request": {
+ "operation": "GetConsoleOutput",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateImage": {
+ "request": {
+ "operation": "CreateImage",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkVpc": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachVolume": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "PasswordData": {
+ "request": {
+ "operation": "GetPasswordData",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReportStatus": {
+ "request": {
+ "operation": "ReportInstanceStatus",
+ "params": [
+ { "target": "Instances[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetKernel": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "kernel" }
+ ]
+ }
+ },
+ "ResetRamdisk": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "ramdisk" }
+ ]
+ }
+ },
+ "ResetSourceDestCheck": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "sourceDestCheck" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "InstanceExists",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Running": {
+ "waiterName": "InstanceRunning",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Stopped": {
+ "waiterName": "InstanceStopped",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Terminated": {
+ "waiterName": "InstanceTerminated",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ }
+ },
+ "has": {
+ "ClassicAddress": {
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIpAddress" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "ImageId" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "KeyName" }
+ ]
+ }
+ },
+ "NetworkInterfaces": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Placement.GroupName" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Volumes": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ }
+ }
+ },
+ "InternetGateway": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InternetGatewayId"
+ }
+ ],
+ "shape": "InternetGateway",
+ "load": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "InternetGatewayIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "InternetGateways[0]"
+ },
+ "actions": {
+ "AttachToVpc": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromVpc": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPair": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPair",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPairInfo": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPairInfo",
+ "load": {
+ "request": {
+ "operation": "DescribeKeyPairs",
+ "params": [
+ { "target": "KeyNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "KeyPairs[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkAcl": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkAclId"
+ }
+ ],
+ "shape": "NetworkAcl",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "NetworkAclIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkAcls[0]"
+ },
+ "actions": {
+ "CreateEntry": {
+ "request": {
+ "operation": "CreateNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkAcl",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DeleteEntry": {
+ "request": {
+ "operation": "DeleteNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceAssociation": {
+ "request": {
+ "operation": "ReplaceNetworkAclAssociation",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceEntry": {
+ "request": {
+ "operation": "ReplaceNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterface": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkInterfaceId"
+ }
+ ],
+ "shape": "NetworkInterface",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "NetworkInterfaceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0]"
+ },
+ "actions": {
+ "AssignPrivateIpAddresses": {
+ "request": {
+ "operation": "AssignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Attach": {
+ "request": {
+ "operation": "AttachNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Detach": {
+ "request": {
+ "operation": "DetachNetworkInterface",
+ "params": [
+ { "target": "AttachmentId", "source": "data", "path": "Attachment.AttachmentId" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UnassignPrivateIpAddresses": {
+ "request": {
+ "operation": "UnassignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "Association.AssociationId" }
+ ],
+ "path": "Association"
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterfaceAssociation": {
+ "identifiers": [
+ {
+ "name": "Id"
+ }
+ ],
+ "shape": "InstanceNetworkInterfaceAssociation",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "association.association-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0].Association"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Address": {
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ }
+ },
+ "PlacementGroup": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "PlacementGroup",
+ "load": {
+ "request": {
+ "operation": "DescribePlacementGroups",
+ "params": [
+ { "target": "GroupNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "PlacementGroups[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePlacementGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "placement-group-name" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ }
+ }
+ },
+ "Route": {
+ "identifiers": [
+ { "name": "RouteTableId" },
+ {
+ "name": "DestinationCidrBlock",
+ "memberName": "DestinationCidrBlock"
+ }
+ ],
+ "shape": "Route",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "Replace": {
+ "request": {
+ "operation": "ReplaceRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "identifier", "name": "RouteTableId" }
+ ]
+ }
+ }
+ }
+ },
+ "RouteTable": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableId"
+ }
+ ],
+ "shape": "RouteTable",
+ "load": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "RouteTables[0]"
+ },
+ "actions": {
+ "AssociateWithSubnet": {
+ "request": {
+ "operation": "AssociateRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "AssociationId" }
+ ]
+ }
+ },
+ "CreateRoute": {
+ "request": {
+ "operation": "CreateRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "requestParameter", "path": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Routes": {
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "data", "path": "Routes[].DestinationCidrBlock" }
+ ],
+ "path": "Routes[]"
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Associations": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[0].Associations[].RouteTableAssociationId" }
+ ],
+ "path": "RouteTables[0].Associations[]"
+ }
+ }
+ }
+ },
+ "RouteTableAssociation": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableAssociationId"
+ }
+ ],
+ "shape": "RouteTableAssociation",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateRouteTable",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceSubnet": {
+ "request": {
+ "operation": "ReplaceRouteTableAssociation",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NewAssociationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RouteTableId" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ }
+ }
+ },
+ "SecurityGroup": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "GroupId"
+ }
+ ],
+ "shape": "SecurityGroup",
+ "load": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "GroupIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "SecurityGroups[0]"
+ },
+ "actions": {
+ "AuthorizeEgress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AuthorizeIngress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSecurityGroup",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeEgress": {
+ "request": {
+ "operation": "RevokeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeIngress": {
+ "request": {
+ "operation": "RevokeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Snapshot": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SnapshotId"
+ }
+ ],
+ "shape": "Snapshot",
+ "load": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "SnapshotIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Snapshots[0]"
+ },
+ "actions": {
+ "Copy": {
+ "request": {
+ "operation": "CopySnapshot",
+ "params": [
+ { "target": "SourceSnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSnapshot",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifySnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Completed": {
+ "waiterName": "SnapshotCompleted",
+ "params": [
+ { "target": "SnapshotIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "has": {
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VolumeId" }
+ ]
+ }
+ }
+ }
+ },
+ "Subnet": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SubnetId"
+ }
+ ],
+ "shape": "Subnet",
+ "load": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "SubnetIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Subnets[0]"
+ },
+ "actions": {
+ "CreateInstances": {
+ "request": {
+ "operation": "RunInstances",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": {
+ "operation": "CreateNetworkInterface",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSubnet",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ }
+ }
+ },
+ "Tag": {
+ "identifiers": [
+ {
+ "name": "ResourceId",
+ "memberName": "ResourceId"
+ },
+ {
+ "name": "Key",
+ "memberName": "Key"
+ },
+ {
+ "name": "Value",
+ "memberName": "Value"
+ }
+ ],
+ "shape": "TagDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTags",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "key" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Key" },
+ { "target": "Filters[1].Name", "source": "string", "value": "value" },
+ { "target": "Filters[1].Values[0]", "source": "identifier", "name": "Value" }
+ ]
+ },
+ "path": "Tags[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[0].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[0].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[*].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[*].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ }
+ },
+ "Volume": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VolumeId"
+ }
+ ],
+ "shape": "Volume",
+ "load": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Volumes[0]"
+ },
+ "actions": {
+ "AttachToInstance": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": {
+ "operation": "CreateSnapshot",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeStatus": {
+ "request": {
+ "operation": "DescribeVolumeStatus",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromInstance": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableIo": {
+ "request": {
+ "operation": "EnableVolumeIO",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Snapshots": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "volume-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ }
+ }
+ },
+ "Vpc": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcId"
+ }
+ ],
+ "shape": "Vpc",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcs",
+ "params": [
+ { "target": "VpcIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Vpcs[0]"
+ },
+ "actions": {
+ "AssociateDhcpOptions": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachClassicLinkInstance": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachInternetGateway": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": {
+ "operation": "CreateNetworkAcl",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateRouteTable": {
+ "request": {
+ "operation": "CreateRouteTable",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": {
+ "operation": "CreateSecurityGroup",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSubnet": {
+ "request": {
+ "operation": "CreateSubnet",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkInstance": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachInternetGateway": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DisableClassicLink": {
+ "request": {
+ "operation": "DisableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableClassicLink": {
+ "request": {
+ "operation": "EnableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RequestVpcPeeringConnection": {
+ "request": {
+ "operation": "CreateVpcPeeringConnection",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "DhcpOptionsId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AcceptedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "accepter-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "RequestedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "requester-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "RouteTables": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Subnets": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ }
+ }
+ },
+ "VpcPeeringConnection": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcPeeringConnectionId"
+ }
+ ],
+ "shape": "VpcPeeringConnection",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "VpcPeeringConnections[0]"
+ },
+ "actions": {
+ "Accept": {
+ "request": {
+ "operation": "AcceptVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reject": {
+ "request": {
+ "operation": "RejectVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "VpcPeeringConnectionExists",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "VpcPeeringConnections[0]"
+ }
+ },
+ "has": {
+ "AccepterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AccepterVpcInfo.VpcId" }
+ ]
+ }
+ },
+ "RequesterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RequesterVpcInfo.VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "VpcAddress": {
+ "identifiers": [
+ {
+ "name": "AllocationId"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "AllocationIds[0]", "source": "identifier", "name": "AllocationId" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "AllocationId", "source": "identifier", "name": "AllocationId" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AssociationId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-04-01/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-04-01/resources-1.json
new file mode 100644
index 0000000..4831a36
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-04-01/resources-1.json
@@ -0,0 +1,2567 @@
+{
+ "service": {
+ "actions": {
+ "CreateDhcpOptions": {
+ "request": { "operation": "CreateDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions.DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions"
+ }
+ },
+ "CreateInstances": {
+ "request": { "operation": "RunInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateInternetGateway": {
+ "request": { "operation": "CreateInternetGateway" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateway.InternetGatewayId" }
+ ],
+ "path": "InternetGateway"
+ }
+ },
+ "CreateKeyPair": {
+ "request": { "operation": "CreateKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": { "operation": "CreateNetworkAcl" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": { "operation": "CreateNetworkInterface" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreatePlacementGroup": {
+ "request": { "operation": "CreatePlacementGroup" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ]
+ }
+ },
+ "CreateRouteTable": {
+ "request": { "operation": "CreateRouteTable" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": { "operation": "CreateSecurityGroup" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": { "operation": "CreateSnapshot" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateSubnet": {
+ "request": { "operation": "CreateSubnet" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": { "operation": "CreateTags" }
+ },
+ "CreateVolume": {
+ "request": { "operation": "CreateVolume" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VolumeId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateVpc": {
+ "request": { "operation": "CreateVpc" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpc.VpcId" }
+ ],
+ "path": "Vpc"
+ }
+ },
+ "CreateVpcPeeringConnection": {
+ "request": { "operation": "CreateVpcPeeringConnection" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ },
+ "DisassociateRouteTable": {
+ "request": { "operation": "DisassociateRouteTable" }
+ },
+ "ImportKeyPair": {
+ "request": { "operation": "ImportKeyPair" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "RegisterImage": {
+ "request": { "operation": "RegisterImage" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Instance": {
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "InternetGateway": {
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "NetworkAcl": {
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "NetworkInterface": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "RouteTableAssociation": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "SecurityGroup": {
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Snapshot": {
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "VpcPeeringConnection": {
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "ClassicAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "standard" }
+ ]
+ },
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "response", "path": "Addresses[].PublicIp" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "DhcpOptionsSets": {
+ "request": { "operation": "DescribeDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions[].DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions[]"
+ }
+ },
+ "Images": {
+ "request": { "operation": "DescribeImages" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Images[].ImageId" }
+ ],
+ "path": "Images[]"
+ }
+ },
+ "Instances": {
+ "request": { "operation": "DescribeInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": { "operation": "DescribeInternetGateways" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "KeyPairs": {
+ "request": { "operation": "DescribeKeyPairs" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyPairs[].KeyName" }
+ ],
+ "path": "KeyPairs[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": { "operation": "DescribeNetworkAcls" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": { "operation": "DescribeNetworkInterfaces" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroups": {
+ "request": { "operation": "DescribePlacementGroups" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PlacementGroups[].GroupName" }
+ ],
+ "path": "PlacementGroups[]"
+ }
+ },
+ "RouteTables": {
+ "request": { "operation": "DescribeRouteTables" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": { "operation": "DescribeSecurityGroups" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Snapshots": {
+ "request": { "operation": "DescribeSnapshots" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "Subnets": {
+ "request": { "operation": "DescribeSubnets" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ },
+ "Volumes": {
+ "request": { "operation": "DescribeVolumes" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "vpc" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "VpcPeeringConnections": {
+ "request": { "operation": "DescribeVpcPeeringConnections" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Vpcs": {
+ "request": { "operation": "DescribeVpcs" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpcs[].VpcId" }
+ ],
+ "path": "Vpcs[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "ClassicAddress": {
+ "identifiers": [
+ {
+ "name": "PublicIp"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "PublicIps[]", "source": "identifier", "name": "PublicIp" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "identifier", "name": "PublicIp" }
+ ]
+ }
+ },
+ "Disassociate": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ }
+ }
+ },
+ "DhcpOptions": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "DhcpOptionsId"
+ }
+ ],
+ "shape": "DhcpOptions",
+ "load": {
+ "request": {
+ "operation": "DescribeDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "DhcpOptions[0]"
+ },
+ "actions": {
+ "AssociateWithVpc": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Image": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "ImageId"
+ }
+ ],
+ "shape": "Image",
+ "load": {
+ "request": {
+ "operation": "DescribeImages",
+ "params": [
+ { "target": "ImageIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Images[0]"
+ },
+ "actions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Deregister": {
+ "request": {
+ "operation": "DeregisterImage",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Instance": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InstanceId"
+ }
+ ],
+ "shape": "Instance",
+ "load": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Reservations[0].Instances[0]"
+ },
+ "actions": {
+ "AttachClassicLinkVpc": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachVolume": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ConsoleOutput": {
+ "request": {
+ "operation": "GetConsoleOutput",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateImage": {
+ "request": {
+ "operation": "CreateImage",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkVpc": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachVolume": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "PasswordData": {
+ "request": {
+ "operation": "GetPasswordData",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReportStatus": {
+ "request": {
+ "operation": "ReportInstanceStatus",
+ "params": [
+ { "target": "Instances[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetKernel": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "kernel" }
+ ]
+ }
+ },
+ "ResetRamdisk": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "ramdisk" }
+ ]
+ }
+ },
+ "ResetSourceDestCheck": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "sourceDestCheck" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "InstanceExists",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Running": {
+ "waiterName": "InstanceRunning",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Stopped": {
+ "waiterName": "InstanceStopped",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Terminated": {
+ "waiterName": "InstanceTerminated",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ }
+ },
+ "has": {
+ "ClassicAddress": {
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIpAddress" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "ImageId" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "KeyName" }
+ ]
+ }
+ },
+ "NetworkInterfaces": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Placement.GroupName" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Volumes": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ }
+ }
+ },
+ "InternetGateway": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InternetGatewayId"
+ }
+ ],
+ "shape": "InternetGateway",
+ "load": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "InternetGatewayIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "InternetGateways[0]"
+ },
+ "actions": {
+ "AttachToVpc": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromVpc": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPair": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPair",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPairInfo": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPairInfo",
+ "load": {
+ "request": {
+ "operation": "DescribeKeyPairs",
+ "params": [
+ { "target": "KeyNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "KeyPairs[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkAcl": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkAclId"
+ }
+ ],
+ "shape": "NetworkAcl",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "NetworkAclIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkAcls[0]"
+ },
+ "actions": {
+ "CreateEntry": {
+ "request": {
+ "operation": "CreateNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkAcl",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DeleteEntry": {
+ "request": {
+ "operation": "DeleteNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceAssociation": {
+ "request": {
+ "operation": "ReplaceNetworkAclAssociation",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceEntry": {
+ "request": {
+ "operation": "ReplaceNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterface": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkInterfaceId"
+ }
+ ],
+ "shape": "NetworkInterface",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "NetworkInterfaceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0]"
+ },
+ "actions": {
+ "AssignPrivateIpAddresses": {
+ "request": {
+ "operation": "AssignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Attach": {
+ "request": {
+ "operation": "AttachNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Detach": {
+ "request": {
+ "operation": "DetachNetworkInterface",
+ "params": [
+ { "target": "AttachmentId", "source": "data", "path": "Attachment.AttachmentId" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UnassignPrivateIpAddresses": {
+ "request": {
+ "operation": "UnassignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "Association.AssociationId" }
+ ],
+ "path": "Association"
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterfaceAssociation": {
+ "identifiers": [
+ {
+ "name": "Id"
+ }
+ ],
+ "shape": "InstanceNetworkInterfaceAssociation",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "association.association-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0].Association"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Address": {
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ }
+ },
+ "PlacementGroup": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "PlacementGroup",
+ "load": {
+ "request": {
+ "operation": "DescribePlacementGroups",
+ "params": [
+ { "target": "GroupNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "PlacementGroups[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePlacementGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "placement-group-name" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ }
+ }
+ },
+ "Route": {
+ "identifiers": [
+ { "name": "RouteTableId" },
+ {
+ "name": "DestinationCidrBlock",
+ "memberName": "DestinationCidrBlock"
+ }
+ ],
+ "shape": "Route",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "Replace": {
+ "request": {
+ "operation": "ReplaceRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "identifier", "name": "RouteTableId" }
+ ]
+ }
+ }
+ }
+ },
+ "RouteTable": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableId"
+ }
+ ],
+ "shape": "RouteTable",
+ "load": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "RouteTables[0]"
+ },
+ "actions": {
+ "AssociateWithSubnet": {
+ "request": {
+ "operation": "AssociateRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "AssociationId" }
+ ]
+ }
+ },
+ "CreateRoute": {
+ "request": {
+ "operation": "CreateRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "requestParameter", "path": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Routes": {
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "data", "path": "Routes[].DestinationCidrBlock" }
+ ],
+ "path": "Routes[]"
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Associations": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[0].Associations[].RouteTableAssociationId" }
+ ],
+ "path": "RouteTables[0].Associations[]"
+ }
+ }
+ }
+ },
+ "RouteTableAssociation": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableAssociationId"
+ }
+ ],
+ "shape": "RouteTableAssociation",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateRouteTable",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceSubnet": {
+ "request": {
+ "operation": "ReplaceRouteTableAssociation",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NewAssociationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RouteTableId" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ }
+ }
+ },
+ "SecurityGroup": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "GroupId"
+ }
+ ],
+ "shape": "SecurityGroup",
+ "load": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "GroupIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "SecurityGroups[0]"
+ },
+ "actions": {
+ "AuthorizeEgress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AuthorizeIngress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSecurityGroup",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeEgress": {
+ "request": {
+ "operation": "RevokeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeIngress": {
+ "request": {
+ "operation": "RevokeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Snapshot": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SnapshotId"
+ }
+ ],
+ "shape": "Snapshot",
+ "load": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "SnapshotIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Snapshots[0]"
+ },
+ "actions": {
+ "Copy": {
+ "request": {
+ "operation": "CopySnapshot",
+ "params": [
+ { "target": "SourceSnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSnapshot",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifySnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Completed": {
+ "waiterName": "SnapshotCompleted",
+ "params": [
+ { "target": "SnapshotIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "has": {
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VolumeId" }
+ ]
+ }
+ }
+ }
+ },
+ "Subnet": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SubnetId"
+ }
+ ],
+ "shape": "Subnet",
+ "load": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "SubnetIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Subnets[0]"
+ },
+ "actions": {
+ "CreateInstances": {
+ "request": {
+ "operation": "RunInstances",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": {
+ "operation": "CreateNetworkInterface",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSubnet",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ }
+ }
+ },
+ "Tag": {
+ "identifiers": [
+ {
+ "name": "ResourceId",
+ "memberName": "ResourceId"
+ },
+ {
+ "name": "Key",
+ "memberName": "Key"
+ },
+ {
+ "name": "Value",
+ "memberName": "Value"
+ }
+ ],
+ "shape": "TagDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTags",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "key" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Key" },
+ { "target": "Filters[1].Name", "source": "string", "value": "value" },
+ { "target": "Filters[1].Values[0]", "source": "identifier", "name": "Value" }
+ ]
+ },
+ "path": "Tags[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[0].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[0].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[*].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[*].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ }
+ },
+ "Volume": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VolumeId"
+ }
+ ],
+ "shape": "Volume",
+ "load": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Volumes[0]"
+ },
+ "actions": {
+ "AttachToInstance": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": {
+ "operation": "CreateSnapshot",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeStatus": {
+ "request": {
+ "operation": "DescribeVolumeStatus",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromInstance": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableIo": {
+ "request": {
+ "operation": "EnableVolumeIO",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Snapshots": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "volume-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ }
+ }
+ },
+ "Vpc": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcId"
+ }
+ ],
+ "shape": "Vpc",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcs",
+ "params": [
+ { "target": "VpcIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Vpcs[0]"
+ },
+ "actions": {
+ "AssociateDhcpOptions": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachClassicLinkInstance": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachInternetGateway": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": {
+ "operation": "CreateNetworkAcl",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateRouteTable": {
+ "request": {
+ "operation": "CreateRouteTable",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": {
+ "operation": "CreateSecurityGroup",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSubnet": {
+ "request": {
+ "operation": "CreateSubnet",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkInstance": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachInternetGateway": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DisableClassicLink": {
+ "request": {
+ "operation": "DisableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableClassicLink": {
+ "request": {
+ "operation": "EnableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RequestVpcPeeringConnection": {
+ "request": {
+ "operation": "CreateVpcPeeringConnection",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "DhcpOptionsId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AcceptedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "accepter-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "RequestedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "requester-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "RouteTables": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Subnets": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ }
+ }
+ },
+ "VpcPeeringConnection": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcPeeringConnectionId"
+ }
+ ],
+ "shape": "VpcPeeringConnection",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "VpcPeeringConnections[0]"
+ },
+ "actions": {
+ "Accept": {
+ "request": {
+ "operation": "AcceptVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reject": {
+ "request": {
+ "operation": "RejectVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "VpcPeeringConnectionExists",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "VpcPeeringConnections[0]"
+ }
+ },
+ "has": {
+ "AccepterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AccepterVpcInfo.VpcId" }
+ ]
+ }
+ },
+ "RequesterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RequesterVpcInfo.VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "VpcAddress": {
+ "identifiers": [
+ {
+ "name": "AllocationId"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "AllocationIds[0]", "source": "identifier", "name": "AllocationId" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "AllocationId", "source": "identifier", "name": "AllocationId" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AssociationId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-09-15/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-09-15/resources-1.json
new file mode 100644
index 0000000..4831a36
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-09-15/resources-1.json
@@ -0,0 +1,2567 @@
+{
+ "service": {
+ "actions": {
+ "CreateDhcpOptions": {
+ "request": { "operation": "CreateDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions.DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions"
+ }
+ },
+ "CreateInstances": {
+ "request": { "operation": "RunInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateInternetGateway": {
+ "request": { "operation": "CreateInternetGateway" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateway.InternetGatewayId" }
+ ],
+ "path": "InternetGateway"
+ }
+ },
+ "CreateKeyPair": {
+ "request": { "operation": "CreateKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": { "operation": "CreateNetworkAcl" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": { "operation": "CreateNetworkInterface" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreatePlacementGroup": {
+ "request": { "operation": "CreatePlacementGroup" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ]
+ }
+ },
+ "CreateRouteTable": {
+ "request": { "operation": "CreateRouteTable" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": { "operation": "CreateSecurityGroup" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": { "operation": "CreateSnapshot" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateSubnet": {
+ "request": { "operation": "CreateSubnet" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": { "operation": "CreateTags" }
+ },
+ "CreateVolume": {
+ "request": { "operation": "CreateVolume" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VolumeId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateVpc": {
+ "request": { "operation": "CreateVpc" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpc.VpcId" }
+ ],
+ "path": "Vpc"
+ }
+ },
+ "CreateVpcPeeringConnection": {
+ "request": { "operation": "CreateVpcPeeringConnection" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ },
+ "DisassociateRouteTable": {
+ "request": { "operation": "DisassociateRouteTable" }
+ },
+ "ImportKeyPair": {
+ "request": { "operation": "ImportKeyPair" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "RegisterImage": {
+ "request": { "operation": "RegisterImage" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Instance": {
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "InternetGateway": {
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "NetworkAcl": {
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "NetworkInterface": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "RouteTableAssociation": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "SecurityGroup": {
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Snapshot": {
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "VpcPeeringConnection": {
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "ClassicAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "standard" }
+ ]
+ },
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "response", "path": "Addresses[].PublicIp" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "DhcpOptionsSets": {
+ "request": { "operation": "DescribeDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions[].DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions[]"
+ }
+ },
+ "Images": {
+ "request": { "operation": "DescribeImages" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Images[].ImageId" }
+ ],
+ "path": "Images[]"
+ }
+ },
+ "Instances": {
+ "request": { "operation": "DescribeInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": { "operation": "DescribeInternetGateways" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "KeyPairs": {
+ "request": { "operation": "DescribeKeyPairs" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyPairs[].KeyName" }
+ ],
+ "path": "KeyPairs[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": { "operation": "DescribeNetworkAcls" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": { "operation": "DescribeNetworkInterfaces" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroups": {
+ "request": { "operation": "DescribePlacementGroups" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PlacementGroups[].GroupName" }
+ ],
+ "path": "PlacementGroups[]"
+ }
+ },
+ "RouteTables": {
+ "request": { "operation": "DescribeRouteTables" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": { "operation": "DescribeSecurityGroups" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Snapshots": {
+ "request": { "operation": "DescribeSnapshots" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "Subnets": {
+ "request": { "operation": "DescribeSubnets" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ },
+ "Volumes": {
+ "request": { "operation": "DescribeVolumes" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "vpc" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "VpcPeeringConnections": {
+ "request": { "operation": "DescribeVpcPeeringConnections" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Vpcs": {
+ "request": { "operation": "DescribeVpcs" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpcs[].VpcId" }
+ ],
+ "path": "Vpcs[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "ClassicAddress": {
+ "identifiers": [
+ {
+ "name": "PublicIp"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "PublicIps[]", "source": "identifier", "name": "PublicIp" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "identifier", "name": "PublicIp" }
+ ]
+ }
+ },
+ "Disassociate": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ }
+ }
+ },
+ "DhcpOptions": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "DhcpOptionsId"
+ }
+ ],
+ "shape": "DhcpOptions",
+ "load": {
+ "request": {
+ "operation": "DescribeDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "DhcpOptions[0]"
+ },
+ "actions": {
+ "AssociateWithVpc": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Image": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "ImageId"
+ }
+ ],
+ "shape": "Image",
+ "load": {
+ "request": {
+ "operation": "DescribeImages",
+ "params": [
+ { "target": "ImageIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Images[0]"
+ },
+ "actions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Deregister": {
+ "request": {
+ "operation": "DeregisterImage",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Instance": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InstanceId"
+ }
+ ],
+ "shape": "Instance",
+ "load": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Reservations[0].Instances[0]"
+ },
+ "actions": {
+ "AttachClassicLinkVpc": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachVolume": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ConsoleOutput": {
+ "request": {
+ "operation": "GetConsoleOutput",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateImage": {
+ "request": {
+ "operation": "CreateImage",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkVpc": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachVolume": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "PasswordData": {
+ "request": {
+ "operation": "GetPasswordData",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReportStatus": {
+ "request": {
+ "operation": "ReportInstanceStatus",
+ "params": [
+ { "target": "Instances[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetKernel": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "kernel" }
+ ]
+ }
+ },
+ "ResetRamdisk": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "ramdisk" }
+ ]
+ }
+ },
+ "ResetSourceDestCheck": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "sourceDestCheck" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "InstanceExists",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Running": {
+ "waiterName": "InstanceRunning",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Stopped": {
+ "waiterName": "InstanceStopped",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Terminated": {
+ "waiterName": "InstanceTerminated",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ }
+ },
+ "has": {
+ "ClassicAddress": {
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIpAddress" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "ImageId" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "KeyName" }
+ ]
+ }
+ },
+ "NetworkInterfaces": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Placement.GroupName" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Volumes": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ }
+ }
+ },
+ "InternetGateway": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InternetGatewayId"
+ }
+ ],
+ "shape": "InternetGateway",
+ "load": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "InternetGatewayIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "InternetGateways[0]"
+ },
+ "actions": {
+ "AttachToVpc": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromVpc": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPair": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPair",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPairInfo": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPairInfo",
+ "load": {
+ "request": {
+ "operation": "DescribeKeyPairs",
+ "params": [
+ { "target": "KeyNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "KeyPairs[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkAcl": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkAclId"
+ }
+ ],
+ "shape": "NetworkAcl",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "NetworkAclIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkAcls[0]"
+ },
+ "actions": {
+ "CreateEntry": {
+ "request": {
+ "operation": "CreateNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkAcl",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DeleteEntry": {
+ "request": {
+ "operation": "DeleteNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceAssociation": {
+ "request": {
+ "operation": "ReplaceNetworkAclAssociation",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceEntry": {
+ "request": {
+ "operation": "ReplaceNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterface": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkInterfaceId"
+ }
+ ],
+ "shape": "NetworkInterface",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "NetworkInterfaceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0]"
+ },
+ "actions": {
+ "AssignPrivateIpAddresses": {
+ "request": {
+ "operation": "AssignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Attach": {
+ "request": {
+ "operation": "AttachNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Detach": {
+ "request": {
+ "operation": "DetachNetworkInterface",
+ "params": [
+ { "target": "AttachmentId", "source": "data", "path": "Attachment.AttachmentId" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UnassignPrivateIpAddresses": {
+ "request": {
+ "operation": "UnassignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "Association.AssociationId" }
+ ],
+ "path": "Association"
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterfaceAssociation": {
+ "identifiers": [
+ {
+ "name": "Id"
+ }
+ ],
+ "shape": "InstanceNetworkInterfaceAssociation",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "association.association-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0].Association"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Address": {
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ }
+ },
+ "PlacementGroup": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "PlacementGroup",
+ "load": {
+ "request": {
+ "operation": "DescribePlacementGroups",
+ "params": [
+ { "target": "GroupNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "PlacementGroups[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePlacementGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "placement-group-name" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ }
+ }
+ },
+ "Route": {
+ "identifiers": [
+ { "name": "RouteTableId" },
+ {
+ "name": "DestinationCidrBlock",
+ "memberName": "DestinationCidrBlock"
+ }
+ ],
+ "shape": "Route",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "Replace": {
+ "request": {
+ "operation": "ReplaceRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "identifier", "name": "RouteTableId" }
+ ]
+ }
+ }
+ }
+ },
+ "RouteTable": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableId"
+ }
+ ],
+ "shape": "RouteTable",
+ "load": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "RouteTables[0]"
+ },
+ "actions": {
+ "AssociateWithSubnet": {
+ "request": {
+ "operation": "AssociateRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "AssociationId" }
+ ]
+ }
+ },
+ "CreateRoute": {
+ "request": {
+ "operation": "CreateRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "requestParameter", "path": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Routes": {
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "data", "path": "Routes[].DestinationCidrBlock" }
+ ],
+ "path": "Routes[]"
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Associations": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[0].Associations[].RouteTableAssociationId" }
+ ],
+ "path": "RouteTables[0].Associations[]"
+ }
+ }
+ }
+ },
+ "RouteTableAssociation": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableAssociationId"
+ }
+ ],
+ "shape": "RouteTableAssociation",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateRouteTable",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceSubnet": {
+ "request": {
+ "operation": "ReplaceRouteTableAssociation",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NewAssociationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RouteTableId" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ }
+ }
+ },
+ "SecurityGroup": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "GroupId"
+ }
+ ],
+ "shape": "SecurityGroup",
+ "load": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "GroupIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "SecurityGroups[0]"
+ },
+ "actions": {
+ "AuthorizeEgress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AuthorizeIngress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSecurityGroup",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeEgress": {
+ "request": {
+ "operation": "RevokeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeIngress": {
+ "request": {
+ "operation": "RevokeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Snapshot": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SnapshotId"
+ }
+ ],
+ "shape": "Snapshot",
+ "load": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "SnapshotIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Snapshots[0]"
+ },
+ "actions": {
+ "Copy": {
+ "request": {
+ "operation": "CopySnapshot",
+ "params": [
+ { "target": "SourceSnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSnapshot",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifySnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Completed": {
+ "waiterName": "SnapshotCompleted",
+ "params": [
+ { "target": "SnapshotIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "has": {
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VolumeId" }
+ ]
+ }
+ }
+ }
+ },
+ "Subnet": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SubnetId"
+ }
+ ],
+ "shape": "Subnet",
+ "load": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "SubnetIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Subnets[0]"
+ },
+ "actions": {
+ "CreateInstances": {
+ "request": {
+ "operation": "RunInstances",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": {
+ "operation": "CreateNetworkInterface",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSubnet",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ }
+ }
+ },
+ "Tag": {
+ "identifiers": [
+ {
+ "name": "ResourceId",
+ "memberName": "ResourceId"
+ },
+ {
+ "name": "Key",
+ "memberName": "Key"
+ },
+ {
+ "name": "Value",
+ "memberName": "Value"
+ }
+ ],
+ "shape": "TagDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTags",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "key" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Key" },
+ { "target": "Filters[1].Name", "source": "string", "value": "value" },
+ { "target": "Filters[1].Values[0]", "source": "identifier", "name": "Value" }
+ ]
+ },
+ "path": "Tags[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[0].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[0].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[*].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[*].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ }
+ },
+ "Volume": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VolumeId"
+ }
+ ],
+ "shape": "Volume",
+ "load": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Volumes[0]"
+ },
+ "actions": {
+ "AttachToInstance": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": {
+ "operation": "CreateSnapshot",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeStatus": {
+ "request": {
+ "operation": "DescribeVolumeStatus",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromInstance": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableIo": {
+ "request": {
+ "operation": "EnableVolumeIO",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Snapshots": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "volume-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ }
+ }
+ },
+ "Vpc": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcId"
+ }
+ ],
+ "shape": "Vpc",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcs",
+ "params": [
+ { "target": "VpcIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Vpcs[0]"
+ },
+ "actions": {
+ "AssociateDhcpOptions": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachClassicLinkInstance": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachInternetGateway": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": {
+ "operation": "CreateNetworkAcl",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateRouteTable": {
+ "request": {
+ "operation": "CreateRouteTable",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": {
+ "operation": "CreateSecurityGroup",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSubnet": {
+ "request": {
+ "operation": "CreateSubnet",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkInstance": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachInternetGateway": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DisableClassicLink": {
+ "request": {
+ "operation": "DisableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableClassicLink": {
+ "request": {
+ "operation": "EnableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RequestVpcPeeringConnection": {
+ "request": {
+ "operation": "CreateVpcPeeringConnection",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "DhcpOptionsId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AcceptedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "accepter-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "RequestedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "requester-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "RouteTables": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Subnets": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ }
+ }
+ },
+ "VpcPeeringConnection": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcPeeringConnectionId"
+ }
+ ],
+ "shape": "VpcPeeringConnection",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "VpcPeeringConnections[0]"
+ },
+ "actions": {
+ "Accept": {
+ "request": {
+ "operation": "AcceptVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reject": {
+ "request": {
+ "operation": "RejectVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "VpcPeeringConnectionExists",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "VpcPeeringConnections[0]"
+ }
+ },
+ "has": {
+ "AccepterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AccepterVpcInfo.VpcId" }
+ ]
+ }
+ },
+ "RequesterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RequesterVpcInfo.VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "VpcAddress": {
+ "identifiers": [
+ {
+ "name": "AllocationId"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "AllocationIds[0]", "source": "identifier", "name": "AllocationId" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "AllocationId", "source": "identifier", "name": "AllocationId" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AssociationId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-11-15/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-11-15/resources-1.json
new file mode 100644
index 0000000..9872201
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/ec2/2016-11-15/resources-1.json
@@ -0,0 +1,2582 @@
+{
+ "service": {
+ "actions": {
+ "CreateDhcpOptions": {
+ "request": { "operation": "CreateDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions.DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions"
+ }
+ },
+ "CreateInstances": {
+ "request": { "operation": "RunInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateInternetGateway": {
+ "request": { "operation": "CreateInternetGateway" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateway.InternetGatewayId" }
+ ],
+ "path": "InternetGateway"
+ }
+ },
+ "CreateKeyPair": {
+ "request": { "operation": "CreateKeyPair" },
+ "resource": {
+ "type": "KeyPair",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": { "operation": "CreateNetworkAcl" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": { "operation": "CreateNetworkInterface" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreatePlacementGroup": {
+ "request": { "operation": "CreatePlacementGroup" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ]
+ }
+ },
+ "CreateRouteTable": {
+ "request": { "operation": "CreateRouteTable" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": { "operation": "CreateSecurityGroup" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": { "operation": "CreateSnapshot" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateSubnet": {
+ "request": { "operation": "CreateSubnet" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": { "operation": "CreateTags" }
+ },
+ "CreateVolume": {
+ "request": { "operation": "CreateVolume" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VolumeId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateVpc": {
+ "request": { "operation": "CreateVpc" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpc.VpcId" }
+ ],
+ "path": "Vpc"
+ }
+ },
+ "CreateVpcPeeringConnection": {
+ "request": { "operation": "CreateVpcPeeringConnection" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ },
+ "DisassociateRouteTable": {
+ "request": { "operation": "DisassociateRouteTable" }
+ },
+ "ImportKeyPair": {
+ "request": { "operation": "ImportKeyPair" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyName" }
+ ]
+ }
+ },
+ "RegisterImage": {
+ "request": { "operation": "RegisterImage" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Instance": {
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "InternetGateway": {
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "NetworkAcl": {
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "NetworkInterface": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "RouteTableAssociation": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "SecurityGroup": {
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Snapshot": {
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "VpcPeeringConnection": {
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "ClassicAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "standard" }
+ ]
+ },
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "response", "path": "Addresses[].PublicIp" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "DhcpOptionsSets": {
+ "request": { "operation": "DescribeDhcpOptions" },
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "DhcpOptions[].DhcpOptionsId" }
+ ],
+ "path": "DhcpOptions[]"
+ }
+ },
+ "Images": {
+ "request": { "operation": "DescribeImages" },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Images[].ImageId" }
+ ],
+ "path": "Images[]"
+ }
+ },
+ "Instances": {
+ "request": { "operation": "DescribeInstances" },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": { "operation": "DescribeInternetGateways" },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "KeyPairs": {
+ "request": { "operation": "DescribeKeyPairs" },
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "KeyPairs[].KeyName" }
+ ],
+ "path": "KeyPairs[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": { "operation": "DescribeNetworkAcls" },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": { "operation": "DescribeNetworkInterfaces" },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroups": {
+ "request": { "operation": "DescribePlacementGroups" },
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PlacementGroups[].GroupName" }
+ ],
+ "path": "PlacementGroups[]"
+ }
+ },
+ "RouteTables": {
+ "request": { "operation": "DescribeRouteTables" },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": { "operation": "DescribeSecurityGroups" },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Snapshots": {
+ "request": { "operation": "DescribeSnapshots" },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "Subnets": {
+ "request": { "operation": "DescribeSubnets" },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ },
+ "Volumes": {
+ "request": { "operation": "DescribeVolumes" },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "domain" },
+ { "target": "Filters[0].Values[0]", "source": "string", "value": "vpc" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ },
+ "VpcPeeringConnections": {
+ "request": { "operation": "DescribeVpcPeeringConnections" },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Vpcs": {
+ "request": { "operation": "DescribeVpcs" },
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Vpcs[].VpcId" }
+ ],
+ "path": "Vpcs[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "ClassicAddress": {
+ "identifiers": [
+ {
+ "name": "PublicIp"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "PublicIps[]", "source": "identifier", "name": "PublicIp" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "identifier", "name": "PublicIp" }
+ ]
+ }
+ },
+ "Disassociate": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIp" }
+ ]
+ }
+ }
+ }
+ },
+ "DhcpOptions": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "DhcpOptionsId"
+ }
+ ],
+ "shape": "DhcpOptions",
+ "load": {
+ "request": {
+ "operation": "DescribeDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "DhcpOptions[0]"
+ },
+ "actions": {
+ "AssociateWithVpc": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteDhcpOptions",
+ "params": [
+ { "target": "DhcpOptionsId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Image": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "ImageId"
+ }
+ ],
+ "shape": "Image",
+ "load": {
+ "request": {
+ "operation": "DescribeImages",
+ "params": [
+ { "target": "ImageIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Images[0]"
+ },
+ "actions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Deregister": {
+ "request": {
+ "operation": "DeregisterImage",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetImageAttribute",
+ "params": [
+ { "target": "ImageId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "ImageExists",
+ "params": [
+ { "target": "ImageIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Images[0]"
+ }
+ }
+ },
+ "Instance": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InstanceId"
+ }
+ ],
+ "shape": "Instance",
+ "load": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Reservations[0].Instances[0]"
+ },
+ "actions": {
+ "AttachClassicLinkVpc": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachVolume": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ConsoleOutput": {
+ "request": {
+ "operation": "GetConsoleOutput",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateImage": {
+ "request": {
+ "operation": "CreateImage",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "ImageId" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkVpc": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachVolume": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "PasswordData": {
+ "request": {
+ "operation": "GetPasswordData",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReportStatus": {
+ "request": {
+ "operation": "ReportInstanceStatus",
+ "params": [
+ { "target": "Instances[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetKernel": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "kernel" }
+ ]
+ }
+ },
+ "ResetRamdisk": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "ramdisk" }
+ ]
+ }
+ },
+ "ResetSourceDestCheck": {
+ "request": {
+ "operation": "ResetInstanceAttribute",
+ "params": [
+ { "target": "InstanceId", "source": "identifier", "name": "Id" },
+ { "target": "Attribute", "source": "string", "value": "sourceDestCheck" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Monitor": {
+ "request": {
+ "operation": "MonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reboot": {
+ "request": {
+ "operation": "RebootInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Start": {
+ "request": {
+ "operation": "StartInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Stop": {
+ "request": {
+ "operation": "StopInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Terminate": {
+ "request": {
+ "operation": "TerminateInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Unmonitor": {
+ "request": {
+ "operation": "UnmonitorInstances",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "InstanceExists",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Running": {
+ "waiterName": "InstanceRunning",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Stopped": {
+ "waiterName": "InstanceStopped",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ },
+ "Terminated": {
+ "waiterName": "InstanceTerminated",
+ "params": [
+ { "target": "InstanceIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Reservations[0].Instances[0]"
+ }
+ },
+ "has": {
+ "ClassicAddress": {
+ "resource": {
+ "type": "ClassicAddress",
+ "identifiers": [
+ { "target": "PublicIp", "source": "data", "path": "PublicIpAddress" }
+ ]
+ }
+ },
+ "Image": {
+ "resource": {
+ "type": "Image",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "ImageId" }
+ ]
+ }
+ },
+ "KeyPair": {
+ "resource": {
+ "type": "KeyPairInfo",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "KeyName" }
+ ]
+ }
+ },
+ "NetworkInterfaces": {
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "PlacementGroup": {
+ "resource": {
+ "type": "PlacementGroup",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Placement.GroupName" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Volumes": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Volumes[].VolumeId" }
+ ],
+ "path": "Volumes[]"
+ }
+ },
+ "VpcAddresses": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "instance-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "response", "path": "Addresses[].AllocationId" }
+ ],
+ "path": "Addresses[]"
+ }
+ }
+ }
+ },
+ "InternetGateway": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "InternetGatewayId"
+ }
+ ],
+ "shape": "InternetGateway",
+ "load": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "InternetGatewayIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "InternetGateways[0]"
+ },
+ "actions": {
+ "AttachToVpc": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromVpc": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "InternetGatewayId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPair": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPair",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "KeyPairInfo": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "KeyName"
+ }
+ ],
+ "shape": "KeyPairInfo",
+ "load": {
+ "request": {
+ "operation": "DescribeKeyPairs",
+ "params": [
+ { "target": "KeyNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "KeyPairs[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteKeyPair",
+ "params": [
+ { "target": "KeyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkAcl": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkAclId"
+ }
+ ],
+ "shape": "NetworkAcl",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "NetworkAclIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkAcls[0]"
+ },
+ "actions": {
+ "CreateEntry": {
+ "request": {
+ "operation": "CreateNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkAcl",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DeleteEntry": {
+ "request": {
+ "operation": "DeleteNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceAssociation": {
+ "request": {
+ "operation": "ReplaceNetworkAclAssociation",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceEntry": {
+ "request": {
+ "operation": "ReplaceNetworkAclEntry",
+ "params": [
+ { "target": "NetworkAclId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterface": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "NetworkInterfaceId"
+ }
+ ],
+ "shape": "NetworkInterface",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "NetworkInterfaceIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0]"
+ },
+ "actions": {
+ "AssignPrivateIpAddresses": {
+ "request": {
+ "operation": "AssignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Attach": {
+ "request": {
+ "operation": "AttachNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteNetworkInterface",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Detach": {
+ "request": {
+ "operation": "DetachNetworkInterface",
+ "params": [
+ { "target": "AttachmentId", "source": "data", "path": "Attachment.AttachmentId" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetNetworkInterfaceAttribute",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UnassignPrivateIpAddresses": {
+ "request": {
+ "operation": "UnassignPrivateIpAddresses",
+ "params": [
+ { "target": "NetworkInterfaceId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "Association.AssociationId" }
+ ],
+ "path": "Association"
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "NetworkInterfaceAssociation": {
+ "identifiers": [
+ {
+ "name": "Id"
+ }
+ ],
+ "shape": "InstanceNetworkInterfaceAssociation",
+ "load": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "association.association-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "NetworkInterfaces[0].Association"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateAddress",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Address": {
+ "resource": {
+ "type": "VpcAddress",
+ "identifiers": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ }
+ },
+ "PlacementGroup": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "PlacementGroup",
+ "load": {
+ "request": {
+ "operation": "DescribePlacementGroups",
+ "params": [
+ { "target": "GroupNames[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "PlacementGroups[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePlacementGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "placement-group-name" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ }
+ }
+ },
+ "Route": {
+ "identifiers": [
+ { "name": "RouteTableId" },
+ {
+ "name": "DestinationCidrBlock",
+ "memberName": "DestinationCidrBlock"
+ }
+ ],
+ "shape": "Route",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "Replace": {
+ "request": {
+ "operation": "ReplaceRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "RouteTableId" },
+ { "target": "DestinationCidrBlock", "source": "identifier", "name": "DestinationCidrBlock" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "identifier", "name": "RouteTableId" }
+ ]
+ }
+ }
+ }
+ },
+ "RouteTable": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableId"
+ }
+ ],
+ "shape": "RouteTable",
+ "load": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "RouteTableIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "RouteTables[0]"
+ },
+ "actions": {
+ "AssociateWithSubnet": {
+ "request": {
+ "operation": "AssociateRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "AssociationId" }
+ ]
+ }
+ },
+ "CreateRoute": {
+ "request": {
+ "operation": "CreateRoute",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "requestParameter", "path": "DestinationCidrBlock" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRouteTable",
+ "params": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Associations": {
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "Associations[].RouteTableAssociationId" }
+ ],
+ "path": "Associations[]"
+ }
+ },
+ "Routes": {
+ "resource": {
+ "type": "Route",
+ "identifiers": [
+ { "target": "RouteTableId", "source": "identifier", "name": "Id" },
+ { "target": "DestinationCidrBlock", "source": "data", "path": "Routes[].DestinationCidrBlock" }
+ ],
+ "path": "Routes[]"
+ }
+ },
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "RouteTableAssociation": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "RouteTableAssociationId"
+ }
+ ],
+ "shape": "RouteTableAssociation",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DisassociateRouteTable",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ReplaceSubnet": {
+ "request": {
+ "operation": "ReplaceRouteTableAssociation",
+ "params": [
+ { "target": "AssociationId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTableAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NewAssociationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "RouteTable": {
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RouteTableId" }
+ ]
+ }
+ },
+ "Subnet": {
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "SubnetId" }
+ ]
+ }
+ }
+ }
+ },
+ "SecurityGroup": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "GroupId"
+ }
+ ],
+ "shape": "SecurityGroup",
+ "load": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "GroupIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "SecurityGroups[0]"
+ },
+ "actions": {
+ "AuthorizeEgress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AuthorizeIngress": {
+ "request": {
+ "operation": "AuthorizeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSecurityGroup",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeEgress": {
+ "request": {
+ "operation": "RevokeSecurityGroupEgress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RevokeIngress": {
+ "request": {
+ "operation": "RevokeSecurityGroupIngress",
+ "params": [
+ { "target": "GroupId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "Snapshot": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SnapshotId"
+ }
+ ],
+ "shape": "Snapshot",
+ "load": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "SnapshotIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Snapshots[0]"
+ },
+ "actions": {
+ "Copy": {
+ "request": {
+ "operation": "CopySnapshot",
+ "params": [
+ { "target": "SourceSnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSnapshot",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifySnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ResetAttribute": {
+ "request": {
+ "operation": "ResetSnapshotAttribute",
+ "params": [
+ { "target": "SnapshotId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Completed": {
+ "waiterName": "SnapshotCompleted",
+ "params": [
+ { "target": "SnapshotIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "Snapshots[]"
+ }
+ },
+ "has": {
+ "Volume": {
+ "resource": {
+ "type": "Volume",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VolumeId" }
+ ]
+ }
+ }
+ }
+ },
+ "Subnet": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "SubnetId"
+ }
+ ],
+ "shape": "Subnet",
+ "load": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "SubnetIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Subnets[0]"
+ },
+ "actions": {
+ "CreateInstances": {
+ "request": {
+ "operation": "RunInstances",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Instances[].InstanceId" }
+ ],
+ "path": "Instances[]"
+ }
+ },
+ "CreateNetworkInterface": {
+ "request": {
+ "operation": "CreateNetworkInterface",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterface.NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterface"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSubnet",
+ "params": [
+ { "target": "SubnetId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "VpcId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "subnet-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ }
+ }
+ },
+ "Tag": {
+ "identifiers": [
+ {
+ "name": "ResourceId",
+ "memberName": "ResourceId"
+ },
+ {
+ "name": "Key",
+ "memberName": "Key"
+ },
+ {
+ "name": "Value",
+ "memberName": "Value"
+ }
+ ],
+ "shape": "TagDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeTags",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "key" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Key" },
+ { "target": "Filters[1].Name", "source": "string", "value": "value" },
+ { "target": "Filters[1].Values[0]", "source": "identifier", "name": "Value" }
+ ]
+ },
+ "path": "Tags[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[0].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[0].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteTags",
+ "params": [
+ { "target": "Resources[]", "source": "identifier", "name": "ResourceId" },
+ { "target": "Tags[*].Key", "source": "identifier", "name": "Key" },
+ { "target": "Tags[*].Value", "source": "identifier", "name": "Value" }
+ ]
+ }
+ }
+ }
+ },
+ "Volume": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VolumeId"
+ }
+ ],
+ "shape": "Volume",
+ "load": {
+ "request": {
+ "operation": "DescribeVolumes",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Volumes[0]"
+ },
+ "actions": {
+ "AttachToInstance": {
+ "request": {
+ "operation": "AttachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateSnapshot": {
+ "request": {
+ "operation": "CreateSnapshot",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SnapshotId" }
+ ],
+ "path": "@"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeStatus": {
+ "request": {
+ "operation": "DescribeVolumeStatus",
+ "params": [
+ { "target": "VolumeIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachFromInstance": {
+ "request": {
+ "operation": "DetachVolume",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableIo": {
+ "request": {
+ "operation": "EnableVolumeIO",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVolumeAttribute",
+ "params": [
+ { "target": "VolumeId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Snapshots": {
+ "request": {
+ "operation": "DescribeSnapshots",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "volume-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Snapshot",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Snapshots[].SnapshotId" }
+ ],
+ "path": "Snapshots[]"
+ }
+ }
+ }
+ },
+ "Vpc": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcId"
+ }
+ ],
+ "shape": "Vpc",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcs",
+ "params": [
+ { "target": "VpcIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Vpcs[0]"
+ },
+ "actions": {
+ "AssociateDhcpOptions": {
+ "request": {
+ "operation": "AssociateDhcpOptions",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachClassicLinkInstance": {
+ "request": {
+ "operation": "AttachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "AttachInternetGateway": {
+ "request": {
+ "operation": "AttachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "CreateNetworkAcl": {
+ "request": {
+ "operation": "CreateNetworkAcl",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcl.NetworkAclId" }
+ ],
+ "path": "NetworkAcl"
+ }
+ },
+ "CreateRouteTable": {
+ "request": {
+ "operation": "CreateRouteTable",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTable.RouteTableId" }
+ ],
+ "path": "RouteTable"
+ }
+ },
+ "CreateSecurityGroup": {
+ "request": {
+ "operation": "CreateSecurityGroup",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "GroupId" }
+ ]
+ }
+ },
+ "CreateSubnet": {
+ "request": {
+ "operation": "CreateSubnet",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnet.SubnetId" }
+ ],
+ "path": "Subnet"
+ }
+ },
+ "CreateTags": {
+ "request": {
+ "operation": "CreateTags",
+ "params": [
+ { "target": "Resources[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Tag",
+ "identifiers": [
+ { "target": "ResourceId", "source": "identifier", "name": "Id" },
+ { "target": "Key", "source": "requestParameter", "path": "Tags[].Key" },
+ { "target": "Value", "source": "requestParameter", "path": "Tags[].Value" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DescribeAttribute": {
+ "request": {
+ "operation": "DescribeVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachClassicLinkInstance": {
+ "request": {
+ "operation": "DetachClassicLinkVpc",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DetachInternetGateway": {
+ "request": {
+ "operation": "DetachInternetGateway",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "DisableClassicLink": {
+ "request": {
+ "operation": "DisableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "EnableClassicLink": {
+ "request": {
+ "operation": "EnableVpcClassicLink",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "ModifyAttribute": {
+ "request": {
+ "operation": "ModifyVpcAttribute",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "RequestVpcPeeringConnection": {
+ "request": {
+ "operation": "CreateVpcPeeringConnection",
+ "params": [
+ { "target": "VpcId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnection.VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnection"
+ }
+ }
+ },
+ "waiters": {
+ "Available": {
+ "waiterName": "VpcAvailable",
+ "params": [
+ { "target": "VpcIds[]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "Exists": {
+ "waiterName": "VpcExists",
+ "params": [
+ { "target": "VpcIds[]", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "has": {
+ "DhcpOptions": {
+ "resource": {
+ "type": "DhcpOptions",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "DhcpOptionsId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AcceptedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "accepter-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "Instances": {
+ "request": {
+ "operation": "DescribeInstances",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Instance",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Reservations[].Instances[].InstanceId" }
+ ],
+ "path": "Reservations[].Instances[]"
+ }
+ },
+ "InternetGateways": {
+ "request": {
+ "operation": "DescribeInternetGateways",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "attachment.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "InternetGateway",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "InternetGateways[].InternetGatewayId" }
+ ],
+ "path": "InternetGateways[]"
+ }
+ },
+ "NetworkAcls": {
+ "request": {
+ "operation": "DescribeNetworkAcls",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkAcl",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkAcls[].NetworkAclId" }
+ ],
+ "path": "NetworkAcls[]"
+ }
+ },
+ "NetworkInterfaces": {
+ "request": {
+ "operation": "DescribeNetworkInterfaces",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "NetworkInterface",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "NetworkInterfaces[].NetworkInterfaceId" }
+ ],
+ "path": "NetworkInterfaces[]"
+ }
+ },
+ "RequestedVpcPeeringConnections": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "requester-vpc-info.vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "VpcPeeringConnection",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "VpcPeeringConnections[].VpcPeeringConnectionId" }
+ ],
+ "path": "VpcPeeringConnections[]"
+ }
+ },
+ "RouteTables": {
+ "request": {
+ "operation": "DescribeRouteTables",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "RouteTable",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "RouteTables[].RouteTableId" }
+ ],
+ "path": "RouteTables[]"
+ }
+ },
+ "SecurityGroups": {
+ "request": {
+ "operation": "DescribeSecurityGroups",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "SecurityGroup",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "SecurityGroups[].GroupId" }
+ ],
+ "path": "SecurityGroups[]"
+ }
+ },
+ "Subnets": {
+ "request": {
+ "operation": "DescribeSubnets",
+ "params": [
+ { "target": "Filters[0].Name", "source": "string", "value": "vpc-id" },
+ { "target": "Filters[0].Values[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Subnet",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Subnets[].SubnetId" }
+ ],
+ "path": "Subnets[]"
+ }
+ }
+ }
+ },
+ "VpcPeeringConnection": {
+ "identifiers": [
+ {
+ "name": "Id",
+ "memberName": "VpcPeeringConnectionId"
+ }
+ ],
+ "shape": "VpcPeeringConnection",
+ "load": {
+ "request": {
+ "operation": "DescribeVpcPeeringConnections",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[0]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "VpcPeeringConnections[0]"
+ },
+ "actions": {
+ "Accept": {
+ "request": {
+ "operation": "AcceptVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Reject": {
+ "request": {
+ "operation": "RejectVpcPeeringConnection",
+ "params": [
+ { "target": "VpcPeeringConnectionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "VpcPeeringConnectionExists",
+ "params": [
+ { "target": "VpcPeeringConnectionIds[]", "source": "identifier", "name": "Id" }
+ ],
+ "path": "VpcPeeringConnections[0]"
+ }
+ },
+ "has": {
+ "AccepterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AccepterVpcInfo.VpcId" }
+ ]
+ }
+ },
+ "RequesterVpc": {
+ "resource": {
+ "type": "Vpc",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "RequesterVpcInfo.VpcId" }
+ ]
+ }
+ }
+ }
+ },
+ "VpcAddress": {
+ "identifiers": [
+ {
+ "name": "AllocationId"
+ }
+ ],
+ "shape": "Address",
+ "load": {
+ "request": {
+ "operation": "DescribeAddresses",
+ "params": [
+ { "target": "AllocationIds[0]", "source": "identifier", "name": "AllocationId" }
+ ]
+ },
+ "path": "Addresses[0]"
+ },
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "AssociateAddress",
+ "params": [
+ { "target": "AllocationId", "source": "identifier", "name": "AllocationId" }
+ ]
+ }
+ },
+ "Release": {
+ "request": {
+ "operation": "ReleaseAddress",
+ "params": [
+ { "target": "AllocationId", "source": "data", "path": "AllocationId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Association": {
+ "resource": {
+ "type": "NetworkInterfaceAssociation",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "AssociationId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/glacier/2012-06-01/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/glacier/2012-06-01/resources-1.json
new file mode 100644
index 0000000..d1ed48f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/glacier/2012-06-01/resources-1.json
@@ -0,0 +1,581 @@
+{
+ "service": {
+ "actions": {
+ "CreateVault": {
+ "request": {
+ "operation": "CreateVault",
+ "params": [
+ { "target": "accountId", "source": "string", "value": "-" }
+ ]
+ },
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "requestParameter", "path": "accountId" },
+ { "target": "Name", "source": "requestParameter", "path": "vaultName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Account": {
+ "resource": {
+ "type": "Account",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Vaults": {
+ "request": {
+ "operation": "ListVaults",
+ "params": [
+ { "target": "accountId", "source": "string", "value": "-" }
+ ]
+ },
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "requestParameter", "path": "accountId" },
+ { "target": "Name", "source": "response", "path": "VaultList[].VaultName" }
+ ],
+ "path": "VaultList[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "Account": {
+ "identifiers": [
+ { "name": "Id" }
+ ],
+ "actions": {
+ "CreateVault": {
+ "request": {
+ "operation": "CreateVault",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "Id" },
+ { "target": "Name", "source": "requestParameter", "path": "vaultName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vault": {
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "Id" },
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Vaults": {
+ "request": {
+ "operation": "ListVaults",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "Id" },
+ { "target": "Name", "source": "response", "path": "VaultList[].VaultName" }
+ ],
+ "path": "VaultList[]"
+ }
+ }
+ }
+ },
+ "Archive": {
+ "identifiers": [
+ { "name": "AccountId" },
+ { "name": "VaultName" },
+ { "name": "Id" }
+ ],
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteArchive",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "archiveId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "InitiateArchiveRetrieval": {
+ "request": {
+ "operation": "InitiateJob",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "jobParameters.Type", "source": "string", "value": "archive-retrieval" },
+ { "target": "jobParameters.ArchiveId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "jobId" },
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "VaultName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vault": {
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "Name", "source": "identifier", "name": "VaultName" }
+ ]
+ }
+ }
+ }
+ },
+ "Job": {
+ "identifiers": [
+ { "name": "AccountId" },
+ { "name": "VaultName" },
+ {
+ "name": "Id",
+ "memberName": "JobId"
+ }
+ ],
+ "shape": "GlacierJobDescription",
+ "load": {
+ "request": {
+ "operation": "DescribeJob",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "jobId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "GetOutput": {
+ "request": {
+ "operation": "GetJobOutput",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "jobId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vault": {
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "Name", "source": "identifier", "name": "VaultName" }
+ ]
+ }
+ }
+ }
+ },
+ "MultipartUpload": {
+ "identifiers": [
+ { "name": "AccountId" },
+ { "name": "VaultName" },
+ {
+ "name": "Id",
+ "memberName": "MultipartUploadId"
+ }
+ ],
+ "shape": "UploadListElement",
+ "actions": {
+ "Abort": {
+ "request": {
+ "operation": "AbortMultipartUpload",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "uploadId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Complete": {
+ "request": {
+ "operation": "CompleteMultipartUpload",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "uploadId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Parts": {
+ "request": {
+ "operation": "ListParts",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "uploadId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "UploadPart": {
+ "request": {
+ "operation": "UploadMultipartPart",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" },
+ { "target": "uploadId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vault": {
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "Name", "source": "identifier", "name": "VaultName" }
+ ]
+ }
+ }
+ }
+ },
+ "Notification": {
+ "identifiers": [
+ { "name": "AccountId" },
+ { "name": "VaultName" }
+ ],
+ "shape": "VaultNotificationConfig",
+ "load": {
+ "request": {
+ "operation": "GetVaultNotifications",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" }
+ ]
+ },
+ "path": "vaultNotificationConfig"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteVaultNotifications",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" }
+ ]
+ }
+ },
+ "Set": {
+ "request": {
+ "operation": "SetVaultNotifications",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "VaultName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Vault": {
+ "resource": {
+ "type": "Vault",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "Name", "source": "identifier", "name": "VaultName" }
+ ]
+ }
+ }
+ }
+ },
+ "Vault": {
+ "identifiers": [
+ { "name": "AccountId" },
+ {
+ "name": "Name",
+ "memberName": "VaultName"
+ }
+ ],
+ "shape": "DescribeVaultOutput",
+ "load": {
+ "request": {
+ "operation": "DescribeVault",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Create": {
+ "request": {
+ "operation": "CreateVault",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteVault",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" }
+ ]
+ }
+ },
+ "InitiateInventoryRetrieval": {
+ "request": {
+ "operation": "InitiateJob",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "jobParameters.Type", "source": "string", "value": "inventory-retrieval" }
+ ]
+ },
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "jobId" },
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "InitiateMultipartUpload": {
+ "request": {
+ "operation": "InitiateMultipartUpload",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" }
+ ]
+ },
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "uploadId" },
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "UploadArchive": {
+ "request": {
+ "operation": "UploadArchive",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" }
+ ]
+ },
+ "resource": {
+ "type": "Archive",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "archiveId" },
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Account": {
+ "resource": {
+ "type": "Account",
+ "identifiers": [
+ { "target": "Id", "source": "identifier", "name": "AccountId" }
+ ]
+ }
+ },
+ "Archive": {
+ "resource": {
+ "type": "Archive",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Job": {
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "MultipartUpload": {
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Notification": {
+ "resource": {
+ "type": "Notification",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "CompletedJobs": {
+ "request": {
+ "operation": "ListJobs",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "completed", "source": "string", "value": "true" }
+ ]
+ },
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "JobList[].JobId" }
+ ],
+ "path": "JobList[]"
+ }
+ },
+ "FailedJobs": {
+ "request": {
+ "operation": "ListJobs",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "statuscode", "source": "string", "value": "Failed" }
+ ]
+ },
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "JobList[].JobId" }
+ ],
+ "path": "JobList[]"
+ }
+ },
+ "Jobs": {
+ "request": {
+ "operation": "ListJobs",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "JobList[].JobId" }
+ ],
+ "path": "JobList[]"
+ }
+ },
+ "JobsInProgress": {
+ "request": {
+ "operation": "ListJobs",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "statuscode", "source": "string", "value": "InProgress" }
+ ]
+ },
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "JobList[].JobId" }
+ ],
+ "path": "JobList[]"
+ }
+ },
+ "MultipartUplaods": {
+ "request": {
+ "operation": "ListMultipartUploads",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" }
+ ]
+ },
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "UploadsList[].MultipartUploadId" }
+ ],
+ "path": "UploadsList[]"
+ }
+ },
+ "MultipartUploads": {
+ "request": {
+ "operation": "ListMultipartUploads",
+ "params": [
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "accountId", "source": "identifier", "name": "AccountId" }
+ ]
+ },
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "UploadsList[].MultipartUploadId" }
+ ],
+ "path": "UploadsList[]"
+ }
+ },
+ "SucceededJobs": {
+ "request": {
+ "operation": "ListJobs",
+ "params": [
+ { "target": "accountId", "source": "identifier", "name": "AccountId" },
+ { "target": "vaultName", "source": "identifier", "name": "Name" },
+ { "target": "statuscode", "source": "string", "value": "Succeeded" }
+ ]
+ },
+ "resource": {
+ "type": "Job",
+ "identifiers": [
+ { "target": "AccountId", "source": "identifier", "name": "AccountId" },
+ { "target": "VaultName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "JobList[].JobId" }
+ ],
+ "path": "JobList[]"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/iam/2010-05-08/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/iam/2010-05-08/resources-1.json
new file mode 100644
index 0000000..59d1855
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/iam/2010-05-08/resources-1.json
@@ -0,0 +1,1721 @@
+{
+ "service": {
+ "actions": {
+ "ChangePassword": {
+ "request": { "operation": "ChangePassword" }
+ },
+ "CreateAccountAlias": {
+ "request": { "operation": "CreateAccountAlias" }
+ },
+ "CreateAccountPasswordPolicy": {
+ "request": { "operation": "UpdateAccountPasswordPolicy" },
+ "resource": {
+ "type": "AccountPasswordPolicy",
+ "identifiers": [ ]
+ }
+ },
+ "CreateGroup": {
+ "request": { "operation": "CreateGroup" },
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ],
+ "path": "Group"
+ }
+ },
+ "CreateInstanceProfile": {
+ "request": { "operation": "CreateInstanceProfile" },
+ "resource": {
+ "type": "InstanceProfile",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "InstanceProfileName" }
+ ],
+ "path": "InstanceProfile"
+ }
+ },
+ "CreatePolicy": {
+ "request": { "operation": "CreatePolicy" },
+ "resource": {
+ "type": "Policy",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "Policy.Arn" }
+ ]
+ }
+ },
+ "CreateRole": {
+ "request": { "operation": "CreateRole" },
+ "resource": {
+ "type": "Role",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "RoleName" }
+ ],
+ "path": "Role"
+ }
+ },
+ "CreateSamlProvider": {
+ "request": { "operation": "CreateSAMLProvider" },
+ "resource": {
+ "type": "SamlProvider",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "SAMLProviderArn" }
+ ]
+ }
+ },
+ "CreateServerCertificate": {
+ "request": { "operation": "UploadServerCertificate" },
+ "resource": {
+ "type": "ServerCertificate",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "ServerCertificateName" }
+ ]
+ }
+ },
+ "CreateSigningCertificate": {
+ "request": { "operation": "UploadSigningCertificate" },
+ "resource": {
+ "type": "SigningCertificate",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Certificate.CertificateId" }
+ ],
+ "path": "Certificate"
+ }
+ },
+ "CreateUser": {
+ "request": { "operation": "CreateUser" },
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "UserName" }
+ ],
+ "path": "User"
+ }
+ },
+ "CreateVirtualMfaDevice": {
+ "request": { "operation": "CreateVirtualMFADevice" },
+ "resource": {
+ "type": "VirtualMfaDevice",
+ "identifiers": [
+ { "target": "SerialNumber", "source": "response", "path": "VirtualMFADevice.SerialNumber" }
+ ],
+ "path": "VirtualMFADevice"
+ }
+ }
+ },
+ "has": {
+ "AccountPasswordPolicy": {
+ "resource": {
+ "type": "AccountPasswordPolicy",
+ "identifiers": [ ]
+ }
+ },
+ "AccountSummary": {
+ "resource": {
+ "type": "AccountSummary",
+ "identifiers": [ ]
+ }
+ },
+ "CurrentUser": {
+ "resource": {
+ "type": "CurrentUser",
+ "identifiers": [ ]
+ }
+ },
+ "Group": {
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "InstanceProfile": {
+ "resource": {
+ "type": "InstanceProfile",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "Policy": {
+ "resource": {
+ "type": "Policy",
+ "identifiers": [
+ { "target": "PolicyArn", "source": "input" }
+ ]
+ }
+ },
+ "Role": {
+ "resource": {
+ "type": "Role",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "SamlProvider": {
+ "resource": {
+ "type": "SamlProvider",
+ "identifiers": [
+ { "target": "Arn", "source": "input" }
+ ]
+ }
+ },
+ "ServerCertificate": {
+ "resource": {
+ "type": "ServerCertificate",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "VirtualMfaDevice": {
+ "resource": {
+ "type": "VirtualMfaDevice",
+ "identifiers": [
+ { "target": "SerialNumber", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Groups": {
+ "request": { "operation": "ListGroups" },
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "Groups[].GroupName" }
+ ],
+ "path": "Groups[]"
+ }
+ },
+ "InstanceProfiles": {
+ "request": { "operation": "ListInstanceProfiles" },
+ "resource": {
+ "type": "InstanceProfile",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "InstanceProfiles[].InstanceProfileName" }
+ ],
+ "path": "InstanceProfiles[]"
+ }
+ },
+ "Policies": {
+ "request": { "operation": "ListPolicies" },
+ "resource": {
+ "type": "Policy",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "Policies[].Arn" }
+ ],
+ "path": "Policies[]"
+ }
+ },
+ "Roles": {
+ "request": { "operation": "ListRoles" },
+ "resource": {
+ "type": "Role",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "Roles[].RoleName" }
+ ],
+ "path": "Roles[]"
+ }
+ },
+ "SamlProviders": {
+ "request": { "operation": "ListSAMLProviders" },
+ "resource": {
+ "type": "SamlProvider",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "SAMLProviderList[].Arn" }
+ ]
+ }
+ },
+ "ServerCertificates": {
+ "request": { "operation": "ListServerCertificates" },
+ "resource": {
+ "type": "ServerCertificate",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "ServerCertificateMetadataList[].ServerCertificateName" }
+ ]
+ }
+ },
+ "Users": {
+ "request": { "operation": "ListUsers" },
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "Users[].UserName" }
+ ],
+ "path": "Users[]"
+ }
+ },
+ "VirtualMfaDevices": {
+ "request": { "operation": "ListVirtualMFADevices" },
+ "resource": {
+ "type": "VirtualMfaDevice",
+ "identifiers": [
+ { "target": "SerialNumber", "source": "response", "path": "VirtualMFADevices[].SerialNumber" }
+ ],
+ "path": "VirtualMFADevices[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "AccessKey": {
+ "identifiers": [
+ {
+ "name": "UserName",
+ "memberName": "UserName"
+ },
+ {
+ "name": "Id",
+ "memberName": "AccessKeyId"
+ }
+ ],
+ "shape": "AccessKeyMetadata",
+ "actions": {
+ "Activate": {
+ "request": {
+ "operation": "UpdateAccessKey",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "AccessKeyId", "source": "identifier", "name": "Id" },
+ { "target": "Status", "source": "string", "value": "Active" }
+ ]
+ }
+ },
+ "Deactivate": {
+ "request": {
+ "operation": "UpdateAccessKey",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "AccessKeyId", "source": "identifier", "name": "Id" },
+ { "target": "Status", "source": "string", "value": "Inactive" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteAccessKey",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "AccessKeyId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "UserName" }
+ ]
+ }
+ }
+ }
+ },
+ "AccessKeyPair": {
+ "identifiers": [
+ {
+ "name": "UserName",
+ "memberName": "UserName"
+ },
+ {
+ "name": "Id",
+ "memberName": "AccessKeyId"
+ },
+ {
+ "name": "Secret",
+ "memberName": "SecretAccessKey"
+ }
+ ],
+ "shape": "AccessKey",
+ "actions": {
+ "Activate": {
+ "request": {
+ "operation": "UpdateAccessKey",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "AccessKeyId", "source": "identifier", "name": "Id" },
+ { "target": "Status", "source": "string", "value": "Active" }
+ ]
+ }
+ },
+ "Deactivate": {
+ "request": {
+ "operation": "UpdateAccessKey",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "AccessKeyId", "source": "identifier", "name": "Id" },
+ { "target": "Status", "source": "string", "value": "Inactive" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteAccessKey",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "AccessKeyId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ }
+ },
+ "AccountPasswordPolicy": {
+ "identifiers": [ ],
+ "shape": "PasswordPolicy",
+ "load": {
+ "request": { "operation": "GetAccountPasswordPolicy" },
+ "path": "PasswordPolicy"
+ },
+ "actions": {
+ "Delete": {
+ "request": { "operation": "DeleteAccountPasswordPolicy" }
+ },
+ "Update": {
+ "request": { "operation": "UpdateAccountPasswordPolicy" }
+ }
+ }
+ },
+ "AccountSummary": {
+ "identifiers": [ ],
+ "shape": "GetAccountSummaryResponse",
+ "load": {
+ "request": { "operation": "GetAccountSummary" },
+ "path": "@"
+ }
+ },
+ "AssumeRolePolicy": {
+ "identifiers": [
+ { "name": "RoleName" }
+ ],
+ "actions": {
+ "Update": {
+ "request": {
+ "operation": "UpdateAssumeRolePolicy",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "RoleName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Role": {
+ "resource": {
+ "type": "Role",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "RoleName" }
+ ]
+ }
+ }
+ }
+ },
+ "CurrentUser": {
+ "identifiers": [ ],
+ "shape": "User",
+ "load": {
+ "request": { "operation": "GetUser" },
+ "path": "User"
+ },
+ "has": {
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "UserName" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AccessKeys": {
+ "request": { "operation": "ListAccessKeys" },
+ "resource": {
+ "type": "AccessKey",
+ "identifiers": [
+ { "target": "UserName", "source": "response", "path": "AccessKeyMetadata[].UserName" },
+ { "target": "Id", "source": "response", "path": "AccessKeyMetadata[].AccessKeyId" }
+ ],
+ "path": "AccessKeyMetadata[]"
+ }
+ },
+ "MfaDevices": {
+ "request": { "operation": "ListMFADevices" },
+ "resource": {
+ "type": "MfaDevice",
+ "identifiers": [
+ { "target": "UserName", "source": "response", "path": "MFADevices[].UserName" },
+ { "target": "SerialNumber", "source": "response", "path": "MFADevices[].SerialNumber" }
+ ],
+ "path": "MFADevices[]"
+ }
+ },
+ "SigningCertificates": {
+ "request": { "operation": "ListSigningCertificates" },
+ "resource": {
+ "type": "SigningCertificate",
+ "identifiers": [
+ { "target": "UserName", "source": "response", "path": "Certificates[].UserName" },
+ { "target": "Id", "source": "response", "path": "Certificates[].CertificateId" }
+ ],
+ "path": "Certificates[]"
+ }
+ }
+ }
+ },
+ "Group": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "GroupName"
+ }
+ ],
+ "shape": "Group",
+ "load": {
+ "request": {
+ "operation": "GetGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "Group"
+ },
+ "actions": {
+ "AddUser": {
+ "request": {
+ "operation": "AddUserToGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "AttachPolicy": {
+ "request": {
+ "operation": "AttachGroupPolicy",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Create": {
+ "request": {
+ "operation": "CreateGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "GroupName" }
+ ],
+ "path": "Group"
+ }
+ },
+ "CreatePolicy": {
+ "request": {
+ "operation": "PutGroupPolicy",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "GroupPolicy",
+ "identifiers": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "requestParameter", "path": "PolicyName" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "DetachPolicy": {
+ "request": {
+ "operation": "DetachGroupPolicy",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "RemoveUser": {
+ "request": {
+ "operation": "RemoveUserFromGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Update": {
+ "request": {
+ "operation": "UpdateGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "NewGroupName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Policy": {
+ "resource": {
+ "type": "GroupPolicy",
+ "identifiers": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AttachedPolicies": {
+ "request": {
+ "operation": "ListAttachedGroupPolicies",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Policy",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "AttachedPolicies[].PolicyArn" }
+ ]
+ }
+ },
+ "Policies": {
+ "request": {
+ "operation": "ListGroupPolicies",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "GroupPolicy",
+ "identifiers": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "response", "path": "PolicyNames[]" }
+ ]
+ }
+ },
+ "Users": {
+ "request": {
+ "operation": "GetGroup",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "Users[].UserName" }
+ ],
+ "path": "Users[]"
+ }
+ }
+ }
+ },
+ "GroupPolicy": {
+ "identifiers": [
+ {
+ "name": "GroupName",
+ "memberName": "GroupName"
+ },
+ {
+ "name": "Name",
+ "memberName": "PolicyName"
+ }
+ ],
+ "shape": "GetGroupPolicyResponse",
+ "load": {
+ "request": {
+ "operation": "GetGroupPolicy",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "GroupName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteGroupPolicy",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "GroupName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutGroupPolicy",
+ "params": [
+ { "target": "GroupName", "source": "identifier", "name": "GroupName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Group": {
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "GroupName" }
+ ]
+ }
+ }
+ }
+ },
+ "InstanceProfile": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "InstanceProfileName"
+ }
+ ],
+ "shape": "InstanceProfile",
+ "load": {
+ "request": {
+ "operation": "GetInstanceProfile",
+ "params": [
+ { "target": "InstanceProfileName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "InstanceProfile"
+ },
+ "actions": {
+ "AddRole": {
+ "request": {
+ "operation": "AddRoleToInstanceProfile",
+ "params": [
+ { "target": "InstanceProfileName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteInstanceProfile",
+ "params": [
+ { "target": "InstanceProfileName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "RemoveRole": {
+ "request": {
+ "operation": "RemoveRoleFromInstanceProfile",
+ "params": [
+ { "target": "InstanceProfileName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Roles": {
+ "resource": {
+ "type": "Role",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "Roles[].RoleName" }
+ ],
+ "path": "Roles[]"
+ }
+ }
+ }
+ },
+ "LoginProfile": {
+ "identifiers": [
+ {
+ "name": "UserName",
+ "memberName": "UserName"
+ }
+ ],
+ "shape": "LoginProfile",
+ "load": {
+ "request": {
+ "operation": "GetLoginProfile",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" }
+ ]
+ },
+ "path": "LoginProfile"
+ },
+ "actions": {
+ "Create": {
+ "request": {
+ "operation": "CreateLoginProfile",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" }
+ ]
+ },
+ "resource": {
+ "type": "LoginProfile",
+ "identifiers": [
+ { "target": "UserName", "source": "response", "path": "LoginProfile.UserName" }
+ ],
+ "path": "LoginProfile"
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteLoginProfile",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" }
+ ]
+ }
+ },
+ "Update": {
+ "request": {
+ "operation": "UpdateLoginProfile",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "UserName" }
+ ]
+ }
+ }
+ }
+ },
+ "MfaDevice": {
+ "identifiers": [
+ {
+ "name": "UserName",
+ "memberName": "UserName"
+ },
+ {
+ "name": "SerialNumber",
+ "memberName": "SerialNumber"
+ }
+ ],
+ "shape": "MFADevice",
+ "actions": {
+ "Associate": {
+ "request": {
+ "operation": "EnableMFADevice",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "SerialNumber", "source": "identifier", "name": "SerialNumber" }
+ ]
+ }
+ },
+ "Disassociate": {
+ "request": {
+ "operation": "DeactivateMFADevice",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "SerialNumber", "source": "identifier", "name": "SerialNumber" }
+ ]
+ }
+ },
+ "Resync": {
+ "request": {
+ "operation": "ResyncMFADevice",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "SerialNumber", "source": "identifier", "name": "SerialNumber" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "UserName" }
+ ]
+ }
+ }
+ }
+ },
+ "Policy": {
+ "identifiers": [
+ {
+ "name": "Arn",
+ "memberName": "Arn"
+ }
+ ],
+ "shape": "Policy",
+ "load": {
+ "request": {
+ "operation": "GetPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "path": "Policy"
+ },
+ "actions": {
+ "AttachGroup": {
+ "request": {
+ "operation": "AttachGroupPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "AttachRole": {
+ "request": {
+ "operation": "AttachRolePolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "AttachUser": {
+ "request": {
+ "operation": "AttachUserPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "CreateVersion": {
+ "request": {
+ "operation": "CreatePolicyVersion",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "resource": {
+ "type": "PolicyVersion",
+ "identifiers": [
+ { "target": "Arn", "source": "identifier", "name": "Arn" },
+ { "target": "VersionId", "source": "response", "path": "PolicyVersion.VersionId" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeletePolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "DetachGroup": {
+ "request": {
+ "operation": "DetachGroupPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "DetachRole": {
+ "request": {
+ "operation": "DetachRolePolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "DetachUser": {
+ "request": {
+ "operation": "DetachUserPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "DefaultVersion": {
+ "resource": {
+ "type": "PolicyVersion",
+ "identifiers": [
+ { "target": "Arn", "source": "identifier", "name": "Arn" },
+ { "target": "VersionId", "source": "data", "path": "DefaultVersionId" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AttachedGroups": {
+ "request": {
+ "operation": "ListEntitiesForPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" },
+ { "target": "EntityFilter", "source": "string", "value": "Group" }
+ ]
+ },
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PolicyGroups[].GroupName" }
+ ]
+ }
+ },
+ "AttachedRoles": {
+ "request": {
+ "operation": "ListEntitiesForPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" },
+ { "target": "EntityFilter", "source": "string", "value": "Role" }
+ ]
+ },
+ "resource": {
+ "type": "Role",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PolicyRoles[].RoleName" }
+ ]
+ }
+ },
+ "AttachedUsers": {
+ "request": {
+ "operation": "ListEntitiesForPolicy",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" },
+ { "target": "EntityFilter", "source": "string", "value": "User" }
+ ]
+ },
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "PolicyUsers[].UserName" }
+ ]
+ }
+ },
+ "Versions": {
+ "request": {
+ "operation": "ListPolicyVersions",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "resource": {
+ "type": "PolicyVersion",
+ "identifiers": [
+ { "target": "Arn", "source": "identifier", "name": "Arn" },
+ { "target": "VersionId", "source": "response", "path": "Versions[].VersionId" }
+ ],
+ "path": "Versions[]"
+ }
+ }
+ }
+ },
+ "PolicyVersion": {
+ "identifiers": [
+ { "name": "Arn" },
+ { "name": "VersionId" }
+ ],
+ "shape": "PolicyVersion",
+ "load": {
+ "request": {
+ "operation": "GetPolicyVersion",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" },
+ { "target": "VersionId", "source": "identifier", "name": "VersionId" }
+ ]
+ },
+ "path": "PolicyVersion"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeletePolicyVersion",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" },
+ { "target": "VersionId", "source": "identifier", "name": "VersionId" }
+ ]
+ }
+ },
+ "SetAsDefault": {
+ "request": {
+ "operation": "SetDefaultPolicyVersion",
+ "params": [
+ { "target": "PolicyArn", "source": "identifier", "name": "Arn" },
+ { "target": "VersionId", "source": "identifier", "name": "VersionId" }
+ ]
+ }
+ }
+ }
+ },
+ "Role": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "RoleName"
+ }
+ ],
+ "shape": "Role",
+ "load": {
+ "request": {
+ "operation": "GetRole",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "Role"
+ },
+ "actions": {
+ "AttachPolicy": {
+ "request": {
+ "operation": "AttachRolePolicy",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteRole",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "DetachPolicy": {
+ "request": {
+ "operation": "DetachRolePolicy",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "AssumeRolePolicy": {
+ "resource": {
+ "type": "AssumeRolePolicy",
+ "identifiers": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Policy": {
+ "resource": {
+ "type": "RolePolicy",
+ "identifiers": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AttachedPolicies": {
+ "request": {
+ "operation": "ListAttachedRolePolicies",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Policy",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "AttachedPolicies[].PolicyArn" }
+ ]
+ }
+ },
+ "InstanceProfiles": {
+ "request": {
+ "operation": "ListInstanceProfilesForRole",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "InstanceProfile",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "InstanceProfiles[].InstanceProfileName" }
+ ],
+ "path": "InstanceProfiles[]"
+ }
+ },
+ "Policies": {
+ "request": {
+ "operation": "ListRolePolicies",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "RolePolicy",
+ "identifiers": [
+ { "target": "RoleName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "response", "path": "PolicyNames[]" }
+ ]
+ }
+ }
+ }
+ },
+ "RolePolicy": {
+ "identifiers": [
+ {
+ "name": "RoleName",
+ "memberName": "RoleName"
+ },
+ {
+ "name": "Name",
+ "memberName": "PolicyName"
+ }
+ ],
+ "shape": "GetRolePolicyResponse",
+ "load": {
+ "request": {
+ "operation": "GetRolePolicy",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "RoleName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteRolePolicy",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "RoleName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutRolePolicy",
+ "params": [
+ { "target": "RoleName", "source": "identifier", "name": "RoleName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Role": {
+ "resource": {
+ "type": "Role",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "RoleName" }
+ ]
+ }
+ }
+ }
+ },
+ "SamlProvider": {
+ "identifiers": [
+ { "name": "Arn" }
+ ],
+ "shape": "GetSAMLProviderResponse",
+ "load": {
+ "request": {
+ "operation": "GetSAMLProvider",
+ "params": [
+ { "target": "SAMLProviderArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteSAMLProvider",
+ "params": [
+ { "target": "SAMLProviderArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "Update": {
+ "request": {
+ "operation": "UpdateSAMLProvider",
+ "params": [
+ { "target": "SAMLProviderArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ }
+ }
+ },
+ "ServerCertificate": {
+ "identifiers": [
+ { "name": "Name" }
+ ],
+ "shape": "ServerCertificate",
+ "load": {
+ "request": {
+ "operation": "GetServerCertificate",
+ "params": [
+ { "target": "ServerCertificateName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "ServerCertificate"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteServerCertificate",
+ "params": [
+ { "target": "ServerCertificateName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Update": {
+ "request": {
+ "operation": "UpdateServerCertificate",
+ "params": [
+ { "target": "ServerCertificateName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "ServerCertificate",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "NewServerCertificateName" }
+ ]
+ }
+ }
+ }
+ },
+ "SigningCertificate": {
+ "identifiers": [
+ {
+ "name": "UserName",
+ "memberName": "UserName"
+ },
+ {
+ "name": "Id",
+ "memberName": "CertificateId"
+ }
+ ],
+ "shape": "SigningCertificate",
+ "actions": {
+ "Activate": {
+ "request": {
+ "operation": "UpdateSigningCertificate",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "CertificateId", "source": "identifier", "name": "Id" },
+ { "target": "Status", "source": "string", "value": "Active" }
+ ]
+ }
+ },
+ "Deactivate": {
+ "request": {
+ "operation": "UpdateSigningCertificate",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "CertificateId", "source": "identifier", "name": "Id" },
+ { "target": "Status", "source": "string", "value": "Inactive" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteSigningCertificate",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "CertificateId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "UserName" }
+ ]
+ }
+ }
+ }
+ },
+ "User": {
+ "identifiers": [
+ {
+ "name": "Name",
+ "memberName": "UserName"
+ }
+ ],
+ "shape": "User",
+ "load": {
+ "request": {
+ "operation": "GetUser",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "User"
+ },
+ "actions": {
+ "AddGroup": {
+ "request": {
+ "operation": "AddUserToGroup",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "AttachPolicy": {
+ "request": {
+ "operation": "AttachUserPolicy",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Create": {
+ "request": {
+ "operation": "CreateUser",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "UserName" }
+ ],
+ "path": "User"
+ }
+ },
+ "CreateAccessKeyPair": {
+ "request": {
+ "operation": "CreateAccessKey",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "AccessKeyPair",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "AccessKey.AccessKeyId" },
+ { "target": "Secret", "source": "response", "path": "AccessKey.SecretAccessKey" }
+ ],
+ "path": "AccessKey"
+ }
+ },
+ "CreateLoginProfile": {
+ "request": {
+ "operation": "CreateLoginProfile",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "LoginProfile",
+ "identifiers": [
+ { "target": "UserName", "source": "response", "path": "LoginProfile.UserName" }
+ ],
+ "path": "LoginProfile"
+ }
+ },
+ "CreatePolicy": {
+ "request": {
+ "operation": "PutUserPolicy",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "UserPolicy",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "requestParameter", "path": "PolicyName" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteUser",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "DetachPolicy": {
+ "request": {
+ "operation": "DetachUserPolicy",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "EnableMfa": {
+ "request": {
+ "operation": "EnableMFADevice",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "MfaDevice",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "SerialNumber", "source": "requestParameter", "path": "SerialNumber" }
+ ]
+ }
+ },
+ "RemoveGroup": {
+ "request": {
+ "operation": "RemoveUserFromGroup",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Update": {
+ "request": {
+ "operation": "UpdateUser",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "NewUserName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "AccessKey": {
+ "resource": {
+ "type": "AccessKey",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "LoginProfile": {
+ "resource": {
+ "type": "LoginProfile",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "MfaDevice": {
+ "resource": {
+ "type": "MfaDevice",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "SerialNumber", "source": "input" }
+ ]
+ }
+ },
+ "Policy": {
+ "resource": {
+ "type": "UserPolicy",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ },
+ "SigningCertificate": {
+ "resource": {
+ "type": "SigningCertificate",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "AccessKeys": {
+ "request": {
+ "operation": "ListAccessKeys",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "AccessKey",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "AccessKeyMetadata[].AccessKeyId" }
+ ],
+ "path": "AccessKeyMetadata[]"
+ }
+ },
+ "AttachedPolicies": {
+ "request": {
+ "operation": "ListAttachedUserPolicies",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Policy",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "AttachedPolicies[].PolicyArn" }
+ ]
+ }
+ },
+ "Groups": {
+ "request": {
+ "operation": "ListGroupsForUser",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Group",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "Groups[].GroupName" }
+ ],
+ "path": "Groups[]"
+ }
+ },
+ "MfaDevices": {
+ "request": {
+ "operation": "ListMFADevices",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "MfaDevice",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "SerialNumber", "source": "response", "path": "MFADevices[].SerialNumber" }
+ ],
+ "path": "MFADevices[]"
+ }
+ },
+ "Policies": {
+ "request": {
+ "operation": "ListUserPolicies",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "UserPolicy",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Name", "source": "response", "path": "PolicyNames[]" }
+ ]
+ }
+ },
+ "SigningCertificates": {
+ "request": {
+ "operation": "ListSigningCertificates",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "SigningCertificate",
+ "identifiers": [
+ { "target": "UserName", "source": "identifier", "name": "Name" },
+ { "target": "Id", "source": "response", "path": "Certificates[].CertificateId" }
+ ],
+ "path": "Certificates[]"
+ }
+ }
+ }
+ },
+ "UserPolicy": {
+ "identifiers": [
+ {
+ "name": "UserName",
+ "memberName": "UserName"
+ },
+ {
+ "name": "Name",
+ "memberName": "PolicyName"
+ }
+ ],
+ "shape": "GetUserPolicyResponse",
+ "load": {
+ "request": {
+ "operation": "GetUserPolicy",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteUserPolicy",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutUserPolicy",
+ "params": [
+ { "target": "UserName", "source": "identifier", "name": "UserName" },
+ { "target": "PolicyName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "UserName" }
+ ]
+ }
+ }
+ }
+ },
+ "VirtualMfaDevice": {
+ "identifiers": [
+ {
+ "name": "SerialNumber",
+ "memberName": "SerialNumber"
+ }
+ ],
+ "shape": "VirtualMFADevice",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteVirtualMFADevice",
+ "params": [
+ { "target": "SerialNumber", "source": "identifier", "name": "SerialNumber" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "User": {
+ "resource": {
+ "type": "User",
+ "identifiers": [
+ { "target": "Name", "source": "data", "path": "User.UserName" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/opsworks/2013-02-18/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/opsworks/2013-02-18/resources-1.json
new file mode 100644
index 0000000..0435b13
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/opsworks/2013-02-18/resources-1.json
@@ -0,0 +1,173 @@
+{
+ "service": {
+ "actions": {
+ "CreateStack": {
+ "request": { "operation": "CreateStack" },
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "StackId" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Layer": {
+ "resource": {
+ "type": "Layer",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Stack": {
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Stacks": {
+ "request": { "operation": "DescribeStacks" },
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Stacks[].StackId" }
+ ],
+ "path": "Stacks[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "Layer": {
+ "identifiers": [
+ { "name": "Id" }
+ ],
+ "shape": "Layer",
+ "load": {
+ "request": {
+ "operation": "DescribeLayers",
+ "params": [
+ { "target": "LayerIds[]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Layers[0]"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteLayer",
+ "params": [
+ { "target": "LayerId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Stack": {
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Id", "source": "data", "path": "StackId" }
+ ]
+ }
+ }
+ }
+ },
+ "Stack": {
+ "identifiers": [
+ { "name": "Id" }
+ ],
+ "shape": "Stack",
+ "load": {
+ "request": {
+ "operation": "DescribeStacks",
+ "params": [
+ { "target": "StackIds[]", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "path": "Stacks[0]"
+ },
+ "actions": {
+ "CreateLayer": {
+ "request": {
+ "operation": "CreateLayer",
+ "params": [
+ { "target": "StackId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Layer",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "LayerId" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteStack",
+ "params": [
+ { "target": "StackId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Summary": {
+ "resource": {
+ "type": "StackSummary",
+ "identifiers": [
+ { "target": "StackId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Layers": {
+ "request": {
+ "operation": "DescribeLayers",
+ "params": [
+ { "target": "StackId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Layer",
+ "identifiers": [
+ { "target": "Id", "source": "response", "path": "Layers[].LayerId" }
+ ],
+ "path": "Layers[]"
+ }
+ }
+ }
+ },
+ "StackSummary": {
+ "identifiers": [
+ { "name": "StackId" }
+ ],
+ "shape": "StackSummary",
+ "load": {
+ "request": {
+ "operation": "DescribeStackSummary",
+ "params": [
+ { "target": "StackId", "source": "identifier", "name": "StackId" }
+ ]
+ },
+ "path": "StackSummary"
+ },
+ "has": {
+ "Stack": {
+ "resource": {
+ "type": "Stack",
+ "identifiers": [
+ { "target": "Id", "source": "identifier", "name": "StackId" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/s3/2006-03-01/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/s3/2006-03-01/resources-1.json
new file mode 100644
index 0000000..f1e88c6
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/s3/2006-03-01/resources-1.json
@@ -0,0 +1,1249 @@
+{
+ "service": {
+ "actions": {
+ "CreateBucket": {
+ "request": { "operation": "CreateBucket" },
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "requestParameter", "path": "Bucket" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Buckets": {
+ "request": { "operation": "ListBuckets" },
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "response", "path": "Buckets[].Name" }
+ ],
+ "path": "Buckets[]"
+ }
+ }
+ }
+ },
+ "resources": {
+ "Bucket": {
+ "identifiers": [
+ { "name": "Name" }
+ ],
+ "shape": "Bucket",
+ "actions": {
+ "Create": {
+ "request": {
+ "operation": "CreateBucket",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteBucket",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "DeleteObjects": {
+ "request": {
+ "operation": "DeleteObjects",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "PutObject": {
+ "request": {
+ "operation": "PutObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "Object",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" },
+ { "target": "Key", "source": "requestParameter", "path": "Key" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "BucketExists",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "NotExists": {
+ "waiterName": "BucketNotExists",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "has": {
+ "Acl": {
+ "resource": {
+ "type": "BucketAcl",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Cors": {
+ "resource": {
+ "type": "BucketCors",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Lifecycle": {
+ "resource": {
+ "type": "BucketLifecycle",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "LifecycleConfiguration": {
+ "resource": {
+ "type": "BucketLifecycleConfiguration",
+ "identifiers": [
+ {
+ "target": "BucketName",
+ "source": "identifier",
+ "name": "Name"
+ }
+ ]
+ }
+ },
+ "Logging": {
+ "resource": {
+ "type": "BucketLogging",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Notification": {
+ "resource": {
+ "type": "BucketNotification",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Object": {
+ "resource": {
+ "type": "Object",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" },
+ { "target": "Key", "source": "input" }
+ ]
+ }
+ },
+ "Policy": {
+ "resource": {
+ "type": "BucketPolicy",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "RequestPayment": {
+ "resource": {
+ "type": "BucketRequestPayment",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Tagging": {
+ "resource": {
+ "type": "BucketTagging",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Versioning": {
+ "resource": {
+ "type": "BucketVersioning",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ },
+ "Website": {
+ "resource": {
+ "type": "BucketWebsite",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "MultipartUploads": {
+ "request": {
+ "operation": "ListMultipartUploads",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" },
+ { "target": "ObjectKey", "source": "response", "path": "Uploads[].Key" },
+ { "target": "Id", "source": "response", "path": "Uploads[].UploadId" }
+ ],
+ "path": "Uploads[]"
+ }
+ },
+ "ObjectVersions": {
+ "request": {
+ "operation": "ListObjectVersions",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "ObjectVersion",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" },
+ { "target": "ObjectKey", "source": "response", "path": "[Versions,DeleteMarkers]|[].Key" },
+ { "target": "Id", "source": "response", "path": "[Versions,DeleteMarkers]|[].VersionId" }
+ ],
+ "path": "[Versions,DeleteMarkers]|[]"
+ }
+ },
+ "Objects": {
+ "request": {
+ "operation": "ListObjects",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "Name" }
+ ]
+ },
+ "resource": {
+ "type": "ObjectSummary",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "Name" },
+ { "target": "Key", "source": "response", "path": "Contents[].Key" }
+ ],
+ "path": "Contents[]"
+ }
+ }
+ }
+ },
+ "BucketAcl": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketAclOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketAcl",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Put": {
+ "request": {
+ "operation": "PutBucketAcl",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketCors": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketCorsOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketCors",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteBucketCors",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutBucketCors",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketLifecycle": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketLifecycleOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketLifecycle",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteBucketLifecycle",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutBucketLifecycle",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketLifecycleConfiguration": {
+ "identifiers": [
+ {
+ "name": "BucketName"
+ }
+ ],
+ "shape": "GetBucketLifecycleConfigurationOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketLifecycleConfiguration",
+ "params": [
+ {
+ "target": "Bucket",
+ "source": "identifier",
+ "name": "BucketName"
+ }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteBucketLifecycle",
+ "params": [
+ {
+ "target": "Bucket",
+ "source": "identifier",
+ "name": "BucketName"
+ }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutBucketLifecycleConfiguration",
+ "params": [
+ {
+ "target": "Bucket",
+ "source": "identifier",
+ "name": "BucketName"
+ }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ {
+ "target": "Name",
+ "source": "identifier",
+ "name": "BucketName"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "BucketLogging": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketLoggingOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketLogging",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Put": {
+ "request": {
+ "operation": "PutBucketLogging",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketNotification": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "NotificationConfiguration",
+ "load": {
+ "request": {
+ "operation": "GetBucketNotificationConfiguration",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Put": {
+ "request": {
+ "operation": "PutBucketNotificationConfiguration",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketPolicy": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketPolicyOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketPolicy",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteBucketPolicy",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutBucketPolicy",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketRequestPayment": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketRequestPaymentOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketRequestPayment",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Put": {
+ "request": {
+ "operation": "PutBucketRequestPayment",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketTagging": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketTaggingOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketTagging",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteBucketTagging",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutBucketTagging",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketVersioning": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketVersioningOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketVersioning",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Enable": {
+ "request": {
+ "operation": "PutBucketVersioning",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "VersioningConfiguration.Status", "source": "string", "value": "Enabled" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutBucketVersioning",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "Suspend": {
+ "request": {
+ "operation": "PutBucketVersioning",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "VersioningConfiguration.Status", "source": "string", "value": "Suspended" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "BucketWebsite": {
+ "identifiers": [
+ { "name": "BucketName" }
+ ],
+ "shape": "GetBucketWebsiteOutput",
+ "load": {
+ "request": {
+ "operation": "GetBucketWebsite",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteBucketWebsite",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutBucketWebsite",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ }
+ }
+ },
+ "MultipartUpload": {
+ "identifiers": [
+ { "name": "BucketName" },
+ { "name": "ObjectKey" },
+ { "name": "Id" }
+ ],
+ "shape": "MultipartUpload",
+ "actions": {
+ "Abort": {
+ "request": {
+ "operation": "AbortMultipartUpload",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "UploadId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Complete": {
+ "request": {
+ "operation": "CompleteMultipartUpload",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "UploadId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "Object",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Object": {
+ "resource": {
+ "type": "Object",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" }
+ ]
+ }
+ },
+ "Part": {
+ "resource": {
+ "type": "MultipartUploadPart",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "ObjectKey" },
+ { "target": "MultipartUploadId", "source": "identifier", "name": "Id" },
+ { "target": "PartNumber", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Parts": {
+ "request": {
+ "operation": "ListParts",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "UploadId", "source": "identifier", "name": "Id" }
+ ]
+ },
+ "resource": {
+ "type": "MultipartUploadPart",
+ "identifiers": [
+ { "target": "BucketName", "source": "requestParameter", "path": "Bucket" },
+ { "target": "ObjectKey", "source": "requestParameter", "path": "Key" },
+ { "target": "MultipartUploadId", "source": "requestParameter", "path": "UploadId" },
+ { "target": "PartNumber", "source": "response", "path": "Parts[].PartNumber" }
+ ],
+ "path": "Parts[]"
+ }
+ }
+ }
+ },
+ "MultipartUploadPart": {
+ "identifiers": [
+ { "name": "BucketName" },
+ { "name": "ObjectKey" },
+ { "name": "MultipartUploadId" },
+ {
+ "name": "PartNumber",
+ "type": "integer",
+ "memberName": "PartNumber"
+ }
+ ],
+ "shape": "Part",
+ "actions": {
+ "CopyFrom": {
+ "request": {
+ "operation": "UploadPartCopy",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "UploadId", "source": "identifier", "name": "MultipartUploadId" },
+ { "target": "PartNumber", "source": "identifier", "name": "PartNumber" }
+ ]
+ }
+ },
+ "Upload": {
+ "request": {
+ "operation": "UploadPart",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "UploadId", "source": "identifier", "name": "MultipartUploadId" },
+ { "target": "PartNumber", "source": "identifier", "name": "PartNumber" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "MultipartUpload": {
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "ObjectKey" },
+ { "target": "Id", "source": "identifier", "name": "MultipartUploadId" }
+ ]
+ }
+ }
+ }
+ },
+ "Object": {
+ "identifiers": [
+ { "name": "BucketName" },
+ { "name": "Key" }
+ ],
+ "shape": "HeadObjectOutput",
+ "load": {
+ "request": {
+ "operation": "HeadObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "CopyFrom": {
+ "request": {
+ "operation": "CopyObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "Get": {
+ "request": {
+ "operation": "GetObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "InitiateMultipartUpload": {
+ "request": {
+ "operation": "CreateMultipartUpload",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ },
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" },
+ { "target": "Id", "source": "response", "path": "UploadId" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "RestoreObject": {
+ "request": {
+ "operation": "RestoreObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteObjects",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Delete.Objects[].Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "ObjectExists",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ },
+ "NotExists": {
+ "waiterName": "ObjectNotExists",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "has": {
+ "Acl": {
+ "resource": {
+ "type": "ObjectAcl",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "MultipartUpload": {
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Version": {
+ "resource": {
+ "type": "ObjectVersion",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ }
+ },
+ "ObjectAcl": {
+ "identifiers": [
+ { "name": "BucketName" },
+ { "name": "ObjectKey" }
+ ],
+ "shape": "GetObjectAclOutput",
+ "load": {
+ "request": {
+ "operation": "GetObjectAcl",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Put": {
+ "request": {
+ "operation": "PutObjectAcl",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Object": {
+ "resource": {
+ "type": "Object",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" }
+ ]
+ }
+ }
+ }
+ },
+ "ObjectSummary": {
+ "identifiers": [
+ { "name": "BucketName" },
+ { "name": "Key" }
+ ],
+ "shape": "Object",
+ "actions": {
+ "CopyFrom": {
+ "request": {
+ "operation": "CopyObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "Get": {
+ "request": {
+ "operation": "GetObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "InitiateMultipartUpload": {
+ "request": {
+ "operation": "CreateMultipartUpload",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ },
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" },
+ { "target": "Id", "source": "response", "path": "UploadId" }
+ ]
+ }
+ },
+ "Put": {
+ "request": {
+ "operation": "PutObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "RestoreObject": {
+ "request": {
+ "operation": "RestoreObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteObjects",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Delete.Objects[].Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ }
+ },
+ "waiters": {
+ "Exists": {
+ "waiterName": "ObjectExists",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ },
+ "NotExists": {
+ "waiterName": "ObjectNotExists",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "has": {
+ "Acl": {
+ "resource": {
+ "type": "ObjectAcl",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "Bucket": {
+ "resource": {
+ "type": "Bucket",
+ "identifiers": [
+ { "target": "Name", "source": "identifier", "name": "BucketName" }
+ ]
+ }
+ },
+ "MultipartUpload": {
+ "resource": {
+ "type": "MultipartUpload",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ },
+ "Object": {
+ "resource": {
+ "type": "Object",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "Key" }
+ ]
+ }
+ },
+ "Version": {
+ "resource": {
+ "type": "ObjectVersion",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "ObjectKey", "source": "identifier", "name": "Key" },
+ { "target": "Id", "source": "input" }
+ ]
+ }
+ }
+ }
+ },
+ "ObjectVersion": {
+ "identifiers": [
+ { "name": "BucketName" },
+ { "name": "ObjectKey" },
+ { "name": "Id" }
+ ],
+ "shape": "ObjectVersion",
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "VersionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Get": {
+ "request": {
+ "operation": "GetObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "VersionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ },
+ "Head": {
+ "request": {
+ "operation": "HeadObject",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "VersionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteObjects",
+ "params": [
+ { "target": "Bucket", "source": "identifier", "name": "BucketName" },
+ { "target": "Delete.Objects[*].Key", "source": "identifier", "name": "ObjectKey" },
+ { "target": "Delete.Objects[*].VersionId", "source": "identifier", "name": "Id" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Object": {
+ "resource": {
+ "type": "Object",
+ "identifiers": [
+ { "target": "BucketName", "source": "identifier", "name": "BucketName" },
+ { "target": "Key", "source": "identifier", "name": "ObjectKey" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/sns/2010-03-31/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/sns/2010-03-31/resources-1.json
new file mode 100644
index 0000000..cee300a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/sns/2010-03-31/resources-1.json
@@ -0,0 +1,327 @@
+{
+ "service": {
+ "actions": {
+ "CreatePlatformApplication": {
+ "request": { "operation": "CreatePlatformApplication" },
+ "resource": {
+ "type": "PlatformApplication",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "PlatformApplicationArn" }
+ ]
+ }
+ },
+ "CreateTopic": {
+ "request": { "operation": "CreateTopic" },
+ "resource": {
+ "type": "Topic",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "TopicArn" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "PlatformApplication": {
+ "resource": {
+ "type": "PlatformApplication",
+ "identifiers": [
+ { "target": "Arn", "source": "input" }
+ ]
+ }
+ },
+ "PlatformEndpoint": {
+ "resource": {
+ "type": "PlatformEndpoint",
+ "identifiers": [
+ { "target": "Arn", "source": "input" }
+ ]
+ }
+ },
+ "Subscription": {
+ "resource": {
+ "type": "Subscription",
+ "identifiers": [
+ { "target": "Arn", "source": "input" }
+ ]
+ }
+ },
+ "Topic": {
+ "resource": {
+ "type": "Topic",
+ "identifiers": [
+ { "target": "Arn", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "PlatformApplications": {
+ "request": { "operation": "ListPlatformApplications" },
+ "resource": {
+ "type": "PlatformApplication",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "PlatformApplications[].PlatformApplicationArn" }
+ ]
+ }
+ },
+ "Subscriptions": {
+ "request": { "operation": "ListSubscriptions" },
+ "resource": {
+ "type": "Subscription",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "Subscriptions[].SubscriptionArn" }
+ ]
+ }
+ },
+ "Topics": {
+ "request": { "operation": "ListTopics" },
+ "resource": {
+ "type": "Topic",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "Topics[].TopicArn" }
+ ]
+ }
+ }
+ }
+ },
+ "resources": {
+ "PlatformApplication": {
+ "identifiers": [
+ { "name": "Arn" }
+ ],
+ "shape": "GetPlatformApplicationAttributesResponse",
+ "load": {
+ "request": {
+ "operation": "GetPlatformApplicationAttributes",
+ "params": [
+ { "target": "PlatformApplicationArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "CreatePlatformEndpoint": {
+ "request": {
+ "operation": "CreatePlatformEndpoint",
+ "params": [
+ { "target": "PlatformApplicationArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "resource": {
+ "type": "PlatformEndpoint",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "EndpointArn" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeletePlatformApplication",
+ "params": [
+ { "target": "PlatformApplicationArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "SetAttributes": {
+ "request": {
+ "operation": "SetPlatformApplicationAttributes",
+ "params": [
+ { "target": "PlatformApplicationArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Endpoints": {
+ "request": {
+ "operation": "ListEndpointsByPlatformApplication",
+ "params": [
+ { "target": "PlatformApplicationArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "resource": {
+ "type": "PlatformEndpoint",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "Endpoints[].EndpointArn" }
+ ]
+ }
+ }
+ }
+ },
+ "PlatformEndpoint": {
+ "identifiers": [
+ { "name": "Arn" }
+ ],
+ "shape": "GetEndpointAttributesResponse",
+ "load": {
+ "request": {
+ "operation": "GetEndpointAttributes",
+ "params": [
+ { "target": "EndpointArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteEndpoint",
+ "params": [
+ { "target": "EndpointArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "Publish": {
+ "request": {
+ "operation": "Publish",
+ "params": [
+ { "target": "TargetArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "SetAttributes": {
+ "request": {
+ "operation": "SetEndpointAttributes",
+ "params": [
+ { "target": "EndpointArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ }
+ }
+ },
+ "Subscription": {
+ "identifiers": [
+ { "name": "Arn" }
+ ],
+ "shape": "GetSubscriptionAttributesResponse",
+ "load": {
+ "request": {
+ "operation": "GetSubscriptionAttributes",
+ "params": [
+ { "target": "SubscriptionArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "Delete": {
+ "request": {
+ "operation": "Unsubscribe",
+ "params": [
+ { "target": "SubscriptionArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "SetAttributes": {
+ "request": {
+ "operation": "SetSubscriptionAttributes",
+ "params": [
+ { "target": "SubscriptionArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ }
+ }
+ },
+ "Topic": {
+ "identifiers": [
+ { "name": "Arn" }
+ ],
+ "shape": "GetTopicAttributesResponse",
+ "load": {
+ "request": {
+ "operation": "GetTopicAttributes",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "AddPermission": {
+ "request": {
+ "operation": "AddPermission",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "ConfirmSubscription": {
+ "request": {
+ "operation": "ConfirmSubscription",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "resource": {
+ "type": "Subscription",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "SubscriptionArn" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteTopic",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "Publish": {
+ "request": {
+ "operation": "Publish",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "RemovePermission": {
+ "request": {
+ "operation": "RemovePermission",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "SetAttributes": {
+ "request": {
+ "operation": "SetTopicAttributes",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ }
+ },
+ "Subscribe": {
+ "request": {
+ "operation": "Subscribe",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "resource": {
+ "type": "Subscription",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "SubscriptionArn" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Subscriptions": {
+ "request": {
+ "operation": "ListSubscriptionsByTopic",
+ "params": [
+ { "target": "TopicArn", "source": "identifier", "name": "Arn" }
+ ]
+ },
+ "resource": {
+ "type": "Subscription",
+ "identifiers": [
+ { "target": "Arn", "source": "response", "path": "Subscriptions[].SubscriptionArn" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/data/sqs/2012-11-05/resources-1.json b/venv/lib/python3.12/site-packages/boto3/data/sqs/2012-11-05/resources-1.json
new file mode 100644
index 0000000..b1e74ab
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/data/sqs/2012-11-05/resources-1.json
@@ -0,0 +1,232 @@
+{
+ "service": {
+ "actions": {
+ "CreateQueue": {
+ "request": { "operation": "CreateQueue" },
+ "resource": {
+ "type": "Queue",
+ "identifiers": [
+ { "target": "Url", "source": "response", "path": "QueueUrl" }
+ ]
+ }
+ },
+ "GetQueueByName": {
+ "request": { "operation": "GetQueueUrl" },
+ "resource": {
+ "type": "Queue",
+ "identifiers": [
+ { "target": "Url", "source": "response", "path": "QueueUrl" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Queue": {
+ "resource": {
+ "type": "Queue",
+ "identifiers": [
+ { "target": "Url", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "Queues": {
+ "request": { "operation": "ListQueues" },
+ "resource": {
+ "type": "Queue",
+ "identifiers": [
+ { "target": "Url", "source": "response", "path": "QueueUrls[]" }
+ ]
+ }
+ }
+ }
+ },
+ "resources": {
+ "Message": {
+ "identifiers": [
+ { "name": "QueueUrl" },
+ {
+ "name": "ReceiptHandle",
+ "memberName": "ReceiptHandle"
+ }
+ ],
+ "shape": "Message",
+ "actions": {
+ "ChangeVisibility": {
+ "request": {
+ "operation": "ChangeMessageVisibility",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "QueueUrl" },
+ { "target": "ReceiptHandle", "source": "identifier", "name": "ReceiptHandle" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteMessage",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "QueueUrl" },
+ { "target": "ReceiptHandle", "source": "identifier", "name": "ReceiptHandle" }
+ ]
+ }
+ }
+ },
+ "batchActions": {
+ "Delete": {
+ "request": {
+ "operation": "DeleteMessageBatch",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "QueueUrl" },
+ { "target": "Entries[*].Id", "source": "data", "path": "MessageId" },
+ { "target": "Entries[*].ReceiptHandle", "source": "identifier", "name": "ReceiptHandle" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Queue": {
+ "resource": {
+ "type": "Queue",
+ "identifiers": [
+ { "target": "Url", "source": "identifier", "name": "QueueUrl" }
+ ]
+ }
+ }
+ }
+ },
+ "Queue": {
+ "identifiers": [
+ { "name": "Url" }
+ ],
+ "shape": "GetQueueAttributesResult",
+ "load": {
+ "request": {
+ "operation": "GetQueueAttributes",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" },
+ { "target": "AttributeNames[]", "source": "string", "value": "All" }
+ ]
+ },
+ "path": "@"
+ },
+ "actions": {
+ "AddPermission": {
+ "request": {
+ "operation": "AddPermission",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "ChangeMessageVisibilityBatch": {
+ "request": {
+ "operation": "ChangeMessageVisibilityBatch",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "Delete": {
+ "request": {
+ "operation": "DeleteQueue",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "DeleteMessages": {
+ "request": {
+ "operation": "DeleteMessageBatch",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "Purge": {
+ "request": {
+ "operation": "PurgeQueue",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "ReceiveMessages": {
+ "request": {
+ "operation": "ReceiveMessage",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ },
+ "resource": {
+ "type": "Message",
+ "identifiers": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" },
+ { "target": "ReceiptHandle", "source": "response", "path": "Messages[].ReceiptHandle" }
+ ],
+ "path": "Messages[]"
+ }
+ },
+ "RemovePermission": {
+ "request": {
+ "operation": "RemovePermission",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "SendMessage": {
+ "request": {
+ "operation": "SendMessage",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "SendMessages": {
+ "request": {
+ "operation": "SendMessageBatch",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ },
+ "SetAttributes": {
+ "request": {
+ "operation": "SetQueueAttributes",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ }
+ }
+ },
+ "has": {
+ "Message": {
+ "resource": {
+ "type": "Message",
+ "identifiers": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" },
+ { "target": "ReceiptHandle", "source": "input" }
+ ]
+ }
+ }
+ },
+ "hasMany": {
+ "DeadLetterSourceQueues": {
+ "request": {
+ "operation": "ListDeadLetterSourceQueues",
+ "params": [
+ { "target": "QueueUrl", "source": "identifier", "name": "Url" }
+ ]
+ },
+ "resource": {
+ "type": "Queue",
+ "identifiers": [
+ { "target": "Url", "source": "response", "path": "queueUrls[]" }
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__init__.py b/venv/lib/python3.12/site-packages/boto3/docs/__init__.py
new file mode 100644
index 0000000..ebd112c
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/__init__.py
@@ -0,0 +1,51 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import os
+
+from botocore.docs import DEPRECATED_SERVICE_NAMES
+
+from boto3.docs.service import ServiceDocumenter
+
+
+def generate_docs(root_dir, session):
+ """Generates the reference documentation for botocore
+
+ This will go through every available AWS service and output ReSTructured
+ text files documenting each service.
+
+ :param root_dir: The directory to write the reference files to. Each
+ service's reference documentation is loacated at
+ root_dir/reference/services/service-name.rst
+
+ :param session: The boto3 session
+ """
+ services_doc_path = os.path.join(root_dir, 'reference', 'services')
+ if not os.path.exists(services_doc_path):
+ os.makedirs(services_doc_path)
+
+ # Prevents deprecated service names from being generated in docs.
+ available_services = [
+ service
+ for service in session.get_available_services()
+ if service not in DEPRECATED_SERVICE_NAMES
+ ]
+
+ for service_name in available_services:
+ docs = ServiceDocumenter(
+ service_name, session, services_doc_path
+ ).document_service()
+ service_doc_path = os.path.join(
+ services_doc_path, service_name + '.rst'
+ )
+ with open(service_doc_path, 'wb') as f:
+ f.write(docs)
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..c149a82
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/action.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/action.cpython-312.pyc
new file mode 100644
index 0000000..35f533c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/action.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/attr.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/attr.cpython-312.pyc
new file mode 100644
index 0000000..47e070b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/attr.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/base.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..273fcea
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/base.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/client.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/client.cpython-312.pyc
new file mode 100644
index 0000000..9e38722
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/client.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/collection.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/collection.cpython-312.pyc
new file mode 100644
index 0000000..7fadc21
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/collection.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/docstring.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/docstring.cpython-312.pyc
new file mode 100644
index 0000000..2cbe541
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/docstring.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/method.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/method.cpython-312.pyc
new file mode 100644
index 0000000..054287a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/method.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/resource.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/resource.cpython-312.pyc
new file mode 100644
index 0000000..e89d12f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/resource.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/service.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/service.cpython-312.pyc
new file mode 100644
index 0000000..26d146a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/service.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/subresource.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/subresource.cpython-312.pyc
new file mode 100644
index 0000000..c3eba70
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/subresource.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/utils.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/utils.cpython-312.pyc
new file mode 100644
index 0000000..9d5be01
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/utils.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/waiter.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/waiter.cpython-312.pyc
new file mode 100644
index 0000000..be5129b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/docs/__pycache__/waiter.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/action.py b/venv/lib/python3.12/site-packages/boto3/docs/action.py
new file mode 100644
index 0000000..5215dca
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/action.py
@@ -0,0 +1,217 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import os
+
+from botocore import xform_name
+from botocore.docs.bcdoc.restdoc import DocumentStructure
+from botocore.docs.method import (
+ document_custom_method,
+ document_model_driven_method,
+)
+from botocore.model import OperationModel
+from botocore.utils import get_service_module_name
+
+from boto3.docs.base import NestedDocumenter
+from boto3.docs.method import document_model_driven_resource_method
+from boto3.docs.utils import (
+ add_resource_type_overview,
+ get_resource_ignore_params,
+ get_resource_public_actions,
+)
+
+PUT_DATA_WARNING_MESSAGE = """
+.. warning::
+ It is recommended to use the :py:meth:`put_metric_data`
+ :doc:`client method <../../cloudwatch/client/put_metric_data>`
+ instead. If you would still like to use this resource method,
+ please make sure that ``MetricData[].MetricName`` is equal to
+ the metric resource's ``name`` attribute.
+"""
+
+WARNING_MESSAGES = {
+ "Metric": {"put_data": PUT_DATA_WARNING_MESSAGE},
+}
+
+IGNORE_PARAMS = {"Metric": {"put_data": ["Namespace"]}}
+
+
+class ActionDocumenter(NestedDocumenter):
+ def document_actions(self, section):
+ modeled_actions_list = self._resource_model.actions
+ modeled_actions = {}
+ for modeled_action in modeled_actions_list:
+ modeled_actions[modeled_action.name] = modeled_action
+ resource_actions = get_resource_public_actions(
+ self._resource.__class__
+ )
+ self.member_map['actions'] = sorted(resource_actions)
+ add_resource_type_overview(
+ section=section,
+ resource_type='Actions',
+ description=(
+ 'Actions call operations on resources. They may '
+ 'automatically handle the passing in of arguments set '
+ 'from identifiers and some attributes.'
+ ),
+ intro_link='actions_intro',
+ )
+ resource_warnings = WARNING_MESSAGES.get(self._resource_name, {})
+ for action_name in sorted(resource_actions):
+ # Create a new DocumentStructure for each action and add contents.
+ action_doc = DocumentStructure(action_name, target='html')
+ breadcrumb_section = action_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(self._resource_class_name, 'index')
+ breadcrumb_section.write(f' / Action / {action_name}')
+ action_doc.add_title_section(action_name)
+ warning_message = resource_warnings.get(action_name)
+ if warning_message is not None:
+ action_doc.add_new_section("warning").write(warning_message)
+ action_section = action_doc.add_new_section(
+ action_name,
+ context={'qualifier': f'{self.class_name}.'},
+ )
+ if action_name in ['load', 'reload'] and self._resource_model.load:
+ document_load_reload_action(
+ section=action_section,
+ action_name=action_name,
+ resource_name=self._resource_name,
+ event_emitter=self._resource.meta.client.meta.events,
+ load_model=self._resource_model.load,
+ service_model=self._service_model,
+ )
+ elif action_name in modeled_actions:
+ document_action(
+ section=action_section,
+ resource_name=self._resource_name,
+ event_emitter=self._resource.meta.client.meta.events,
+ action_model=modeled_actions[action_name],
+ service_model=self._service_model,
+ )
+ else:
+ document_custom_method(
+ action_section, action_name, resource_actions[action_name]
+ )
+ # Write actions in individual/nested files.
+ # Path: /reference/services///.rst
+ actions_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{self._resource_sub_path}',
+ )
+ action_doc.write_to_file(actions_dir_path, action_name)
+
+
+def document_action(
+ section,
+ resource_name,
+ event_emitter,
+ action_model,
+ service_model,
+ include_signature=True,
+):
+ """Documents a resource action
+
+ :param section: The section to write to
+
+ :param resource_name: The name of the resource
+
+ :param event_emitter: The event emitter to use to emit events
+
+ :param action_model: The model of the action
+
+ :param service_model: The model of the service
+
+ :param include_signature: Whether or not to include the signature.
+ It is useful for generating docstrings.
+ """
+ operation_model = service_model.operation_model(
+ action_model.request.operation
+ )
+ ignore_params = IGNORE_PARAMS.get(resource_name, {}).get(
+ action_model.name,
+ get_resource_ignore_params(action_model.request.params),
+ )
+ example_return_value = 'response'
+ if action_model.resource:
+ example_return_value = xform_name(action_model.resource.type)
+ example_resource_name = xform_name(resource_name)
+ if service_model.service_name == resource_name:
+ example_resource_name = resource_name
+ example_prefix = '{} = {}.{}'.format(
+ example_return_value, example_resource_name, action_model.name
+ )
+ full_action_name = (
+ f"{section.context.get('qualifier', '')}{action_model.name}"
+ )
+ document_model_driven_resource_method(
+ section=section,
+ method_name=full_action_name,
+ operation_model=operation_model,
+ event_emitter=event_emitter,
+ method_description=operation_model.documentation,
+ example_prefix=example_prefix,
+ exclude_input=ignore_params,
+ resource_action_model=action_model,
+ include_signature=include_signature,
+ )
+
+
+def document_load_reload_action(
+ section,
+ action_name,
+ resource_name,
+ event_emitter,
+ load_model,
+ service_model,
+ include_signature=True,
+):
+ """Documents the resource load action
+
+ :param section: The section to write to
+
+ :param action_name: The name of the loading action should be load or reload
+
+ :param resource_name: The name of the resource
+
+ :param event_emitter: The event emitter to use to emit events
+
+ :param load_model: The model of the load action
+
+ :param service_model: The model of the service
+
+ :param include_signature: Whether or not to include the signature.
+ It is useful for generating docstrings.
+ """
+ description = (
+ 'Calls :py:meth:`{}.Client.{}` to update the attributes of the '
+ '{} resource. Note that the load and reload methods are '
+ 'the same method and can be used interchangeably.'.format(
+ get_service_module_name(service_model),
+ xform_name(load_model.request.operation),
+ resource_name,
+ )
+ )
+ example_resource_name = xform_name(resource_name)
+ if service_model.service_name == resource_name:
+ example_resource_name = resource_name
+ example_prefix = f'{example_resource_name}.{action_name}'
+ full_action_name = f"{section.context.get('qualifier', '')}{action_name}"
+ document_model_driven_method(
+ section=section,
+ method_name=full_action_name,
+ operation_model=OperationModel({}, service_model),
+ event_emitter=event_emitter,
+ method_description=description,
+ example_prefix=example_prefix,
+ include_signature=include_signature,
+ )
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/attr.py b/venv/lib/python3.12/site-packages/boto3/docs/attr.py
new file mode 100644
index 0000000..a968da2
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/attr.py
@@ -0,0 +1,72 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from botocore.docs.params import ResponseParamsDocumenter
+
+from boto3.docs.utils import get_identifier_description
+
+
+class ResourceShapeDocumenter(ResponseParamsDocumenter):
+ EVENT_NAME = 'resource-shape'
+
+
+def document_attribute(
+ section,
+ service_name,
+ resource_name,
+ attr_name,
+ event_emitter,
+ attr_model,
+ include_signature=True,
+):
+ if include_signature:
+ full_attr_name = f"{section.context.get('qualifier', '')}{attr_name}"
+ section.style.start_sphinx_py_attr(full_attr_name)
+ # Note that an attribute may have one, may have many, or may have no
+ # operations that back the resource's shape. So we just set the
+ # operation_name to the resource name if we ever to hook in and modify
+ # a particular attribute.
+ ResourceShapeDocumenter(
+ service_name=service_name,
+ operation_name=resource_name,
+ event_emitter=event_emitter,
+ ).document_params(section=section, shape=attr_model)
+
+
+def document_identifier(
+ section,
+ resource_name,
+ identifier_model,
+ include_signature=True,
+):
+ if include_signature:
+ full_identifier_name = (
+ f"{section.context.get('qualifier', '')}{identifier_model.name}"
+ )
+ section.style.start_sphinx_py_attr(full_identifier_name)
+ description = get_identifier_description(
+ resource_name, identifier_model.name
+ )
+ section.write(f'*(string)* {description}')
+
+
+def document_reference(section, reference_model, include_signature=True):
+ if include_signature:
+ full_reference_name = (
+ f"{section.context.get('qualifier', '')}{reference_model.name}"
+ )
+ section.style.start_sphinx_py_attr(full_reference_name)
+ reference_type = f'(:py:class:`{reference_model.resource.type}`) '
+ section.write(reference_type)
+ section.include_doc_string(
+ f'The related {reference_model.name} if set, otherwise ``None``.'
+ )
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/base.py b/venv/lib/python3.12/site-packages/boto3/docs/base.py
new file mode 100644
index 0000000..ee49646
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/base.py
@@ -0,0 +1,51 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from botocore.compat import OrderedDict
+
+
+class BaseDocumenter:
+ def __init__(self, resource):
+ self._resource = resource
+ self._client = self._resource.meta.client
+ self._resource_model = self._resource.meta.resource_model
+ self._service_model = self._client.meta.service_model
+ self._resource_name = self._resource.meta.resource_model.name
+ self._service_name = self._service_model.service_name
+ self._service_docs_name = self._client.__class__.__name__
+ self.member_map = OrderedDict()
+ self.represents_service_resource = (
+ self._service_name == self._resource_name
+ )
+ self._resource_class_name = self._resource_name
+ if self._resource_name == self._service_name:
+ self._resource_class_name = 'ServiceResource'
+
+ @property
+ def class_name(self):
+ return f'{self._service_docs_name}.{self._resource_name}'
+
+
+class NestedDocumenter(BaseDocumenter):
+ def __init__(self, resource, root_docs_path):
+ super().__init__(resource)
+ self._root_docs_path = root_docs_path
+ self._resource_sub_path = self._resource_name.lower()
+ if self._resource_name == self._service_name:
+ self._resource_sub_path = 'service-resource'
+
+ @property
+ def class_name(self):
+ resource_class_name = self._resource_name
+ if self._resource_name == self._service_name:
+ resource_class_name = 'ServiceResource'
+ return f'{self._service_docs_name}.{resource_class_name}'
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/client.py b/venv/lib/python3.12/site-packages/boto3/docs/client.py
new file mode 100644
index 0000000..51e92e3
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/client.py
@@ -0,0 +1,28 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from botocore.docs.client import ClientDocumenter
+
+
+class Boto3ClientDocumenter(ClientDocumenter):
+ def _add_client_creation_example(self, section):
+ section.style.start_codeblock()
+ section.style.new_line()
+ section.write('import boto3')
+ section.style.new_line()
+ section.style.new_line()
+ section.write(
+ 'client = boto3.client(\'{service}\')'.format(
+ service=self._service_name
+ )
+ )
+ section.style.end_codeblock()
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/collection.py b/venv/lib/python3.12/site-packages/boto3/docs/collection.py
new file mode 100644
index 0000000..ea65e87
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/collection.py
@@ -0,0 +1,312 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import os
+
+from botocore import xform_name
+from botocore.docs.bcdoc.restdoc import DocumentStructure
+from botocore.docs.method import get_instance_public_methods
+from botocore.docs.utils import DocumentedShape
+
+from boto3.docs.base import NestedDocumenter
+from boto3.docs.method import document_model_driven_resource_method
+from boto3.docs.utils import (
+ add_resource_type_overview,
+ get_resource_ignore_params,
+)
+
+
+class CollectionDocumenter(NestedDocumenter):
+ def document_collections(self, section):
+ collections = self._resource.meta.resource_model.collections
+ collections_list = []
+ add_resource_type_overview(
+ section=section,
+ resource_type='Collections',
+ description=(
+ 'Collections provide an interface to iterate over and '
+ 'manipulate groups of resources. '
+ ),
+ intro_link='guide_collections',
+ )
+ self.member_map['collections'] = collections_list
+ for collection in collections:
+ collections_list.append(collection.name)
+ # Create a new DocumentStructure for each collection and add contents.
+ collection_doc = DocumentStructure(collection.name, target='html')
+ breadcrumb_section = collection_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(self._resource_class_name, 'index')
+ breadcrumb_section.write(f' / Collection / {collection.name}')
+ collection_doc.add_title_section(collection.name)
+ collection_section = collection_doc.add_new_section(
+ collection.name,
+ context={'qualifier': f'{self.class_name}.'},
+ )
+ self._document_collection(collection_section, collection)
+
+ # Write collections in individual/nested files.
+ # Path: /reference/services///.rst
+ collections_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{self._resource_sub_path}',
+ )
+ collection_doc.write_to_file(collections_dir_path, collection.name)
+
+ def _document_collection(self, section, collection):
+ methods = get_instance_public_methods(
+ getattr(self._resource, collection.name)
+ )
+ document_collection_object(section, collection)
+ batch_actions = {}
+ for batch_action in collection.batch_actions:
+ batch_actions[batch_action.name] = batch_action
+
+ for method in sorted(methods):
+ method_section = section.add_new_section(method)
+ if method in batch_actions:
+ document_batch_action(
+ section=method_section,
+ resource_name=self._resource_name,
+ event_emitter=self._resource.meta.client.meta.events,
+ batch_action_model=batch_actions[method],
+ collection_model=collection,
+ service_model=self._resource.meta.client.meta.service_model,
+ )
+ else:
+ document_collection_method(
+ section=method_section,
+ resource_name=self._resource_name,
+ action_name=method,
+ event_emitter=self._resource.meta.client.meta.events,
+ collection_model=collection,
+ service_model=self._resource.meta.client.meta.service_model,
+ )
+
+
+def document_collection_object(
+ section,
+ collection_model,
+ include_signature=True,
+):
+ """Documents a collection resource object
+
+ :param section: The section to write to
+
+ :param collection_model: The model of the collection
+
+ :param include_signature: Whether or not to include the signature.
+ It is useful for generating docstrings.
+ """
+ if include_signature:
+ full_collection_name = (
+ f"{section.context.get('qualifier', '')}{collection_model.name}"
+ )
+ section.style.start_sphinx_py_attr(full_collection_name)
+ section.include_doc_string(
+ f'A collection of {collection_model.resource.type} resources.'
+ )
+ section.include_doc_string(
+ f'A {collection_model.resource.type} Collection will include all '
+ f'resources by default, and extreme caution should be taken when '
+ f'performing actions on all resources.'
+ )
+
+
+def document_batch_action(
+ section,
+ resource_name,
+ event_emitter,
+ batch_action_model,
+ service_model,
+ collection_model,
+ include_signature=True,
+):
+ """Documents a collection's batch action
+
+ :param section: The section to write to
+
+ :param resource_name: The name of the resource
+
+ :param action_name: The name of collection action. Currently only
+ can be all, filter, limit, or page_size
+
+ :param event_emitter: The event emitter to use to emit events
+
+ :param batch_action_model: The model of the batch action
+
+ :param collection_model: The model of the collection
+
+ :param service_model: The model of the service
+
+ :param include_signature: Whether or not to include the signature.
+ It is useful for generating docstrings.
+ """
+ operation_model = service_model.operation_model(
+ batch_action_model.request.operation
+ )
+ ignore_params = get_resource_ignore_params(
+ batch_action_model.request.params
+ )
+
+ example_return_value = 'response'
+ if batch_action_model.resource:
+ example_return_value = xform_name(batch_action_model.resource.type)
+
+ example_resource_name = xform_name(resource_name)
+ if service_model.service_name == resource_name:
+ example_resource_name = resource_name
+ example_prefix = '{} = {}.{}.{}'.format(
+ example_return_value,
+ example_resource_name,
+ collection_model.name,
+ batch_action_model.name,
+ )
+ document_model_driven_resource_method(
+ section=section,
+ method_name=batch_action_model.name,
+ operation_model=operation_model,
+ event_emitter=event_emitter,
+ method_description=operation_model.documentation,
+ example_prefix=example_prefix,
+ exclude_input=ignore_params,
+ resource_action_model=batch_action_model,
+ include_signature=include_signature,
+ )
+
+
+def document_collection_method(
+ section,
+ resource_name,
+ action_name,
+ event_emitter,
+ collection_model,
+ service_model,
+ include_signature=True,
+):
+ """Documents a collection method
+
+ :param section: The section to write to
+
+ :param resource_name: The name of the resource
+
+ :param action_name: The name of collection action. Currently only
+ can be all, filter, limit, or page_size
+
+ :param event_emitter: The event emitter to use to emit events
+
+ :param collection_model: The model of the collection
+
+ :param service_model: The model of the service
+
+ :param include_signature: Whether or not to include the signature.
+ It is useful for generating docstrings.
+ """
+ operation_model = service_model.operation_model(
+ collection_model.request.operation
+ )
+
+ underlying_operation_members = []
+ if operation_model.input_shape:
+ underlying_operation_members = operation_model.input_shape.members
+
+ example_resource_name = xform_name(resource_name)
+ if service_model.service_name == resource_name:
+ example_resource_name = resource_name
+
+ custom_action_info_dict = {
+ 'all': {
+ 'method_description': (
+ f'Creates an iterable of all {collection_model.resource.type} '
+ f'resources in the collection.'
+ ),
+ 'example_prefix': '{}_iterator = {}.{}.all'.format(
+ xform_name(collection_model.resource.type),
+ example_resource_name,
+ collection_model.name,
+ ),
+ 'exclude_input': underlying_operation_members,
+ },
+ 'filter': {
+ 'method_description': (
+ f'Creates an iterable of all {collection_model.resource.type} '
+ f'resources in the collection filtered by kwargs passed to '
+ f'method. A {collection_model.resource.type} collection will '
+ f'include all resources by default if no filters are provided, '
+ f'and extreme caution should be taken when performing actions '
+ f'on all resources.'
+ ),
+ 'example_prefix': '{}_iterator = {}.{}.filter'.format(
+ xform_name(collection_model.resource.type),
+ example_resource_name,
+ collection_model.name,
+ ),
+ 'exclude_input': get_resource_ignore_params(
+ collection_model.request.params
+ ),
+ },
+ 'limit': {
+ 'method_description': (
+ f'Creates an iterable up to a specified amount of '
+ f'{collection_model.resource.type} resources in the collection.'
+ ),
+ 'example_prefix': '{}_iterator = {}.{}.limit'.format(
+ xform_name(collection_model.resource.type),
+ example_resource_name,
+ collection_model.name,
+ ),
+ 'include_input': [
+ DocumentedShape(
+ name='count',
+ type_name='integer',
+ documentation=(
+ 'The limit to the number of resources '
+ 'in the iterable.'
+ ),
+ )
+ ],
+ 'exclude_input': underlying_operation_members,
+ },
+ 'page_size': {
+ 'method_description': (
+ f'Creates an iterable of all {collection_model.resource.type} '
+ f'resources in the collection, but limits the number of '
+ f'items returned by each service call by the specified amount.'
+ ),
+ 'example_prefix': '{}_iterator = {}.{}.page_size'.format(
+ xform_name(collection_model.resource.type),
+ example_resource_name,
+ collection_model.name,
+ ),
+ 'include_input': [
+ DocumentedShape(
+ name='count',
+ type_name='integer',
+ documentation=(
+ 'The number of items returned by each ' 'service call'
+ ),
+ )
+ ],
+ 'exclude_input': underlying_operation_members,
+ },
+ }
+ if action_name in custom_action_info_dict:
+ action_info = custom_action_info_dict[action_name]
+ document_model_driven_resource_method(
+ section=section,
+ method_name=action_name,
+ operation_model=operation_model,
+ event_emitter=event_emitter,
+ resource_action_model=collection_model,
+ include_signature=include_signature,
+ **action_info,
+ )
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/docstring.py b/venv/lib/python3.12/site-packages/boto3/docs/docstring.py
new file mode 100644
index 0000000..daf6787
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/docstring.py
@@ -0,0 +1,77 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from botocore.docs.docstring import LazyLoadedDocstring
+
+from boto3.docs.action import document_action, document_load_reload_action
+from boto3.docs.attr import (
+ document_attribute,
+ document_identifier,
+ document_reference,
+)
+from boto3.docs.collection import (
+ document_batch_action,
+ document_collection_method,
+ document_collection_object,
+)
+from boto3.docs.subresource import document_sub_resource
+from boto3.docs.waiter import document_resource_waiter
+
+
+class ActionDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_action(*args, **kwargs)
+
+
+class LoadReloadDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_load_reload_action(*args, **kwargs)
+
+
+class SubResourceDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_sub_resource(*args, **kwargs)
+
+
+class AttributeDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_attribute(*args, **kwargs)
+
+
+class IdentifierDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_identifier(*args, **kwargs)
+
+
+class ReferenceDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_reference(*args, **kwargs)
+
+
+class CollectionDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_collection_object(*args, **kwargs)
+
+
+class CollectionMethodDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_collection_method(*args, **kwargs)
+
+
+class BatchActionDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_batch_action(*args, **kwargs)
+
+
+class ResourceWaiterDocstring(LazyLoadedDocstring):
+ def _write_docstring(self, *args, **kwargs):
+ document_resource_waiter(*args, **kwargs)
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/method.py b/venv/lib/python3.12/site-packages/boto3/docs/method.py
new file mode 100644
index 0000000..8613367
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/method.py
@@ -0,0 +1,77 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from botocore.docs.method import document_model_driven_method
+
+
+def document_model_driven_resource_method(
+ section,
+ method_name,
+ operation_model,
+ event_emitter,
+ method_description=None,
+ example_prefix=None,
+ include_input=None,
+ include_output=None,
+ exclude_input=None,
+ exclude_output=None,
+ document_output=True,
+ resource_action_model=None,
+ include_signature=True,
+):
+ document_model_driven_method(
+ section=section,
+ method_name=method_name,
+ operation_model=operation_model,
+ event_emitter=event_emitter,
+ method_description=method_description,
+ example_prefix=example_prefix,
+ include_input=include_input,
+ include_output=include_output,
+ exclude_input=exclude_input,
+ exclude_output=exclude_output,
+ document_output=document_output,
+ include_signature=include_signature,
+ )
+
+ # If this action returns a resource modify the return example to
+ # appropriately reflect that.
+ if resource_action_model.resource:
+ if 'return' in section.available_sections:
+ section.delete_section('return')
+ resource_type = resource_action_model.resource.type
+
+ new_return_section = section.add_new_section('return')
+ return_resource_type = '{}.{}'.format(
+ operation_model.service_model.service_name, resource_type
+ )
+
+ return_type = f':py:class:`{return_resource_type}`'
+ return_description = f'{resource_type} resource'
+
+ if _method_returns_resource_list(resource_action_model.resource):
+ return_type = f'list({return_type})'
+ return_description = f'A list of {resource_type} resources'
+
+ new_return_section.style.new_line()
+ new_return_section.write(f':rtype: {return_type}')
+ new_return_section.style.new_line()
+ new_return_section.write(f':returns: {return_description}')
+ new_return_section.style.new_line()
+
+
+def _method_returns_resource_list(resource):
+ for identifier in resource.identifiers:
+ if identifier.path and '[]' in identifier.path:
+ return True
+
+ return False
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/resource.py b/venv/lib/python3.12/site-packages/boto3/docs/resource.py
new file mode 100644
index 0000000..d4dff1d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/resource.py
@@ -0,0 +1,364 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import os
+
+from botocore import xform_name
+from botocore.docs.bcdoc.restdoc import DocumentStructure
+from botocore.docs.utils import get_official_service_name
+
+from boto3.docs.action import ActionDocumenter
+from boto3.docs.attr import (
+ document_attribute,
+ document_identifier,
+ document_reference,
+)
+from boto3.docs.base import BaseDocumenter
+from boto3.docs.collection import CollectionDocumenter
+from boto3.docs.subresource import SubResourceDocumenter
+from boto3.docs.utils import (
+ add_resource_type_overview,
+ get_identifier_args_for_signature,
+ get_identifier_description,
+ get_identifier_values_for_example,
+)
+from boto3.docs.waiter import WaiterResourceDocumenter
+
+
+class ResourceDocumenter(BaseDocumenter):
+ def __init__(self, resource, botocore_session, root_docs_path):
+ super().__init__(resource)
+ self._botocore_session = botocore_session
+ self._root_docs_path = root_docs_path
+ self._resource_sub_path = self._resource_name.lower()
+ if self._resource_name == self._service_name:
+ self._resource_sub_path = 'service-resource'
+
+ def document_resource(self, section):
+ self._add_title(section)
+ self._add_resource_note(section)
+ self._add_intro(section)
+ self._add_identifiers(section)
+ self._add_attributes(section)
+ self._add_references(section)
+ self._add_actions(section)
+ self._add_sub_resources(section)
+ self._add_collections(section)
+ self._add_waiters(section)
+
+ def _add_title(self, section):
+ title_section = section.add_new_section('title')
+ title_section.style.h2(self._resource_name)
+
+ def _add_intro(self, section):
+ identifier_names = []
+ if self._resource_model.identifiers:
+ for identifier in self._resource_model.identifiers:
+ identifier_names.append(identifier.name)
+
+ # Write out the class signature.
+ class_args = get_identifier_args_for_signature(identifier_names)
+ start_class = section.add_new_section('start_class')
+ start_class.style.start_sphinx_py_class(
+ class_name=f'{self.class_name}({class_args})'
+ )
+
+ # Add as short description about the resource
+ description_section = start_class.add_new_section('description')
+ self._add_description(description_section)
+
+ # Add an example of how to instantiate the resource
+ example_section = start_class.add_new_section('example')
+ self._add_example(example_section, identifier_names)
+
+ # Add the description for the parameters to instantiate the
+ # resource.
+ param_section = start_class.add_new_section('params')
+ self._add_params_description(param_section, identifier_names)
+
+ end_class = section.add_new_section('end_class')
+ end_class.style.end_sphinx_py_class()
+
+ def _add_description(self, section):
+ official_service_name = get_official_service_name(self._service_model)
+ section.write(
+ 'A resource representing an {} {}'.format(
+ official_service_name, self._resource_name
+ )
+ )
+
+ def _add_example(self, section, identifier_names):
+ section.style.start_codeblock()
+ section.style.new_line()
+ section.write('import boto3')
+ section.style.new_line()
+ section.style.new_line()
+ section.write(
+ '{} = boto3.resource(\'{}\')'.format(
+ self._service_name, self._service_name
+ )
+ )
+ section.style.new_line()
+ example_values = get_identifier_values_for_example(identifier_names)
+ section.write(
+ '{} = {}.{}({})'.format(
+ xform_name(self._resource_name),
+ self._service_name,
+ self._resource_name,
+ example_values,
+ )
+ )
+ section.style.end_codeblock()
+
+ def _add_params_description(self, section, identifier_names):
+ for identifier_name in identifier_names:
+ description = get_identifier_description(
+ self._resource_name, identifier_name
+ )
+ section.write(f':type {identifier_name}: string')
+ section.style.new_line()
+ section.write(f':param {identifier_name}: {description}')
+ section.style.new_line()
+
+ def _add_overview_of_member_type(self, section, resource_member_type):
+ section.style.new_line()
+ section.write(
+ f'These are the resource\'s available {resource_member_type}:'
+ )
+ section.style.new_line()
+ section.style.toctree()
+ for member in self.member_map[resource_member_type]:
+ section.style.tocitem(f'{member}')
+
+ def _add_identifiers(self, section):
+ identifiers = self._resource.meta.resource_model.identifiers
+ section = section.add_new_section('identifiers')
+ member_list = []
+ if identifiers:
+ self.member_map['identifiers'] = member_list
+ add_resource_type_overview(
+ section=section,
+ resource_type='Identifiers',
+ description=(
+ 'Identifiers are properties of a resource that are '
+ 'set upon instantiation of the resource.'
+ ),
+ intro_link='identifiers_attributes_intro',
+ )
+ for identifier in identifiers:
+ member_list.append(identifier.name)
+ # Create a new DocumentStructure for each identifier and add contents.
+ identifier_doc = DocumentStructure(identifier.name, target='html')
+ breadcrumb_section = identifier_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(self._resource_class_name, 'index')
+ breadcrumb_section.write(f' / Identifier / {identifier.name}')
+ identifier_doc.add_title_section(identifier.name)
+ identifier_section = identifier_doc.add_new_section(
+ identifier.name,
+ context={'qualifier': f'{self.class_name}.'},
+ )
+ document_identifier(
+ section=identifier_section,
+ resource_name=self._resource_name,
+ identifier_model=identifier,
+ )
+ # Write identifiers in individual/nested files.
+ # Path: /reference/services///.rst
+ identifiers_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{self._resource_sub_path}',
+ )
+ identifier_doc.write_to_file(identifiers_dir_path, identifier.name)
+
+ if identifiers:
+ self._add_overview_of_member_type(section, 'identifiers')
+
+ def _add_attributes(self, section):
+ service_model = self._resource.meta.client.meta.service_model
+ attributes = {}
+ if self._resource.meta.resource_model.shape:
+ shape = service_model.shape_for(
+ self._resource.meta.resource_model.shape
+ )
+ attributes = self._resource.meta.resource_model.get_attributes(
+ shape
+ )
+ section = section.add_new_section('attributes')
+ attribute_list = []
+ if attributes:
+ add_resource_type_overview(
+ section=section,
+ resource_type='Attributes',
+ description=(
+ 'Attributes provide access'
+ ' to the properties of a resource. Attributes are lazy-'
+ 'loaded the first time one is accessed via the'
+ ' :py:meth:`load` method.'
+ ),
+ intro_link='identifiers_attributes_intro',
+ )
+ self.member_map['attributes'] = attribute_list
+ for attr_name in sorted(attributes):
+ _, attr_shape = attributes[attr_name]
+ attribute_list.append(attr_name)
+ # Create a new DocumentStructure for each attribute and add contents.
+ attribute_doc = DocumentStructure(attr_name, target='html')
+ breadcrumb_section = attribute_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(self._resource_class_name, 'index')
+ breadcrumb_section.write(f' / Attribute / {attr_name}')
+ attribute_doc.add_title_section(attr_name)
+ attribute_section = attribute_doc.add_new_section(
+ attr_name,
+ context={'qualifier': f'{self.class_name}.'},
+ )
+ document_attribute(
+ section=attribute_section,
+ service_name=self._service_name,
+ resource_name=self._resource_name,
+ attr_name=attr_name,
+ event_emitter=self._resource.meta.client.meta.events,
+ attr_model=attr_shape,
+ )
+ # Write attributes in individual/nested files.
+ # Path: /reference/services///.rst
+ attributes_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{self._resource_sub_path}',
+ )
+ attribute_doc.write_to_file(attributes_dir_path, attr_name)
+ if attributes:
+ self._add_overview_of_member_type(section, 'attributes')
+
+ def _add_references(self, section):
+ section = section.add_new_section('references')
+ references = self._resource.meta.resource_model.references
+ reference_list = []
+ if references:
+ add_resource_type_overview(
+ section=section,
+ resource_type='References',
+ description=(
+ 'References are related resource instances that have '
+ 'a belongs-to relationship.'
+ ),
+ intro_link='references_intro',
+ )
+ self.member_map['references'] = reference_list
+ self._add_overview_of_member_type(section, 'references')
+ for reference in references:
+ reference_list.append(reference.name)
+ # Create a new DocumentStructure for each reference and add contents.
+ reference_doc = DocumentStructure(reference.name, target='html')
+ breadcrumb_section = reference_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(self._resource_class_name, 'index')
+ breadcrumb_section.write(f' / Reference / {reference.name}')
+ reference_doc.add_title_section(reference.name)
+ reference_section = reference_doc.add_new_section(
+ reference.name,
+ context={'qualifier': f'{self.class_name}.'},
+ )
+ document_reference(
+ section=reference_section,
+ reference_model=reference,
+ )
+ # Write references in individual/nested files.
+ # Path: /reference/services///.rst
+ references_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{self._resource_sub_path}',
+ )
+ reference_doc.write_to_file(references_dir_path, reference.name)
+ if references:
+ self._add_overview_of_member_type(section, 'references')
+
+ def _add_actions(self, section):
+ section = section.add_new_section('actions')
+ actions = self._resource.meta.resource_model.actions
+ if actions:
+ documenter = ActionDocumenter(self._resource, self._root_docs_path)
+ documenter.member_map = self.member_map
+ documenter.document_actions(section)
+ self._add_overview_of_member_type(section, 'actions')
+
+ def _add_sub_resources(self, section):
+ section = section.add_new_section('sub-resources')
+ sub_resources = self._resource.meta.resource_model.subresources
+ if sub_resources:
+ documenter = SubResourceDocumenter(
+ self._resource, self._root_docs_path
+ )
+ documenter.member_map = self.member_map
+ documenter.document_sub_resources(section)
+ self._add_overview_of_member_type(section, 'sub-resources')
+
+ def _add_collections(self, section):
+ section = section.add_new_section('collections')
+ collections = self._resource.meta.resource_model.collections
+ if collections:
+ documenter = CollectionDocumenter(
+ self._resource, self._root_docs_path
+ )
+ documenter.member_map = self.member_map
+ documenter.document_collections(section)
+ self._add_overview_of_member_type(section, 'collections')
+
+ def _add_waiters(self, section):
+ section = section.add_new_section('waiters')
+ waiters = self._resource.meta.resource_model.waiters
+ if waiters:
+ service_waiter_model = self._botocore_session.get_waiter_model(
+ self._service_name
+ )
+ documenter = WaiterResourceDocumenter(
+ self._resource, service_waiter_model, self._root_docs_path
+ )
+ documenter.member_map = self.member_map
+ documenter.document_resource_waiters(section)
+ self._add_overview_of_member_type(section, 'waiters')
+
+ def _add_resource_note(self, section):
+ section = section.add_new_section('feature-freeze')
+ section.style.start_note()
+ section.write(
+ "Before using anything on this page, please refer to the resources "
+ ":doc:`user guide <../../../../guide/resources>` for the most recent "
+ "guidance on using resources."
+ )
+ section.style.end_note()
+
+
+class ServiceResourceDocumenter(ResourceDocumenter):
+ @property
+ def class_name(self):
+ return f'{self._service_docs_name}.ServiceResource'
+
+ def _add_title(self, section):
+ title_section = section.add_new_section('title')
+ title_section.style.h2('Service Resource')
+
+ def _add_description(self, section):
+ official_service_name = get_official_service_name(self._service_model)
+ section.write(f'A resource representing {official_service_name}')
+
+ def _add_example(self, section, identifier_names):
+ section.style.start_codeblock()
+ section.style.new_line()
+ section.write('import boto3')
+ section.style.new_line()
+ section.style.new_line()
+ section.write(
+ f'{self._service_name} = boto3.resource(\'{self._service_name}\')'
+ )
+ section.style.end_codeblock()
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/service.py b/venv/lib/python3.12/site-packages/boto3/docs/service.py
new file mode 100644
index 0000000..39ed89b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/service.py
@@ -0,0 +1,202 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import os
+
+from botocore.docs.bcdoc.restdoc import DocumentStructure
+from botocore.docs.service import ServiceDocumenter as BaseServiceDocumenter
+from botocore.exceptions import DataNotFoundError
+
+import boto3
+from boto3.docs.client import Boto3ClientDocumenter
+from boto3.docs.resource import ResourceDocumenter, ServiceResourceDocumenter
+from boto3.utils import ServiceContext
+
+
+class ServiceDocumenter(BaseServiceDocumenter):
+ # The path used to find examples
+ EXAMPLE_PATH = os.path.join(os.path.dirname(boto3.__file__), 'examples')
+
+ def __init__(self, service_name, session, root_docs_path):
+ super().__init__(
+ service_name=service_name,
+ # I know that this is an internal attribute, but the botocore session
+ # is needed to load the paginator and waiter models.
+ session=session._session,
+ root_docs_path=root_docs_path,
+ )
+ self._boto3_session = session
+ self._client = self._boto3_session.client(service_name)
+ self._service_resource = None
+ if self._service_name in self._boto3_session.get_available_resources():
+ self._service_resource = self._boto3_session.resource(service_name)
+ self.sections = [
+ 'title',
+ 'client',
+ 'paginators',
+ 'waiters',
+ 'resources',
+ 'examples',
+ 'context-params',
+ ]
+ self._root_docs_path = root_docs_path
+ self._USER_GUIDE_LINK = (
+ 'https://boto3.amazonaws.com/'
+ 'v1/documentation/api/latest/guide/resources.html'
+ )
+
+ def document_service(self):
+ """Documents an entire service.
+
+ :returns: The reStructured text of the documented service.
+ """
+ doc_structure = DocumentStructure(
+ self._service_name, section_names=self.sections, target='html'
+ )
+ self.title(doc_structure.get_section('title'))
+
+ self.client_api(doc_structure.get_section('client'))
+ self.paginator_api(doc_structure.get_section('paginators'))
+ self.waiter_api(doc_structure.get_section('waiters'))
+ if self._service_resource:
+ self.resource_section(doc_structure.get_section('resources'))
+ self._document_examples(doc_structure.get_section('examples'))
+ context_params_section = doc_structure.get_section('context-params')
+ self.client_context_params(context_params_section)
+ return doc_structure.flush_structure()
+
+ def client_api(self, section):
+ examples = None
+ try:
+ examples = self.get_examples(self._service_name)
+ except DataNotFoundError:
+ pass
+
+ Boto3ClientDocumenter(
+ self._client, self._root_docs_path, examples
+ ).document_client(section)
+
+ def resource_section(self, section):
+ section.style.h2('Resources')
+ section.style.new_line()
+ section.write(
+ 'Resources are available in boto3 via the '
+ '``resource`` method. For more detailed instructions '
+ 'and examples on the usage of resources, see the '
+ 'resources '
+ )
+ section.style.external_link(
+ title='user guide',
+ link=self._USER_GUIDE_LINK,
+ )
+ section.write('.')
+ section.style.new_line()
+ section.style.new_line()
+ section.write('The available resources are:')
+ section.style.new_line()
+ section.style.toctree()
+ self._document_service_resource(section)
+ self._document_resources(section)
+
+ def _document_service_resource(self, section):
+ # Create a new DocumentStructure for each Service Resource and add contents.
+ service_resource_doc = DocumentStructure(
+ 'service-resource', target='html'
+ )
+ breadcrumb_section = service_resource_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(
+ self._client.__class__.__name__, f'../../{self._service_name}'
+ )
+ breadcrumb_section.write(' / Resource / ServiceResource')
+ ServiceResourceDocumenter(
+ self._service_resource, self._session, self._root_docs_path
+ ).document_resource(service_resource_doc)
+ # Write collections in individual/nested files.
+ # Path: /reference/services///.rst
+ resource_name = self._service_resource.meta.resource_model.name
+ if resource_name == self._service_name:
+ resource_name = 'service-resource'
+ service_resource_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{resource_name.lower()}',
+ )
+ service_resource_doc.write_to_file(service_resource_dir_path, 'index')
+ section.style.tocitem(f'{self._service_name}/{resource_name}/index')
+
+ def _document_resources(self, section):
+ temp_identifier_value = 'foo'
+ loader = self._session.get_component('data_loader')
+ json_resource_model = loader.load_service_model(
+ self._service_name, 'resources-1'
+ )
+ service_model = self._service_resource.meta.client.meta.service_model
+ for resource_name in json_resource_model['resources']:
+ resource_model = json_resource_model['resources'][resource_name]
+ resource_cls = (
+ self._boto3_session.resource_factory.load_from_definition(
+ resource_name=resource_name,
+ single_resource_json_definition=resource_model,
+ service_context=ServiceContext(
+ service_name=self._service_name,
+ resource_json_definitions=json_resource_model[
+ 'resources'
+ ],
+ service_model=service_model,
+ service_waiter_model=None,
+ ),
+ )
+ )
+ identifiers = resource_cls.meta.resource_model.identifiers
+ args = []
+ for _ in identifiers:
+ args.append(temp_identifier_value)
+ resource = resource_cls(*args, client=self._client)
+ # Create a new DocumentStructure for each Resource and add contents.
+ resource_name = resource.meta.resource_model.name.lower()
+ resource_doc = DocumentStructure(resource_name, target='html')
+ breadcrumb_section = resource_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(
+ self._client.__class__.__name__, f'../../{self._service_name}'
+ )
+ breadcrumb_section.write(
+ f' / Resource / {resource.meta.resource_model.name}'
+ )
+ ResourceDocumenter(
+ resource, self._session, self._root_docs_path
+ ).document_resource(
+ resource_doc.add_new_section(resource.meta.resource_model.name)
+ )
+ # Write collections in individual/nested files.
+ # Path: /reference/services///.rst
+ service_resource_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{resource_name}',
+ )
+ resource_doc.write_to_file(service_resource_dir_path, 'index')
+ section.style.tocitem(
+ f'{self._service_name}/{resource_name}/index'
+ )
+
+ def _get_example_file(self):
+ return os.path.realpath(
+ os.path.join(self.EXAMPLE_PATH, self._service_name + '.rst')
+ )
+
+ def _document_examples(self, section):
+ examples_file = self._get_example_file()
+ if os.path.isfile(examples_file):
+ section.style.h2('Examples')
+ section.style.new_line()
+ with open(examples_file) as f:
+ section.write(f.read())
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/subresource.py b/venv/lib/python3.12/site-packages/boto3/docs/subresource.py
new file mode 100644
index 0000000..792abf9
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/subresource.py
@@ -0,0 +1,153 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import os
+
+from botocore import xform_name
+from botocore.docs.bcdoc.restdoc import DocumentStructure
+from botocore.utils import get_service_module_name
+
+from boto3.docs.base import NestedDocumenter
+from boto3.docs.utils import (
+ add_resource_type_overview,
+ get_identifier_args_for_signature,
+ get_identifier_description,
+ get_identifier_values_for_example,
+)
+
+
+class SubResourceDocumenter(NestedDocumenter):
+ def document_sub_resources(self, section):
+ add_resource_type_overview(
+ section=section,
+ resource_type='Sub-resources',
+ description=(
+ 'Sub-resources are methods that create a new instance of a'
+ ' child resource. This resource\'s identifiers get passed'
+ ' along to the child.'
+ ),
+ intro_link='subresources_intro',
+ )
+ sub_resources = sorted(
+ self._resource.meta.resource_model.subresources,
+ key=lambda sub_resource: sub_resource.name,
+ )
+ sub_resources_list = []
+ self.member_map['sub-resources'] = sub_resources_list
+ for sub_resource in sub_resources:
+ sub_resources_list.append(sub_resource.name)
+ # Create a new DocumentStructure for each sub_resource and add contents.
+ sub_resource_doc = DocumentStructure(
+ sub_resource.name, target='html'
+ )
+ breadcrumb_section = sub_resource_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(self._resource_class_name, 'index')
+ breadcrumb_section.write(f' / Sub-Resource / {sub_resource.name}')
+ sub_resource_doc.add_title_section(sub_resource.name)
+ sub_resource_section = sub_resource_doc.add_new_section(
+ sub_resource.name,
+ context={'qualifier': f'{self.class_name}.'},
+ )
+ document_sub_resource(
+ section=sub_resource_section,
+ resource_name=self._resource_name,
+ sub_resource_model=sub_resource,
+ service_model=self._service_model,
+ )
+
+ # Write sub_resources in individual/nested files.
+ # Path: /reference/services///.rst
+ sub_resources_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{self._resource_sub_path}',
+ )
+ sub_resource_doc.write_to_file(
+ sub_resources_dir_path, sub_resource.name
+ )
+
+
+def document_sub_resource(
+ section,
+ resource_name,
+ sub_resource_model,
+ service_model,
+ include_signature=True,
+):
+ """Documents a resource action
+
+ :param section: The section to write to
+
+ :param resource_name: The name of the resource
+
+ :param sub_resource_model: The model of the subresource
+
+ :param service_model: The model of the service
+
+ :param include_signature: Whether or not to include the signature.
+ It is useful for generating docstrings.
+ """
+ identifiers_needed = []
+ for identifier in sub_resource_model.resource.identifiers:
+ if identifier.source == 'input':
+ identifiers_needed.append(xform_name(identifier.target))
+
+ if include_signature:
+ signature_args = get_identifier_args_for_signature(identifiers_needed)
+ full_sub_resource_name = (
+ f"{section.context.get('qualifier', '')}{sub_resource_model.name}"
+ )
+ section.style.start_sphinx_py_method(
+ full_sub_resource_name, signature_args
+ )
+
+ method_intro_section = section.add_new_section('method-intro')
+ description = f'Creates a {sub_resource_model.resource.type} resource.'
+ method_intro_section.include_doc_string(description)
+ example_section = section.add_new_section('example')
+ example_values = get_identifier_values_for_example(identifiers_needed)
+ example_resource_name = xform_name(resource_name)
+ if service_model.service_name == resource_name:
+ example_resource_name = resource_name
+ example = '{} = {}.{}({})'.format(
+ xform_name(sub_resource_model.resource.type),
+ example_resource_name,
+ sub_resource_model.name,
+ example_values,
+ )
+ example_section.style.start_codeblock()
+ example_section.write(example)
+ example_section.style.end_codeblock()
+
+ param_section = section.add_new_section('params')
+ for identifier in identifiers_needed:
+ description = get_identifier_description(
+ sub_resource_model.name, identifier
+ )
+ param_section.write(f':type {identifier}: string')
+ param_section.style.new_line()
+ param_section.write(f':param {identifier}: {description}')
+ param_section.style.new_line()
+
+ return_section = section.add_new_section('return')
+ return_section.style.new_line()
+ return_section.write(
+ ':rtype: :py:class:`{}.{}`'.format(
+ get_service_module_name(service_model),
+ sub_resource_model.resource.type,
+ )
+ )
+ return_section.style.new_line()
+ return_section.write(
+ f':returns: A {sub_resource_model.resource.type} resource'
+ )
+ return_section.style.new_line()
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/utils.py b/venv/lib/python3.12/site-packages/boto3/docs/utils.py
new file mode 100644
index 0000000..0830af5
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/utils.py
@@ -0,0 +1,146 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import inspect
+
+import jmespath
+
+
+def get_resource_ignore_params(params):
+ """Helper method to determine which parameters to ignore for actions
+
+ :returns: A list of the parameter names that does not need to be
+ included in a resource's method call for documentation purposes.
+ """
+ ignore_params = []
+ for param in params:
+ result = jmespath.compile(param.target)
+ current = result.parsed
+ # Use JMESPath to find the left most element in the target expression
+ # which will be the parameter to ignore in the action call.
+ while current['children']:
+ current = current['children'][0]
+ # Make sure the parameter we are about to ignore is a field.
+ # If it is not, we should ignore the result to avoid false positives.
+ if current['type'] == 'field':
+ ignore_params.append(current['value'])
+ return ignore_params
+
+
+def is_resource_action(action_handle):
+ return inspect.isfunction(action_handle)
+
+
+def get_resource_public_actions(resource_class):
+ resource_class_members = inspect.getmembers(resource_class)
+ resource_methods = {}
+ for name, member in resource_class_members:
+ if not name.startswith('_'):
+ if not name[0].isupper():
+ if not name.startswith('wait_until'):
+ if is_resource_action(member):
+ resource_methods[name] = member
+ return resource_methods
+
+
+def get_identifier_values_for_example(identifier_names):
+ return ','.join([f'\'{identifier}\'' for identifier in identifier_names])
+
+
+def get_identifier_args_for_signature(identifier_names):
+ return ','.join(identifier_names)
+
+
+def get_identifier_description(resource_name, identifier_name):
+ return (
+ f"The {resource_name}'s {identifier_name} identifier. "
+ f"This **must** be set."
+ )
+
+
+def add_resource_type_overview(
+ section, resource_type, description, intro_link=None
+):
+ section.style.new_line()
+ section.style.h3(resource_type)
+ section.style.new_line()
+ section.style.new_line()
+ section.write(description)
+ section.style.new_line()
+ if intro_link is not None:
+ section.write(
+ f'For more information about {resource_type.lower()} refer to the '
+ f':ref:`Resources Introduction Guide<{intro_link}>`.'
+ )
+ section.style.new_line()
+
+
+class DocumentModifiedShape:
+ def __init__(
+ self, shape_name, new_type, new_description, new_example_value
+ ):
+ self._shape_name = shape_name
+ self._new_type = new_type
+ self._new_description = new_description
+ self._new_example_value = new_example_value
+
+ def replace_documentation_for_matching_shape(
+ self, event_name, section, **kwargs
+ ):
+ if self._shape_name == section.context.get('shape'):
+ self._replace_documentation(event_name, section)
+ for section_name in section.available_sections:
+ sub_section = section.get_section(section_name)
+ if self._shape_name == sub_section.context.get('shape'):
+ self._replace_documentation(event_name, sub_section)
+ else:
+ self.replace_documentation_for_matching_shape(
+ event_name, sub_section
+ )
+
+ def _replace_documentation(self, event_name, section):
+ if event_name.startswith(
+ 'docs.request-example'
+ ) or event_name.startswith('docs.response-example'):
+ section.remove_all_sections()
+ section.clear_text()
+ section.write(self._new_example_value)
+
+ if event_name.startswith(
+ 'docs.request-params'
+ ) or event_name.startswith('docs.response-params'):
+ allowed_sections = (
+ 'param-name',
+ 'param-documentation',
+ 'end-structure',
+ 'param-type',
+ 'end-param',
+ )
+ for section_name in section.available_sections:
+ # Delete any extra members as a new shape is being
+ # used.
+ if section_name not in allowed_sections:
+ section.delete_section(section_name)
+
+ # Update the documentation
+ description_section = section.get_section('param-documentation')
+ description_section.clear_text()
+ description_section.write(self._new_description)
+
+ # Update the param type
+ type_section = section.get_section('param-type')
+ if type_section.getvalue().decode('utf-8').startswith(':type'):
+ type_section.clear_text()
+ type_section.write(f':type {section.name}: {self._new_type}')
+ else:
+ type_section.clear_text()
+ type_section.style.italics(f'({self._new_type}) -- ')
diff --git a/venv/lib/python3.12/site-packages/boto3/docs/waiter.py b/venv/lib/python3.12/site-packages/boto3/docs/waiter.py
new file mode 100644
index 0000000..a135d97
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/docs/waiter.py
@@ -0,0 +1,130 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import os
+
+from botocore import xform_name
+from botocore.docs.bcdoc.restdoc import DocumentStructure
+from botocore.docs.method import document_model_driven_method
+from botocore.utils import get_service_module_name
+
+from boto3.docs.base import NestedDocumenter
+from boto3.docs.utils import (
+ add_resource_type_overview,
+ get_resource_ignore_params,
+)
+
+
+class WaiterResourceDocumenter(NestedDocumenter):
+ def __init__(self, resource, service_waiter_model, root_docs_path):
+ super().__init__(resource, root_docs_path)
+ self._service_waiter_model = service_waiter_model
+
+ def document_resource_waiters(self, section):
+ waiters = self._resource.meta.resource_model.waiters
+ add_resource_type_overview(
+ section=section,
+ resource_type='Waiters',
+ description=(
+ 'Waiters provide an interface to wait for a resource'
+ ' to reach a specific state.'
+ ),
+ intro_link='waiters_intro',
+ )
+ waiter_list = []
+ self.member_map['waiters'] = waiter_list
+ for waiter in waiters:
+ waiter_list.append(waiter.name)
+ # Create a new DocumentStructure for each waiter and add contents.
+ waiter_doc = DocumentStructure(waiter.name, target='html')
+ breadcrumb_section = waiter_doc.add_new_section('breadcrumb')
+ breadcrumb_section.style.ref(self._resource_class_name, 'index')
+ breadcrumb_section.write(f' / Waiter / {waiter.name}')
+ waiter_doc.add_title_section(waiter.name)
+ waiter_section = waiter_doc.add_new_section(
+ waiter.name,
+ context={'qualifier': f'{self.class_name}.'},
+ )
+ document_resource_waiter(
+ section=waiter_section,
+ resource_name=self._resource_name,
+ event_emitter=self._resource.meta.client.meta.events,
+ service_model=self._service_model,
+ resource_waiter_model=waiter,
+ service_waiter_model=self._service_waiter_model,
+ )
+ # Write waiters in individual/nested files.
+ # Path: /reference/services///.rst
+ waiters_dir_path = os.path.join(
+ self._root_docs_path,
+ f'{self._service_name}',
+ f'{self._resource_sub_path}',
+ )
+ waiter_doc.write_to_file(waiters_dir_path, waiter.name)
+
+
+def document_resource_waiter(
+ section,
+ resource_name,
+ event_emitter,
+ service_model,
+ resource_waiter_model,
+ service_waiter_model,
+ include_signature=True,
+):
+ waiter_model = service_waiter_model.get_waiter(
+ resource_waiter_model.waiter_name
+ )
+ operation_model = service_model.operation_model(waiter_model.operation)
+
+ ignore_params = get_resource_ignore_params(resource_waiter_model.params)
+ service_module_name = get_service_module_name(service_model)
+ description = (
+ 'Waits until this {} is {}. This method calls '
+ ':py:meth:`{}.Waiter.{}.wait` which polls '
+ ':py:meth:`{}.Client.{}` every {} seconds until '
+ 'a successful state is reached. An error is returned '
+ 'after {} failed checks.'.format(
+ resource_name,
+ ' '.join(resource_waiter_model.name.split('_')[2:]),
+ service_module_name,
+ xform_name(resource_waiter_model.waiter_name),
+ service_module_name,
+ xform_name(waiter_model.operation),
+ waiter_model.delay,
+ waiter_model.max_attempts,
+ )
+ )
+ example_prefix = '{}.{}'.format(
+ xform_name(resource_name), resource_waiter_model.name
+ )
+ full_waiter_name = (
+ f"{section.context.get('qualifier', '')}{resource_waiter_model.name}"
+ )
+ document_model_driven_method(
+ section=section,
+ method_name=full_waiter_name,
+ operation_model=operation_model,
+ event_emitter=event_emitter,
+ example_prefix=example_prefix,
+ method_description=description,
+ exclude_input=ignore_params,
+ include_signature=include_signature,
+ )
+ if 'return' in section.available_sections:
+ # Waiters do not return anything so we should remove
+ # any sections that may document the underlying return
+ # value of the client method.
+ return_section = section.get_section('return')
+ return_section.clear_text()
+ return_section.remove_all_sections()
+ return_section.write(':returns: None')
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/__init__.py b/venv/lib/python3.12/site-packages/boto3/dynamodb/__init__.py
new file mode 100644
index 0000000..6001b27
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/dynamodb/__init__.py
@@ -0,0 +1,12 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.12/site-packages/boto3/dynamodb/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..5deaaf9
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/conditions.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/conditions.cpython-312.pyc
new file mode 100644
index 0000000..8bd17ce
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/conditions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/table.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/table.cpython-312.pyc
new file mode 100644
index 0000000..2bb2622
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/table.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/transform.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/transform.cpython-312.pyc
new file mode 100644
index 0000000..889de71
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/transform.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/types.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/types.cpython-312.pyc
new file mode 100644
index 0000000..bd43240
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/dynamodb/__pycache__/types.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/conditions.py b/venv/lib/python3.12/site-packages/boto3/dynamodb/conditions.py
new file mode 100644
index 0000000..74b3e8e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/dynamodb/conditions.py
@@ -0,0 +1,461 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import re
+from collections import namedtuple
+
+from boto3.exceptions import (
+ DynamoDBNeedsConditionError,
+ DynamoDBNeedsKeyConditionError,
+ DynamoDBOperationNotSupportedError,
+)
+
+ATTR_NAME_REGEX = re.compile(r'[^.\[\]]+(?![^\[]*\])')
+
+
+class ConditionBase:
+ expression_format = ''
+ expression_operator = ''
+ has_grouped_values = False
+
+ def __init__(self, *values):
+ self._values = values
+
+ def __and__(self, other):
+ if not isinstance(other, ConditionBase):
+ raise DynamoDBOperationNotSupportedError('AND', other)
+ return And(self, other)
+
+ def __or__(self, other):
+ if not isinstance(other, ConditionBase):
+ raise DynamoDBOperationNotSupportedError('OR', other)
+ return Or(self, other)
+
+ def __invert__(self):
+ return Not(self)
+
+ def get_expression(self):
+ return {
+ 'format': self.expression_format,
+ 'operator': self.expression_operator,
+ 'values': self._values,
+ }
+
+ def __eq__(self, other):
+ if isinstance(other, type(self)):
+ if self._values == other._values:
+ return True
+ return False
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class AttributeBase:
+ def __init__(self, name):
+ self.name = name
+
+ def __and__(self, value):
+ raise DynamoDBOperationNotSupportedError('AND', self)
+
+ def __or__(self, value):
+ raise DynamoDBOperationNotSupportedError('OR', self)
+
+ def __invert__(self):
+ raise DynamoDBOperationNotSupportedError('NOT', self)
+
+ def eq(self, value):
+ """Creates a condition where the attribute is equal to the value.
+
+ :param value: The value that the attribute is equal to.
+ """
+ return Equals(self, value)
+
+ def lt(self, value):
+ """Creates a condition where the attribute is less than the value.
+
+ :param value: The value that the attribute is less than.
+ """
+ return LessThan(self, value)
+
+ def lte(self, value):
+ """Creates a condition where the attribute is less than or equal to the
+ value.
+
+ :param value: The value that the attribute is less than or equal to.
+ """
+ return LessThanEquals(self, value)
+
+ def gt(self, value):
+ """Creates a condition where the attribute is greater than the value.
+
+ :param value: The value that the attribute is greater than.
+ """
+ return GreaterThan(self, value)
+
+ def gte(self, value):
+ """Creates a condition where the attribute is greater than or equal to
+ the value.
+
+ :param value: The value that the attribute is greater than or equal to.
+ """
+ return GreaterThanEquals(self, value)
+
+ def begins_with(self, value):
+ """Creates a condition where the attribute begins with the value.
+
+ :param value: The value that the attribute begins with.
+ """
+ return BeginsWith(self, value)
+
+ def between(self, low_value, high_value):
+ """Creates a condition where the attribute is greater than or equal
+ to the low value and less than or equal to the high value.
+
+ :param low_value: The value that the attribute is greater than or equal to.
+ :param high_value: The value that the attribute is less than or equal to.
+ """
+ return Between(self, low_value, high_value)
+
+ def __eq__(self, other):
+ return isinstance(other, type(self)) and self.name == other.name
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class ConditionAttributeBase(ConditionBase, AttributeBase):
+ """This base class is for conditions that can have attribute methods.
+
+ One example is the Size condition. To complete a condition, you need
+ to apply another AttributeBase method like eq().
+ """
+
+ def __init__(self, *values):
+ ConditionBase.__init__(self, *values)
+ # This is assuming the first value to the condition is the attribute
+ # in which can be used to generate its attribute base.
+ AttributeBase.__init__(self, values[0].name)
+
+ def __eq__(self, other):
+ return ConditionBase.__eq__(self, other) and AttributeBase.__eq__(
+ self, other
+ )
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+class ComparisonCondition(ConditionBase):
+ expression_format = '{0} {operator} {1}'
+
+
+class Equals(ComparisonCondition):
+ expression_operator = '='
+
+
+class NotEquals(ComparisonCondition):
+ expression_operator = '<>'
+
+
+class LessThan(ComparisonCondition):
+ expression_operator = '<'
+
+
+class LessThanEquals(ComparisonCondition):
+ expression_operator = '<='
+
+
+class GreaterThan(ComparisonCondition):
+ expression_operator = '>'
+
+
+class GreaterThanEquals(ComparisonCondition):
+ expression_operator = '>='
+
+
+class In(ComparisonCondition):
+ expression_operator = 'IN'
+ has_grouped_values = True
+
+
+class Between(ConditionBase):
+ expression_operator = 'BETWEEN'
+ expression_format = '{0} {operator} {1} AND {2}'
+
+
+class BeginsWith(ConditionBase):
+ expression_operator = 'begins_with'
+ expression_format = '{operator}({0}, {1})'
+
+
+class Contains(ConditionBase):
+ expression_operator = 'contains'
+ expression_format = '{operator}({0}, {1})'
+
+
+class Size(ConditionAttributeBase):
+ expression_operator = 'size'
+ expression_format = '{operator}({0})'
+
+
+class AttributeType(ConditionBase):
+ expression_operator = 'attribute_type'
+ expression_format = '{operator}({0}, {1})'
+
+
+class AttributeExists(ConditionBase):
+ expression_operator = 'attribute_exists'
+ expression_format = '{operator}({0})'
+
+
+class AttributeNotExists(ConditionBase):
+ expression_operator = 'attribute_not_exists'
+ expression_format = '{operator}({0})'
+
+
+class And(ConditionBase):
+ expression_operator = 'AND'
+ expression_format = '({0} {operator} {1})'
+
+
+class Or(ConditionBase):
+ expression_operator = 'OR'
+ expression_format = '({0} {operator} {1})'
+
+
+class Not(ConditionBase):
+ expression_operator = 'NOT'
+ expression_format = '({operator} {0})'
+
+
+class Key(AttributeBase):
+ pass
+
+
+class Attr(AttributeBase):
+ """Represents an DynamoDB item's attribute."""
+
+ def ne(self, value):
+ """Creates a condition where the attribute is not equal to the value
+
+ :param value: The value that the attribute is not equal to.
+ """
+ return NotEquals(self, value)
+
+ def is_in(self, value):
+ """Creates a condition where the attribute is in the value,
+
+ :type value: list
+ :param value: The value that the attribute is in.
+ """
+ return In(self, value)
+
+ def exists(self):
+ """Creates a condition where the attribute exists."""
+ return AttributeExists(self)
+
+ def not_exists(self):
+ """Creates a condition where the attribute does not exist."""
+ return AttributeNotExists(self)
+
+ def contains(self, value):
+ """Creates a condition where the attribute contains the value.
+
+ :param value: The value the attribute contains.
+ """
+ return Contains(self, value)
+
+ def size(self):
+ """Creates a condition for the attribute size.
+
+ Note another AttributeBase method must be called on the returned
+ size condition to be a valid DynamoDB condition.
+ """
+ return Size(self)
+
+ def attribute_type(self, value):
+ """Creates a condition for the attribute type.
+
+ :param value: The type of the attribute.
+ """
+ return AttributeType(self, value)
+
+
+BuiltConditionExpression = namedtuple(
+ 'BuiltConditionExpression',
+ [
+ 'condition_expression',
+ 'attribute_name_placeholders',
+ 'attribute_value_placeholders',
+ ],
+)
+
+
+class ConditionExpressionBuilder:
+ """This class is used to build condition expressions with placeholders"""
+
+ def __init__(self):
+ self._name_count = 0
+ self._value_count = 0
+ self._name_placeholder = 'n'
+ self._value_placeholder = 'v'
+
+ def _get_name_placeholder(self):
+ return '#' + self._name_placeholder + str(self._name_count)
+
+ def _get_value_placeholder(self):
+ return ':' + self._value_placeholder + str(self._value_count)
+
+ def reset(self):
+ """Resets the placeholder name and values"""
+ self._name_count = 0
+ self._value_count = 0
+
+ def build_expression(self, condition, is_key_condition=False):
+ """Builds the condition expression and the dictionary of placeholders.
+
+ :type condition: ConditionBase
+ :param condition: A condition to be built into a condition expression
+ string with any necessary placeholders.
+
+ :type is_key_condition: Boolean
+ :param is_key_condition: True if the expression is for a
+ KeyConditionExpression. False otherwise.
+
+ :rtype: (string, dict, dict)
+ :returns: Will return a string representing the condition with
+ placeholders inserted where necessary, a dictionary of
+ placeholders for attribute names, and a dictionary of
+ placeholders for attribute values. Here is a sample return value:
+
+ ('#n0 = :v0', {'#n0': 'myattribute'}, {':v1': 'myvalue'})
+ """
+ if not isinstance(condition, ConditionBase):
+ raise DynamoDBNeedsConditionError(condition)
+ attribute_name_placeholders = {}
+ attribute_value_placeholders = {}
+ condition_expression = self._build_expression(
+ condition,
+ attribute_name_placeholders,
+ attribute_value_placeholders,
+ is_key_condition=is_key_condition,
+ )
+ return BuiltConditionExpression(
+ condition_expression=condition_expression,
+ attribute_name_placeholders=attribute_name_placeholders,
+ attribute_value_placeholders=attribute_value_placeholders,
+ )
+
+ def _build_expression(
+ self,
+ condition,
+ attribute_name_placeholders,
+ attribute_value_placeholders,
+ is_key_condition,
+ ):
+ expression_dict = condition.get_expression()
+ replaced_values = []
+ for value in expression_dict['values']:
+ # Build the necessary placeholders for that value.
+ # Placeholders are built for both attribute names and values.
+ replaced_value = self._build_expression_component(
+ value,
+ attribute_name_placeholders,
+ attribute_value_placeholders,
+ condition.has_grouped_values,
+ is_key_condition,
+ )
+ replaced_values.append(replaced_value)
+ # Fill out the expression using the operator and the
+ # values that have been replaced with placeholders.
+ return expression_dict['format'].format(
+ *replaced_values, operator=expression_dict['operator']
+ )
+
+ def _build_expression_component(
+ self,
+ value,
+ attribute_name_placeholders,
+ attribute_value_placeholders,
+ has_grouped_values,
+ is_key_condition,
+ ):
+ # Continue to recurse if the value is a ConditionBase in order
+ # to extract out all parts of the expression.
+ if isinstance(value, ConditionBase):
+ return self._build_expression(
+ value,
+ attribute_name_placeholders,
+ attribute_value_placeholders,
+ is_key_condition,
+ )
+ # If it is not a ConditionBase, we can recurse no further.
+ # So we check if it is an attribute and add placeholders for
+ # its name
+ elif isinstance(value, AttributeBase):
+ if is_key_condition and not isinstance(value, Key):
+ raise DynamoDBNeedsKeyConditionError(
+ f'Attribute object {value.name} is of type {type(value)}. '
+ f'KeyConditionExpression only supports Attribute objects '
+ f'of type Key'
+ )
+ return self._build_name_placeholder(
+ value, attribute_name_placeholders
+ )
+ # If it is anything else, we treat it as a value and thus placeholders
+ # are needed for the value.
+ else:
+ return self._build_value_placeholder(
+ value, attribute_value_placeholders, has_grouped_values
+ )
+
+ def _build_name_placeholder(self, value, attribute_name_placeholders):
+ attribute_name = value.name
+ # Figure out which parts of the attribute name that needs replacement.
+ attribute_name_parts = ATTR_NAME_REGEX.findall(attribute_name)
+
+ # Add a temporary placeholder for each of these parts.
+ placeholder_format = ATTR_NAME_REGEX.sub('%s', attribute_name)
+ str_format_args = []
+ for part in attribute_name_parts:
+ name_placeholder = self._get_name_placeholder()
+ self._name_count += 1
+ str_format_args.append(name_placeholder)
+ # Add the placeholder and value to dictionary of name placeholders.
+ attribute_name_placeholders[name_placeholder] = part
+ # Replace the temporary placeholders with the designated placeholders.
+ return placeholder_format % tuple(str_format_args)
+
+ def _build_value_placeholder(
+ self, value, attribute_value_placeholders, has_grouped_values=False
+ ):
+ # If the values are grouped, we need to add a placeholder for
+ # each element inside of the actual value.
+ if has_grouped_values:
+ placeholder_list = []
+ for v in value:
+ value_placeholder = self._get_value_placeholder()
+ self._value_count += 1
+ placeholder_list.append(value_placeholder)
+ attribute_value_placeholders[value_placeholder] = v
+ # Assuming the values are grouped by parenthesis.
+ # IN is the currently the only one that uses this so it maybe
+ # needed to be changed in future.
+ return '(' + ', '.join(placeholder_list) + ')'
+ # Otherwise, treat the value as a single value that needs only
+ # one placeholder.
+ else:
+ value_placeholder = self._get_value_placeholder()
+ self._value_count += 1
+ attribute_value_placeholders[value_placeholder] = value
+ return value_placeholder
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/table.py b/venv/lib/python3.12/site-packages/boto3/dynamodb/table.py
new file mode 100644
index 0000000..931296b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/dynamodb/table.py
@@ -0,0 +1,167 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def register_table_methods(base_classes, **kwargs):
+ base_classes.insert(0, TableResource)
+
+
+# This class can be used to add any additional methods we want
+# onto a table resource. Ideally to avoid creating a new
+# base class for every method we can just update this
+# class instead. Just be sure to move the bulk of the
+# actual method implementation to another class.
+class TableResource:
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def batch_writer(self, overwrite_by_pkeys=None):
+ """Create a batch writer object.
+
+ This method creates a context manager for writing
+ objects to Amazon DynamoDB in batch.
+
+ The batch writer will automatically handle buffering and sending items
+ in batches. In addition, the batch writer will also automatically
+ handle any unprocessed items and resend them as needed. All you need
+ to do is call ``put_item`` for any items you want to add, and
+ ``delete_item`` for any items you want to delete.
+
+ Example usage::
+
+ with table.batch_writer() as batch:
+ for _ in range(1000000):
+ batch.put_item(Item={'HashKey': '...',
+ 'Otherstuff': '...'})
+ # You can also delete_items in a batch.
+ batch.delete_item(Key={'HashKey': 'SomeHashKey'})
+
+ :type overwrite_by_pkeys: list(string)
+ :param overwrite_by_pkeys: De-duplicate request items in buffer
+ if match new request item on specified primary keys. i.e
+ ``["partition_key1", "sort_key2", "sort_key3"]``
+
+ """
+ return BatchWriter(
+ self.name, self.meta.client, overwrite_by_pkeys=overwrite_by_pkeys
+ )
+
+
+class BatchWriter:
+ """Automatically handle batch writes to DynamoDB for a single table."""
+
+ def __init__(
+ self, table_name, client, flush_amount=25, overwrite_by_pkeys=None
+ ):
+ """
+
+ :type table_name: str
+ :param table_name: The name of the table. The class handles
+ batch writes to a single table.
+
+ :type client: ``botocore.client.Client``
+ :param client: A botocore client. Note this client
+ **must** have the dynamodb customizations applied
+ to it for transforming AttributeValues into the
+ wire protocol. What this means in practice is that
+ you need to use a client that comes from a DynamoDB
+ resource if you're going to instantiate this class
+ directly, i.e
+ ``boto3.resource('dynamodb').Table('foo').meta.client``.
+
+ :type flush_amount: int
+ :param flush_amount: The number of items to keep in
+ a local buffer before sending a batch_write_item
+ request to DynamoDB.
+
+ :type overwrite_by_pkeys: list(string)
+ :param overwrite_by_pkeys: De-duplicate request items in buffer
+ if match new request item on specified primary keys. i.e
+ ``["partition_key1", "sort_key2", "sort_key3"]``
+
+ """
+ self._table_name = table_name
+ self._client = client
+ self._items_buffer = []
+ self._flush_amount = flush_amount
+ self._overwrite_by_pkeys = overwrite_by_pkeys
+
+ def put_item(self, Item):
+ self._add_request_and_process({'PutRequest': {'Item': Item}})
+
+ def delete_item(self, Key):
+ self._add_request_and_process({'DeleteRequest': {'Key': Key}})
+
+ def _add_request_and_process(self, request):
+ if self._overwrite_by_pkeys:
+ self._remove_dup_pkeys_request_if_any(request)
+ self._items_buffer.append(request)
+ self._flush_if_needed()
+
+ def _remove_dup_pkeys_request_if_any(self, request):
+ pkey_values_new = self._extract_pkey_values(request)
+ for item in self._items_buffer:
+ if self._extract_pkey_values(item) == pkey_values_new:
+ self._items_buffer.remove(item)
+ logger.debug(
+ "With overwrite_by_pkeys enabled, skipping " "request:%s",
+ item,
+ )
+
+ def _extract_pkey_values(self, request):
+ if request.get('PutRequest'):
+ return [
+ request['PutRequest']['Item'][key]
+ for key in self._overwrite_by_pkeys
+ ]
+ elif request.get('DeleteRequest'):
+ return [
+ request['DeleteRequest']['Key'][key]
+ for key in self._overwrite_by_pkeys
+ ]
+ return None
+
+ def _flush_if_needed(self):
+ if len(self._items_buffer) >= self._flush_amount:
+ self._flush()
+
+ def _flush(self):
+ items_to_send = self._items_buffer[: self._flush_amount]
+ self._items_buffer = self._items_buffer[self._flush_amount :]
+ response = self._client.batch_write_item(
+ RequestItems={self._table_name: items_to_send}
+ )
+ unprocessed_items = response['UnprocessedItems']
+ if not unprocessed_items:
+ unprocessed_items = {}
+ item_list = unprocessed_items.get(self._table_name, [])
+ # Any unprocessed_items are immediately added to the
+ # next batch we send.
+ self._items_buffer.extend(item_list)
+ logger.debug(
+ "Batch write sent %s, unprocessed: %s",
+ len(items_to_send),
+ len(self._items_buffer),
+ )
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ # When we exit, we need to keep flushing whatever's left
+ # until there's nothing left in our items buffer.
+ while self._items_buffer:
+ self._flush()
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/transform.py b/venv/lib/python3.12/site-packages/boto3/dynamodb/transform.py
new file mode 100644
index 0000000..3944f31
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/dynamodb/transform.py
@@ -0,0 +1,343 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import copy
+
+from boto3.compat import collections_abc
+from boto3.docs.utils import DocumentModifiedShape
+from boto3.dynamodb.conditions import ConditionBase, ConditionExpressionBuilder
+from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
+
+
+def register_high_level_interface(base_classes, **kwargs):
+ base_classes.insert(0, DynamoDBHighLevelResource)
+
+
+class _ForgetfulDict(dict):
+ """A dictionary that discards any items set on it. For use as `memo` in
+ `copy.deepcopy()` when every instance of a repeated object in the deepcopied
+ data structure should result in a separate copy.
+ """
+
+ def __setitem__(self, key, value):
+ pass
+
+
+def copy_dynamodb_params(params, **kwargs):
+ return copy.deepcopy(params, memo=_ForgetfulDict())
+
+
+class DynamoDBHighLevelResource:
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Apply handler that creates a copy of the user provided dynamodb
+ # item such that it can be modified.
+ self.meta.client.meta.events.register(
+ 'provide-client-params.dynamodb',
+ copy_dynamodb_params,
+ unique_id='dynamodb-create-params-copy',
+ )
+
+ self._injector = TransformationInjector()
+ # Apply the handler that generates condition expressions including
+ # placeholders.
+ self.meta.client.meta.events.register(
+ 'before-parameter-build.dynamodb',
+ self._injector.inject_condition_expressions,
+ unique_id='dynamodb-condition-expression',
+ )
+
+ # Apply the handler that serializes the request from python
+ # types to dynamodb types.
+ self.meta.client.meta.events.register(
+ 'before-parameter-build.dynamodb',
+ self._injector.inject_attribute_value_input,
+ unique_id='dynamodb-attr-value-input',
+ )
+
+ # Apply the handler that deserializes the response from dynamodb
+ # types to python types.
+ self.meta.client.meta.events.register(
+ 'after-call.dynamodb',
+ self._injector.inject_attribute_value_output,
+ unique_id='dynamodb-attr-value-output',
+ )
+
+ # Apply the documentation customizations to account for
+ # the transformations.
+ attr_value_shape_docs = DocumentModifiedShape(
+ 'AttributeValue',
+ new_type='valid DynamoDB type',
+ new_description=(
+ '- The value of the attribute. The valid value types are '
+ 'listed in the '
+ ':ref:`DynamoDB Reference Guide`.'
+ ),
+ new_example_value=(
+ '\'string\'|123|Binary(b\'bytes\')|True|None|set([\'string\'])'
+ '|set([123])|set([Binary(b\'bytes\')])|[]|{}'
+ ),
+ )
+
+ key_expression_shape_docs = DocumentModifiedShape(
+ 'KeyExpression',
+ new_type=(
+ 'condition from :py:class:`boto3.dynamodb.conditions.Key` '
+ 'method'
+ ),
+ new_description=(
+ 'The condition(s) a key(s) must meet. Valid conditions are '
+ 'listed in the '
+ ':ref:`DynamoDB Reference Guide`.'
+ ),
+ new_example_value='Key(\'mykey\').eq(\'myvalue\')',
+ )
+
+ con_expression_shape_docs = DocumentModifiedShape(
+ 'ConditionExpression',
+ new_type=(
+ 'condition from :py:class:`boto3.dynamodb.conditions.Attr` '
+ 'method'
+ ),
+ new_description=(
+ 'The condition(s) an attribute(s) must meet. Valid conditions '
+ 'are listed in the '
+ ':ref:`DynamoDB Reference Guide`.'
+ ),
+ new_example_value='Attr(\'myattribute\').eq(\'myvalue\')',
+ )
+
+ self.meta.client.meta.events.register(
+ 'docs.*.dynamodb.*.complete-section',
+ attr_value_shape_docs.replace_documentation_for_matching_shape,
+ unique_id='dynamodb-attr-value-docs',
+ )
+
+ self.meta.client.meta.events.register(
+ 'docs.*.dynamodb.*.complete-section',
+ key_expression_shape_docs.replace_documentation_for_matching_shape,
+ unique_id='dynamodb-key-expression-docs',
+ )
+
+ self.meta.client.meta.events.register(
+ 'docs.*.dynamodb.*.complete-section',
+ con_expression_shape_docs.replace_documentation_for_matching_shape,
+ unique_id='dynamodb-cond-expression-docs',
+ )
+
+
+class TransformationInjector:
+ """Injects the transformations into the user provided parameters."""
+
+ def __init__(
+ self,
+ transformer=None,
+ condition_builder=None,
+ serializer=None,
+ deserializer=None,
+ ):
+ self._transformer = transformer
+ if transformer is None:
+ self._transformer = ParameterTransformer()
+
+ self._condition_builder = condition_builder
+ if condition_builder is None:
+ self._condition_builder = ConditionExpressionBuilder()
+
+ self._serializer = serializer
+ if serializer is None:
+ self._serializer = TypeSerializer()
+
+ self._deserializer = deserializer
+ if deserializer is None:
+ self._deserializer = TypeDeserializer()
+
+ def inject_condition_expressions(self, params, model, **kwargs):
+ """Injects the condition expression transformation into the parameters
+
+ This injection includes transformations for ConditionExpression shapes
+ and KeyExpression shapes. It also handles any placeholder names and
+ values that are generated when transforming the condition expressions.
+ """
+ self._condition_builder.reset()
+ generated_names = {}
+ generated_values = {}
+
+ # Create and apply the Condition Expression transformation.
+ transformation = ConditionExpressionTransformation(
+ self._condition_builder,
+ placeholder_names=generated_names,
+ placeholder_values=generated_values,
+ is_key_condition=False,
+ )
+ self._transformer.transform(
+ params, model.input_shape, transformation, 'ConditionExpression'
+ )
+
+ # Create and apply the Key Condition Expression transformation.
+ transformation = ConditionExpressionTransformation(
+ self._condition_builder,
+ placeholder_names=generated_names,
+ placeholder_values=generated_values,
+ is_key_condition=True,
+ )
+ self._transformer.transform(
+ params, model.input_shape, transformation, 'KeyExpression'
+ )
+
+ expr_attr_names_input = 'ExpressionAttributeNames'
+ expr_attr_values_input = 'ExpressionAttributeValues'
+
+ # Now that all of the condition expression transformation are done,
+ # update the placeholder dictionaries in the request.
+ if expr_attr_names_input in params:
+ params[expr_attr_names_input].update(generated_names)
+ else:
+ if generated_names:
+ params[expr_attr_names_input] = generated_names
+
+ if expr_attr_values_input in params:
+ params[expr_attr_values_input].update(generated_values)
+ else:
+ if generated_values:
+ params[expr_attr_values_input] = generated_values
+
+ def inject_attribute_value_input(self, params, model, **kwargs):
+ """Injects DynamoDB serialization into parameter input"""
+ self._transformer.transform(
+ params,
+ model.input_shape,
+ self._serializer.serialize,
+ 'AttributeValue',
+ )
+
+ def inject_attribute_value_output(self, parsed, model, **kwargs):
+ """Injects DynamoDB deserialization into responses"""
+ if model.output_shape is not None:
+ self._transformer.transform(
+ parsed,
+ model.output_shape,
+ self._deserializer.deserialize,
+ 'AttributeValue',
+ )
+
+
+class ConditionExpressionTransformation:
+ """Provides a transformation for condition expressions
+
+ The ``ParameterTransformer`` class can call this class directly
+ to transform the condition expressions in the parameters provided.
+ """
+
+ def __init__(
+ self,
+ condition_builder,
+ placeholder_names,
+ placeholder_values,
+ is_key_condition=False,
+ ):
+ self._condition_builder = condition_builder
+ self._placeholder_names = placeholder_names
+ self._placeholder_values = placeholder_values
+ self._is_key_condition = is_key_condition
+
+ def __call__(self, value):
+ if isinstance(value, ConditionBase):
+ # Create a conditional expression string with placeholders
+ # for the provided condition.
+ built_expression = self._condition_builder.build_expression(
+ value, is_key_condition=self._is_key_condition
+ )
+
+ self._placeholder_names.update(
+ built_expression.attribute_name_placeholders
+ )
+ self._placeholder_values.update(
+ built_expression.attribute_value_placeholders
+ )
+
+ return built_expression.condition_expression
+ # Use the user provided value if it is not a ConditonBase object.
+ return value
+
+
+class ParameterTransformer:
+ """Transforms the input to and output from botocore based on shape"""
+
+ def transform(self, params, model, transformation, target_shape):
+ """Transforms the dynamodb input to or output from botocore
+
+ It applies a specified transformation whenever a specific shape name
+ is encountered while traversing the parameters in the dictionary.
+
+ :param params: The parameters structure to transform.
+ :param model: The operation model.
+ :param transformation: The function to apply the parameter
+ :param target_shape: The name of the shape to apply the
+ transformation to
+ """
+ self._transform_parameters(model, params, transformation, target_shape)
+
+ def _transform_parameters(
+ self, model, params, transformation, target_shape
+ ):
+ type_name = model.type_name
+ if type_name in ('structure', 'map', 'list'):
+ getattr(self, f'_transform_{type_name}')(
+ model, params, transformation, target_shape
+ )
+
+ def _transform_structure(
+ self, model, params, transformation, target_shape
+ ):
+ if not isinstance(params, collections_abc.Mapping):
+ return
+ for param in params:
+ if param in model.members:
+ member_model = model.members[param]
+ member_shape = member_model.name
+ if member_shape == target_shape:
+ params[param] = transformation(params[param])
+ else:
+ self._transform_parameters(
+ member_model,
+ params[param],
+ transformation,
+ target_shape,
+ )
+
+ def _transform_map(self, model, params, transformation, target_shape):
+ if not isinstance(params, collections_abc.Mapping):
+ return
+ value_model = model.value
+ value_shape = value_model.name
+ for key, value in params.items():
+ if value_shape == target_shape:
+ params[key] = transformation(value)
+ else:
+ self._transform_parameters(
+ value_model, params[key], transformation, target_shape
+ )
+
+ def _transform_list(self, model, params, transformation, target_shape):
+ if not isinstance(params, collections_abc.MutableSequence):
+ return
+ member_model = model.member
+ member_shape = member_model.name
+ for i, item in enumerate(params):
+ if member_shape == target_shape:
+ params[i] = transformation(item)
+ else:
+ self._transform_parameters(
+ member_model, params[i], transformation, target_shape
+ )
diff --git a/venv/lib/python3.12/site-packages/boto3/dynamodb/types.py b/venv/lib/python3.12/site-packages/boto3/dynamodb/types.py
new file mode 100644
index 0000000..f358b12
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/dynamodb/types.py
@@ -0,0 +1,310 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from decimal import (
+ Clamped,
+ Context,
+ Decimal,
+ Inexact,
+ Overflow,
+ Rounded,
+ Underflow,
+)
+
+from boto3.compat import collections_abc
+
+STRING = 'S'
+NUMBER = 'N'
+BINARY = 'B'
+STRING_SET = 'SS'
+NUMBER_SET = 'NS'
+BINARY_SET = 'BS'
+NULL = 'NULL'
+BOOLEAN = 'BOOL'
+MAP = 'M'
+LIST = 'L'
+
+
+DYNAMODB_CONTEXT = Context(
+ Emin=-128,
+ Emax=126,
+ prec=38,
+ traps=[Clamped, Overflow, Inexact, Rounded, Underflow],
+)
+
+
+BINARY_TYPES = (bytearray, bytes)
+
+
+class Binary:
+ """A class for representing Binary in dynamodb
+
+ Especially for Python 2, use this class to explicitly specify
+ binary data for item in DynamoDB. It is essentially a wrapper around
+ binary. Unicode and Python 3 string types are not allowed.
+ """
+
+ def __init__(self, value):
+ if not isinstance(value, BINARY_TYPES):
+ types = ', '.join([str(t) for t in BINARY_TYPES])
+ raise TypeError(f'Value must be of the following types: {types}')
+ self.value = value
+
+ def __eq__(self, other):
+ if isinstance(other, Binary):
+ return self.value == other.value
+ return self.value == other
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return f'Binary({self.value!r})'
+
+ def __str__(self):
+ return self.value
+
+ def __bytes__(self):
+ return self.value
+
+ def __hash__(self):
+ return hash(self.value)
+
+
+class TypeSerializer:
+ """This class serializes Python data types to DynamoDB types."""
+
+ def serialize(self, value):
+ """The method to serialize the Python data types.
+
+ :param value: A python value to be serialized to DynamoDB. Here are
+ the various conversions:
+
+ Python DynamoDB
+ ------ --------
+ None {'NULL': True}
+ True/False {'BOOL': True/False}
+ int/Decimal {'N': str(value)}
+ string {'S': string}
+ Binary/bytearray/bytes (py3 only) {'B': bytes}
+ set([int/Decimal]) {'NS': [str(value)]}
+ set([string]) {'SS': [string])
+ set([Binary/bytearray/bytes]) {'BS': [bytes]}
+ list {'L': list}
+ dict {'M': dict}
+
+ For types that involve numbers, it is recommended that ``Decimal``
+ objects are used to be able to round-trip the Python type.
+ For types that involve binary, it is recommended that ``Binary``
+ objects are used to be able to round-trip the Python type.
+
+ :rtype: dict
+ :returns: A dictionary that represents a dynamoDB data type. These
+ dictionaries can be directly passed to botocore methods.
+ """
+ dynamodb_type = self._get_dynamodb_type(value)
+ serializer = getattr(self, f'_serialize_{dynamodb_type}'.lower())
+ return {dynamodb_type: serializer(value)}
+
+ def _get_dynamodb_type(self, value):
+ dynamodb_type = None
+
+ if self._is_null(value):
+ dynamodb_type = NULL
+
+ elif self._is_boolean(value):
+ dynamodb_type = BOOLEAN
+
+ elif self._is_number(value):
+ dynamodb_type = NUMBER
+
+ elif self._is_string(value):
+ dynamodb_type = STRING
+
+ elif self._is_binary(value):
+ dynamodb_type = BINARY
+
+ elif self._is_type_set(value, self._is_number):
+ dynamodb_type = NUMBER_SET
+
+ elif self._is_type_set(value, self._is_string):
+ dynamodb_type = STRING_SET
+
+ elif self._is_type_set(value, self._is_binary):
+ dynamodb_type = BINARY_SET
+
+ elif self._is_map(value):
+ dynamodb_type = MAP
+
+ elif self._is_listlike(value):
+ dynamodb_type = LIST
+
+ else:
+ msg = f'Unsupported type "{type(value)}" for value "{value}"'
+ raise TypeError(msg)
+
+ return dynamodb_type
+
+ def _is_null(self, value):
+ if value is None:
+ return True
+ return False
+
+ def _is_boolean(self, value):
+ if isinstance(value, bool):
+ return True
+ return False
+
+ def _is_number(self, value):
+ if isinstance(value, (int, Decimal)):
+ return True
+ elif isinstance(value, float):
+ raise TypeError(
+ 'Float types are not supported. Use Decimal types instead.'
+ )
+ return False
+
+ def _is_string(self, value):
+ if isinstance(value, str):
+ return True
+ return False
+
+ def _is_binary(self, value):
+ if isinstance(value, (Binary, bytearray, bytes)):
+ return True
+ return False
+
+ def _is_set(self, value):
+ if isinstance(value, collections_abc.Set):
+ return True
+ return False
+
+ def _is_type_set(self, value, type_validator):
+ if self._is_set(value):
+ if False not in map(type_validator, value):
+ return True
+ return False
+
+ def _is_map(self, value):
+ if isinstance(value, collections_abc.Mapping):
+ return True
+ return False
+
+ def _is_listlike(self, value):
+ if isinstance(value, (list, tuple)):
+ return True
+ return False
+
+ def _serialize_null(self, value):
+ return True
+
+ def _serialize_bool(self, value):
+ return value
+
+ def _serialize_n(self, value):
+ number = str(DYNAMODB_CONTEXT.create_decimal(value))
+ if number in ['Infinity', 'NaN']:
+ raise TypeError('Infinity and NaN not supported')
+ return number
+
+ def _serialize_s(self, value):
+ return value
+
+ def _serialize_b(self, value):
+ if isinstance(value, Binary):
+ value = value.value
+ return value
+
+ def _serialize_ss(self, value):
+ return [self._serialize_s(s) for s in value]
+
+ def _serialize_ns(self, value):
+ return [self._serialize_n(n) for n in value]
+
+ def _serialize_bs(self, value):
+ return [self._serialize_b(b) for b in value]
+
+ def _serialize_l(self, value):
+ return [self.serialize(v) for v in value]
+
+ def _serialize_m(self, value):
+ return {k: self.serialize(v) for k, v in value.items()}
+
+
+class TypeDeserializer:
+ """This class deserializes DynamoDB types to Python types."""
+
+ def deserialize(self, value):
+ """The method to deserialize the DynamoDB data types.
+
+ :param value: A DynamoDB value to be deserialized to a pythonic value.
+ Here are the various conversions:
+
+ DynamoDB Python
+ -------- ------
+ {'NULL': True} None
+ {'BOOL': True/False} True/False
+ {'N': str(value)} Decimal(str(value))
+ {'S': string} string
+ {'B': bytes} Binary(bytes)
+ {'NS': [str(value)]} set([Decimal(str(value))])
+ {'SS': [string]} set([string])
+ {'BS': [bytes]} set([bytes])
+ {'L': list} list
+ {'M': dict} dict
+
+ :returns: The pythonic value of the DynamoDB type.
+ """
+
+ if not value:
+ raise TypeError(
+ 'Value must be a nonempty dictionary whose key '
+ 'is a valid dynamodb type.'
+ )
+ dynamodb_type = list(value.keys())[0]
+ try:
+ deserializer = getattr(
+ self, f'_deserialize_{dynamodb_type}'.lower()
+ )
+ except AttributeError:
+ raise TypeError(f'Dynamodb type {dynamodb_type} is not supported')
+ return deserializer(value[dynamodb_type])
+
+ def _deserialize_null(self, value):
+ return None
+
+ def _deserialize_bool(self, value):
+ return value
+
+ def _deserialize_n(self, value):
+ return DYNAMODB_CONTEXT.create_decimal(value)
+
+ def _deserialize_s(self, value):
+ return value
+
+ def _deserialize_b(self, value):
+ return Binary(value)
+
+ def _deserialize_ns(self, value):
+ return set(map(self._deserialize_n, value))
+
+ def _deserialize_ss(self, value):
+ return set(map(self._deserialize_s, value))
+
+ def _deserialize_bs(self, value):
+ return set(map(self._deserialize_b, value))
+
+ def _deserialize_l(self, value):
+ return [self.deserialize(v) for v in value]
+
+ def _deserialize_m(self, value):
+ return {k: self.deserialize(v) for k, v in value.items()}
diff --git a/venv/lib/python3.12/site-packages/boto3/ec2/__init__.py b/venv/lib/python3.12/site-packages/boto3/ec2/__init__.py
new file mode 100644
index 0000000..6001b27
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/ec2/__init__.py
@@ -0,0 +1,12 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.12/site-packages/boto3/ec2/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..6514baa
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/createtags.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/createtags.cpython-312.pyc
new file mode 100644
index 0000000..562222a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/createtags.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/deletetags.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/deletetags.cpython-312.pyc
new file mode 100644
index 0000000..6478a27
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/ec2/__pycache__/deletetags.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/ec2/createtags.py b/venv/lib/python3.12/site-packages/boto3/ec2/createtags.py
new file mode 100644
index 0000000..ec0ff1a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/ec2/createtags.py
@@ -0,0 +1,40 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+
+def inject_create_tags(event_name, class_attributes, **kwargs):
+ """This injects a custom create_tags method onto the ec2 service resource
+
+ This is needed because the resource model is not able to express
+ creating multiple tag resources based on the fact you can apply a set
+ of tags to multiple ec2 resources.
+ """
+ class_attributes['create_tags'] = create_tags
+
+
+def create_tags(self, **kwargs):
+ # Call the client method
+ self.meta.client.create_tags(**kwargs)
+ resources = kwargs.get('Resources', [])
+ tags = kwargs.get('Tags', [])
+ tag_resources = []
+
+ # Generate all of the tag resources that just were created with the
+ # preceding client call.
+ for resource in resources:
+ for tag in tags:
+ # Add each tag from the tag set for each resource to the list
+ # that is returned by the method.
+ tag_resource = self.Tag(resource, tag['Key'], tag['Value'])
+ tag_resources.append(tag_resource)
+ return tag_resources
diff --git a/venv/lib/python3.12/site-packages/boto3/ec2/deletetags.py b/venv/lib/python3.12/site-packages/boto3/ec2/deletetags.py
new file mode 100644
index 0000000..19876d0
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/ec2/deletetags.py
@@ -0,0 +1,37 @@
+# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+from boto3.resources.action import CustomModeledAction
+
+
+def inject_delete_tags(event_emitter, **kwargs):
+ action_model = {
+ 'request': {
+ 'operation': 'DeleteTags',
+ 'params': [
+ {
+ 'target': 'Resources[0]',
+ 'source': 'identifier',
+ 'name': 'Id',
+ }
+ ],
+ }
+ }
+ action = CustomModeledAction(
+ 'delete_tags', action_model, delete_tags, event_emitter
+ )
+ action.inject(**kwargs)
+
+
+def delete_tags(self, **kwargs):
+ kwargs['Resources'] = [self.id]
+ return self.meta.client.delete_tags(**kwargs)
diff --git a/venv/lib/python3.12/site-packages/boto3/examples/cloudfront.rst b/venv/lib/python3.12/site-packages/boto3/examples/cloudfront.rst
new file mode 100644
index 0000000..ddec198
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/examples/cloudfront.rst
@@ -0,0 +1,35 @@
+Generate a signed URL for Amazon CloudFront
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The following example shows how to generate a signed URL for Amazon CloudFront.
+Note that you will need the ``cryptography`` `library `__ to follow this example::
+
+ import datetime
+
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives import serialization
+ from cryptography.hazmat.primitives.asymmetric import padding
+ from botocore.signers import CloudFrontSigner
+
+
+ def rsa_signer(message):
+ with open('path/to/key.pem', 'rb') as key_file:
+ private_key = serialization.load_pem_private_key(
+ key_file.read(),
+ password=None,
+ backend=default_backend()
+ )
+ return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())
+
+ key_id = 'AKIAIOSFODNN7EXAMPLE'
+ url = 'http://d2949o5mkkp72v.cloudfront.net/hello.txt'
+ expire_date = datetime.datetime(2017, 1, 1)
+
+ cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)
+
+ # Create a signed url that will be valid until the specific expiry date
+ # provided using a canned policy.
+ signed_url = cloudfront_signer.generate_presigned_url(
+ url, date_less_than=expire_date)
+ print(signed_url)
diff --git a/venv/lib/python3.12/site-packages/boto3/examples/s3.rst b/venv/lib/python3.12/site-packages/boto3/examples/s3.rst
new file mode 100644
index 0000000..0a79fb0
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/examples/s3.rst
@@ -0,0 +1,185 @@
+List objects in an Amazon S3 bucket
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The following example shows how to use an Amazon S3 bucket resource to list
+the objects in the bucket.
+
+.. code-block:: python
+
+ import boto3
+
+ s3 = boto3.resource('s3')
+ bucket = s3.Bucket('my-bucket')
+ for obj in bucket.objects.all():
+ print(obj.key)
+
+
+List top-level common prefixes in Amazon S3 bucket
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This example shows how to list all of the top-level common prefixes in an
+Amazon S3 bucket:
+
+.. code-block:: python
+
+ import boto3
+
+ client = boto3.client('s3')
+ paginator = client.get_paginator('list_objects')
+ result = paginator.paginate(Bucket='my-bucket', Delimiter='/')
+ for prefix in result.search('CommonPrefixes'):
+ print(prefix.get('Prefix'))
+
+
+Restore Glacier objects in an Amazon S3 bucket
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The following example shows how to initiate restoration of glacier objects in
+an Amazon S3 bucket, determine if a restoration is on-going, and determine if a
+restoration is finished.
+
+.. code-block:: python
+
+ import boto3
+
+ s3 = boto3.resource('s3')
+ bucket = s3.Bucket('glacier-bucket')
+ for obj_sum in bucket.objects.all():
+ obj = s3.Object(obj_sum.bucket_name, obj_sum.key)
+ if obj.storage_class == 'GLACIER':
+ # Try to restore the object if the storage class is glacier and
+ # the object does not have a completed or ongoing restoration
+ # request.
+ if obj.restore is None:
+ print('Submitting restoration request: %s' % obj.key)
+ obj.restore_object(RestoreRequest={'Days': 1})
+ # Print out objects whose restoration is on-going
+ elif 'ongoing-request="true"' in obj.restore:
+ print('Restoration in-progress: %s' % obj.key)
+ # Print out objects whose restoration is complete
+ elif 'ongoing-request="false"' in obj.restore:
+ print('Restoration complete: %s' % obj.key)
+
+
+Uploading/downloading files using SSE KMS
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This example shows how to use SSE-KMS to upload objects using
+server side encryption with a key managed by KMS.
+
+We can either use the default KMS master key, or create a
+custom key in AWS and use it to encrypt the object by passing in its
+key id.
+
+With KMS, nothing else needs to be provided for getting the
+object; S3 already knows how to decrypt the object.
+
+
+.. code-block:: python
+
+ import boto3
+ import os
+
+ BUCKET = 'your-bucket-name'
+ s3 = boto3.client('s3')
+ keyid = ''
+
+ print("Uploading S3 object with SSE-KMS")
+ s3.put_object(Bucket=BUCKET,
+ Key='encrypt-key',
+ Body=b'foobar',
+ ServerSideEncryption='aws:kms',
+ # Optional: SSEKMSKeyId
+ SSEKMSKeyId=keyid)
+ print("Done")
+
+ # Getting the object:
+ print("Getting S3 object...")
+ response = s3.get_object(Bucket=BUCKET,
+ Key='encrypt-key')
+ print("Done, response body:")
+ print(response['Body'].read())
+
+
+Uploading/downloading files using SSE Customer Keys
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This example shows how to use SSE-C to upload objects using
+server side encryption with a customer provided key.
+
+First, we'll need a 32 byte key. For this example, we'll
+randomly generate a key but you can use any 32 byte key
+you want. Remember, you must the same key to download
+the object. If you lose the encryption key, you lose
+the object.
+
+Also note how we don't have to provide the SSECustomerKeyMD5.
+Boto3 will automatically compute this value for us.
+
+
+.. code-block:: python
+
+ import boto3
+ import os
+
+ BUCKET = 'your-bucket-name'
+ KEY = os.urandom(32)
+ s3 = boto3.client('s3')
+
+ print("Uploading S3 object with SSE-C")
+ s3.put_object(Bucket=BUCKET,
+ Key='encrypt-key',
+ Body=b'foobar',
+ SSECustomerKey=KEY,
+ SSECustomerAlgorithm='AES256')
+ print("Done")
+
+ # Getting the object:
+ print("Getting S3 object...")
+ # Note how we're using the same ``KEY`` we
+ # created earlier.
+ response = s3.get_object(Bucket=BUCKET,
+ Key='encrypt-key',
+ SSECustomerKey=KEY,
+ SSECustomerAlgorithm='AES256')
+ print("Done, response body:")
+ print(response['Body'].read())
+
+
+Downloading a specific version of an S3 object
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This example shows how to download a specific version of an
+S3 object.
+
+.. code-block:: python
+
+ import boto3
+ s3 = boto3.client('s3')
+
+ s3.download_file(
+ "bucket-name", "key-name", "tmp.txt",
+ ExtraArgs={"VersionId": "my-version-id"}
+ )
+
+
+Filter objects by last modified time using JMESPath
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This example shows how to filter objects by last modified time
+using JMESPath.
+
+.. code-block:: python
+
+ import boto3
+ s3 = boto3.client("s3")
+
+ s3_paginator = s3.get_paginator('list_objects_v2')
+ s3_iterator = s3_paginator.paginate(Bucket='your-bucket-name')
+
+ filtered_iterator = s3_iterator.search(
+ "Contents[?to_string(LastModified)>='\"2022-01-05 08:05:37+00:00\"'].Key"
+ )
+
+ for key_data in filtered_iterator:
+ print(key_data)
diff --git a/venv/lib/python3.12/site-packages/boto3/exceptions.py b/venv/lib/python3.12/site-packages/boto3/exceptions.py
new file mode 100644
index 0000000..7d9ceaf
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/exceptions.py
@@ -0,0 +1,126 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+# All exceptions in this class should subclass from Boto3Error.
+import botocore.exceptions
+
+
+# All exceptions should subclass from Boto3Error in this module.
+class Boto3Error(Exception):
+ """Base class for all Boto3 errors."""
+
+
+class ResourceLoadException(Boto3Error):
+ pass
+
+
+# NOTE: This doesn't appear to be used anywhere.
+# It's probably safe to remove this.
+class NoVersionFound(Boto3Error):
+ pass
+
+
+# We're subclassing from botocore.exceptions.DataNotFoundError
+# to keep backwards compatibility with anyone that was catching
+# this low level Botocore error before this exception was
+# introduced in boto3.
+# Same thing for ResourceNotExistsError below.
+class UnknownAPIVersionError(
+ Boto3Error, botocore.exceptions.DataNotFoundError
+):
+ def __init__(self, service_name, bad_api_version, available_api_versions):
+ msg = (
+ f"The '{service_name}' resource does not support an API version of: {bad_api_version}\n"
+ f"Valid API versions are: {available_api_versions}"
+ )
+ # Not using super because we don't want the DataNotFoundError
+ # to be called, it has a different __init__ signature.
+ Boto3Error.__init__(self, msg)
+
+
+class ResourceNotExistsError(
+ Boto3Error, botocore.exceptions.DataNotFoundError
+):
+ """Raised when you attempt to create a resource that does not exist."""
+
+ def __init__(self, service_name, available_services, has_low_level_client):
+ msg = (
+ "The '{}' resource does not exist.\n"
+ "The available resources are:\n"
+ " - {}\n".format(
+ service_name, '\n - '.join(available_services)
+ )
+ )
+ if has_low_level_client:
+ msg = (
+ f"{msg}\nConsider using a boto3.client('{service_name}') "
+ f"instead of a resource for '{service_name}'"
+ )
+ # Not using super because we don't want the DataNotFoundError
+ # to be called, it has a different __init__ signature.
+ Boto3Error.__init__(self, msg)
+
+
+class RetriesExceededError(Boto3Error):
+ def __init__(self, last_exception, msg='Max Retries Exceeded'):
+ super().__init__(msg)
+ self.last_exception = last_exception
+
+
+class S3TransferFailedError(Boto3Error):
+ pass
+
+
+class S3UploadFailedError(Boto3Error):
+ pass
+
+
+class DynamoDBOperationNotSupportedError(Boto3Error):
+ """Raised for operations that are not supported for an operand."""
+
+ def __init__(self, operation, value):
+ msg = (
+ f'{operation} operation cannot be applied to value {value} of type '
+ f'{type(value)} directly. Must use AttributeBase object methods '
+ f'(i.e. Attr().eq()). to generate ConditionBase instances first.'
+ )
+ Exception.__init__(self, msg)
+
+
+# FIXME: Backward compatibility
+DynanmoDBOperationNotSupportedError = DynamoDBOperationNotSupportedError
+
+
+class DynamoDBNeedsConditionError(Boto3Error):
+ """Raised when input is not a condition"""
+
+ def __init__(self, value):
+ msg = (
+ f'Expecting a ConditionBase object. Got {value} of type {type(value)}. '
+ f'Use AttributeBase object methods (i.e. Attr().eq()). to '
+ f'generate ConditionBase instances.'
+ )
+ Exception.__init__(self, msg)
+
+
+class DynamoDBNeedsKeyConditionError(Boto3Error):
+ pass
+
+
+class PythonDeprecationWarning(Warning):
+ """
+ Python version being used is scheduled to become unsupported
+ in an future release. See warning for specifics.
+ """
+
+ pass
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__init__.py b/venv/lib/python3.12/site-packages/boto3/resources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..5efb2a8
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/action.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/action.cpython-312.pyc
new file mode 100644
index 0000000..33680bb
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/action.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/base.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..bd4f042
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/base.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/collection.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/collection.cpython-312.pyc
new file mode 100644
index 0000000..8181f37
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/collection.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/factory.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/factory.cpython-312.pyc
new file mode 100644
index 0000000..0a6c994
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/factory.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/model.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/model.cpython-312.pyc
new file mode 100644
index 0000000..e79b2c8
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/model.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/params.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/params.cpython-312.pyc
new file mode 100644
index 0000000..c20e717
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/params.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/response.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/response.cpython-312.pyc
new file mode 100644
index 0000000..8148837
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/resources/__pycache__/response.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/action.py b/venv/lib/python3.12/site-packages/boto3/resources/action.py
new file mode 100644
index 0000000..7c7d839
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/resources/action.py
@@ -0,0 +1,257 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import logging
+
+from botocore import xform_name
+
+from boto3.docs.docstring import ActionDocstring
+from boto3.utils import inject_attribute
+
+from .model import Action
+from .params import create_request_parameters
+from .response import RawHandler, ResourceHandler
+
+logger = logging.getLogger(__name__)
+
+
+class ServiceAction:
+ """
+ A class representing a callable action on a resource, for example
+ ``sqs.get_queue_by_name(...)`` or ``s3.Bucket('foo').delete()``.
+ The action may construct parameters from existing resource identifiers
+ and may return either a raw response or a new resource instance.
+
+ :type action_model: :py:class`~boto3.resources.model.Action`
+ :param action_model: The action model.
+
+ :type factory: ResourceFactory
+ :param factory: The factory that created the resource class to which
+ this action is attached.
+
+ :type service_context: :py:class:`~boto3.utils.ServiceContext`
+ :param service_context: Context about the AWS service
+ """
+
+ def __init__(self, action_model, factory=None, service_context=None):
+ self._action_model = action_model
+
+ # In the simplest case we just return the response, but if a
+ # resource is defined, then we must create these before returning.
+ resource_response_model = action_model.resource
+ if resource_response_model:
+ self._response_handler = ResourceHandler(
+ search_path=resource_response_model.path,
+ factory=factory,
+ resource_model=resource_response_model,
+ service_context=service_context,
+ operation_name=action_model.request.operation,
+ )
+ else:
+ self._response_handler = RawHandler(action_model.path)
+
+ def __call__(self, parent, *args, **kwargs):
+ """
+ Perform the action's request operation after building operation
+ parameters and build any defined resources from the response.
+
+ :type parent: :py:class:`~boto3.resources.base.ServiceResource`
+ :param parent: The resource instance to which this action is attached.
+ :rtype: dict or ServiceResource or list(ServiceResource)
+ :return: The response, either as a raw dict or resource instance(s).
+ """
+ operation_name = xform_name(self._action_model.request.operation)
+
+ # First, build predefined params and then update with the
+ # user-supplied kwargs, which allows overriding the pre-built
+ # params if needed.
+ params = create_request_parameters(parent, self._action_model.request)
+ params.update(kwargs)
+
+ logger.debug(
+ 'Calling %s:%s with %r',
+ parent.meta.service_name,
+ operation_name,
+ params,
+ )
+
+ response = getattr(parent.meta.client, operation_name)(*args, **params)
+
+ logger.debug('Response: %r', response)
+
+ return self._response_handler(parent, params, response)
+
+
+class BatchAction(ServiceAction):
+ """
+ An action which operates on a batch of items in a collection, typically
+ a single page of results from the collection's underlying service
+ operation call. For example, this allows you to delete up to 999
+ S3 objects in a single operation rather than calling ``.delete()`` on
+ each one individually.
+
+ :type action_model: :py:class`~boto3.resources.model.Action`
+ :param action_model: The action model.
+
+ :type factory: ResourceFactory
+ :param factory: The factory that created the resource class to which
+ this action is attached.
+
+ :type service_context: :py:class:`~boto3.utils.ServiceContext`
+ :param service_context: Context about the AWS service
+ """
+
+ def __call__(self, parent, *args, **kwargs):
+ """
+ Perform the batch action's operation on every page of results
+ from the collection.
+
+ :type parent:
+ :py:class:`~boto3.resources.collection.ResourceCollection`
+ :param parent: The collection iterator to which this action
+ is attached.
+ :rtype: list(dict)
+ :return: A list of low-level response dicts from each call.
+ """
+ service_name = None
+ client = None
+ responses = []
+ operation_name = xform_name(self._action_model.request.operation)
+
+ # Unlike the simple action above, a batch action must operate
+ # on batches (or pages) of items. So we get each page, construct
+ # the necessary parameters and call the batch operation.
+ for page in parent.pages():
+ params = {}
+ for index, resource in enumerate(page):
+ # There is no public interface to get a service name
+ # or low-level client from a collection, so we get
+ # these from the first resource in the collection.
+ if service_name is None:
+ service_name = resource.meta.service_name
+ if client is None:
+ client = resource.meta.client
+
+ create_request_parameters(
+ resource,
+ self._action_model.request,
+ params=params,
+ index=index,
+ )
+
+ if not params:
+ # There are no items, no need to make a call.
+ break
+
+ params.update(kwargs)
+
+ logger.debug(
+ 'Calling %s:%s with %r', service_name, operation_name, params
+ )
+
+ response = getattr(client, operation_name)(*args, **params)
+
+ logger.debug('Response: %r', response)
+
+ responses.append(self._response_handler(parent, params, response))
+
+ return responses
+
+
+class WaiterAction:
+ """
+ A class representing a callable waiter action on a resource, for example
+ ``s3.Bucket('foo').wait_until_bucket_exists()``.
+ The waiter action may construct parameters from existing resource
+ identifiers.
+
+ :type waiter_model: :py:class`~boto3.resources.model.Waiter`
+ :param waiter_model: The action waiter.
+ :type waiter_resource_name: string
+ :param waiter_resource_name: The name of the waiter action for the
+ resource. It usually begins with a
+ ``wait_until_``
+ """
+
+ def __init__(self, waiter_model, waiter_resource_name):
+ self._waiter_model = waiter_model
+ self._waiter_resource_name = waiter_resource_name
+
+ def __call__(self, parent, *args, **kwargs):
+ """
+ Perform the wait operation after building operation
+ parameters.
+
+ :type parent: :py:class:`~boto3.resources.base.ServiceResource`
+ :param parent: The resource instance to which this action is attached.
+ """
+ client_waiter_name = xform_name(self._waiter_model.waiter_name)
+
+ # First, build predefined params and then update with the
+ # user-supplied kwargs, which allows overriding the pre-built
+ # params if needed.
+ params = create_request_parameters(parent, self._waiter_model)
+ params.update(kwargs)
+
+ logger.debug(
+ 'Calling %s:%s with %r',
+ parent.meta.service_name,
+ self._waiter_resource_name,
+ params,
+ )
+
+ client = parent.meta.client
+ waiter = client.get_waiter(client_waiter_name)
+ response = waiter.wait(**params)
+
+ logger.debug('Response: %r', response)
+
+
+class CustomModeledAction:
+ """A custom, modeled action to inject into a resource."""
+
+ def __init__(self, action_name, action_model, function, event_emitter):
+ """
+ :type action_name: str
+ :param action_name: The name of the action to inject, e.g.
+ 'delete_tags'
+
+ :type action_model: dict
+ :param action_model: A JSON definition of the action, as if it were
+ part of the resource model.
+
+ :type function: function
+ :param function: The function to perform when the action is called.
+ The first argument should be 'self', which will be the resource
+ the function is to be called on.
+
+ :type event_emitter: :py:class:`botocore.hooks.BaseEventHooks`
+ :param event_emitter: The session event emitter.
+ """
+ self.name = action_name
+ self.model = action_model
+ self.function = function
+ self.emitter = event_emitter
+
+ def inject(self, class_attributes, service_context, event_name, **kwargs):
+ resource_name = event_name.rsplit(".")[-1]
+ action = Action(self.name, self.model, {})
+ self.function.__name__ = self.name
+ self.function.__doc__ = ActionDocstring(
+ resource_name=resource_name,
+ event_emitter=self.emitter,
+ action_model=action,
+ service_model=service_context.service_model,
+ include_signature=False,
+ )
+ inject_attribute(class_attributes, self.name, self.function)
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/base.py b/venv/lib/python3.12/site-packages/boto3/resources/base.py
new file mode 100644
index 0000000..c398280
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/resources/base.py
@@ -0,0 +1,155 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import logging
+
+import boto3
+
+logger = logging.getLogger(__name__)
+
+
+class ResourceMeta:
+ """
+ An object containing metadata about a resource.
+ """
+
+ def __init__(
+ self,
+ service_name,
+ identifiers=None,
+ client=None,
+ data=None,
+ resource_model=None,
+ ):
+ #: (``string``) The service name, e.g. 's3'
+ self.service_name = service_name
+
+ if identifiers is None:
+ identifiers = []
+ #: (``list``) List of identifier names
+ self.identifiers = identifiers
+
+ #: (:py:class:`~botocore.client.BaseClient`) Low-level Botocore client
+ self.client = client
+ #: (``dict``) Loaded resource data attributes
+ self.data = data
+
+ # The resource model for that resource
+ self.resource_model = resource_model
+
+ def __repr__(self):
+ return 'ResourceMeta(\'{}\', identifiers={})'.format(
+ self.service_name, self.identifiers
+ )
+
+ def __eq__(self, other):
+ # Two metas are equal if their components are all equal
+ if other.__class__.__name__ != self.__class__.__name__:
+ return False
+
+ return self.__dict__ == other.__dict__
+
+ def copy(self):
+ """
+ Create a copy of this metadata object.
+ """
+ params = self.__dict__.copy()
+ service_name = params.pop('service_name')
+ return ResourceMeta(service_name, **params)
+
+
+class ServiceResource:
+ """
+ A base class for resources.
+
+ :type client: botocore.client
+ :param client: A low-level Botocore client instance
+ """
+
+ meta = None
+ """
+ Stores metadata about this resource instance, such as the
+ ``service_name``, the low-level ``client`` and any cached ``data``
+ from when the instance was hydrated. For example::
+
+ # Get a low-level client from a resource instance
+ client = resource.meta.client
+ response = client.operation(Param='foo')
+
+ # Print the resource instance's service short name
+ print(resource.meta.service_name)
+
+ See :py:class:`ResourceMeta` for more information.
+ """
+
+ def __init__(self, *args, **kwargs):
+ # Always work on a copy of meta, otherwise we would affect other
+ # instances of the same subclass.
+ self.meta = self.meta.copy()
+
+ # Create a default client if none was passed
+ if kwargs.get('client') is not None:
+ self.meta.client = kwargs.get('client')
+ else:
+ self.meta.client = boto3.client(self.meta.service_name)
+
+ # Allow setting identifiers as positional arguments in the order
+ # in which they were defined in the ResourceJSON.
+ for i, value in enumerate(args):
+ setattr(self, '_' + self.meta.identifiers[i], value)
+
+ # Allow setting identifiers via keyword arguments. Here we need
+ # extra logic to ignore other keyword arguments like ``client``.
+ for name, value in kwargs.items():
+ if name == 'client':
+ continue
+
+ if name not in self.meta.identifiers:
+ raise ValueError(f'Unknown keyword argument: {name}')
+
+ setattr(self, '_' + name, value)
+
+ # Validate that all identifiers have been set.
+ for identifier in self.meta.identifiers:
+ if getattr(self, identifier) is None:
+ raise ValueError(f'Required parameter {identifier} not set')
+
+ def __repr__(self):
+ identifiers = []
+ for identifier in self.meta.identifiers:
+ identifiers.append(
+ f'{identifier}={repr(getattr(self, identifier))}'
+ )
+ return "{}({})".format(
+ self.__class__.__name__,
+ ', '.join(identifiers),
+ )
+
+ def __eq__(self, other):
+ # Should be instances of the same resource class
+ if other.__class__.__name__ != self.__class__.__name__:
+ return False
+
+ # Each of the identifiers should have the same value in both
+ # instances, e.g. two buckets need the same name to be equal.
+ for identifier in self.meta.identifiers:
+ if getattr(self, identifier) != getattr(other, identifier):
+ return False
+
+ return True
+
+ def __hash__(self):
+ identifiers = []
+ for identifier in self.meta.identifiers:
+ identifiers.append(getattr(self, identifier))
+ return hash((self.__class__.__name__, tuple(identifiers)))
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/collection.py b/venv/lib/python3.12/site-packages/boto3/resources/collection.py
new file mode 100644
index 0000000..7f7862e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/resources/collection.py
@@ -0,0 +1,572 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import copy
+import logging
+
+from botocore import xform_name
+from botocore.utils import merge_dicts
+
+from ..docs import docstring
+from .action import BatchAction
+from .params import create_request_parameters
+from .response import ResourceHandler
+
+logger = logging.getLogger(__name__)
+
+
+class ResourceCollection:
+ """
+ Represents a collection of resources, which can be iterated through,
+ optionally with filtering. Collections automatically handle pagination
+ for you.
+
+ See :ref:`guide_collections` for a high-level overview of collections,
+ including when remote service requests are performed.
+
+ :type model: :py:class:`~boto3.resources.model.Collection`
+ :param model: Collection model
+ :type parent: :py:class:`~boto3.resources.base.ServiceResource`
+ :param parent: The collection's parent resource
+ :type handler: :py:class:`~boto3.resources.response.ResourceHandler`
+ :param handler: The resource response handler used to create resource
+ instances
+ """
+
+ def __init__(self, model, parent, handler, **kwargs):
+ self._model = model
+ self._parent = parent
+ self._py_operation_name = xform_name(model.request.operation)
+ self._handler = handler
+ self._params = copy.deepcopy(kwargs)
+
+ def __repr__(self):
+ return '{}({}, {})'.format(
+ self.__class__.__name__,
+ self._parent,
+ '{}.{}'.format(
+ self._parent.meta.service_name, self._model.resource.type
+ ),
+ )
+
+ def __iter__(self):
+ """
+ A generator which yields resource instances after doing the
+ appropriate service operation calls and handling any pagination
+ on your behalf.
+
+ Page size, item limit, and filter parameters are applied
+ if they have previously been set.
+
+ >>> bucket = s3.Bucket('boto3')
+ >>> for obj in bucket.objects.all():
+ ... print(obj.key)
+ 'key1'
+ 'key2'
+
+ """
+ limit = self._params.get('limit', None)
+
+ count = 0
+ for page in self.pages():
+ for item in page:
+ yield item
+
+ # If the limit is set and has been reached, then
+ # we stop processing items here.
+ count += 1
+ if limit is not None and count >= limit:
+ return
+
+ def _clone(self, **kwargs):
+ """
+ Create a clone of this collection. This is used by the methods
+ below to provide a chainable interface that returns copies
+ rather than the original. This allows things like:
+
+ >>> base = collection.filter(Param1=1)
+ >>> query1 = base.filter(Param2=2)
+ >>> query2 = base.filter(Param3=3)
+ >>> query1.params
+ {'Param1': 1, 'Param2': 2}
+ >>> query2.params
+ {'Param1': 1, 'Param3': 3}
+
+ :rtype: :py:class:`ResourceCollection`
+ :return: A clone of this resource collection
+ """
+ params = copy.deepcopy(self._params)
+ merge_dicts(params, kwargs, append_lists=True)
+ clone = self.__class__(
+ self._model, self._parent, self._handler, **params
+ )
+ return clone
+
+ def pages(self):
+ """
+ A generator which yields pages of resource instances after
+ doing the appropriate service operation calls and handling
+ any pagination on your behalf. Non-paginated calls will
+ return a single page of items.
+
+ Page size, item limit, and filter parameters are applied
+ if they have previously been set.
+
+ >>> bucket = s3.Bucket('boto3')
+ >>> for page in bucket.objects.pages():
+ ... for obj in page:
+ ... print(obj.key)
+ 'key1'
+ 'key2'
+
+ :rtype: list(:py:class:`~boto3.resources.base.ServiceResource`)
+ :return: List of resource instances
+ """
+ client = self._parent.meta.client
+ cleaned_params = self._params.copy()
+ limit = cleaned_params.pop('limit', None)
+ page_size = cleaned_params.pop('page_size', None)
+ params = create_request_parameters(self._parent, self._model.request)
+ merge_dicts(params, cleaned_params, append_lists=True)
+
+ # Is this a paginated operation? If so, we need to get an
+ # iterator for the various pages. If not, then we simply
+ # call the operation and return the result as a single
+ # page in a list. For non-paginated results, we just ignore
+ # the page size parameter.
+ if client.can_paginate(self._py_operation_name):
+ logger.debug(
+ 'Calling paginated %s:%s with %r',
+ self._parent.meta.service_name,
+ self._py_operation_name,
+ params,
+ )
+ paginator = client.get_paginator(self._py_operation_name)
+ pages = paginator.paginate(
+ PaginationConfig={'MaxItems': limit, 'PageSize': page_size},
+ **params
+ )
+ else:
+ logger.debug(
+ 'Calling %s:%s with %r',
+ self._parent.meta.service_name,
+ self._py_operation_name,
+ params,
+ )
+ pages = [getattr(client, self._py_operation_name)(**params)]
+
+ # Now that we have a page iterator or single page of results
+ # we start processing and yielding individual items.
+ count = 0
+ for page in pages:
+ page_items = []
+ for item in self._handler(self._parent, params, page):
+ page_items.append(item)
+
+ # If the limit is set and has been reached, then
+ # we stop processing items here.
+ count += 1
+ if limit is not None and count >= limit:
+ break
+
+ yield page_items
+
+ # Stop reading pages if we've reached out limit
+ if limit is not None and count >= limit:
+ break
+
+ def all(self):
+ """
+ Get all items from the collection, optionally with a custom
+ page size and item count limit.
+
+ This method returns an iterable generator which yields
+ individual resource instances. Example use::
+
+ # Iterate through items
+ >>> for queue in sqs.queues.all():
+ ... print(queue.url)
+ 'https://url1'
+ 'https://url2'
+
+ # Convert to list
+ >>> queues = list(sqs.queues.all())
+ >>> len(queues)
+ 2
+ """
+ return self._clone()
+
+ def filter(self, **kwargs):
+ """
+ Get items from the collection, passing keyword arguments along
+ as parameters to the underlying service operation, which are
+ typically used to filter the results.
+
+ This method returns an iterable generator which yields
+ individual resource instances. Example use::
+
+ # Iterate through items
+ >>> for queue in sqs.queues.filter(Param='foo'):
+ ... print(queue.url)
+ 'https://url1'
+ 'https://url2'
+
+ # Convert to list
+ >>> queues = list(sqs.queues.filter(Param='foo'))
+ >>> len(queues)
+ 2
+
+ :rtype: :py:class:`ResourceCollection`
+ """
+ return self._clone(**kwargs)
+
+ def limit(self, count):
+ """
+ Return at most this many resources.
+
+ >>> for bucket in s3.buckets.limit(5):
+ ... print(bucket.name)
+ 'bucket1'
+ 'bucket2'
+ 'bucket3'
+ 'bucket4'
+ 'bucket5'
+
+ :type count: int
+ :param count: Return no more than this many items
+ :rtype: :py:class:`ResourceCollection`
+ """
+ return self._clone(limit=count)
+
+ def page_size(self, count):
+ """
+ Fetch at most this many resources per service request.
+
+ >>> for obj in s3.Bucket('boto3').objects.page_size(100):
+ ... print(obj.key)
+
+ :type count: int
+ :param count: Fetch this many items per request
+ :rtype: :py:class:`ResourceCollection`
+ """
+ return self._clone(page_size=count)
+
+
+class CollectionManager:
+ """
+ A collection manager provides access to resource collection instances,
+ which can be iterated and filtered. The manager exposes some
+ convenience functions that are also found on resource collections,
+ such as :py:meth:`~ResourceCollection.all` and
+ :py:meth:`~ResourceCollection.filter`.
+
+ Get all items::
+
+ >>> for bucket in s3.buckets.all():
+ ... print(bucket.name)
+
+ Get only some items via filtering::
+
+ >>> for queue in sqs.queues.filter(QueueNamePrefix='AWS'):
+ ... print(queue.url)
+
+ Get whole pages of items:
+
+ >>> for page in s3.Bucket('boto3').objects.pages():
+ ... for obj in page:
+ ... print(obj.key)
+
+ A collection manager is not iterable. You **must** call one of the
+ methods that return a :py:class:`ResourceCollection` before trying
+ to iterate, slice, or convert to a list.
+
+ See the :ref:`guide_collections` guide for a high-level overview
+ of collections, including when remote service requests are performed.
+
+ :type collection_model: :py:class:`~boto3.resources.model.Collection`
+ :param model: Collection model
+
+ :type parent: :py:class:`~boto3.resources.base.ServiceResource`
+ :param parent: The collection's parent resource
+
+ :type factory: :py:class:`~boto3.resources.factory.ResourceFactory`
+ :param factory: The resource factory to create new resources
+
+ :type service_context: :py:class:`~boto3.utils.ServiceContext`
+ :param service_context: Context about the AWS service
+ """
+
+ # The class to use when creating an iterator
+ _collection_cls = ResourceCollection
+
+ def __init__(self, collection_model, parent, factory, service_context):
+ self._model = collection_model
+ operation_name = self._model.request.operation
+ self._parent = parent
+
+ search_path = collection_model.resource.path
+ self._handler = ResourceHandler(
+ search_path=search_path,
+ factory=factory,
+ resource_model=collection_model.resource,
+ service_context=service_context,
+ operation_name=operation_name,
+ )
+
+ def __repr__(self):
+ return '{}({}, {})'.format(
+ self.__class__.__name__,
+ self._parent,
+ '{}.{}'.format(
+ self._parent.meta.service_name, self._model.resource.type
+ ),
+ )
+
+ def iterator(self, **kwargs):
+ """
+ Get a resource collection iterator from this manager.
+
+ :rtype: :py:class:`ResourceCollection`
+ :return: An iterable representing the collection of resources
+ """
+ return self._collection_cls(
+ self._model, self._parent, self._handler, **kwargs
+ )
+
+ # Set up some methods to proxy ResourceCollection methods
+ def all(self):
+ return self.iterator()
+
+ all.__doc__ = ResourceCollection.all.__doc__
+
+ def filter(self, **kwargs):
+ return self.iterator(**kwargs)
+
+ filter.__doc__ = ResourceCollection.filter.__doc__
+
+ def limit(self, count):
+ return self.iterator(limit=count)
+
+ limit.__doc__ = ResourceCollection.limit.__doc__
+
+ def page_size(self, count):
+ return self.iterator(page_size=count)
+
+ page_size.__doc__ = ResourceCollection.page_size.__doc__
+
+ def pages(self):
+ return self.iterator().pages()
+
+ pages.__doc__ = ResourceCollection.pages.__doc__
+
+
+class CollectionFactory:
+ """
+ A factory to create new
+ :py:class:`CollectionManager` and :py:class:`ResourceCollection`
+ subclasses from a :py:class:`~boto3.resources.model.Collection`
+ model. These subclasses include methods to perform batch operations.
+ """
+
+ def load_from_definition(
+ self, resource_name, collection_model, service_context, event_emitter
+ ):
+ """
+ Loads a collection from a model, creating a new
+ :py:class:`CollectionManager` subclass
+ with the correct properties and methods, named based on the service
+ and resource name, e.g. ec2.InstanceCollectionManager. It also
+ creates a new :py:class:`ResourceCollection` subclass which is used
+ by the new manager class.
+
+ :type resource_name: string
+ :param resource_name: Name of the resource to look up. For services,
+ this should match the ``service_name``.
+
+ :type service_context: :py:class:`~boto3.utils.ServiceContext`
+ :param service_context: Context about the AWS service
+
+ :type event_emitter: :py:class:`~botocore.hooks.HierarchialEmitter`
+ :param event_emitter: An event emitter
+
+ :rtype: Subclass of :py:class:`CollectionManager`
+ :return: The collection class.
+ """
+ attrs = {}
+ collection_name = collection_model.name
+
+ # Create the batch actions for a collection
+ self._load_batch_actions(
+ attrs,
+ resource_name,
+ collection_model,
+ service_context.service_model,
+ event_emitter,
+ )
+ # Add the documentation to the collection class's methods
+ self._load_documented_collection_methods(
+ attrs=attrs,
+ resource_name=resource_name,
+ collection_model=collection_model,
+ service_model=service_context.service_model,
+ event_emitter=event_emitter,
+ base_class=ResourceCollection,
+ )
+
+ if service_context.service_name == resource_name:
+ cls_name = '{}.{}Collection'.format(
+ service_context.service_name, collection_name
+ )
+ else:
+ cls_name = '{}.{}.{}Collection'.format(
+ service_context.service_name, resource_name, collection_name
+ )
+
+ collection_cls = type(str(cls_name), (ResourceCollection,), attrs)
+
+ # Add the documentation to the collection manager's methods
+ self._load_documented_collection_methods(
+ attrs=attrs,
+ resource_name=resource_name,
+ collection_model=collection_model,
+ service_model=service_context.service_model,
+ event_emitter=event_emitter,
+ base_class=CollectionManager,
+ )
+ attrs['_collection_cls'] = collection_cls
+ cls_name += 'Manager'
+
+ return type(str(cls_name), (CollectionManager,), attrs)
+
+ def _load_batch_actions(
+ self,
+ attrs,
+ resource_name,
+ collection_model,
+ service_model,
+ event_emitter,
+ ):
+ """
+ Batch actions on the collection become methods on both
+ the collection manager and iterators.
+ """
+ for action_model in collection_model.batch_actions:
+ snake_cased = xform_name(action_model.name)
+ attrs[snake_cased] = self._create_batch_action(
+ resource_name,
+ snake_cased,
+ action_model,
+ collection_model,
+ service_model,
+ event_emitter,
+ )
+
+ def _load_documented_collection_methods(
+ factory_self,
+ attrs,
+ resource_name,
+ collection_model,
+ service_model,
+ event_emitter,
+ base_class,
+ ):
+ # The base class already has these methods defined. However
+ # the docstrings are generic and not based for a particular service
+ # or resource. So we override these methods by proxying to the
+ # base class's builtin method and adding a docstring
+ # that pertains to the resource.
+
+ # A collection's all() method.
+ def all(self):
+ return base_class.all(self)
+
+ all.__doc__ = docstring.CollectionMethodDocstring(
+ resource_name=resource_name,
+ action_name='all',
+ event_emitter=event_emitter,
+ collection_model=collection_model,
+ service_model=service_model,
+ include_signature=False,
+ )
+ attrs['all'] = all
+
+ # The collection's filter() method.
+ def filter(self, **kwargs):
+ return base_class.filter(self, **kwargs)
+
+ filter.__doc__ = docstring.CollectionMethodDocstring(
+ resource_name=resource_name,
+ action_name='filter',
+ event_emitter=event_emitter,
+ collection_model=collection_model,
+ service_model=service_model,
+ include_signature=False,
+ )
+ attrs['filter'] = filter
+
+ # The collection's limit method.
+ def limit(self, count):
+ return base_class.limit(self, count)
+
+ limit.__doc__ = docstring.CollectionMethodDocstring(
+ resource_name=resource_name,
+ action_name='limit',
+ event_emitter=event_emitter,
+ collection_model=collection_model,
+ service_model=service_model,
+ include_signature=False,
+ )
+ attrs['limit'] = limit
+
+ # The collection's page_size method.
+ def page_size(self, count):
+ return base_class.page_size(self, count)
+
+ page_size.__doc__ = docstring.CollectionMethodDocstring(
+ resource_name=resource_name,
+ action_name='page_size',
+ event_emitter=event_emitter,
+ collection_model=collection_model,
+ service_model=service_model,
+ include_signature=False,
+ )
+ attrs['page_size'] = page_size
+
+ def _create_batch_action(
+ factory_self,
+ resource_name,
+ snake_cased,
+ action_model,
+ collection_model,
+ service_model,
+ event_emitter,
+ ):
+ """
+ Creates a new method which makes a batch operation request
+ to the underlying service API.
+ """
+ action = BatchAction(action_model)
+
+ def batch_action(self, *args, **kwargs):
+ return action(self, *args, **kwargs)
+
+ batch_action.__name__ = str(snake_cased)
+ batch_action.__doc__ = docstring.BatchActionDocstring(
+ resource_name=resource_name,
+ event_emitter=event_emitter,
+ batch_action_model=action_model,
+ service_model=service_model,
+ collection_model=collection_model,
+ include_signature=False,
+ )
+ return batch_action
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/factory.py b/venv/lib/python3.12/site-packages/boto3/resources/factory.py
new file mode 100644
index 0000000..4cdd2f0
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/resources/factory.py
@@ -0,0 +1,601 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import logging
+from functools import partial
+
+from ..docs import docstring
+from ..exceptions import ResourceLoadException
+from .action import ServiceAction, WaiterAction
+from .base import ResourceMeta, ServiceResource
+from .collection import CollectionFactory
+from .model import ResourceModel
+from .response import ResourceHandler, build_identifiers
+
+logger = logging.getLogger(__name__)
+
+
+class ResourceFactory:
+ """
+ A factory to create new :py:class:`~boto3.resources.base.ServiceResource`
+ classes from a :py:class:`~boto3.resources.model.ResourceModel`. There are
+ two types of lookups that can be done: one on the service itself (e.g. an
+ SQS resource) and another on models contained within the service (e.g. an
+ SQS Queue resource).
+ """
+
+ def __init__(self, emitter):
+ self._collection_factory = CollectionFactory()
+ self._emitter = emitter
+
+ def load_from_definition(
+ self, resource_name, single_resource_json_definition, service_context
+ ):
+ """
+ Loads a resource from a model, creating a new
+ :py:class:`~boto3.resources.base.ServiceResource` subclass
+ with the correct properties and methods, named based on the service
+ and resource name, e.g. EC2.Instance.
+
+ :type resource_name: string
+ :param resource_name: Name of the resource to look up. For services,
+ this should match the ``service_name``.
+
+ :type single_resource_json_definition: dict
+ :param single_resource_json_definition:
+ The loaded json of a single service resource or resource
+ definition.
+
+ :type service_context: :py:class:`~boto3.utils.ServiceContext`
+ :param service_context: Context about the AWS service
+
+ :rtype: Subclass of :py:class:`~boto3.resources.base.ServiceResource`
+ :return: The service or resource class.
+ """
+ logger.debug(
+ 'Loading %s:%s', service_context.service_name, resource_name
+ )
+
+ # Using the loaded JSON create a ResourceModel object.
+ resource_model = ResourceModel(
+ resource_name,
+ single_resource_json_definition,
+ service_context.resource_json_definitions,
+ )
+
+ # Do some renaming of the shape if there was a naming collision
+ # that needed to be accounted for.
+ shape = None
+ if resource_model.shape:
+ shape = service_context.service_model.shape_for(
+ resource_model.shape
+ )
+ resource_model.load_rename_map(shape)
+
+ # Set some basic info
+ meta = ResourceMeta(
+ service_context.service_name, resource_model=resource_model
+ )
+ attrs = {
+ 'meta': meta,
+ }
+
+ # Create and load all of attributes of the resource class based
+ # on the models.
+
+ # Identifiers
+ self._load_identifiers(
+ attrs=attrs,
+ meta=meta,
+ resource_name=resource_name,
+ resource_model=resource_model,
+ )
+
+ # Load/Reload actions
+ self._load_actions(
+ attrs=attrs,
+ resource_name=resource_name,
+ resource_model=resource_model,
+ service_context=service_context,
+ )
+
+ # Attributes that get auto-loaded
+ self._load_attributes(
+ attrs=attrs,
+ meta=meta,
+ resource_name=resource_name,
+ resource_model=resource_model,
+ service_context=service_context,
+ )
+
+ # Collections and their corresponding methods
+ self._load_collections(
+ attrs=attrs,
+ resource_model=resource_model,
+ service_context=service_context,
+ )
+
+ # References and Subresources
+ self._load_has_relations(
+ attrs=attrs,
+ resource_name=resource_name,
+ resource_model=resource_model,
+ service_context=service_context,
+ )
+
+ # Waiter resource actions
+ self._load_waiters(
+ attrs=attrs,
+ resource_name=resource_name,
+ resource_model=resource_model,
+ service_context=service_context,
+ )
+
+ # Create the name based on the requested service and resource
+ cls_name = resource_name
+ if service_context.service_name == resource_name:
+ cls_name = 'ServiceResource'
+ cls_name = service_context.service_name + '.' + cls_name
+
+ base_classes = [ServiceResource]
+ if self._emitter is not None:
+ self._emitter.emit(
+ f'creating-resource-class.{cls_name}',
+ class_attributes=attrs,
+ base_classes=base_classes,
+ service_context=service_context,
+ )
+ return type(str(cls_name), tuple(base_classes), attrs)
+
+ def _load_identifiers(self, attrs, meta, resource_model, resource_name):
+ """
+ Populate required identifiers. These are arguments without which
+ the resource cannot be used. Identifiers become arguments for
+ operations on the resource.
+ """
+ for identifier in resource_model.identifiers:
+ meta.identifiers.append(identifier.name)
+ attrs[identifier.name] = self._create_identifier(
+ identifier, resource_name
+ )
+
+ def _load_actions(
+ self, attrs, resource_name, resource_model, service_context
+ ):
+ """
+ Actions on the resource become methods, with the ``load`` method
+ being a special case which sets internal data for attributes, and
+ ``reload`` is an alias for ``load``.
+ """
+ if resource_model.load:
+ attrs['load'] = self._create_action(
+ action_model=resource_model.load,
+ resource_name=resource_name,
+ service_context=service_context,
+ is_load=True,
+ )
+ attrs['reload'] = attrs['load']
+
+ for action in resource_model.actions:
+ attrs[action.name] = self._create_action(
+ action_model=action,
+ resource_name=resource_name,
+ service_context=service_context,
+ )
+
+ def _load_attributes(
+ self, attrs, meta, resource_name, resource_model, service_context
+ ):
+ """
+ Load resource attributes based on the resource shape. The shape
+ name is referenced in the resource JSON, but the shape itself
+ is defined in the Botocore service JSON, hence the need for
+ access to the ``service_model``.
+ """
+ if not resource_model.shape:
+ return
+
+ shape = service_context.service_model.shape_for(resource_model.shape)
+
+ identifiers = {
+ i.member_name: i
+ for i in resource_model.identifiers
+ if i.member_name
+ }
+ attributes = resource_model.get_attributes(shape)
+ for name, (orig_name, member) in attributes.items():
+ if name in identifiers:
+ prop = self._create_identifier_alias(
+ resource_name=resource_name,
+ identifier=identifiers[name],
+ member_model=member,
+ service_context=service_context,
+ )
+ else:
+ prop = self._create_autoload_property(
+ resource_name=resource_name,
+ name=orig_name,
+ snake_cased=name,
+ member_model=member,
+ service_context=service_context,
+ )
+ attrs[name] = prop
+
+ def _load_collections(self, attrs, resource_model, service_context):
+ """
+ Load resource collections from the model. Each collection becomes
+ a :py:class:`~boto3.resources.collection.CollectionManager` instance
+ on the resource instance, which allows you to iterate and filter
+ through the collection's items.
+ """
+ for collection_model in resource_model.collections:
+ attrs[collection_model.name] = self._create_collection(
+ resource_name=resource_model.name,
+ collection_model=collection_model,
+ service_context=service_context,
+ )
+
+ def _load_has_relations(
+ self, attrs, resource_name, resource_model, service_context
+ ):
+ """
+ Load related resources, which are defined via a ``has``
+ relationship but conceptually come in two forms:
+
+ 1. A reference, which is a related resource instance and can be
+ ``None``, such as an EC2 instance's ``vpc``.
+ 2. A subresource, which is a resource constructor that will always
+ return a resource instance which shares identifiers/data with
+ this resource, such as ``s3.Bucket('name').Object('key')``.
+ """
+ for reference in resource_model.references:
+ # This is a dangling reference, i.e. we have all
+ # the data we need to create the resource, so
+ # this instance becomes an attribute on the class.
+ attrs[reference.name] = self._create_reference(
+ reference_model=reference,
+ resource_name=resource_name,
+ service_context=service_context,
+ )
+
+ for subresource in resource_model.subresources:
+ # This is a sub-resource class you can create
+ # by passing in an identifier, e.g. s3.Bucket(name).
+ attrs[subresource.name] = self._create_class_partial(
+ subresource_model=subresource,
+ resource_name=resource_name,
+ service_context=service_context,
+ )
+
+ self._create_available_subresources_command(
+ attrs, resource_model.subresources
+ )
+
+ def _create_available_subresources_command(self, attrs, subresources):
+ _subresources = [subresource.name for subresource in subresources]
+ _subresources = sorted(_subresources)
+
+ def get_available_subresources(factory_self):
+ """
+ Returns a list of all the available sub-resources for this
+ Resource.
+
+ :returns: A list containing the name of each sub-resource for this
+ resource
+ :rtype: list of str
+ """
+ return _subresources
+
+ attrs['get_available_subresources'] = get_available_subresources
+
+ def _load_waiters(
+ self, attrs, resource_name, resource_model, service_context
+ ):
+ """
+ Load resource waiters from the model. Each waiter allows you to
+ wait until a resource reaches a specific state by polling the state
+ of the resource.
+ """
+ for waiter in resource_model.waiters:
+ attrs[waiter.name] = self._create_waiter(
+ resource_waiter_model=waiter,
+ resource_name=resource_name,
+ service_context=service_context,
+ )
+
+ def _create_identifier(factory_self, identifier, resource_name):
+ """
+ Creates a read-only property for identifier attributes.
+ """
+
+ def get_identifier(self):
+ # The default value is set to ``None`` instead of
+ # raising an AttributeError because when resources are
+ # instantiated a check is made such that none of the
+ # identifiers have a value ``None``. If any are ``None``,
+ # a more informative user error than a generic AttributeError
+ # is raised.
+ return getattr(self, '_' + identifier.name, None)
+
+ get_identifier.__name__ = str(identifier.name)
+ get_identifier.__doc__ = docstring.IdentifierDocstring(
+ resource_name=resource_name,
+ identifier_model=identifier,
+ include_signature=False,
+ )
+
+ return property(get_identifier)
+
+ def _create_identifier_alias(
+ factory_self, resource_name, identifier, member_model, service_context
+ ):
+ """
+ Creates a read-only property that aliases an identifier.
+ """
+
+ def get_identifier(self):
+ return getattr(self, '_' + identifier.name, None)
+
+ get_identifier.__name__ = str(identifier.member_name)
+ get_identifier.__doc__ = docstring.AttributeDocstring(
+ service_name=service_context.service_name,
+ resource_name=resource_name,
+ attr_name=identifier.member_name,
+ event_emitter=factory_self._emitter,
+ attr_model=member_model,
+ include_signature=False,
+ )
+
+ return property(get_identifier)
+
+ def _create_autoload_property(
+ factory_self,
+ resource_name,
+ name,
+ snake_cased,
+ member_model,
+ service_context,
+ ):
+ """
+ Creates a new property on the resource to lazy-load its value
+ via the resource's ``load`` method (if it exists).
+ """
+
+ # The property loader will check to see if this resource has already
+ # been loaded and return the cached value if possible. If not, then
+ # it first checks to see if it CAN be loaded (raise if not), then
+ # calls the load before returning the value.
+ def property_loader(self):
+ if self.meta.data is None:
+ if hasattr(self, 'load'):
+ self.load()
+ else:
+ raise ResourceLoadException(
+ f'{self.__class__.__name__} has no load method'
+ )
+
+ return self.meta.data.get(name)
+
+ property_loader.__name__ = str(snake_cased)
+ property_loader.__doc__ = docstring.AttributeDocstring(
+ service_name=service_context.service_name,
+ resource_name=resource_name,
+ attr_name=snake_cased,
+ event_emitter=factory_self._emitter,
+ attr_model=member_model,
+ include_signature=False,
+ )
+
+ return property(property_loader)
+
+ def _create_waiter(
+ factory_self, resource_waiter_model, resource_name, service_context
+ ):
+ """
+ Creates a new wait method for each resource where both a waiter and
+ resource model is defined.
+ """
+ waiter = WaiterAction(
+ resource_waiter_model,
+ waiter_resource_name=resource_waiter_model.name,
+ )
+
+ def do_waiter(self, *args, **kwargs):
+ waiter(self, *args, **kwargs)
+
+ do_waiter.__name__ = str(resource_waiter_model.name)
+ do_waiter.__doc__ = docstring.ResourceWaiterDocstring(
+ resource_name=resource_name,
+ event_emitter=factory_self._emitter,
+ service_model=service_context.service_model,
+ resource_waiter_model=resource_waiter_model,
+ service_waiter_model=service_context.service_waiter_model,
+ include_signature=False,
+ )
+ return do_waiter
+
+ def _create_collection(
+ factory_self, resource_name, collection_model, service_context
+ ):
+ """
+ Creates a new property on the resource to lazy-load a collection.
+ """
+ cls = factory_self._collection_factory.load_from_definition(
+ resource_name=resource_name,
+ collection_model=collection_model,
+ service_context=service_context,
+ event_emitter=factory_self._emitter,
+ )
+
+ def get_collection(self):
+ return cls(
+ collection_model=collection_model,
+ parent=self,
+ factory=factory_self,
+ service_context=service_context,
+ )
+
+ get_collection.__name__ = str(collection_model.name)
+ get_collection.__doc__ = docstring.CollectionDocstring(
+ collection_model=collection_model, include_signature=False
+ )
+ return property(get_collection)
+
+ def _create_reference(
+ factory_self, reference_model, resource_name, service_context
+ ):
+ """
+ Creates a new property on the resource to lazy-load a reference.
+ """
+ # References are essentially an action with no request
+ # or response, so we can re-use the response handlers to
+ # build up resources from identifiers and data members.
+ handler = ResourceHandler(
+ search_path=reference_model.resource.path,
+ factory=factory_self,
+ resource_model=reference_model.resource,
+ service_context=service_context,
+ )
+
+ # Are there any identifiers that need access to data members?
+ # This is important when building the resource below since
+ # it requires the data to be loaded.
+ needs_data = any(
+ i.source == 'data' for i in reference_model.resource.identifiers
+ )
+
+ def get_reference(self):
+ # We need to lazy-evaluate the reference to handle circular
+ # references between resources. We do this by loading the class
+ # when first accessed.
+ # This is using a *response handler* so we need to make sure
+ # our data is loaded (if possible) and pass that data into
+ # the handler as if it were a response. This allows references
+ # to have their data loaded properly.
+ if needs_data and self.meta.data is None and hasattr(self, 'load'):
+ self.load()
+ return handler(self, {}, self.meta.data)
+
+ get_reference.__name__ = str(reference_model.name)
+ get_reference.__doc__ = docstring.ReferenceDocstring(
+ reference_model=reference_model, include_signature=False
+ )
+ return property(get_reference)
+
+ def _create_class_partial(
+ factory_self, subresource_model, resource_name, service_context
+ ):
+ """
+ Creates a new method which acts as a functools.partial, passing
+ along the instance's low-level `client` to the new resource
+ class' constructor.
+ """
+ name = subresource_model.resource.type
+
+ def create_resource(self, *args, **kwargs):
+ # We need a new method here because we want access to the
+ # instance's client.
+ positional_args = []
+
+ # We lazy-load the class to handle circular references.
+ json_def = service_context.resource_json_definitions.get(name, {})
+ resource_cls = factory_self.load_from_definition(
+ resource_name=name,
+ single_resource_json_definition=json_def,
+ service_context=service_context,
+ )
+
+ # Assumes that identifiers are in order, which lets you do
+ # e.g. ``sqs.Queue('foo').Message('bar')`` to create a new message
+ # linked with the ``foo`` queue and which has a ``bar`` receipt
+ # handle. If we did kwargs here then future positional arguments
+ # would lead to failure.
+ identifiers = subresource_model.resource.identifiers
+ if identifiers is not None:
+ for identifier, value in build_identifiers(identifiers, self):
+ positional_args.append(value)
+
+ return partial(
+ resource_cls, *positional_args, client=self.meta.client
+ )(*args, **kwargs)
+
+ create_resource.__name__ = str(name)
+ create_resource.__doc__ = docstring.SubResourceDocstring(
+ resource_name=resource_name,
+ sub_resource_model=subresource_model,
+ service_model=service_context.service_model,
+ include_signature=False,
+ )
+ return create_resource
+
+ def _create_action(
+ factory_self,
+ action_model,
+ resource_name,
+ service_context,
+ is_load=False,
+ ):
+ """
+ Creates a new method which makes a request to the underlying
+ AWS service.
+ """
+ # Create the action in in this closure but before the ``do_action``
+ # method below is invoked, which allows instances of the resource
+ # to share the ServiceAction instance.
+ action = ServiceAction(
+ action_model, factory=factory_self, service_context=service_context
+ )
+
+ # A resource's ``load`` method is special because it sets
+ # values on the resource instead of returning the response.
+ if is_load:
+ # We need a new method here because we want access to the
+ # instance via ``self``.
+ def do_action(self, *args, **kwargs):
+ response = action(self, *args, **kwargs)
+ self.meta.data = response
+
+ # Create the docstring for the load/reload methods.
+ lazy_docstring = docstring.LoadReloadDocstring(
+ action_name=action_model.name,
+ resource_name=resource_name,
+ event_emitter=factory_self._emitter,
+ load_model=action_model,
+ service_model=service_context.service_model,
+ include_signature=False,
+ )
+ else:
+ # We need a new method here because we want access to the
+ # instance via ``self``.
+ def do_action(self, *args, **kwargs):
+ response = action(self, *args, **kwargs)
+
+ if hasattr(self, 'load'):
+ # Clear cached data. It will be reloaded the next
+ # time that an attribute is accessed.
+ # TODO: Make this configurable in the future?
+ self.meta.data = None
+
+ return response
+
+ lazy_docstring = docstring.ActionDocstring(
+ resource_name=resource_name,
+ event_emitter=factory_self._emitter,
+ action_model=action_model,
+ service_model=service_context.service_model,
+ include_signature=False,
+ )
+
+ do_action.__name__ = str(action_model.name)
+ do_action.__doc__ = lazy_docstring
+ return do_action
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/model.py b/venv/lib/python3.12/site-packages/boto3/resources/model.py
new file mode 100644
index 0000000..29371ee
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/resources/model.py
@@ -0,0 +1,632 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+"""
+The models defined in this file represent the resource JSON description
+format and provide a layer of abstraction from the raw JSON. The advantages
+of this are:
+
+* Pythonic interface (e.g. ``action.request.operation``)
+* Consumers need not change for minor JSON changes (e.g. renamed field)
+
+These models are used both by the resource factory to generate resource
+classes as well as by the documentation generator.
+"""
+
+import logging
+
+from botocore import xform_name
+
+logger = logging.getLogger(__name__)
+
+
+class Identifier:
+ """
+ A resource identifier, given by its name.
+
+ :type name: string
+ :param name: The name of the identifier
+ """
+
+ def __init__(self, name, member_name=None):
+ #: (``string``) The name of the identifier
+ self.name = name
+ self.member_name = member_name
+
+
+class Action:
+ """
+ A service operation action.
+
+ :type name: string
+ :param name: The name of the action
+ :type definition: dict
+ :param definition: The JSON definition
+ :type resource_defs: dict
+ :param resource_defs: All resources defined in the service
+ """
+
+ def __init__(self, name, definition, resource_defs):
+ self._definition = definition
+
+ #: (``string``) The name of the action
+ self.name = name
+ #: (:py:class:`Request`) This action's request or ``None``
+ self.request = None
+ if 'request' in definition:
+ self.request = Request(definition.get('request', {}))
+ #: (:py:class:`ResponseResource`) This action's resource or ``None``
+ self.resource = None
+ if 'resource' in definition:
+ self.resource = ResponseResource(
+ definition.get('resource', {}), resource_defs
+ )
+ #: (``string``) The JMESPath search path or ``None``
+ self.path = definition.get('path')
+
+
+class DefinitionWithParams:
+ """
+ An item which has parameters exposed via the ``params`` property.
+ A request has an operation and parameters, while a waiter has
+ a name, a low-level waiter name and parameters.
+
+ :type definition: dict
+ :param definition: The JSON definition
+ """
+
+ def __init__(self, definition):
+ self._definition = definition
+
+ @property
+ def params(self):
+ """
+ Get a list of auto-filled parameters for this request.
+
+ :type: list(:py:class:`Parameter`)
+ """
+ params = []
+
+ for item in self._definition.get('params', []):
+ params.append(Parameter(**item))
+
+ return params
+
+
+class Parameter:
+ """
+ An auto-filled parameter which has a source and target. For example,
+ the ``QueueUrl`` may be auto-filled from a resource's ``url`` identifier
+ when making calls to ``queue.receive_messages``.
+
+ :type target: string
+ :param target: The destination parameter name, e.g. ``QueueUrl``
+ :type source_type: string
+ :param source_type: Where the source is defined.
+ :type source: string
+ :param source: The source name, e.g. ``Url``
+ """
+
+ def __init__(
+ self, target, source, name=None, path=None, value=None, **kwargs
+ ):
+ #: (``string``) The destination parameter name
+ self.target = target
+ #: (``string``) Where the source is defined
+ self.source = source
+ #: (``string``) The name of the source, if given
+ self.name = name
+ #: (``string``) The JMESPath query of the source
+ self.path = path
+ #: (``string|int|float|bool``) The source constant value
+ self.value = value
+
+ # Complain if we encounter any unknown values.
+ if kwargs:
+ logger.warning('Unknown parameter options found: %s', kwargs)
+
+
+class Request(DefinitionWithParams):
+ """
+ A service operation action request.
+
+ :type definition: dict
+ :param definition: The JSON definition
+ """
+
+ def __init__(self, definition):
+ super().__init__(definition)
+
+ #: (``string``) The name of the low-level service operation
+ self.operation = definition.get('operation')
+
+
+class Waiter(DefinitionWithParams):
+ """
+ An event waiter specification.
+
+ :type name: string
+ :param name: Name of the waiter
+ :type definition: dict
+ :param definition: The JSON definition
+ """
+
+ PREFIX = 'WaitUntil'
+
+ def __init__(self, name, definition):
+ super().__init__(definition)
+
+ #: (``string``) The name of this waiter
+ self.name = name
+
+ #: (``string``) The name of the underlying event waiter
+ self.waiter_name = definition.get('waiterName')
+
+
+class ResponseResource:
+ """
+ A resource response to create after performing an action.
+
+ :type definition: dict
+ :param definition: The JSON definition
+ :type resource_defs: dict
+ :param resource_defs: All resources defined in the service
+ """
+
+ def __init__(self, definition, resource_defs):
+ self._definition = definition
+ self._resource_defs = resource_defs
+
+ #: (``string``) The name of the response resource type
+ self.type = definition.get('type')
+
+ #: (``string``) The JMESPath search query or ``None``
+ self.path = definition.get('path')
+
+ @property
+ def identifiers(self):
+ """
+ A list of resource identifiers.
+
+ :type: list(:py:class:`Identifier`)
+ """
+ identifiers = []
+
+ for item in self._definition.get('identifiers', []):
+ identifiers.append(Parameter(**item))
+
+ return identifiers
+
+ @property
+ def model(self):
+ """
+ Get the resource model for the response resource.
+
+ :type: :py:class:`ResourceModel`
+ """
+ return ResourceModel(
+ self.type, self._resource_defs[self.type], self._resource_defs
+ )
+
+
+class Collection(Action):
+ """
+ A group of resources. See :py:class:`Action`.
+
+ :type name: string
+ :param name: The name of the collection
+ :type definition: dict
+ :param definition: The JSON definition
+ :type resource_defs: dict
+ :param resource_defs: All resources defined in the service
+ """
+
+ @property
+ def batch_actions(self):
+ """
+ Get a list of batch actions supported by the resource type
+ contained in this action. This is a shortcut for accessing
+ the same information through the resource model.
+
+ :rtype: list(:py:class:`Action`)
+ """
+ return self.resource.model.batch_actions
+
+
+class ResourceModel:
+ """
+ A model representing a resource, defined via a JSON description
+ format. A resource has identifiers, attributes, actions,
+ sub-resources, references and collections. For more information
+ on resources, see :ref:`guide_resources`.
+
+ :type name: string
+ :param name: The name of this resource, e.g. ``sqs`` or ``Queue``
+ :type definition: dict
+ :param definition: The JSON definition
+ :type resource_defs: dict
+ :param resource_defs: All resources defined in the service
+ """
+
+ def __init__(self, name, definition, resource_defs):
+ self._definition = definition
+ self._resource_defs = resource_defs
+ self._renamed = {}
+
+ #: (``string``) The name of this resource
+ self.name = name
+ #: (``string``) The service shape name for this resource or ``None``
+ self.shape = definition.get('shape')
+
+ def load_rename_map(self, shape=None):
+ """
+ Load a name translation map given a shape. This will set
+ up renamed values for any collisions, e.g. if the shape,
+ an action, and a subresource all are all named ``foo``
+ then the resource will have an action ``foo``, a subresource
+ named ``Foo`` and a property named ``foo_attribute``.
+ This is the order of precedence, from most important to
+ least important:
+
+ * Load action (resource.load)
+ * Identifiers
+ * Actions
+ * Subresources
+ * References
+ * Collections
+ * Waiters
+ * Attributes (shape members)
+
+ Batch actions are only exposed on collections, so do not
+ get modified here. Subresources use upper camel casing, so
+ are unlikely to collide with anything but other subresources.
+
+ Creates a structure like this::
+
+ renames = {
+ ('action', 'id'): 'id_action',
+ ('collection', 'id'): 'id_collection',
+ ('attribute', 'id'): 'id_attribute'
+ }
+
+ # Get the final name for an action named 'id'
+ name = renames.get(('action', 'id'), 'id')
+
+ :type shape: botocore.model.Shape
+ :param shape: The underlying shape for this resource.
+ """
+ # Meta is a reserved name for resources
+ names = {'meta'}
+ self._renamed = {}
+
+ if self._definition.get('load'):
+ names.add('load')
+
+ for item in self._definition.get('identifiers', []):
+ self._load_name_with_category(names, item['name'], 'identifier')
+
+ for name in self._definition.get('actions', {}):
+ self._load_name_with_category(names, name, 'action')
+
+ for name, ref in self._get_has_definition().items():
+ # Subresources require no data members, just typically
+ # identifiers and user input.
+ data_required = False
+ for identifier in ref['resource']['identifiers']:
+ if identifier['source'] == 'data':
+ data_required = True
+ break
+
+ if not data_required:
+ self._load_name_with_category(
+ names, name, 'subresource', snake_case=False
+ )
+ else:
+ self._load_name_with_category(names, name, 'reference')
+
+ for name in self._definition.get('hasMany', {}):
+ self._load_name_with_category(names, name, 'collection')
+
+ for name in self._definition.get('waiters', {}):
+ self._load_name_with_category(
+ names, Waiter.PREFIX + name, 'waiter'
+ )
+
+ if shape is not None:
+ for name in shape.members.keys():
+ self._load_name_with_category(names, name, 'attribute')
+
+ def _load_name_with_category(self, names, name, category, snake_case=True):
+ """
+ Load a name with a given category, possibly renaming it
+ if that name is already in use. The name will be stored
+ in ``names`` and possibly be set up in ``self._renamed``.
+
+ :type names: set
+ :param names: Existing names (Python attributes, properties, or
+ methods) on the resource.
+ :type name: string
+ :param name: The original name of the value.
+ :type category: string
+ :param category: The value type, such as 'identifier' or 'action'
+ :type snake_case: bool
+ :param snake_case: True (default) if the name should be snake cased.
+ """
+ if snake_case:
+ name = xform_name(name)
+
+ if name in names:
+ logger.debug(f'Renaming {self.name} {category} {name}')
+ self._renamed[(category, name)] = name + '_' + category
+ name += '_' + category
+
+ if name in names:
+ # This isn't good, let's raise instead of trying to keep
+ # renaming this value.
+ raise ValueError(
+ 'Problem renaming {} {} to {}!'.format(
+ self.name, category, name
+ )
+ )
+
+ names.add(name)
+
+ def _get_name(self, category, name, snake_case=True):
+ """
+ Get a possibly renamed value given a category and name. This
+ uses the rename map set up in ``load_rename_map``, so that
+ method must be called once first.
+
+ :type category: string
+ :param category: The value type, such as 'identifier' or 'action'
+ :type name: string
+ :param name: The original name of the value
+ :type snake_case: bool
+ :param snake_case: True (default) if the name should be snake cased.
+ :rtype: string
+ :return: Either the renamed value if it is set, otherwise the
+ original name.
+ """
+ if snake_case:
+ name = xform_name(name)
+
+ return self._renamed.get((category, name), name)
+
+ def get_attributes(self, shape):
+ """
+ Get a dictionary of attribute names to original name and shape
+ models that represent the attributes of this resource. Looks
+ like the following:
+
+ {
+ 'some_name': ('SomeName', )
+ }
+
+ :type shape: botocore.model.Shape
+ :param shape: The underlying shape for this resource.
+ :rtype: dict
+ :return: Mapping of resource attributes.
+ """
+ attributes = {}
+ identifier_names = [i.name for i in self.identifiers]
+
+ for name, member in shape.members.items():
+ snake_cased = xform_name(name)
+ if snake_cased in identifier_names:
+ # Skip identifiers, these are set through other means
+ continue
+ snake_cased = self._get_name(
+ 'attribute', snake_cased, snake_case=False
+ )
+ attributes[snake_cased] = (name, member)
+
+ return attributes
+
+ @property
+ def identifiers(self):
+ """
+ Get a list of resource identifiers.
+
+ :type: list(:py:class:`Identifier`)
+ """
+ identifiers = []
+
+ for item in self._definition.get('identifiers', []):
+ name = self._get_name('identifier', item['name'])
+ member_name = item.get('memberName', None)
+ if member_name:
+ member_name = self._get_name('attribute', member_name)
+ identifiers.append(Identifier(name, member_name))
+
+ return identifiers
+
+ @property
+ def load(self):
+ """
+ Get the load action for this resource, if it is defined.
+
+ :type: :py:class:`Action` or ``None``
+ """
+ action = self._definition.get('load')
+
+ if action is not None:
+ action = Action('load', action, self._resource_defs)
+
+ return action
+
+ @property
+ def actions(self):
+ """
+ Get a list of actions for this resource.
+
+ :type: list(:py:class:`Action`)
+ """
+ actions = []
+
+ for name, item in self._definition.get('actions', {}).items():
+ name = self._get_name('action', name)
+ actions.append(Action(name, item, self._resource_defs))
+
+ return actions
+
+ @property
+ def batch_actions(self):
+ """
+ Get a list of batch actions for this resource.
+
+ :type: list(:py:class:`Action`)
+ """
+ actions = []
+
+ for name, item in self._definition.get('batchActions', {}).items():
+ name = self._get_name('batch_action', name)
+ actions.append(Action(name, item, self._resource_defs))
+
+ return actions
+
+ def _get_has_definition(self):
+ """
+ Get a ``has`` relationship definition from a model, where the
+ service resource model is treated special in that it contains
+ a relationship to every resource defined for the service. This
+ allows things like ``s3.Object('bucket-name', 'key')`` to
+ work even though the JSON doesn't define it explicitly.
+
+ :rtype: dict
+ :return: Mapping of names to subresource and reference
+ definitions.
+ """
+ if self.name not in self._resource_defs:
+ # This is the service resource, so let us expose all of
+ # the defined resources as subresources.
+ definition = {}
+
+ for name, resource_def in self._resource_defs.items():
+ # It's possible for the service to have renamed a
+ # resource or to have defined multiple names that
+ # point to the same resource type, so we need to
+ # take that into account.
+ found = False
+ has_items = self._definition.get('has', {}).items()
+ for has_name, has_def in has_items:
+ if has_def.get('resource', {}).get('type') == name:
+ definition[has_name] = has_def
+ found = True
+
+ if not found:
+ # Create a relationship definition and attach it
+ # to the model, such that all identifiers must be
+ # supplied by the user. It will look something like:
+ #
+ # {
+ # 'resource': {
+ # 'type': 'ResourceName',
+ # 'identifiers': [
+ # {'target': 'Name1', 'source': 'input'},
+ # {'target': 'Name2', 'source': 'input'},
+ # ...
+ # ]
+ # }
+ # }
+ #
+ fake_has = {'resource': {'type': name, 'identifiers': []}}
+
+ for identifier in resource_def.get('identifiers', []):
+ fake_has['resource']['identifiers'].append(
+ {'target': identifier['name'], 'source': 'input'}
+ )
+
+ definition[name] = fake_has
+ else:
+ definition = self._definition.get('has', {})
+
+ return definition
+
+ def _get_related_resources(self, subresources):
+ """
+ Get a list of sub-resources or references.
+
+ :type subresources: bool
+ :param subresources: ``True`` to get sub-resources, ``False`` to
+ get references.
+ :rtype: list(:py:class:`Action`)
+ """
+ resources = []
+
+ for name, definition in self._get_has_definition().items():
+ if subresources:
+ name = self._get_name('subresource', name, snake_case=False)
+ else:
+ name = self._get_name('reference', name)
+ action = Action(name, definition, self._resource_defs)
+
+ data_required = False
+ for identifier in action.resource.identifiers:
+ if identifier.source == 'data':
+ data_required = True
+ break
+
+ if subresources and not data_required:
+ resources.append(action)
+ elif not subresources and data_required:
+ resources.append(action)
+
+ return resources
+
+ @property
+ def subresources(self):
+ """
+ Get a list of sub-resources.
+
+ :type: list(:py:class:`Action`)
+ """
+ return self._get_related_resources(True)
+
+ @property
+ def references(self):
+ """
+ Get a list of reference resources.
+
+ :type: list(:py:class:`Action`)
+ """
+ return self._get_related_resources(False)
+
+ @property
+ def collections(self):
+ """
+ Get a list of collections for this resource.
+
+ :type: list(:py:class:`Collection`)
+ """
+ collections = []
+
+ for name, item in self._definition.get('hasMany', {}).items():
+ name = self._get_name('collection', name)
+ collections.append(Collection(name, item, self._resource_defs))
+
+ return collections
+
+ @property
+ def waiters(self):
+ """
+ Get a list of waiters for this resource.
+
+ :type: list(:py:class:`Waiter`)
+ """
+ waiters = []
+
+ for name, item in self._definition.get('waiters', {}).items():
+ name = self._get_name('waiter', Waiter.PREFIX + name)
+ waiters.append(Waiter(name, item))
+
+ return waiters
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/params.py b/venv/lib/python3.12/site-packages/boto3/resources/params.py
new file mode 100644
index 0000000..3c5c74b
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/resources/params.py
@@ -0,0 +1,167 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import re
+
+import jmespath
+from botocore import xform_name
+
+from ..exceptions import ResourceLoadException
+
+INDEX_RE = re.compile(r'\[(.*)\]$')
+
+
+def get_data_member(parent, path):
+ """
+ Get a data member from a parent using a JMESPath search query,
+ loading the parent if required. If the parent cannot be loaded
+ and no data is present then an exception is raised.
+
+ :type parent: ServiceResource
+ :param parent: The resource instance to which contains data we
+ are interested in.
+ :type path: string
+ :param path: The JMESPath expression to query
+ :raises ResourceLoadException: When no data is present and the
+ resource cannot be loaded.
+ :returns: The queried data or ``None``.
+ """
+ # Ensure the parent has its data loaded, if possible.
+ if parent.meta.data is None:
+ if hasattr(parent, 'load'):
+ parent.load()
+ else:
+ raise ResourceLoadException(
+ f'{parent.__class__.__name__} has no load method!'
+ )
+
+ return jmespath.search(path, parent.meta.data)
+
+
+def create_request_parameters(parent, request_model, params=None, index=None):
+ """
+ Handle request parameters that can be filled in from identifiers,
+ resource data members or constants.
+
+ By passing ``params``, you can invoke this method multiple times and
+ build up a parameter dict over time, which is particularly useful
+ for reverse JMESPath expressions that append to lists.
+
+ :type parent: ServiceResource
+ :param parent: The resource instance to which this action is attached.
+ :type request_model: :py:class:`~boto3.resources.model.Request`
+ :param request_model: The action request model.
+ :type params: dict
+ :param params: If set, then add to this existing dict. It is both
+ edited in-place and returned.
+ :type index: int
+ :param index: The position of an item within a list
+ :rtype: dict
+ :return: Pre-filled parameters to be sent to the request operation.
+ """
+ if params is None:
+ params = {}
+
+ for param in request_model.params:
+ source = param.source
+ target = param.target
+
+ if source == 'identifier':
+ # Resource identifier, e.g. queue.url
+ value = getattr(parent, xform_name(param.name))
+ elif source == 'data':
+ # If this is a data member then it may incur a load
+ # action before returning the value.
+ value = get_data_member(parent, param.path)
+ elif source in ['string', 'integer', 'boolean']:
+ # These are hard-coded values in the definition
+ value = param.value
+ elif source == 'input':
+ # This is provided by the user, so ignore it here
+ continue
+ else:
+ raise NotImplementedError(f'Unsupported source type: {source}')
+
+ build_param_structure(params, target, value, index)
+
+ return params
+
+
+def build_param_structure(params, target, value, index=None):
+ """
+ This method provides a basic reverse JMESPath implementation that
+ lets you go from a JMESPath-like string to a possibly deeply nested
+ object. The ``params`` are mutated in-place, so subsequent calls
+ can modify the same element by its index.
+
+ >>> build_param_structure(params, 'test[0]', 1)
+ >>> print(params)
+ {'test': [1]}
+
+ >>> build_param_structure(params, 'foo.bar[0].baz', 'hello world')
+ >>> print(params)
+ {'test': [1], 'foo': {'bar': [{'baz': 'hello, world'}]}}
+
+ """
+ pos = params
+ parts = target.split('.')
+
+ # First, split into parts like 'foo', 'bar[0]', 'baz' and process
+ # each piece. It can either be a list or a dict, depending on if
+ # an index like `[0]` is present. We detect this via a regular
+ # expression, and keep track of where we are in params via the
+ # pos variable, walking down to the last item. Once there, we
+ # set the value.
+ for i, part in enumerate(parts):
+ # Is it indexing an array?
+ result = INDEX_RE.search(part)
+ if result:
+ if result.group(1):
+ if result.group(1) == '*':
+ part = part[:-3]
+ else:
+ # We have an explicit index
+ index = int(result.group(1))
+ part = part[: -len(str(index) + '[]')]
+ else:
+ # Index will be set after we know the proper part
+ # name and that it's a list instance.
+ index = None
+ part = part[:-2]
+
+ if part not in pos or not isinstance(pos[part], list):
+ pos[part] = []
+
+ # This means we should append, e.g. 'foo[]'
+ if index is None:
+ index = len(pos[part])
+
+ while len(pos[part]) <= index:
+ # Assume it's a dict until we set the final value below
+ pos[part].append({})
+
+ # Last item? Set the value, otherwise set the new position
+ if i == len(parts) - 1:
+ pos[part][index] = value
+ else:
+ # The new pos is the *item* in the array, not the array!
+ pos = pos[part][index]
+ else:
+ if part not in pos:
+ pos[part] = {}
+
+ # Last item? Set the value, otherwise set the new position
+ if i == len(parts) - 1:
+ pos[part] = value
+ else:
+ pos = pos[part]
diff --git a/venv/lib/python3.12/site-packages/boto3/resources/response.py b/venv/lib/python3.12/site-packages/boto3/resources/response.py
new file mode 100644
index 0000000..6dd92ac
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/resources/response.py
@@ -0,0 +1,318 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import jmespath
+from botocore import xform_name
+
+from .params import get_data_member
+
+
+def all_not_none(iterable):
+ """
+ Return True if all elements of the iterable are not None (or if the
+ iterable is empty). This is like the built-in ``all``, except checks
+ against None, so 0 and False are allowable values.
+ """
+ for element in iterable:
+ if element is None:
+ return False
+ return True
+
+
+def build_identifiers(identifiers, parent, params=None, raw_response=None):
+ """
+ Builds a mapping of identifier names to values based on the
+ identifier source location, type, and target. Identifier
+ values may be scalars or lists depending on the source type
+ and location.
+
+ :type identifiers: list
+ :param identifiers: List of :py:class:`~boto3.resources.model.Parameter`
+ definitions
+ :type parent: ServiceResource
+ :param parent: The resource instance to which this action is attached.
+ :type params: dict
+ :param params: Request parameters sent to the service.
+ :type raw_response: dict
+ :param raw_response: Low-level operation response.
+ :rtype: list
+ :return: An ordered list of ``(name, value)`` identifier tuples.
+ """
+ results = []
+
+ for identifier in identifiers:
+ source = identifier.source
+ target = identifier.target
+
+ if source == 'response':
+ value = jmespath.search(identifier.path, raw_response)
+ elif source == 'requestParameter':
+ value = jmespath.search(identifier.path, params)
+ elif source == 'identifier':
+ value = getattr(parent, xform_name(identifier.name))
+ elif source == 'data':
+ # If this is a data member then it may incur a load
+ # action before returning the value.
+ value = get_data_member(parent, identifier.path)
+ elif source == 'input':
+ # This value is set by the user, so ignore it here
+ continue
+ else:
+ raise NotImplementedError(f'Unsupported source type: {source}')
+
+ results.append((xform_name(target), value))
+
+ return results
+
+
+def build_empty_response(search_path, operation_name, service_model):
+ """
+ Creates an appropriate empty response for the type that is expected,
+ based on the service model's shape type. For example, a value that
+ is normally a list would then return an empty list. A structure would
+ return an empty dict, and a number would return None.
+
+ :type search_path: string
+ :param search_path: JMESPath expression to search in the response
+ :type operation_name: string
+ :param operation_name: Name of the underlying service operation.
+ :type service_model: :ref:`botocore.model.ServiceModel`
+ :param service_model: The Botocore service model
+ :rtype: dict, list, or None
+ :return: An appropriate empty value
+ """
+ response = None
+
+ operation_model = service_model.operation_model(operation_name)
+ shape = operation_model.output_shape
+
+ if search_path:
+ # Walk the search path and find the final shape. For example, given
+ # a path of ``foo.bar[0].baz``, we first find the shape for ``foo``,
+ # then the shape for ``bar`` (ignoring the indexing), and finally
+ # the shape for ``baz``.
+ for item in search_path.split('.'):
+ item = item.strip('[0123456789]$')
+
+ if shape.type_name == 'structure':
+ shape = shape.members[item]
+ elif shape.type_name == 'list':
+ shape = shape.member
+ else:
+ raise NotImplementedError(
+ 'Search path hits shape type {} from {}'.format(
+ shape.type_name, item
+ )
+ )
+
+ # Anything not handled here is set to None
+ if shape.type_name == 'structure':
+ response = {}
+ elif shape.type_name == 'list':
+ response = []
+ elif shape.type_name == 'map':
+ response = {}
+
+ return response
+
+
+class RawHandler:
+ """
+ A raw action response handler. This passed through the response
+ dictionary, optionally after performing a JMESPath search if one
+ has been defined for the action.
+
+ :type search_path: string
+ :param search_path: JMESPath expression to search in the response
+ :rtype: dict
+ :return: Service response
+ """
+
+ def __init__(self, search_path):
+ self.search_path = search_path
+
+ def __call__(self, parent, params, response):
+ """
+ :type parent: ServiceResource
+ :param parent: The resource instance to which this action is attached.
+ :type params: dict
+ :param params: Request parameters sent to the service.
+ :type response: dict
+ :param response: Low-level operation response.
+ """
+ # TODO: Remove the '$' check after JMESPath supports it
+ if self.search_path and self.search_path != '$':
+ response = jmespath.search(self.search_path, response)
+
+ return response
+
+
+class ResourceHandler:
+ """
+ Creates a new resource or list of new resources from the low-level
+ response based on the given response resource definition.
+
+ :type search_path: string
+ :param search_path: JMESPath expression to search in the response
+
+ :type factory: ResourceFactory
+ :param factory: The factory that created the resource class to which
+ this action is attached.
+
+ :type resource_model: :py:class:`~boto3.resources.model.ResponseResource`
+ :param resource_model: Response resource model.
+
+ :type service_context: :py:class:`~boto3.utils.ServiceContext`
+ :param service_context: Context about the AWS service
+
+ :type operation_name: string
+ :param operation_name: Name of the underlying service operation, if it
+ exists.
+
+ :rtype: ServiceResource or list
+ :return: New resource instance(s).
+ """
+
+ def __init__(
+ self,
+ search_path,
+ factory,
+ resource_model,
+ service_context,
+ operation_name=None,
+ ):
+ self.search_path = search_path
+ self.factory = factory
+ self.resource_model = resource_model
+ self.operation_name = operation_name
+ self.service_context = service_context
+
+ def __call__(self, parent, params, response):
+ """
+ :type parent: ServiceResource
+ :param parent: The resource instance to which this action is attached.
+ :type params: dict
+ :param params: Request parameters sent to the service.
+ :type response: dict
+ :param response: Low-level operation response.
+ """
+ resource_name = self.resource_model.type
+ json_definition = self.service_context.resource_json_definitions.get(
+ resource_name
+ )
+
+ # Load the new resource class that will result from this action.
+ resource_cls = self.factory.load_from_definition(
+ resource_name=resource_name,
+ single_resource_json_definition=json_definition,
+ service_context=self.service_context,
+ )
+ raw_response = response
+ search_response = None
+
+ # Anytime a path is defined, it means the response contains the
+ # resource's attributes, so resource_data gets set here. It
+ # eventually ends up in resource.meta.data, which is where
+ # the attribute properties look for data.
+ if self.search_path:
+ search_response = jmespath.search(self.search_path, raw_response)
+
+ # First, we parse all the identifiers, then create the individual
+ # response resources using them. Any identifiers that are lists
+ # will have one item consumed from the front of the list for each
+ # resource that is instantiated. Items which are not a list will
+ # be set as the same value on each new resource instance.
+ identifiers = dict(
+ build_identifiers(
+ self.resource_model.identifiers, parent, params, raw_response
+ )
+ )
+
+ # If any of the identifiers is a list, then the response is plural
+ plural = [v for v in identifiers.values() if isinstance(v, list)]
+
+ if plural:
+ response = []
+
+ # The number of items in an identifier that is a list will
+ # determine how many resource instances to create.
+ for i in range(len(plural[0])):
+ # Response item data is *only* available if a search path
+ # was given. This prevents accidentally loading unrelated
+ # data that may be in the response.
+ response_item = None
+ if search_response:
+ response_item = search_response[i]
+ response.append(
+ self.handle_response_item(
+ resource_cls, parent, identifiers, response_item
+ )
+ )
+ elif all_not_none(identifiers.values()):
+ # All identifiers must always exist, otherwise the resource
+ # cannot be instantiated.
+ response = self.handle_response_item(
+ resource_cls, parent, identifiers, search_response
+ )
+ else:
+ # The response should be empty, but that may mean an
+ # empty dict, list, or None based on whether we make
+ # a remote service call and what shape it is expected
+ # to return.
+ response = None
+ if self.operation_name is not None:
+ # A remote service call was made, so try and determine
+ # its shape.
+ response = build_empty_response(
+ self.search_path,
+ self.operation_name,
+ self.service_context.service_model,
+ )
+
+ return response
+
+ def handle_response_item(
+ self, resource_cls, parent, identifiers, resource_data
+ ):
+ """
+ Handles the creation of a single response item by setting
+ parameters and creating the appropriate resource instance.
+
+ :type resource_cls: ServiceResource subclass
+ :param resource_cls: The resource class to instantiate.
+ :type parent: ServiceResource
+ :param parent: The resource instance to which this action is attached.
+ :type identifiers: dict
+ :param identifiers: Map of identifier names to value or values.
+ :type resource_data: dict or None
+ :param resource_data: Data for resource attributes.
+ :rtype: ServiceResource
+ :return: New resource instance.
+ """
+ kwargs = {
+ 'client': parent.meta.client,
+ }
+
+ for name, value in identifiers.items():
+ # If value is a list, then consume the next item
+ if isinstance(value, list):
+ value = value.pop(0)
+
+ kwargs[name] = value
+
+ resource = resource_cls(**kwargs)
+
+ if resource_data is not None:
+ resource.meta.data = resource_data
+
+ return resource
diff --git a/venv/lib/python3.12/site-packages/boto3/s3/__init__.py b/venv/lib/python3.12/site-packages/boto3/s3/__init__.py
new file mode 100644
index 0000000..6001b27
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/s3/__init__.py
@@ -0,0 +1,12 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.12/site-packages/boto3/s3/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..534c81e
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/constants.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/constants.cpython-312.pyc
new file mode 100644
index 0000000..8b13aba
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/constants.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/inject.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/inject.cpython-312.pyc
new file mode 100644
index 0000000..b449000
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/inject.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/transfer.cpython-312.pyc b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/transfer.cpython-312.pyc
new file mode 100644
index 0000000..421157d
Binary files /dev/null and b/venv/lib/python3.12/site-packages/boto3/s3/__pycache__/transfer.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/boto3/s3/constants.py b/venv/lib/python3.12/site-packages/boto3/s3/constants.py
new file mode 100644
index 0000000..c7f691f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/s3/constants.py
@@ -0,0 +1,17 @@
+# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+
+# TransferConfig preferred_transfer_client settings
+CLASSIC_TRANSFER_CLIENT = "classic"
+AUTO_RESOLVE_TRANSFER_CLIENT = "auto"
diff --git a/venv/lib/python3.12/site-packages/boto3/s3/inject.py b/venv/lib/python3.12/site-packages/boto3/s3/inject.py
new file mode 100644
index 0000000..440be5a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/s3/inject.py
@@ -0,0 +1,897 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import copy as python_copy
+
+from botocore.exceptions import ClientError
+
+from boto3 import utils
+from boto3.s3.transfer import (
+ ProgressCallbackInvoker,
+ S3Transfer,
+ TransferConfig,
+ create_transfer_manager,
+)
+
+
+def inject_s3_transfer_methods(class_attributes, **kwargs):
+ utils.inject_attribute(class_attributes, 'upload_file', upload_file)
+ utils.inject_attribute(class_attributes, 'download_file', download_file)
+ utils.inject_attribute(class_attributes, 'copy', copy)
+ utils.inject_attribute(class_attributes, 'upload_fileobj', upload_fileobj)
+ utils.inject_attribute(
+ class_attributes, 'download_fileobj', download_fileobj
+ )
+
+
+def inject_bucket_methods(class_attributes, **kwargs):
+ utils.inject_attribute(class_attributes, 'load', bucket_load)
+ utils.inject_attribute(class_attributes, 'upload_file', bucket_upload_file)
+ utils.inject_attribute(
+ class_attributes, 'download_file', bucket_download_file
+ )
+ utils.inject_attribute(class_attributes, 'copy', bucket_copy)
+ utils.inject_attribute(
+ class_attributes, 'upload_fileobj', bucket_upload_fileobj
+ )
+ utils.inject_attribute(
+ class_attributes, 'download_fileobj', bucket_download_fileobj
+ )
+
+
+def inject_object_methods(class_attributes, **kwargs):
+ utils.inject_attribute(class_attributes, 'upload_file', object_upload_file)
+ utils.inject_attribute(
+ class_attributes, 'download_file', object_download_file
+ )
+ utils.inject_attribute(class_attributes, 'copy', object_copy)
+ utils.inject_attribute(
+ class_attributes, 'upload_fileobj', object_upload_fileobj
+ )
+ utils.inject_attribute(
+ class_attributes, 'download_fileobj', object_download_fileobj
+ )
+
+
+def inject_object_summary_methods(class_attributes, **kwargs):
+ utils.inject_attribute(class_attributes, 'load', object_summary_load)
+
+
+def bucket_load(self, *args, **kwargs):
+ """
+ Calls s3.Client.list_buckets() to update the attributes of the Bucket
+ resource.
+ """
+ # The docstring above is phrased this way to match what the autogenerated
+ # docs produce.
+
+ # We can't actually get the bucket's attributes from a HeadBucket,
+ # so we need to use a ListBuckets and search for our bucket.
+ # However, we may fail if we lack permissions to ListBuckets
+ # or the bucket is in another account. In which case, creation_date
+ # will be None.
+ self.meta.data = {}
+ try:
+ response = self.meta.client.list_buckets()
+ for bucket_data in response['Buckets']:
+ if bucket_data['Name'] == self.name:
+ self.meta.data = bucket_data
+ break
+ except ClientError as e:
+ if not e.response.get('Error', {}).get('Code') == 'AccessDenied':
+ raise
+
+
+def object_summary_load(self, *args, **kwargs):
+ """
+ Calls s3.Client.head_object to update the attributes of the ObjectSummary
+ resource.
+ """
+ response = self.meta.client.head_object(
+ Bucket=self.bucket_name, Key=self.key
+ )
+ if 'ContentLength' in response:
+ response['Size'] = response.pop('ContentLength')
+ self.meta.data = response
+
+
+def upload_file(
+ self, Filename, Bucket, Key, ExtraArgs=None, Callback=None, Config=None
+):
+ """Upload a file to an S3 object.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.client('s3')
+ s3.upload_file('/tmp/hello.txt', 'mybucket', 'hello.txt')
+
+ Similar behavior as S3Transfer's upload_file() method, except that
+ argument names are capitalized. Detailed examples can be found at
+ :ref:`S3Transfer's Usage `.
+
+ :type Filename: str
+ :param Filename: The path to the file to upload.
+
+ :type Bucket: str
+ :param Bucket: The name of the bucket to upload to.
+
+ :type Key: str
+ :param Key: The name of the key to upload to.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed upload arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the upload.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ transfer.
+ """
+ with S3Transfer(self, Config) as transfer:
+ return transfer.upload_file(
+ filename=Filename,
+ bucket=Bucket,
+ key=Key,
+ extra_args=ExtraArgs,
+ callback=Callback,
+ )
+
+
+def download_file(
+ self, Bucket, Key, Filename, ExtraArgs=None, Callback=None, Config=None
+):
+ """Download an S3 object to a file.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ s3.meta.client.download_file('mybucket', 'hello.txt', '/tmp/hello.txt')
+
+ Similar behavior as S3Transfer's download_file() method,
+ except that parameters are capitalized. Detailed examples can be found at
+ :ref:`S3Transfer's Usage `.
+
+ :type Bucket: str
+ :param Bucket: The name of the bucket to download from.
+
+ :type Key: str
+ :param Key: The name of the key to download from.
+
+ :type Filename: str
+ :param Filename: The path to the file to download to.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the download.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ transfer.
+ """
+ with S3Transfer(self, Config) as transfer:
+ return transfer.download_file(
+ bucket=Bucket,
+ key=Key,
+ filename=Filename,
+ extra_args=ExtraArgs,
+ callback=Callback,
+ )
+
+
+def bucket_upload_file(
+ self, Filename, Key, ExtraArgs=None, Callback=None, Config=None
+):
+ """Upload a file to an S3 object.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ s3.Bucket('mybucket').upload_file('/tmp/hello.txt', 'hello.txt')
+
+ Similar behavior as S3Transfer's upload_file() method,
+ except that parameters are capitalized. Detailed examples can be found at
+ :ref:`S3Transfer's Usage `.
+
+ :type Filename: str
+ :param Filename: The path to the file to upload.
+
+ :type Key: str
+ :param Key: The name of the key to upload to.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed upload arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the upload.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ transfer.
+ """
+ return self.meta.client.upload_file(
+ Filename=Filename,
+ Bucket=self.name,
+ Key=Key,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
+
+
+def bucket_download_file(
+ self, Key, Filename, ExtraArgs=None, Callback=None, Config=None
+):
+ """Download an S3 object to a file.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ s3.Bucket('mybucket').download_file('hello.txt', '/tmp/hello.txt')
+
+ Similar behavior as S3Transfer's download_file() method,
+ except that parameters are capitalized. Detailed examples can be found at
+ :ref:`S3Transfer's Usage `.
+
+ :type Key: str
+ :param Key: The name of the key to download from.
+
+ :type Filename: str
+ :param Filename: The path to the file to download to.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the download.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ transfer.
+ """
+ return self.meta.client.download_file(
+ Bucket=self.name,
+ Key=Key,
+ Filename=Filename,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
+
+
+def object_upload_file(
+ self, Filename, ExtraArgs=None, Callback=None, Config=None
+):
+ """Upload a file to an S3 object.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ s3.Object('mybucket', 'hello.txt').upload_file('/tmp/hello.txt')
+
+ Similar behavior as S3Transfer's upload_file() method,
+ except that parameters are capitalized. Detailed examples can be found at
+ :ref:`S3Transfer's Usage `.
+
+ :type Filename: str
+ :param Filename: The path to the file to upload.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed upload arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the upload.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ transfer.
+ """
+ return self.meta.client.upload_file(
+ Filename=Filename,
+ Bucket=self.bucket_name,
+ Key=self.key,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
+
+
+def object_download_file(
+ self, Filename, ExtraArgs=None, Callback=None, Config=None
+):
+ """Download an S3 object to a file.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ s3.Object('mybucket', 'hello.txt').download_file('/tmp/hello.txt')
+
+ Similar behavior as S3Transfer's download_file() method,
+ except that parameters are capitalized. Detailed examples can be found at
+ :ref:`S3Transfer's Usage `.
+
+ :type Filename: str
+ :param Filename: The path to the file to download to.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the download.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ transfer.
+ """
+ return self.meta.client.download_file(
+ Bucket=self.bucket_name,
+ Key=self.key,
+ Filename=Filename,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
+
+
+def copy(
+ self,
+ CopySource,
+ Bucket,
+ Key,
+ ExtraArgs=None,
+ Callback=None,
+ SourceClient=None,
+ Config=None,
+):
+ """Copy an object from one S3 location to another.
+
+ This is a managed transfer which will perform a multipart copy in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ copy_source = {
+ 'Bucket': 'mybucket',
+ 'Key': 'mykey'
+ }
+ s3.meta.client.copy(copy_source, 'otherbucket', 'otherkey')
+
+ :type CopySource: dict
+ :param CopySource: The name of the source bucket, key name of the
+ source object, and optional version ID of the source object. The
+ dictionary format is:
+ ``{'Bucket': 'bucket', 'Key': 'key', 'VersionId': 'id'}``. Note
+ that the ``VersionId`` key is optional and may be omitted.
+
+ :type Bucket: str
+ :param Bucket: The name of the bucket to copy to
+
+ :type Key: str
+ :param Key: The name of the key to copy to
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the copy.
+
+ :type SourceClient: botocore or boto3 Client
+ :param SourceClient: The client to be used for operation that
+ may happen at the source object. For example, this client is
+ used for the head_object that determines the size of the copy.
+ If no client is provided, the current client is used as the client
+ for the source object.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ copy.
+ """
+ subscribers = None
+ if Callback is not None:
+ subscribers = [ProgressCallbackInvoker(Callback)]
+
+ config = Config
+ if config is None:
+ config = TransferConfig()
+
+ # copy is not supported in the CRT
+ new_config = python_copy.copy(config)
+ new_config.preferred_transfer_client = "classic"
+
+ with create_transfer_manager(self, new_config) as manager:
+ future = manager.copy(
+ copy_source=CopySource,
+ bucket=Bucket,
+ key=Key,
+ extra_args=ExtraArgs,
+ subscribers=subscribers,
+ source_client=SourceClient,
+ )
+ return future.result()
+
+
+def bucket_copy(
+ self,
+ CopySource,
+ Key,
+ ExtraArgs=None,
+ Callback=None,
+ SourceClient=None,
+ Config=None,
+):
+ """Copy an object from one S3 location to an object in this bucket.
+
+ This is a managed transfer which will perform a multipart copy in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ copy_source = {
+ 'Bucket': 'mybucket',
+ 'Key': 'mykey'
+ }
+ bucket = s3.Bucket('otherbucket')
+ bucket.copy(copy_source, 'otherkey')
+
+ :type CopySource: dict
+ :param CopySource: The name of the source bucket, key name of the
+ source object, and optional version ID of the source object. The
+ dictionary format is:
+ ``{'Bucket': 'bucket', 'Key': 'key', 'VersionId': 'id'}``. Note
+ that the ``VersionId`` key is optional and may be omitted.
+
+ :type Key: str
+ :param Key: The name of the key to copy to
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the copy.
+
+ :type SourceClient: botocore or boto3 Client
+ :param SourceClient: The client to be used for operation that
+ may happen at the source object. For example, this client is
+ used for the head_object that determines the size of the copy.
+ If no client is provided, the current client is used as the client
+ for the source object.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ copy.
+ """
+ return self.meta.client.copy(
+ CopySource=CopySource,
+ Bucket=self.name,
+ Key=Key,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ SourceClient=SourceClient,
+ Config=Config,
+ )
+
+
+def object_copy(
+ self,
+ CopySource,
+ ExtraArgs=None,
+ Callback=None,
+ SourceClient=None,
+ Config=None,
+):
+ """Copy an object from one S3 location to this object.
+
+ This is a managed transfer which will perform a multipart copy in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ copy_source = {
+ 'Bucket': 'mybucket',
+ 'Key': 'mykey'
+ }
+ bucket = s3.Bucket('otherbucket')
+ obj = bucket.Object('otherkey')
+ obj.copy(copy_source)
+
+ :type CopySource: dict
+ :param CopySource: The name of the source bucket, key name of the
+ source object, and optional version ID of the source object. The
+ dictionary format is:
+ ``{'Bucket': 'bucket', 'Key': 'key', 'VersionId': 'id'}``. Note
+ that the ``VersionId`` key is optional and may be omitted.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the copy.
+
+ :type SourceClient: botocore or boto3 Client
+ :param SourceClient: The client to be used for operation that
+ may happen at the source object. For example, this client is
+ used for the head_object that determines the size of the copy.
+ If no client is provided, the current client is used as the client
+ for the source object.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ copy.
+ """
+ return self.meta.client.copy(
+ CopySource=CopySource,
+ Bucket=self.bucket_name,
+ Key=self.key,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ SourceClient=SourceClient,
+ Config=Config,
+ )
+
+
+def upload_fileobj(
+ self, Fileobj, Bucket, Key, ExtraArgs=None, Callback=None, Config=None
+):
+ """Upload a file-like object to S3.
+
+ The file-like object must be in binary mode.
+
+ This is a managed transfer which will perform a multipart upload in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.client('s3')
+
+ with open('filename', 'rb') as data:
+ s3.upload_fileobj(data, 'mybucket', 'mykey')
+
+ :type Fileobj: a file-like object
+ :param Fileobj: A file-like object to upload. At a minimum, it must
+ implement the `read` method, and must return bytes.
+
+ :type Bucket: str
+ :param Bucket: The name of the bucket to upload to.
+
+ :type Key: str
+ :param Key: The name of the key to upload to.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed upload arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the upload.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ upload.
+ """
+ if not hasattr(Fileobj, 'read'):
+ raise ValueError('Fileobj must implement read')
+
+ subscribers = None
+ if Callback is not None:
+ subscribers = [ProgressCallbackInvoker(Callback)]
+
+ config = Config
+ if config is None:
+ config = TransferConfig()
+
+ with create_transfer_manager(self, config) as manager:
+ future = manager.upload(
+ fileobj=Fileobj,
+ bucket=Bucket,
+ key=Key,
+ extra_args=ExtraArgs,
+ subscribers=subscribers,
+ )
+ return future.result()
+
+
+def bucket_upload_fileobj(
+ self, Fileobj, Key, ExtraArgs=None, Callback=None, Config=None
+):
+ """Upload a file-like object to this bucket.
+
+ The file-like object must be in binary mode.
+
+ This is a managed transfer which will perform a multipart upload in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ bucket = s3.Bucket('mybucket')
+
+ with open('filename', 'rb') as data:
+ bucket.upload_fileobj(data, 'mykey')
+
+ :type Fileobj: a file-like object
+ :param Fileobj: A file-like object to upload. At a minimum, it must
+ implement the `read` method, and must return bytes.
+
+ :type Key: str
+ :param Key: The name of the key to upload to.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed upload arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the upload.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ upload.
+ """
+ return self.meta.client.upload_fileobj(
+ Fileobj=Fileobj,
+ Bucket=self.name,
+ Key=Key,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
+
+
+def object_upload_fileobj(
+ self, Fileobj, ExtraArgs=None, Callback=None, Config=None
+):
+ """Upload a file-like object to this object.
+
+ The file-like object must be in binary mode.
+
+ This is a managed transfer which will perform a multipart upload in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ bucket = s3.Bucket('mybucket')
+ obj = bucket.Object('mykey')
+
+ with open('filename', 'rb') as data:
+ obj.upload_fileobj(data)
+
+ :type Fileobj: a file-like object
+ :param Fileobj: A file-like object to upload. At a minimum, it must
+ implement the `read` method, and must return bytes.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed upload arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the upload.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ upload.
+ """
+ return self.meta.client.upload_fileobj(
+ Fileobj=Fileobj,
+ Bucket=self.bucket_name,
+ Key=self.key,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
+
+
+def download_fileobj(
+ self, Bucket, Key, Fileobj, ExtraArgs=None, Callback=None, Config=None
+):
+ """Download an object from S3 to a file-like object.
+
+ The file-like object must be in binary mode.
+
+ This is a managed transfer which will perform a multipart download in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.client('s3')
+
+ with open('filename', 'wb') as data:
+ s3.download_fileobj('mybucket', 'mykey', data)
+
+ :type Bucket: str
+ :param Bucket: The name of the bucket to download from.
+
+ :type Key: str
+ :param Key: The name of the key to download from.
+
+ :type Fileobj: a file-like object
+ :param Fileobj: A file-like object to download into. At a minimum, it must
+ implement the `write` method and must accept bytes.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the download.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ download.
+ """
+ if not hasattr(Fileobj, 'write'):
+ raise ValueError('Fileobj must implement write')
+
+ subscribers = None
+ if Callback is not None:
+ subscribers = [ProgressCallbackInvoker(Callback)]
+
+ config = Config
+ if config is None:
+ config = TransferConfig()
+
+ with create_transfer_manager(self, config) as manager:
+ future = manager.download(
+ bucket=Bucket,
+ key=Key,
+ fileobj=Fileobj,
+ extra_args=ExtraArgs,
+ subscribers=subscribers,
+ )
+ return future.result()
+
+
+def bucket_download_fileobj(
+ self, Key, Fileobj, ExtraArgs=None, Callback=None, Config=None
+):
+ """Download an object from this bucket to a file-like-object.
+
+ The file-like object must be in binary mode.
+
+ This is a managed transfer which will perform a multipart download in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ bucket = s3.Bucket('mybucket')
+
+ with open('filename', 'wb') as data:
+ bucket.download_fileobj('mykey', data)
+
+ :type Fileobj: a file-like object
+ :param Fileobj: A file-like object to download into. At a minimum, it must
+ implement the `write` method and must accept bytes.
+
+ :type Key: str
+ :param Key: The name of the key to download from.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the download.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ download.
+ """
+ return self.meta.client.download_fileobj(
+ Bucket=self.name,
+ Key=Key,
+ Fileobj=Fileobj,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
+
+
+def object_download_fileobj(
+ self, Fileobj, ExtraArgs=None, Callback=None, Config=None
+):
+ """Download this object from S3 to a file-like object.
+
+ The file-like object must be in binary mode.
+
+ This is a managed transfer which will perform a multipart download in
+ multiple threads if necessary.
+
+ Usage::
+
+ import boto3
+ s3 = boto3.resource('s3')
+ bucket = s3.Bucket('mybucket')
+ obj = bucket.Object('mykey')
+
+ with open('filename', 'wb') as data:
+ obj.download_fileobj(data)
+
+ :type Fileobj: a file-like object
+ :param Fileobj: A file-like object to download into. At a minimum, it must
+ implement the `write` method and must accept bytes.
+
+ :type ExtraArgs: dict
+ :param ExtraArgs: Extra arguments that may be passed to the
+ client operation. For allowed download arguments see
+ boto3.s3.transfer.S3Transfer.ALLOWED_DOWNLOAD_ARGS.
+
+ :type Callback: function
+ :param Callback: A method which takes a number of bytes transferred to
+ be periodically called during the download.
+
+ :type Config: boto3.s3.transfer.TransferConfig
+ :param Config: The transfer configuration to be used when performing the
+ download.
+ """
+ return self.meta.client.download_fileobj(
+ Bucket=self.bucket_name,
+ Key=self.key,
+ Fileobj=Fileobj,
+ ExtraArgs=ExtraArgs,
+ Callback=Callback,
+ Config=Config,
+ )
diff --git a/venv/lib/python3.12/site-packages/boto3/s3/transfer.py b/venv/lib/python3.12/site-packages/boto3/s3/transfer.py
new file mode 100644
index 0000000..1c8efd4
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/s3/transfer.py
@@ -0,0 +1,437 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+"""Abstractions over S3's upload/download operations.
+
+This module provides high level abstractions for efficient
+uploads/downloads. It handles several things for the user:
+
+* Automatically switching to multipart transfers when
+ a file is over a specific size threshold
+* Uploading/downloading a file in parallel
+* Progress callbacks to monitor transfers
+* Retries. While botocore handles retries for streaming uploads,
+ it is not possible for it to handle retries for streaming
+ downloads. This module handles retries for both cases so
+ you don't need to implement any retry logic yourself.
+
+This module has a reasonable set of defaults. It also allows you
+to configure many aspects of the transfer process including:
+
+* Multipart threshold size
+* Max parallel downloads
+* Socket timeouts
+* Retry amounts
+
+There is no support for s3->s3 multipart copies at this
+time.
+
+
+.. _ref_s3transfer_usage:
+
+Usage
+=====
+
+The simplest way to use this module is:
+
+.. code-block:: python
+
+ client = boto3.client('s3', 'us-west-2')
+ transfer = S3Transfer(client)
+ # Upload /tmp/myfile to s3://bucket/key
+ transfer.upload_file('/tmp/myfile', 'bucket', 'key')
+
+ # Download s3://bucket/key to /tmp/myfile
+ transfer.download_file('bucket', 'key', '/tmp/myfile')
+
+The ``upload_file`` and ``download_file`` methods also accept
+``**kwargs``, which will be forwarded through to the corresponding
+client operation. Here are a few examples using ``upload_file``::
+
+ # Making the object public
+ transfer.upload_file('/tmp/myfile', 'bucket', 'key',
+ extra_args={'ACL': 'public-read'})
+
+ # Setting metadata
+ transfer.upload_file('/tmp/myfile', 'bucket', 'key',
+ extra_args={'Metadata': {'a': 'b', 'c': 'd'}})
+
+ # Setting content type
+ transfer.upload_file('/tmp/myfile.json', 'bucket', 'key',
+ extra_args={'ContentType': "application/json"})
+
+
+The ``S3Transfer`` class also supports progress callbacks so you can
+provide transfer progress to users. Both the ``upload_file`` and
+``download_file`` methods take an optional ``callback`` parameter.
+Here's an example of how to print a simple progress percentage
+to the user:
+
+.. code-block:: python
+
+ class ProgressPercentage(object):
+ def __init__(self, filename):
+ self._filename = filename
+ self._size = float(os.path.getsize(filename))
+ self._seen_so_far = 0
+ self._lock = threading.Lock()
+
+ def __call__(self, bytes_amount):
+ # To simplify we'll assume this is hooked up
+ # to a single filename.
+ with self._lock:
+ self._seen_so_far += bytes_amount
+ percentage = (self._seen_so_far / self._size) * 100
+ sys.stdout.write(
+ "\r%s %s / %s (%.2f%%)" % (
+ self._filename, self._seen_so_far, self._size,
+ percentage))
+ sys.stdout.flush()
+
+
+ transfer = S3Transfer(boto3.client('s3', 'us-west-2'))
+ # Upload /tmp/myfile to s3://bucket/key and print upload progress.
+ transfer.upload_file('/tmp/myfile', 'bucket', 'key',
+ callback=ProgressPercentage('/tmp/myfile'))
+
+
+
+You can also provide a TransferConfig object to the S3Transfer
+object that gives you more fine grained control over the
+transfer. For example:
+
+.. code-block:: python
+
+ client = boto3.client('s3', 'us-west-2')
+ config = TransferConfig(
+ multipart_threshold=8 * 1024 * 1024,
+ max_concurrency=10,
+ num_download_attempts=10,
+ )
+ transfer = S3Transfer(client, config)
+ transfer.upload_file('/tmp/foo', 'bucket', 'key')
+
+
+"""
+import logging
+import threading
+from os import PathLike, fspath, getpid
+
+from botocore.compat import HAS_CRT
+from botocore.exceptions import ClientError
+from s3transfer.exceptions import (
+ RetriesExceededError as S3TransferRetriesExceededError,
+)
+from s3transfer.futures import NonThreadedExecutor
+from s3transfer.manager import TransferConfig as S3TransferConfig
+from s3transfer.manager import TransferManager
+from s3transfer.subscribers import BaseSubscriber
+from s3transfer.utils import OSUtils
+
+import boto3.s3.constants as constants
+from boto3.exceptions import RetriesExceededError, S3UploadFailedError
+
+if HAS_CRT:
+ import awscrt.s3
+
+ from boto3.crt import create_crt_transfer_manager
+
+KB = 1024
+MB = KB * KB
+
+logger = logging.getLogger(__name__)
+
+
+def create_transfer_manager(client, config, osutil=None):
+ """Creates a transfer manager based on configuration
+
+ :type client: boto3.client
+ :param client: The S3 client to use
+
+ :type config: boto3.s3.transfer.TransferConfig
+ :param config: The transfer config to use
+
+ :type osutil: s3transfer.utils.OSUtils
+ :param osutil: The os utility to use
+
+ :rtype: s3transfer.manager.TransferManager
+ :returns: A transfer manager based on parameters provided
+ """
+ if _should_use_crt(config):
+ crt_transfer_manager = create_crt_transfer_manager(client, config)
+ if crt_transfer_manager is not None:
+ logger.debug(
+ f"Using CRT client. pid: {getpid()}, thread: {threading.get_ident()}"
+ )
+ return crt_transfer_manager
+
+ # If we don't resolve something above, fallback to the default.
+ logger.debug(
+ f"Using default client. pid: {getpid()}, thread: {threading.get_ident()}"
+ )
+ return _create_default_transfer_manager(client, config, osutil)
+
+
+def _should_use_crt(config):
+ # This feature requires awscrt>=0.19.17
+ if HAS_CRT and has_minimum_crt_version((0, 19, 17)):
+ is_optimized_instance = awscrt.s3.is_optimized_for_system()
+ else:
+ is_optimized_instance = False
+ pref_transfer_client = config.preferred_transfer_client.lower()
+
+ if (
+ is_optimized_instance
+ and pref_transfer_client == constants.AUTO_RESOLVE_TRANSFER_CLIENT
+ ):
+ logger.debug(
+ "Attempting to use CRTTransferManager. Config settings may be ignored."
+ )
+ return True
+
+ logger.debug(
+ "Opting out of CRT Transfer Manager. Preferred client: "
+ f"{pref_transfer_client}, CRT available: {HAS_CRT}, "
+ f"Instance Optimized: {is_optimized_instance}."
+ )
+ return False
+
+
+def has_minimum_crt_version(minimum_version):
+ """Not intended for use outside boto3."""
+ if not HAS_CRT:
+ return False
+
+ crt_version_str = awscrt.__version__
+ try:
+ crt_version_ints = map(int, crt_version_str.split("."))
+ crt_version_tuple = tuple(crt_version_ints)
+ except (TypeError, ValueError):
+ return False
+
+ return crt_version_tuple >= minimum_version
+
+
+def _create_default_transfer_manager(client, config, osutil):
+ """Create the default TransferManager implementation for s3transfer."""
+ executor_cls = None
+ if not config.use_threads:
+ executor_cls = NonThreadedExecutor
+ return TransferManager(client, config, osutil, executor_cls)
+
+
+class TransferConfig(S3TransferConfig):
+ ALIAS = {
+ 'max_concurrency': 'max_request_concurrency',
+ 'max_io_queue': 'max_io_queue_size',
+ }
+
+ def __init__(
+ self,
+ multipart_threshold=8 * MB,
+ max_concurrency=10,
+ multipart_chunksize=8 * MB,
+ num_download_attempts=5,
+ max_io_queue=100,
+ io_chunksize=256 * KB,
+ use_threads=True,
+ max_bandwidth=None,
+ preferred_transfer_client=constants.AUTO_RESOLVE_TRANSFER_CLIENT,
+ ):
+ """Configuration object for managed S3 transfers
+
+ :param multipart_threshold: The transfer size threshold for which
+ multipart uploads, downloads, and copies will automatically be
+ triggered.
+
+ :param max_concurrency: The maximum number of threads that will be
+ making requests to perform a transfer. If ``use_threads`` is
+ set to ``False``, the value provided is ignored as the transfer
+ will only ever use the main thread.
+
+ :param multipart_chunksize: The partition size of each part for a
+ multipart transfer.
+
+ :param num_download_attempts: The number of download attempts that
+ will be retried upon errors with downloading an object in S3.
+ Note that these retries account for errors that occur when
+ streaming down the data from s3 (i.e. socket errors and read
+ timeouts that occur after receiving an OK response from s3).
+ Other retryable exceptions such as throttling errors and 5xx
+ errors are already retried by botocore (this default is 5). This
+ does not take into account the number of exceptions retried by
+ botocore.
+
+ :param max_io_queue: The maximum amount of read parts that can be
+ queued in memory to be written for a download. The size of each
+ of these read parts is at most the size of ``io_chunksize``.
+
+ :param io_chunksize: The max size of each chunk in the io queue.
+ Currently, this is size used when ``read`` is called on the
+ downloaded stream as well.
+
+ :param use_threads: If True, threads will be used when performing
+ S3 transfers. If False, no threads will be used in
+ performing transfers; all logic will be run in the main thread.
+
+ :param max_bandwidth: The maximum bandwidth that will be consumed
+ in uploading and downloading file content. The value is an integer
+ in terms of bytes per second.
+
+ :param preferred_transfer_client: String specifying preferred transfer
+ client for transfer operations.
+
+ Current supported settings are:
+ * auto (default) - Use the CRTTransferManager when calls
+ are made with supported environment and settings.
+ * classic - Only use the origin S3TransferManager with
+ requests. Disables possible CRT upgrade on requests.
+ """
+ super().__init__(
+ multipart_threshold=multipart_threshold,
+ max_request_concurrency=max_concurrency,
+ multipart_chunksize=multipart_chunksize,
+ num_download_attempts=num_download_attempts,
+ max_io_queue_size=max_io_queue,
+ io_chunksize=io_chunksize,
+ max_bandwidth=max_bandwidth,
+ )
+ # Some of the argument names are not the same as the inherited
+ # S3TransferConfig so we add aliases so you can still access the
+ # old version of the names.
+ for alias in self.ALIAS:
+ setattr(self, alias, getattr(self, self.ALIAS[alias]))
+ self.use_threads = use_threads
+ self.preferred_transfer_client = preferred_transfer_client
+
+ def __setattr__(self, name, value):
+ # If the alias name is used, make sure we set the name that it points
+ # to as that is what actually is used in governing the TransferManager.
+ if name in self.ALIAS:
+ super().__setattr__(self.ALIAS[name], value)
+ # Always set the value of the actual name provided.
+ super().__setattr__(name, value)
+
+
+class S3Transfer:
+ ALLOWED_DOWNLOAD_ARGS = TransferManager.ALLOWED_DOWNLOAD_ARGS
+ ALLOWED_UPLOAD_ARGS = TransferManager.ALLOWED_UPLOAD_ARGS
+
+ def __init__(self, client=None, config=None, osutil=None, manager=None):
+ if not client and not manager:
+ raise ValueError(
+ 'Either a boto3.Client or s3transfer.manager.TransferManager '
+ 'must be provided'
+ )
+ if manager and any([client, config, osutil]):
+ raise ValueError(
+ 'Manager cannot be provided with client, config, '
+ 'nor osutil. These parameters are mutually exclusive.'
+ )
+ if config is None:
+ config = TransferConfig()
+ if osutil is None:
+ osutil = OSUtils()
+ if manager:
+ self._manager = manager
+ else:
+ self._manager = create_transfer_manager(client, config, osutil)
+
+ def upload_file(
+ self, filename, bucket, key, callback=None, extra_args=None
+ ):
+ """Upload a file to an S3 object.
+
+ Variants have also been injected into S3 client, Bucket and Object.
+ You don't have to use S3Transfer.upload_file() directly.
+
+ .. seealso::
+ :py:meth:`S3.Client.upload_file`
+ :py:meth:`S3.Client.upload_fileobj`
+ """
+ if isinstance(filename, PathLike):
+ filename = fspath(filename)
+ if not isinstance(filename, str):
+ raise ValueError('Filename must be a string or a path-like object')
+
+ subscribers = self._get_subscribers(callback)
+ future = self._manager.upload(
+ filename, bucket, key, extra_args, subscribers
+ )
+ try:
+ future.result()
+ # If a client error was raised, add the backwards compatibility layer
+ # that raises a S3UploadFailedError. These specific errors were only
+ # ever thrown for upload_parts but now can be thrown for any related
+ # client error.
+ except ClientError as e:
+ raise S3UploadFailedError(
+ "Failed to upload {} to {}: {}".format(
+ filename, '/'.join([bucket, key]), e
+ )
+ )
+
+ def download_file(
+ self, bucket, key, filename, extra_args=None, callback=None
+ ):
+ """Download an S3 object to a file.
+
+ Variants have also been injected into S3 client, Bucket and Object.
+ You don't have to use S3Transfer.download_file() directly.
+
+ .. seealso::
+ :py:meth:`S3.Client.download_file`
+ :py:meth:`S3.Client.download_fileobj`
+ """
+ if isinstance(filename, PathLike):
+ filename = fspath(filename)
+ if not isinstance(filename, str):
+ raise ValueError('Filename must be a string or a path-like object')
+
+ subscribers = self._get_subscribers(callback)
+ future = self._manager.download(
+ bucket, key, filename, extra_args, subscribers
+ )
+ try:
+ future.result()
+ # This is for backwards compatibility where when retries are
+ # exceeded we need to throw the same error from boto3 instead of
+ # s3transfer's built in RetriesExceededError as current users are
+ # catching the boto3 one instead of the s3transfer exception to do
+ # their own retries.
+ except S3TransferRetriesExceededError as e:
+ raise RetriesExceededError(e.last_exception)
+
+ def _get_subscribers(self, callback):
+ if not callback:
+ return None
+ return [ProgressCallbackInvoker(callback)]
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self._manager.__exit__(*args)
+
+
+class ProgressCallbackInvoker(BaseSubscriber):
+ """A back-compat wrapper to invoke a provided callback via a subscriber
+
+ :param callback: A callable that takes a single positional argument for
+ how many bytes were transferred.
+ """
+
+ def __init__(self, callback):
+ self._callback = callback
+
+ def on_progress(self, bytes_transferred, **kwargs):
+ self._callback(bytes_transferred)
diff --git a/venv/lib/python3.12/site-packages/boto3/session.py b/venv/lib/python3.12/site-packages/boto3/session.py
new file mode 100644
index 0000000..37890ad
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/session.py
@@ -0,0 +1,531 @@
+# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import copy
+import os
+
+import botocore.session
+from botocore.client import Config
+from botocore.exceptions import DataNotFoundError, UnknownServiceError
+
+import boto3
+import boto3.utils
+from boto3.exceptions import ResourceNotExistsError, UnknownAPIVersionError
+
+from .resources.factory import ResourceFactory
+
+
+class Session:
+ """
+ A session stores configuration state and allows you to create service
+ clients and resources.
+
+ :type aws_access_key_id: string
+ :param aws_access_key_id: AWS access key ID
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: AWS secret access key
+ :type aws_session_token: string
+ :param aws_session_token: AWS temporary session token
+ :type region_name: string
+ :param region_name: Default region when creating new connections
+ :type botocore_session: botocore.session.Session
+ :param botocore_session: Use this Botocore session instead of creating
+ a new default one.
+ :type profile_name: string
+ :param profile_name: The name of a profile to use. If not given, then
+ the default profile is used.
+ """
+
+ def __init__(
+ self,
+ aws_access_key_id=None,
+ aws_secret_access_key=None,
+ aws_session_token=None,
+ region_name=None,
+ botocore_session=None,
+ profile_name=None,
+ ):
+ if botocore_session is not None:
+ self._session = botocore_session
+ else:
+ # Create a new default session
+ self._session = botocore.session.get_session()
+
+ # Setup custom user-agent string if it isn't already customized
+ if self._session.user_agent_name == 'Botocore':
+ botocore_info = 'Botocore/{}'.format(
+ self._session.user_agent_version
+ )
+ if self._session.user_agent_extra:
+ self._session.user_agent_extra += ' ' + botocore_info
+ else:
+ self._session.user_agent_extra = botocore_info
+ self._session.user_agent_name = 'Boto3'
+ self._session.user_agent_version = boto3.__version__
+
+ if profile_name is not None:
+ self._session.set_config_variable('profile', profile_name)
+
+ if aws_access_key_id or aws_secret_access_key or aws_session_token:
+ self._session.set_credentials(
+ aws_access_key_id, aws_secret_access_key, aws_session_token
+ )
+
+ if region_name is not None:
+ self._session.set_config_variable('region', region_name)
+
+ self.resource_factory = ResourceFactory(
+ self._session.get_component('event_emitter')
+ )
+ self._setup_loader()
+ self._register_default_handlers()
+
+ def __repr__(self):
+ return '{}(region_name={})'.format(
+ self.__class__.__name__,
+ repr(self._session.get_config_variable('region')),
+ )
+
+ @property
+ def profile_name(self):
+ """
+ The **read-only** profile name.
+ """
+ return self._session.profile or 'default'
+
+ @property
+ def region_name(self):
+ """
+ The **read-only** region name.
+ """
+ return self._session.get_config_variable('region')
+
+ @property
+ def events(self):
+ """
+ The event emitter for a session
+ """
+ return self._session.get_component('event_emitter')
+
+ @property
+ def available_profiles(self):
+ """
+ The profiles available to the session credentials
+ """
+ return self._session.available_profiles
+
+ def _setup_loader(self):
+ """
+ Setup loader paths so that we can load resources.
+ """
+ self._loader = self._session.get_component('data_loader')
+ self._loader.search_paths.append(
+ os.path.join(os.path.dirname(__file__), 'data')
+ )
+
+ def get_available_services(self):
+ """
+ Get a list of available services that can be loaded as low-level
+ clients via :py:meth:`Session.client`.
+
+ :rtype: list
+ :return: List of service names
+ """
+ return self._session.get_available_services()
+
+ def get_available_resources(self):
+ """
+ Get a list of available services that can be loaded as resource
+ clients via :py:meth:`Session.resource`.
+
+ :rtype: list
+ :return: List of service names
+ """
+ return self._loader.list_available_services(type_name='resources-1')
+
+ def get_available_partitions(self):
+ """Lists the available partitions
+
+ :rtype: list
+ :return: Returns a list of partition names (e.g., ["aws", "aws-cn"])
+ """
+ return self._session.get_available_partitions()
+
+ def get_available_regions(
+ self, service_name, partition_name='aws', allow_non_regional=False
+ ):
+ """Lists the region and endpoint names of a particular partition.
+
+ The list of regions returned by this method are regions that are
+ explicitly known by the client to exist and is not comprehensive. A
+ region not returned in this list may still be available for the
+ provided service.
+
+ :type service_name: string
+ :param service_name: Name of a service to list endpoint for (e.g., s3).
+
+ :type partition_name: string
+ :param partition_name: Name of the partition to limit endpoints to.
+ (e.g., aws for the public AWS endpoints, aws-cn for AWS China
+ endpoints, aws-us-gov for AWS GovCloud (US) Endpoints, etc.)
+
+ :type allow_non_regional: bool
+ :param allow_non_regional: Set to True to include endpoints that are
+ not regional endpoints (e.g., s3-external-1,
+ fips-us-gov-west-1, etc).
+
+ :return: Returns a list of endpoint names (e.g., ["us-east-1"]).
+ """
+ return self._session.get_available_regions(
+ service_name=service_name,
+ partition_name=partition_name,
+ allow_non_regional=allow_non_regional,
+ )
+
+ def get_credentials(self):
+ """
+ Return the :class:`botocore.credentials.Credentials` object
+ associated with this session. If the credentials have not
+ yet been loaded, this will attempt to load them. If they
+ have already been loaded, this will return the cached
+ credentials.
+ """
+ return self._session.get_credentials()
+
+ def get_partition_for_region(self, region_name):
+ """Lists the partition name of a particular region.
+
+ :type region_name: string
+ :param region_name: Name of the region to list partition for (e.g.,
+ us-east-1).
+
+ :rtype: string
+ :return: Returns the respective partition name (e.g., aws).
+ """
+ return self._session.get_partition_for_region(region_name)
+
+ def client(
+ self,
+ service_name,
+ region_name=None,
+ api_version=None,
+ use_ssl=True,
+ verify=None,
+ endpoint_url=None,
+ aws_access_key_id=None,
+ aws_secret_access_key=None,
+ aws_session_token=None,
+ config=None,
+ ):
+ """
+ Create a low-level service client by name.
+
+ :type service_name: string
+ :param service_name: The name of a service, e.g. 's3' or 'ec2'. You
+ can get a list of available services via
+ :py:meth:`get_available_services`.
+
+ :type region_name: string
+ :param region_name: The name of the region associated with the client.
+ A client is associated with a single region.
+
+ :type api_version: string
+ :param api_version: The API version to use. By default, botocore will
+ use the latest API version when creating a client. You only need
+ to specify this parameter if you want to use a previous API version
+ of the client.
+
+ :type use_ssl: boolean
+ :param use_ssl: Whether or not to use SSL. By default, SSL is used.
+ Note that not all services support non-ssl connections.
+
+ :type verify: boolean/string
+ :param verify: Whether or not to verify SSL certificates. By default
+ SSL certificates are verified. You can provide the following
+ values:
+
+ * False - do not validate SSL certificates. SSL will still be
+ used (unless use_ssl is False), but SSL certificates
+ will not be verified.
+ * path/to/cert/bundle.pem - A filename of the CA cert bundle to
+ uses. You can specify this argument if you want to use a
+ different CA cert bundle than the one used by botocore.
+
+ :type endpoint_url: string
+ :param endpoint_url: The complete URL to use for the constructed
+ client. Normally, botocore will automatically construct the
+ appropriate URL to use when communicating with a service. You
+ can specify a complete URL (including the "http/https" scheme)
+ to override this behavior. If this value is provided,
+ then ``use_ssl`` is ignored.
+
+ :type aws_access_key_id: string
+ :param aws_access_key_id: The access key to use when creating
+ the client. This is entirely optional, and if not provided,
+ the credentials configured for the session will automatically
+ be used. You only need to provide this argument if you want
+ to override the credentials used for this specific client.
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: The secret key to use when creating
+ the client. Same semantics as aws_access_key_id above.
+
+ :type aws_session_token: string
+ :param aws_session_token: The session token to use when creating
+ the client. Same semantics as aws_access_key_id above.
+
+ :type config: botocore.client.Config
+ :param config: Advanced client configuration options. If region_name
+ is specified in the client config, its value will take precedence
+ over environment variables and configuration values, but not over
+ a region_name value passed explicitly to the method. See
+ `botocore config documentation
+ `_
+ for more details.
+
+ :return: Service client instance
+
+ """
+ return self._session.create_client(
+ service_name,
+ region_name=region_name,
+ api_version=api_version,
+ use_ssl=use_ssl,
+ verify=verify,
+ endpoint_url=endpoint_url,
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ aws_session_token=aws_session_token,
+ config=config,
+ )
+
+ def resource(
+ self,
+ service_name,
+ region_name=None,
+ api_version=None,
+ use_ssl=True,
+ verify=None,
+ endpoint_url=None,
+ aws_access_key_id=None,
+ aws_secret_access_key=None,
+ aws_session_token=None,
+ config=None,
+ ):
+ """
+ Create a resource service client by name.
+
+ :type service_name: string
+ :param service_name: The name of a service, e.g. 's3' or 'ec2'. You
+ can get a list of available services via
+ :py:meth:`get_available_resources`.
+
+ :type region_name: string
+ :param region_name: The name of the region associated with the client.
+ A client is associated with a single region.
+
+ :type api_version: string
+ :param api_version: The API version to use. By default, botocore will
+ use the latest API version when creating a client. You only need
+ to specify this parameter if you want to use a previous API version
+ of the client.
+
+ :type use_ssl: boolean
+ :param use_ssl: Whether or not to use SSL. By default, SSL is used.
+ Note that not all services support non-ssl connections.
+
+ :type verify: boolean/string
+ :param verify: Whether or not to verify SSL certificates. By default
+ SSL certificates are verified. You can provide the following
+ values:
+
+ * False - do not validate SSL certificates. SSL will still be
+ used (unless use_ssl is False), but SSL certificates
+ will not be verified.
+ * path/to/cert/bundle.pem - A filename of the CA cert bundle to
+ uses. You can specify this argument if you want to use a
+ different CA cert bundle than the one used by botocore.
+
+ :type endpoint_url: string
+ :param endpoint_url: The complete URL to use for the constructed
+ client. Normally, botocore will automatically construct the
+ appropriate URL to use when communicating with a service. You
+ can specify a complete URL (including the "http/https" scheme)
+ to override this behavior. If this value is provided,
+ then ``use_ssl`` is ignored.
+
+ :type aws_access_key_id: string
+ :param aws_access_key_id: The access key to use when creating
+ the client. This is entirely optional, and if not provided,
+ the credentials configured for the session will automatically
+ be used. You only need to provide this argument if you want
+ to override the credentials used for this specific client.
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: The secret key to use when creating
+ the client. Same semantics as aws_access_key_id above.
+
+ :type aws_session_token: string
+ :param aws_session_token: The session token to use when creating
+ the client. Same semantics as aws_access_key_id above.
+
+ :type config: botocore.client.Config
+ :param config: Advanced client configuration options. If region_name
+ is specified in the client config, its value will take precedence
+ over environment variables and configuration values, but not over
+ a region_name value passed explicitly to the method. If
+ user_agent_extra is specified in the client config, it overrides
+ the default user_agent_extra provided by the resource API. See
+ `botocore config documentation
+ `_
+ for more details.
+
+ :return: Subclass of :py:class:`~boto3.resources.base.ServiceResource`
+ """
+ try:
+ resource_model = self._loader.load_service_model(
+ service_name, 'resources-1', api_version
+ )
+ except UnknownServiceError:
+ available = self.get_available_resources()
+ has_low_level_client = (
+ service_name in self.get_available_services()
+ )
+ raise ResourceNotExistsError(
+ service_name, available, has_low_level_client
+ )
+ except DataNotFoundError:
+ # This is because we've provided an invalid API version.
+ available_api_versions = self._loader.list_api_versions(
+ service_name, 'resources-1'
+ )
+ raise UnknownAPIVersionError(
+ service_name, api_version, ', '.join(available_api_versions)
+ )
+
+ if api_version is None:
+ # Even though botocore's load_service_model() can handle
+ # using the latest api_version if not provided, we need
+ # to track this api_version in boto3 in order to ensure
+ # we're pairing a resource model with a client model
+ # of the same API version. It's possible for the latest
+ # API version of a resource model in boto3 to not be
+ # the same API version as a service model in botocore.
+ # So we need to look up the api_version if one is not
+ # provided to ensure we load the same API version of the
+ # client.
+ #
+ # Note: This is relying on the fact that
+ # loader.load_service_model(..., api_version=None)
+ # and loader.determine_latest_version(..., 'resources-1')
+ # both load the same api version of the file.
+ api_version = self._loader.determine_latest_version(
+ service_name, 'resources-1'
+ )
+
+ # Creating a new resource instance requires the low-level client
+ # and service model, the resource version and resource JSON data.
+ # We pass these to the factory and get back a class, which is
+ # instantiated on top of the low-level client.
+ if config is not None:
+ if config.user_agent_extra is None:
+ config = copy.deepcopy(config)
+ config.user_agent_extra = 'Resource'
+ else:
+ config = Config(user_agent_extra='Resource')
+ client = self.client(
+ service_name,
+ region_name=region_name,
+ api_version=api_version,
+ use_ssl=use_ssl,
+ verify=verify,
+ endpoint_url=endpoint_url,
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ aws_session_token=aws_session_token,
+ config=config,
+ )
+ service_model = client.meta.service_model
+
+ # Create a ServiceContext object to serve as a reference to
+ # important read-only information about the general service.
+ service_context = boto3.utils.ServiceContext(
+ service_name=service_name,
+ service_model=service_model,
+ resource_json_definitions=resource_model['resources'],
+ service_waiter_model=boto3.utils.LazyLoadedWaiterModel(
+ self._session, service_name, api_version
+ ),
+ )
+
+ # Create the service resource class.
+ cls = self.resource_factory.load_from_definition(
+ resource_name=service_name,
+ single_resource_json_definition=resource_model['service'],
+ service_context=service_context,
+ )
+
+ return cls(client=client)
+
+ def _register_default_handlers(self):
+ # S3 customizations
+ self._session.register(
+ 'creating-client-class.s3',
+ boto3.utils.lazy_call(
+ 'boto3.s3.inject.inject_s3_transfer_methods'
+ ),
+ )
+ self._session.register(
+ 'creating-resource-class.s3.Bucket',
+ boto3.utils.lazy_call('boto3.s3.inject.inject_bucket_methods'),
+ )
+ self._session.register(
+ 'creating-resource-class.s3.Object',
+ boto3.utils.lazy_call('boto3.s3.inject.inject_object_methods'),
+ )
+ self._session.register(
+ 'creating-resource-class.s3.ObjectSummary',
+ boto3.utils.lazy_call(
+ 'boto3.s3.inject.inject_object_summary_methods'
+ ),
+ )
+
+ # DynamoDb customizations
+ self._session.register(
+ 'creating-resource-class.dynamodb',
+ boto3.utils.lazy_call(
+ 'boto3.dynamodb.transform.register_high_level_interface'
+ ),
+ unique_id='high-level-dynamodb',
+ )
+ self._session.register(
+ 'creating-resource-class.dynamodb.Table',
+ boto3.utils.lazy_call(
+ 'boto3.dynamodb.table.register_table_methods'
+ ),
+ unique_id='high-level-dynamodb-table',
+ )
+
+ # EC2 Customizations
+ self._session.register(
+ 'creating-resource-class.ec2.ServiceResource',
+ boto3.utils.lazy_call('boto3.ec2.createtags.inject_create_tags'),
+ )
+
+ self._session.register(
+ 'creating-resource-class.ec2.Instance',
+ boto3.utils.lazy_call(
+ 'boto3.ec2.deletetags.inject_delete_tags',
+ event_emitter=self.events,
+ ),
+ )
diff --git a/venv/lib/python3.12/site-packages/boto3/utils.py b/venv/lib/python3.12/site-packages/boto3/utils.py
new file mode 100644
index 0000000..27561ad
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/boto3/utils.py
@@ -0,0 +1,100 @@
+# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# https://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import sys
+from collections import namedtuple
+
+_ServiceContext = namedtuple(
+ 'ServiceContext',
+ [
+ 'service_name',
+ 'service_model',
+ 'service_waiter_model',
+ 'resource_json_definitions',
+ ],
+)
+
+
+class ServiceContext(_ServiceContext):
+ """Provides important service-wide, read-only information about a service
+
+ :type service_name: str
+ :param service_name: The name of the service
+
+ :type service_model: :py:class:`botocore.model.ServiceModel`
+ :param service_model: The model of the service.
+
+ :type service_waiter_model: :py:class:`botocore.waiter.WaiterModel` or
+ a waiter model-like object such as
+ :py:class:`boto3.utils.LazyLoadedWaiterModel`
+ :param service_waiter_model: The waiter model of the service.
+
+ :type resource_json_definitions: dict
+ :param resource_json_definitions: The loaded json models of all resource
+ shapes for a service. It is equivalient of loading a
+ ``resource-1.json`` and retrieving the value at the key "resources".
+ """
+
+ pass
+
+
+def import_module(name):
+ """Import module given a name.
+
+ Does not support relative imports.
+
+ """
+ __import__(name)
+ return sys.modules[name]
+
+
+def lazy_call(full_name, **kwargs):
+ parent_kwargs = kwargs
+
+ def _handler(**kwargs):
+ module, function_name = full_name.rsplit('.', 1)
+ module = import_module(module)
+ kwargs.update(parent_kwargs)
+ return getattr(module, function_name)(**kwargs)
+
+ return _handler
+
+
+def inject_attribute(class_attributes, name, value):
+ if name in class_attributes:
+ raise RuntimeError(
+ f'Cannot inject class attribute "{name}", attribute '
+ f'already exists in class dict.'
+ )
+ else:
+ class_attributes[name] = value
+
+
+class LazyLoadedWaiterModel:
+ """A lazily loaded waiter model
+
+ This does not load the service waiter model until an attempt is made
+ to retrieve the waiter model for a specific waiter. This is helpful
+ in docstring generation where we do not need to actually need to grab
+ the waiter-2.json until it is accessed through a ``get_waiter`` call
+ when the docstring is generated/accessed.
+ """
+
+ def __init__(self, bc_session, service_name, api_version):
+ self._session = bc_session
+ self._service_name = service_name
+ self._api_version = api_version
+
+ def get_waiter(self, waiter_name):
+ return self._session.get_waiter_model(
+ self._service_name, self._api_version
+ ).get_waiter(waiter_name)
diff --git a/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/LICENSE.txt b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/LICENSE.txt
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/LICENSE.txt
@@ -0,0 +1,177 @@
+
+ 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
diff --git a/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/METADATA b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/METADATA
new file mode 100644
index 0000000..a7a745d
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/METADATA
@@ -0,0 +1,149 @@
+Metadata-Version: 2.1
+Name: botocore
+Version: 1.34.162
+Summary: Low-level, data-driven core of boto 3.
+Home-page: https://github.com/boto/botocore
+Author: Amazon Web Services
+License: Apache License 2.0
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: Natural Language :: English
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3
+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
+Requires-Python: >= 3.8
+License-File: LICENSE.txt
+License-File: NOTICE
+Requires-Dist: jmespath (<2.0.0,>=0.7.1)
+Requires-Dist: python-dateutil (<3.0.0,>=2.1)
+Requires-Dist: urllib3 (<1.27,>=1.25.4) ; python_version < "3.10"
+Requires-Dist: urllib3 (!=2.2.0,<3,>=1.25.4) ; python_version >= "3.10"
+Provides-Extra: crt
+Requires-Dist: awscrt (==0.21.2) ; extra == 'crt'
+
+botocore
+========
+
+|Version| |Python| |License|
+
+A low-level interface to a growing number of Amazon Web Services. The
+botocore package is the foundation for the
+`AWS CLI `__ as well as
+`boto3 `__.
+
+Botocore is maintained and published by `Amazon Web Services`_.
+
+Notices
+-------
+
+On 2023-12-13, support was dropped for Python 3.7. This follows the
+Python Software Foundation `end of support `__
+for the runtime which occurred on 2023-06-27.
+For more information, see this `blog post `__.
+
+.. _`Amazon Web Services`: https://aws.amazon.com/what-is-aws/
+.. |Python| image:: https://img.shields.io/pypi/pyversions/botocore.svg?style=flat
+ :target: https://pypi.python.org/pypi/botocore/
+ :alt: Python Versions
+.. |Version| image:: http://img.shields.io/pypi/v/botocore.svg?style=flat
+ :target: https://pypi.python.org/pypi/botocore/
+ :alt: Package Version
+.. |License| image:: http://img.shields.io/pypi/l/botocore.svg?style=flat
+ :target: https://github.com/boto/botocore/blob/develop/LICENSE.txt
+ :alt: License
+
+Getting Started
+---------------
+Assuming that you have Python and ``virtualenv`` installed, set up your environment and install the required dependencies like this or you can install the library using ``pip``:
+
+.. code-block:: sh
+
+ $ git clone https://github.com/boto/botocore.git
+ $ cd botocore
+ $ virtualenv venv
+ ...
+ $ . venv/bin/activate
+ $ pip install -r requirements.txt
+ $ pip install -e .
+
+.. code-block:: sh
+
+ $ pip install botocore
+
+Using Botocore
+~~~~~~~~~~~~~~
+After installing botocore
+
+Next, set up credentials (in e.g. ``~/.aws/credentials``):
+
+.. code-block:: ini
+
+ [default]
+ aws_access_key_id = YOUR_KEY
+ aws_secret_access_key = YOUR_SECRET
+
+Then, set up a default region (in e.g. ``~/.aws/config``):
+
+.. code-block:: ini
+
+ [default]
+ region=us-east-1
+
+Other credentials configuration method can be found `here `__
+
+Then, from a Python interpreter:
+
+.. code-block:: python
+
+ >>> import botocore.session
+ >>> session = botocore.session.get_session()
+ >>> client = session.create_client('ec2')
+ >>> print(client.describe_instances())
+
+
+Getting Help
+------------
+
+We use GitHub issues for tracking bugs and feature requests and have limited
+bandwidth to address them. Please use these community resources for getting
+help. Please note many of the same resources available for ``boto3`` are
+applicable for ``botocore``:
+
+* Ask a question on `Stack Overflow `__ and tag it with `boto3 `__
+* Open a support ticket with `AWS Support `__
+* If it turns out that you may have found a bug, please `open an issue `__
+
+
+Contributing
+------------
+
+We value feedback and contributions from our community. Whether it's a bug report, new feature, correction, or additional documentation, we welcome your issues and pull requests. Please read through this `CONTRIBUTING `__ document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your contribution.
+
+
+Maintenance and Support for SDK Major Versions
+----------------------------------------------
+
+Botocore was made generally available on 06/22/2015 and is currently in the full support phase of the availability life cycle.
+
+For information about maintenance and support for SDK major versions and their underlying dependencies, see the following in the AWS SDKs and Tools Reference Guide:
+
+* `AWS SDKs and Tools Maintenance Policy `__
+* `AWS SDKs and Tools Version Support Matrix `__
+
+
+More Resources
+--------------
+
+* `NOTICE `__
+* `Changelog `__
+* `License `__
+
+
diff --git a/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/NOTICE b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/NOTICE
new file mode 100644
index 0000000..edcc3cd
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/NOTICE
@@ -0,0 +1,60 @@
+Botocore
+Copyright 2012-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+----
+
+Botocore includes vendorized parts of the requests python library for backwards compatibility.
+
+Requests License
+================
+
+Copyright 2013 Kenneth Reitz
+
+ 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.
+
+Botocore includes vendorized parts of the urllib3 library for backwards compatibility.
+
+Urllib3 License
+===============
+
+This is the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+Copyright 2008-2011 Andrey Petrov and contributors (see CONTRIBUTORS.txt),
+Modifications copyright 2012 Kenneth Reitz.
+
+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.
+
+Bundle of CA Root Certificates
+==============================
+
+***** 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 *****
diff --git a/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/RECORD b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/RECORD
new file mode 100644
index 0000000..b845209
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/RECORD
@@ -0,0 +1,1848 @@
+botocore-1.34.162.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+botocore-1.34.162.dist-info/LICENSE.txt,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
+botocore-1.34.162.dist-info/METADATA,sha256=w7Tl5Wfwg2UxWWhLtlIiIbdWCmlzW4rTkfexkld2hjs,5662
+botocore-1.34.162.dist-info/NOTICE,sha256=HRxabz1oyxH0-tGvqGp0UNAobxXBdu8OoEjyVbRtlbA,2467
+botocore-1.34.162.dist-info/RECORD,,
+botocore-1.34.162.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
+botocore-1.34.162.dist-info/top_level.txt,sha256=IdlNr9dnwi3lQt66dKnShE5HBUhIqBFqJmVhm11aijk,9
+botocore/__init__.py,sha256=4k6z3K8BMBl8r8efxl-bTdiacg3af4-DHp6FCPdITOM,4915
+botocore/__pycache__/__init__.cpython-312.pyc,,
+botocore/__pycache__/args.cpython-312.pyc,,
+botocore/__pycache__/auth.cpython-312.pyc,,
+botocore/__pycache__/awsrequest.cpython-312.pyc,,
+botocore/__pycache__/client.cpython-312.pyc,,
+botocore/__pycache__/compat.cpython-312.pyc,,
+botocore/__pycache__/compress.cpython-312.pyc,,
+botocore/__pycache__/config.cpython-312.pyc,,
+botocore/__pycache__/configloader.cpython-312.pyc,,
+botocore/__pycache__/configprovider.cpython-312.pyc,,
+botocore/__pycache__/credentials.cpython-312.pyc,,
+botocore/__pycache__/discovery.cpython-312.pyc,,
+botocore/__pycache__/endpoint.cpython-312.pyc,,
+botocore/__pycache__/endpoint_provider.cpython-312.pyc,,
+botocore/__pycache__/errorfactory.cpython-312.pyc,,
+botocore/__pycache__/eventstream.cpython-312.pyc,,
+botocore/__pycache__/exceptions.cpython-312.pyc,,
+botocore/__pycache__/handlers.cpython-312.pyc,,
+botocore/__pycache__/history.cpython-312.pyc,,
+botocore/__pycache__/hooks.cpython-312.pyc,,
+botocore/__pycache__/httpchecksum.cpython-312.pyc,,
+botocore/__pycache__/httpsession.cpython-312.pyc,,
+botocore/__pycache__/loaders.cpython-312.pyc,,
+botocore/__pycache__/model.cpython-312.pyc,,
+botocore/__pycache__/monitoring.cpython-312.pyc,,
+botocore/__pycache__/paginate.cpython-312.pyc,,
+botocore/__pycache__/parsers.cpython-312.pyc,,
+botocore/__pycache__/regions.cpython-312.pyc,,
+botocore/__pycache__/response.cpython-312.pyc,,
+botocore/__pycache__/retryhandler.cpython-312.pyc,,
+botocore/__pycache__/serialize.cpython-312.pyc,,
+botocore/__pycache__/session.cpython-312.pyc,,
+botocore/__pycache__/signers.cpython-312.pyc,,
+botocore/__pycache__/stub.cpython-312.pyc,,
+botocore/__pycache__/tokens.cpython-312.pyc,,
+botocore/__pycache__/translate.cpython-312.pyc,,
+botocore/__pycache__/useragent.cpython-312.pyc,,
+botocore/__pycache__/utils.cpython-312.pyc,,
+botocore/__pycache__/validate.cpython-312.pyc,,
+botocore/__pycache__/waiter.cpython-312.pyc,,
+botocore/args.py,sha256=foRm7a2ET2AWCoVTB6B5M5UeHBtUfcktQRvAXqMdLys,30639
+botocore/auth.py,sha256=32cF4-zHrITiegc4nUGbPpzXFGwqvmNGrgKIcIfktTc,43793
+botocore/awsrequest.py,sha256=7isOJRNGzhbcxON4P6d7WhYZtzglAhYRfEYc5y-2bbo,23152
+botocore/cacert.pem,sha256=nW1QIfzIoiMvzo60s_mC3EhCUtVVSTrFwqPL8ssZQ4o,266617
+botocore/client.py,sha256=D4U2MYkBLr7sEQAJNPikMAXoME16N2ruv6gdRl3nNxw,51442
+botocore/compat.py,sha256=iAxOnj214khLn4KvvSTDSUnQNVRYdAkDTWQeWRRJEic,11091
+botocore/compress.py,sha256=F0eVNLLHA9aIKWAB_QnMb29hg50BCCpAjCZwhFohVi4,4430
+botocore/config.py,sha256=LHyBLgZnxTx_2l3Bk0NZSX2ZWfYsuhh7rO9PZhnWHQQ,15601
+botocore/configloader.py,sha256=NTejI7b9UGUXBv2uKiPaXH19Lgl30LY5ujZkXRcFpHs,10039
+botocore/configprovider.py,sha256=lP-AEvhyaBtgHzgP7vVk1YNRtxtQ1W7GtkdRfIAqGuY,37145
+botocore/credentials.py,sha256=ha0b3jlD3XFnYTu9vqO7xfdH8XgPJgPvIMCDIq1pjeM,84780
+botocore/crt/__init__.py,sha256=kCXQL93gdg5yBQJOTp7YFLl9wYNy4tV_5TAyJq0asD0,1006
+botocore/crt/__pycache__/__init__.cpython-312.pyc,,
+botocore/crt/__pycache__/auth.cpython-312.pyc,,
+botocore/crt/auth.py,sha256=EBIImARWx3g4euY3r9OwZWtt3ZOEH5GTcgDZYXoTJGY,25318
+botocore/data/_retry.json,sha256=9YkW5V-FMGzj8zwHX1Payit1aRaKjbaqKtKqmePpfXU,7025
+botocore/data/accessanalyzer/2019-11-01/endpoint-rule-set-1.json.gz,sha256=O4c0HtCuoFGJ50VJsWAWekY3zPkrqJOIPLb5NFKfLvk,1241
+botocore/data/accessanalyzer/2019-11-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/accessanalyzer/2019-11-01/paginators-1.json,sha256=8cCsaqYmzBJj1naqrZSRYos_QGyOVec_G_9xuLEApG8,1908
+botocore/data/accessanalyzer/2019-11-01/paginators-1.sdk-extras.json,sha256=nwsOcoMZ1GDzrFfMc6_Gx0tNRKwt5b8XddGiZmkAS2s,600
+botocore/data/accessanalyzer/2019-11-01/service-2.json.gz,sha256=f4qxGkoO1O8X6d4mtejs81-_dVlf1inn9PDiRgqF86Y,23468
+botocore/data/account/2021-02-01/endpoint-rule-set-1.json.gz,sha256=W_cvXeFsL2xqNEHlN_wZQZJ7KOs9jeV45O4rN4Sbprg,1370
+botocore/data/account/2021-02-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/account/2021-02-01/paginators-1.json,sha256=TCku1Qs1la1Ggv8u8dKSYP2E5i5sWpmhRmL4zSR87RQ,185
+botocore/data/account/2021-02-01/service-2.json.gz,sha256=tLbd6dN__Xs2H-tCYRsSQVpZ6A3azLeLJzEAqmW3Q0M,5492
+botocore/data/acm-pca/2017-08-22/endpoint-rule-set-1.json.gz,sha256=qmxnX08s1Ty8rywcch-W4VFtKltGKJ8QYwsMVjQSqPE,1236
+botocore/data/acm-pca/2017-08-22/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/acm-pca/2017-08-22/paginators-1.json,sha256=q2wFRetchlBt43qtOCTJ_Qw49u-LnRgmPdEn1j_j50A,537
+botocore/data/acm-pca/2017-08-22/service-2.json.gz,sha256=JSLrKMSMjIvmHKFiLGqelllMh-m_DK2HNv-CZUCfbn4,23817
+botocore/data/acm-pca/2017-08-22/waiters-2.json,sha256=AhcMWQLb4hSGr4Wnr75QBfzek_3hNd5odPZvlbOIuag,2447
+botocore/data/acm/2015-12-08/endpoint-rule-set-1.json.gz,sha256=EKF0YGIqFexRO5D4PTKKK2lQjxTFXGvkS78cidwtGqs,1232
+botocore/data/acm/2015-12-08/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/acm/2015-12-08/paginators-1.json,sha256=oB2exj3JKzcsLCvfBeMqYawlxz6YghtvUQlwOfdTY4g,203
+botocore/data/acm/2015-12-08/service-2.json.gz,sha256=6GdZALetxjcPfMWrLL-jw8h3MZXKtaOZ5rPzZQOdu9o,14187
+botocore/data/acm/2015-12-08/waiters-2.json,sha256=S3uw0vWaMVDBNCST96n0BIyzhiBFuX0Oqp9C-SCxYeE,874
+botocore/data/amp/2020-08-01/endpoint-rule-set-1.json.gz,sha256=4sjuqebntcoGTQAJBl4KkJwYPdfl3REy5btDDnN8W2I,1146
+botocore/data/amp/2020-08-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/amp/2020-08-01/paginators-1.json,sha256=4pEkF8Q0jIMIgsFHCAtQ6edC4NmKKwFWsJWU54kN4Fg,539
+botocore/data/amp/2020-08-01/service-2.json.gz,sha256=2UT0kc8q0NTRkume3z6zw2detFLQDYuk4Bc1Atk60X0,10299
+botocore/data/amp/2020-08-01/waiters-2.json,sha256=9dx5obvXJDOgd3ZoJLbi6ZykWm7Ae698VQeNfR8TO5o,2177
+botocore/data/amplify/2017-07-25/endpoint-rule-set-1.json.gz,sha256=yA1kf7I-hRPzKN9tC2eujY_pofwgw-S20MLfaQ2UURw,1150
+botocore/data/amplify/2017-07-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/amplify/2017-07-25/paginators-1.json,sha256=XJ4xwNrUExhAxy-8K8JJAPnBhdRZO7FB6NGTrgr_qZQ,685
+botocore/data/amplify/2017-07-25/service-2.json.gz,sha256=9KXAuWY7GB5Z55Pw9gP9r02GTLanJlRYJB7ZyqlaenU,15464
+botocore/data/amplifybackend/2020-08-11/endpoint-rule-set-1.json.gz,sha256=1noD_tbpesQQNS-0X0S5OlXskil1FMofcQKwoF54fR4,1152
+botocore/data/amplifybackend/2020-08-11/paginators-1.json,sha256=0JG13-2KlCwca-Pwz7d5Mp3WIttu4BpwDusqxMXF9XY,186
+botocore/data/amplifybackend/2020-08-11/service-2.json.gz,sha256=nA80tEyDZBAGY7SXUW9v5t4J6wsXYwYrIXMEmpnytAc,10990
+botocore/data/amplifyuibuilder/2021-08-11/endpoint-rule-set-1.json.gz,sha256=3nJ7G0t0Xz3VBa0z1h6nrJf9MCUwSxB1upVRnPRQtT0,1158
+botocore/data/amplifyuibuilder/2021-08-11/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/amplifyuibuilder/2021-08-11/paginators-1.json,sha256=idtki67MCJcfs_brVKsvknxJbZtDfS-IK3cakM1IFCI,1063
+botocore/data/amplifyuibuilder/2021-08-11/service-2.json.gz,sha256=m3fxXYNWxUnuCxliR0K51MM-V-vGxdV7Sc-KqiEBDRg,15634
+botocore/data/amplifyuibuilder/2021-08-11/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/apigateway/2015-07-09/endpoint-rule-set-1.json.gz,sha256=lemqgUPhTmCiUYJny9QStckhbMM4SzpFZHQxsISGGzY,1152
+botocore/data/apigateway/2015-07-09/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/apigateway/2015-07-09/paginators-1.json,sha256=gwAb1K7CkHdC49pAfwZMgaT18Hm1r5qDK1m_6m-Ki9w,2913
+botocore/data/apigateway/2015-07-09/service-2.json.gz,sha256=sRZLGnbWohP2aWeaFZpTQXLWa7gGcsOHEkv_9GxMkjs,37298
+botocore/data/apigatewaymanagementapi/2018-11-29/endpoint-rule-set-1.json.gz,sha256=yh-BCcuFkVgUpaVo6ktrsuVPeZgviC0k2rKIOBsFyQg,1150
+botocore/data/apigatewaymanagementapi/2018-11-29/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/apigatewaymanagementapi/2018-11-29/service-2.json.gz,sha256=VB4AR_QS7AH6U3X2ygFZEZnKpBOOyrO207T5YJa1VhM,1422
+botocore/data/apigatewayv2/2018-11-29/endpoint-rule-set-1.json.gz,sha256=Qv-BVHQjDb50j-9uLwcnxXbWFAb62_EJxLal6pIX1so,1149
+botocore/data/apigatewayv2/2018-11-29/paginators-1.json,sha256=auWh91zAZKEKRTA0qdDSA_eeveKmFAqH20BdEd3wM6M,1626
+botocore/data/apigatewayv2/2018-11-29/service-2.json.gz,sha256=okVcZVYxgibCglL8Z_jvpS33kZMDexVTf_bvklnTrwg,40465
+botocore/data/appconfig/2019-10-09/endpoint-rule-set-1.json.gz,sha256=2RewyA910uFtt1i7H-gj5YTBLHgN3KKoVgSF-h8hYqE,1232
+botocore/data/appconfig/2019-10-09/examples-1.json,sha256=lm2meYHY2djHXZ_3lYZa2PxELHhVDtZdMkVw4IWCI8Y,25502
+botocore/data/appconfig/2019-10-09/paginators-1.json,sha256=DlvXrqKcTiVi3Yv2rStPwl5O1kqSQaiyRGD_fQugFEQ,1367
+botocore/data/appconfig/2019-10-09/service-2.json.gz,sha256=LlolH3U5pa3RFua5eH293PLSewsg143Ic3-HF5GDY-A,17955
+botocore/data/appconfigdata/2021-11-11/endpoint-rule-set-1.json.gz,sha256=loApwR_hg4hPocnjULdXYmVAQxpyRL7bvrx-4iRacGo,1237
+botocore/data/appconfigdata/2021-11-11/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/appconfigdata/2021-11-11/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/appconfigdata/2021-11-11/service-2.json.gz,sha256=C_pKyaJ1c29irgUcQiMKzEONCuIrPx-oKzlL853SmGo,3114
+botocore/data/appfabric/2023-05-19/endpoint-rule-set-1.json.gz,sha256=D8PIPvaZTSQBLBwwbJ9oQE3K8L5vsNqopqxrei7xuN0,1290
+botocore/data/appfabric/2023-05-19/paginators-1.json,sha256=AceDN9kDs832sLebyXTQMYza-dMZ8m2hsVyzbqxUXnQ,745
+botocore/data/appfabric/2023-05-19/service-2.json.gz,sha256=WG8oOBcD5K_GIc_6BcuPBk3HoTtxwbMNsc_91_g3Cj8,8601
+botocore/data/appfabric/2023-05-19/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/appflow/2020-08-23/endpoint-rule-set-1.json.gz,sha256=ISa4zhk9JOn1yWKFhnquHRknb21GOd3JVm043Xk2xdU,1148
+botocore/data/appflow/2020-08-23/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/appflow/2020-08-23/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/appflow/2020-08-23/service-2.json.gz,sha256=JfLeW2Vx9MCxPTlId3hjlBfo1UWO5kheUMmZcEHXgIs,32788
+botocore/data/appintegrations/2020-07-29/endpoint-rule-set-1.json.gz,sha256=W8ZgKP0W_Q8Uc3A64T_LBWzOHUEJqW971i9TjHL4_Zo,1154
+botocore/data/appintegrations/2020-07-29/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/appintegrations/2020-07-29/paginators-1.json,sha256=BYTiBgFZxnU-sQgufFZqZnJtClnQxZqlwuhNGC6J1vw,1147
+botocore/data/appintegrations/2020-07-29/service-2.json.gz,sha256=1eywdwDYS_4TXDwDWCNkylCO-UbPTEOnZPtbogb_4Gs,6412
+botocore/data/application-autoscaling/2016-02-06/endpoint-rule-set-1.json.gz,sha256=60wSDneHo4w3O__xx8eBdBj60oKJyaxDoS5-dX7VpMc,1245
+botocore/data/application-autoscaling/2016-02-06/examples-1.json,sha256=_IICzVD2rqZHmWHwRCsR313_WXRitdmWhlhDtSzomVE,8473
+botocore/data/application-autoscaling/2016-02-06/paginators-1.json,sha256=Yg5NHu8W50qc_r8JCtkNGMbKd861R4w8wQFdrbV0rR0,751
+botocore/data/application-autoscaling/2016-02-06/service-2.json.gz,sha256=V1Sifec_LQA9mf6AEP20gGImg6q1hYSYRPluNy-1eH8,19453
+botocore/data/application-insights/2018-11-25/endpoint-rule-set-1.json.gz,sha256=6KjAfj8UKcfsntAbBVoVTy1V7LG_-sJGSuiFcSVAR4A,1160
+botocore/data/application-insights/2018-11-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/application-insights/2018-11-25/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/application-insights/2018-11-25/service-2.json.gz,sha256=iQTD6WVBmnLyg2ijji8jJ1uzzfCK6POFhIic_V_TtOc,12183
+botocore/data/application-signals/2024-04-15/endpoint-rule-set-1.json.gz,sha256=sgKQyLvx64XStVH5spTYdIh-32KeKLpThDsjb4bPxdQ,842
+botocore/data/application-signals/2024-04-15/paginators-1.json,sha256=KAFXJrqSeA45SYk41zTZLhD-A8c9mEWgYmNgktV6aZc,919
+botocore/data/application-signals/2024-04-15/paginators-1.sdk-extras.json,sha256=E-MpvfjjDRmqApvk8WUBDZCWK0MjUubUZT_YTUaD5nc,659
+botocore/data/application-signals/2024-04-15/service-2.json.gz,sha256=xR-j3lOw8882n2ZOTJWdwm3bNd5dp_W941ENL5tNyUM,13726
+botocore/data/applicationcostprofiler/2020-09-10/endpoint-rule-set-1.json.gz,sha256=OeUmN82PURPhcPV_BHT986ARf-Xd0g4GrMKGav89zaY,1159
+botocore/data/applicationcostprofiler/2020-09-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/applicationcostprofiler/2020-09-10/paginators-1.json,sha256=2by8SKjvkqf2tkVd1NxlMiNsOoEUr6V3LekGj4k4yWg,205
+botocore/data/applicationcostprofiler/2020-09-10/service-2.json.gz,sha256=NO_SBhIemvhvGL7K97DWJxfG1zoJYZufZK7lqMYTgh8,2827
+botocore/data/appmesh/2018-10-01/endpoint-rule-set-1.json.gz,sha256=OqZR_UKuZG5j3sMDESdVvIoIHQqWi-PTK4VJd047TZ0,1289
+botocore/data/appmesh/2018-10-01/examples-1.json,sha256=IKnIAQr_hsb-b42MXo7jKoBKd1lTzVS0bsbWMSTIwg8,41
+botocore/data/appmesh/2018-10-01/paginators-1.json,sha256=-TPoHMW78DG37BJz5SNi67CsUIs4PTTccyUhlXtMBm4,665
+botocore/data/appmesh/2018-10-01/service-2.json.gz,sha256=2aqJUgBKgckuDd_bmhstbybHtRelfmUR7LCdlFopmWY,7902
+botocore/data/appmesh/2019-01-25/endpoint-rule-set-1.json.gz,sha256=Zq2vFVFEjiAtzf7yVm5zWebwdN8B2x_zWiJzz2gAXRQ,1150
+botocore/data/appmesh/2019-01-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/appmesh/2019-01-25/paginators-1.json,sha256=z6PCEVS0COSk5Nf9KXgXsZ3I9gcq9whv7yonh8s1YMM,1334
+botocore/data/appmesh/2019-01-25/service-2.json.gz,sha256=5bV4BYMvznJSg9RdXvMM-Bkq4gRv_fkDdk2I_32ZYgU,23271
+botocore/data/apprunner/2020-05-15/endpoint-rule-set-1.json.gz,sha256=TikkxsxdtxkA57M2BGdhpd7StC-70wvTI20eM_hCMtA,1151
+botocore/data/apprunner/2020-05-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/apprunner/2020-05-15/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/apprunner/2020-05-15/service-2.json.gz,sha256=hQDxJ5RZ5lYTAQDh76gf04BnPpQMZXymCarvl0nZD_M,19780
+botocore/data/appstream/2016-12-01/endpoint-rule-set-1.json.gz,sha256=nFbeaegf1NsfuvuofUhCk6DexKydyN7mRhgPuVFB2Y8,1244
+botocore/data/appstream/2016-12-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/appstream/2016-12-01/paginators-1.json,sha256=agUpesJpo5f8dC0kH0m0asMYWn4N4MUHae5LK8W0Hwo,1584
+botocore/data/appstream/2016-12-01/service-2.json.gz,sha256=KTSHKVGINRaaeydLQFU2QDCheGh9M7QDTkZaf4KpGuw,32013
+botocore/data/appstream/2016-12-01/waiters-2.json,sha256=XZ1LQBLoJ56YEhaTqi2Bs5XKhax6pr9LRsQVIo7kHck,1245
+botocore/data/appsync/2017-07-25/endpoint-rule-set-1.json.gz,sha256=wLcHLj6SrougePmxF1piSVGhvuEqamL3VYmTRiTJZaY,1151
+botocore/data/appsync/2017-07-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/appsync/2017-07-25/paginators-1.json,sha256=yw3q85DCAkWbeLL1REQTmWfIRBBNhCOgkjWDeQbQwq0,1718
+botocore/data/appsync/2017-07-25/service-2.json.gz,sha256=uzrKMMpXmVNMlA6eYyTAxbXjcDUAwMLLddh9DoZDajA,27638
+botocore/data/apptest/2022-12-06/endpoint-rule-set-1.json.gz,sha256=Tjsd1Cfsz5lYTKbUie9rRqFK1KYF2FH5_lpGxotNsDQ,1297
+botocore/data/apptest/2022-12-06/paginators-1.json,sha256=97fTR04UPghOU82917YJ8m93FG-RjLhipfqosH4Xfyk,1047
+botocore/data/apptest/2022-12-06/service-2.json.gz,sha256=fH6_kJyvtfKO3CQLs-xMfTqq3NY3kM63P28ey8xIR0I,9493
+botocore/data/apptest/2022-12-06/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/arc-zonal-shift/2022-10-30/endpoint-rule-set-1.json.gz,sha256=nJpOkZ2WUftq3uoswR6Mdh9UklTtLvf8YX0SMOcoI5c,1306
+botocore/data/arc-zonal-shift/2022-10-30/paginators-1.json,sha256=wx99_DrI6RWKkZuUiP1HQ1xacRiIoUsgPuxVHGpvZGU,515
+botocore/data/arc-zonal-shift/2022-10-30/service-2.json.gz,sha256=ZM4dBLjLCXyAL9UDjgnOOSicBiJueiseNxbr0IRkJTs,10072
+botocore/data/artifact/2018-05-10/endpoint-rule-set-1.json.gz,sha256=avvU0juJizMz5reDNJpT3dJw7Vnauy4KQtcF3goDML4,1298
+botocore/data/artifact/2018-05-10/paginators-1.json,sha256=zOrnkQZYE4TWGeJWU9aJBY_BvA5gOIJf1AAlyRPSQLI,185
+botocore/data/artifact/2018-05-10/service-2.json.gz,sha256=d3UzW6LWSlNin5gwRuFcdiX-TVMwQEMlTJkLLZXDYKo,2911
+botocore/data/artifact/2018-05-10/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/athena/2017-05-18/endpoint-rule-set-1.json.gz,sha256=Fn-uigxPjMa48FIQoSjht5lhShDmtKyBnquTlzg0ZIE,1149
+botocore/data/athena/2017-05-18/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/athena/2017-05-18/paginators-1.json,sha256=lLXYrCWDDFVhjAdFEhKyoc0-zEe2YYUM4nR9vXRBDgE,1330
+botocore/data/athena/2017-05-18/service-2.json.gz,sha256=UzCBpzgyFD7pA00aMK5I9gk3wE5Pnej9sXqD92yPUmg,29587
+botocore/data/auditmanager/2017-07-25/endpoint-rule-set-1.json.gz,sha256=MiO4ncYllLK03Px96BifWZi-KukCZnqs7F6CQGxcrFc,1153
+botocore/data/auditmanager/2017-07-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/auditmanager/2017-07-25/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/auditmanager/2017-07-25/service-2.json.gz,sha256=pUBnxB6oVSPWpeBBVGOQY8thYQ66S5xWzxljR044jms,27539
+botocore/data/autoscaling-plans/2018-01-06/endpoint-rule-set-1.json.gz,sha256=RfXzw2go-Px_w7jEET1yr9pHxKRIZy9qgphy51BFywQ,1154
+botocore/data/autoscaling-plans/2018-01-06/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/autoscaling-plans/2018-01-06/paginators-1.json,sha256=Au_RY0jJAvQZ-sAmZQk8FXYyrw1rDVD4YILlb6sDxh8,389
+botocore/data/autoscaling-plans/2018-01-06/service-2.json.gz,sha256=yG1jLS9Pu6Ex5rYQemP1maSZnZ_wUCqxyHg7NCZ-GKo,9084
+botocore/data/autoscaling/2011-01-01/endpoint-rule-set-1.json.gz,sha256=G-M7HwSccgdt0xHkYzQFXdkG9CCob3v99mfufT4Nc4k,1238
+botocore/data/autoscaling/2011-01-01/examples-1.json,sha256=-VLit9j2MnCph5AkDejxys_Iqt3JaUweEkC1B0_37j4,54289
+botocore/data/autoscaling/2011-01-01/paginators-1.json,sha256=hM_o0QSb61rvEQvua3IVpSLBUVCEy2BcwdQv1D_wSXk,2033
+botocore/data/autoscaling/2011-01-01/paginators-1.sdk-extras.json,sha256=FWBD5vKeS-MHcMzdipl2xKN3ddQu81Dk19sMd_82lKs,177
+botocore/data/autoscaling/2011-01-01/service-2.json.gz,sha256=Oxbq7BH2hiAYUJ5VXb0opOFTahINbgqfLMqEifgKzJM,56496
+botocore/data/b2bi/2022-06-23/endpoint-rule-set-1.json.gz,sha256=2UpA7oJfargDsFzlqhY4ZBYXjQENbWODz0xn4fCuOow,1299
+botocore/data/b2bi/2022-06-23/paginators-1.json,sha256=7ttS6Z0bHTlax4HX4atDWB9qbLUxoE9OTzdYeT62jiE,697
+botocore/data/b2bi/2022-06-23/service-2.json.gz,sha256=hYG1GJXK0GAw1L1Lfo2_KFVA7TyVD-ZjDMnRI-pk1RY,9145
+botocore/data/backup-gateway/2021-01-01/endpoint-rule-set-1.json.gz,sha256=JhzJtbRSZcCgty6MzFKSsGh9xqPMsSPh8asi0NFKV1s,1153
+botocore/data/backup-gateway/2021-01-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/backup-gateway/2021-01-01/paginators-1.json,sha256=SBncJ16jo9My_HRd-t9A7KPTxlId0ZP7A9JGuJ8tsiA,531
+botocore/data/backup-gateway/2021-01-01/service-2.json.gz,sha256=fMwuwu3YX9_T5d2e3b44aREMSyi7aZReSe0FmRj01B0,7373
+botocore/data/backup/2018-11-15/endpoint-rule-set-1.json.gz,sha256=xcAFPXzAAL-hi6da1OfAdKSMiHiODO3vA1UC6H1I43Q,1149
+botocore/data/backup/2018-11-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/backup/2018-11-15/paginators-1.json,sha256=A_0YIIgbnP324IxbCHk48GlF8enAgN3PcsYpscwqj_0,3064
+botocore/data/backup/2018-11-15/service-2.json.gz,sha256=mEvhVkusTlPFSkqot8W90semAYRjacX_d10hx_J0ovw,48373
+botocore/data/batch/2016-08-10/endpoint-rule-set-1.json.gz,sha256=WYIWyuVpUidZD7zIYaUyK0Oqq8NRBP0H1_O7_l2OB9o,1268
+botocore/data/batch/2016-08-10/examples-1.json,sha256=OVGvwREzgw_LYc8FpiMwLMNKVBoPq2uadWkT4icK_aM,20292
+botocore/data/batch/2016-08-10/paginators-1.json,sha256=_Q14DEUaF7-Y_MEg_UgaxxtzcTQSkn9nPZLNA55Uc1k,905
+botocore/data/batch/2016-08-10/service-2.json.gz,sha256=hPs_CR0bnOBJPF53ax_1bXLCYkzOvc7dXbBKfxLHwrw,51300
+botocore/data/bcm-data-exports/2023-11-26/endpoint-rule-set-1.json.gz,sha256=qkGLF90F-ZDathi6kWcS-_NNkFKZxG6gW7zJ2t-jxaw,1288
+botocore/data/bcm-data-exports/2023-11-26/paginators-1.json,sha256=O6FqSUDC5izLwZBKGsqYvMoy2ROOd85-Hb7II57VJoY,509
+botocore/data/bcm-data-exports/2023-11-26/service-2.json.gz,sha256=g5sxuS5LF3gK8vHNcxw-YhAGq1hz3t6MB74QzbdbfQQ,5122
+botocore/data/bedrock-agent-runtime/2023-07-26/endpoint-rule-set-1.json.gz,sha256=mS9wNUvoWT7VJbWo3e2pXuJ3VHJfSo3SLFvHOs3WfNw,1311
+botocore/data/bedrock-agent-runtime/2023-07-26/paginators-1.json,sha256=Xee8DFbphO8Z3pzUz4GiJW6EKWaITOnxwqHsxxmI68A,326
+botocore/data/bedrock-agent-runtime/2023-07-26/service-2.json.gz,sha256=TcUQIBSVdk3ht7NW6sfuY60aQOrMRKyHILtUYkhgkEU,19572
+botocore/data/bedrock-agent/2023-06-05/endpoint-rule-set-1.json.gz,sha256=onMp_g7piWOgVPh24RvSgIAbrOC0cH9xsfwClPYkYko,1305
+botocore/data/bedrock-agent/2023-06-05/paginators-1.json,sha256=GdkcDa4DafUtCQ2whFVNyeN7lLnL4MGDpwpgo3ERJUU,2146
+botocore/data/bedrock-agent/2023-06-05/service-2.json.gz,sha256=P4AD9FO0h_cJAPOaxvC8TB2Y-_9Y9H3-zivFqISx52g,37131
+botocore/data/bedrock-runtime/2023-09-30/endpoint-rule-set-1.json.gz,sha256=W39kkrhpoH3gNK1KNnK0mgvtb6T93zn5tMtE_Aptues,1306
+botocore/data/bedrock-runtime/2023-09-30/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/bedrock-runtime/2023-09-30/service-2.json.gz,sha256=-BHqyaETs7pX41ioh3e9RzpV2-Lb7areHy2rkUuGfWM,12070
+botocore/data/bedrock-runtime/2023-09-30/waiters-2.json,sha256=tj1ZnaqhwmJkUEQlwH7wm1SqY3lg1BvZDfzfPaIgNrY,38
+botocore/data/bedrock/2023-04-20/endpoint-rule-set-1.json.gz,sha256=z_vvY3_3HGocJBlp6DhgbXjwFM64-L7g_VqPbSAeywQ,1300
+botocore/data/bedrock/2023-04-20/paginators-1.json,sha256=NCOtaK1ej1g4hovP7TkreI8-3ktuWYQI9C6YCv_l-w0,1111
+botocore/data/bedrock/2023-04-20/service-2.json.gz,sha256=IYUPKk9JS5f5suR3xYfZ3WO_-QlDasqudwmE3g2uDXM,24252
+botocore/data/bedrock/2023-04-20/waiters-2.json,sha256=tj1ZnaqhwmJkUEQlwH7wm1SqY3lg1BvZDfzfPaIgNrY,38
+botocore/data/billingconductor/2021-07-30/endpoint-rule-set-1.json.gz,sha256=ag93DHGt1vKm8PYksVXomrwD-9rfWTNPZSUjLyCm5NI,1313
+botocore/data/billingconductor/2021-07-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/billingconductor/2021-07-30/paginators-1.json,sha256=C1lDM7aIG0KK8L7HotZs6eXvTQLuxzETH2wAHQdDzqI,2192
+botocore/data/billingconductor/2021-07-30/service-2.json.gz,sha256=21Bp2mOl7xbW3KoU_kboyBFB9Eh9IzI240hc9QzrYUc,15064
+botocore/data/billingconductor/2021-07-30/waiters-2.json,sha256=sAGuGxokCpXh7GUF-AzqqNR6DLDE-wgRMhjNJb41AHc,36
+botocore/data/braket/2019-09-01/endpoint-rule-set-1.json.gz,sha256=4M3OH4oCgNsHINv1EFIDtvXrjsOJI_-qWqpXM7XR1i8,1149
+botocore/data/braket/2019-09-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/braket/2019-09-01/paginators-1.json,sha256=KRlsOoRgrAwhnJjx41OVvcVESSL0GGupxAdB-CpgK7w,515
+botocore/data/braket/2019-09-01/service-2.json.gz,sha256=-f4Ay-ucO2mY78t4iD_F9vdY_jyG5FS0Y3nDhaGTY4Q,8100
+botocore/data/budgets/2016-10-20/endpoint-rule-set-1.json.gz,sha256=U0DNs-HgtPV5kj0wwX9xhqExwOSTkrtAxYDez4pKGe8,1369
+botocore/data/budgets/2016-10-20/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/budgets/2016-10-20/paginators-1.json,sha256=4lIRhlnV70H90OPO79aAX2cps42vBAkZcxWDSS40zis,1512
+botocore/data/budgets/2016-10-20/service-2.json.gz,sha256=TR6_xQnLTPLqmQ3UMHt20g0e4uJ5XSyaxmiXFaUWJPc,12413
+botocore/data/ce/2017-10-25/endpoint-rule-set-1.json.gz,sha256=2lt-9DPyMIXlCSHS6BvEMey453i2BtUDANXy6PdHEgg,1366
+botocore/data/ce/2017-10-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ce/2017-10-25/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/ce/2017-10-25/service-2.json.gz,sha256=10ULpjfSMQ43Rb2BpiFLzzQD0ulg65OiOLDaKNeOf1Y,38484
+botocore/data/chatbot/2017-10-11/endpoint-rule-set-1.json.gz,sha256=Pf6uUaEfSbZW1N1uc0NttlJ4jJWq8yfojON2VrcHVro,1298
+botocore/data/chatbot/2017-10-11/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/chatbot/2017-10-11/service-2.json.gz,sha256=AtCeBPgEI9A0FG_2x0beYqpg0h3G2oceH3eK-QtFZec,6630
+botocore/data/chime-sdk-identity/2021-04-20/endpoint-rule-set-1.json.gz,sha256=pnMFbKnAJa94tcZaN_d21b600wHUsMhR3kefmMQ7UiA,1152
+botocore/data/chime-sdk-identity/2021-04-20/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/chime-sdk-identity/2021-04-20/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/chime-sdk-identity/2021-04-20/service-2.json.gz,sha256=-GsE8eLWWILuqtbSoQf1e1G1V3j_Llzda14vzrJI7YI,8111
+botocore/data/chime-sdk-media-pipelines/2021-07-15/endpoint-rule-set-1.json.gz,sha256=MRvy-zVr_v38CRz37h8B61yVmBhcFVZFLPNBcAFbyA8,1159
+botocore/data/chime-sdk-media-pipelines/2021-07-15/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/chime-sdk-media-pipelines/2021-07-15/service-2.json.gz,sha256=RfSC2JU2fATDcPhaBAwlrHZlSwcdiSADmSUYjBx9_hs,17644
+botocore/data/chime-sdk-meetings/2021-07-15/endpoint-rule-set-1.json.gz,sha256=HiHByR5jSLRBZBx2YEf9bbRKujuFONVHJUbkZWxLip0,1155
+botocore/data/chime-sdk-meetings/2021-07-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/chime-sdk-meetings/2021-07-15/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/chime-sdk-meetings/2021-07-15/service-2.json.gz,sha256=sZlvOD-B7pYgI-izOTvQdKvEg7OzhVf603k3AlQTvnU,11217
+botocore/data/chime-sdk-messaging/2021-05-15/endpoint-rule-set-1.json.gz,sha256=waxwl_7CfkVHE8awJNlUfP_pNYP3UdYs-_9qwTirgu4,1153
+botocore/data/chime-sdk-messaging/2021-05-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/chime-sdk-messaging/2021-05-15/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/chime-sdk-messaging/2021-05-15/service-2.json.gz,sha256=DUDxD2Ruda68oBK-2iK3Tzy-Z4iqSbyhyPTBKJSBcUU,16213
+botocore/data/chime-sdk-voice/2022-08-03/endpoint-rule-set-1.json.gz,sha256=V-_ekD6Q2KmtZQoC2WqH-yPr6_tFUQRVHfQn3MJzMCo,1303
+botocore/data/chime-sdk-voice/2022-08-03/paginators-1.json,sha256=28096cSFWwRSuJQMmk9A3HNyMAH8wFdjz3F_5pukB8Q,373
+botocore/data/chime-sdk-voice/2022-08-03/service-2.json.gz,sha256=22GvhLv7iEcEhC-X-GA_StrYu2IJnLCnz8ooawKJ1Zg,21897
+botocore/data/chime/2018-05-01/endpoint-rule-set-1.json.gz,sha256=SEJe3UQqXb0XkM3cmiWWymCqQMDyB1VQnJI1F1KRhyc,1307
+botocore/data/chime/2018-05-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/chime/2018-05-01/paginators-1.json,sha256=eU07vcRnjLd-9RmN_aGGPffN0ZXkpMRFYD_XbcyDy3A,343
+botocore/data/chime/2018-05-01/service-2.json.gz,sha256=MnRWY1EXbPz3nOk26VtWdL4RC307SgrKhjYlY3osRzI,51890
+botocore/data/cleanrooms/2022-02-17/endpoint-rule-set-1.json.gz,sha256=96hA7S1HnSWYHlKAcmO8jRYBb9utjqQpQUqcFUg2yn0,1301
+botocore/data/cleanrooms/2022-02-17/paginators-1.json,sha256=wOdAg_iazObGAXMhjjhg3HW7ZEgwl6E5ejhsDIiqY0w,3584
+botocore/data/cleanrooms/2022-02-17/service-2.json.gz,sha256=eav6Z3lzMfdyqqmB6bhnEtmgxwPm_PJT_rAjes4xV1I,31043
+botocore/data/cleanrooms/2022-02-17/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/cleanroomsml/2023-09-06/endpoint-rule-set-1.json.gz,sha256=0piclL3_faFv5odTZo-Yuws44ALm39BW-rKOXz-Yywk,1304
+botocore/data/cleanroomsml/2023-09-06/paginators-1.json,sha256=qYSDy4rOquZcN5vkZbldrv6idgkXZ6P0gB0aeXetlaM,943
+botocore/data/cleanroomsml/2023-09-06/service-2.json.gz,sha256=EDa1lJa04x-ofmAZlNfgrcfXhKWPaAaMRA-elSqUA9M,10297
+botocore/data/cleanroomsml/2023-09-06/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/cloud9/2017-09-23/endpoint-rule-set-1.json.gz,sha256=lvpDvoHS1UnMD4_5yPlovNKBIilOOsEPtuRZ2hgt_Mc,1150
+botocore/data/cloud9/2017-09-23/examples-1.json,sha256=Jbbei88MR8S4MFnfmPKNTEk_b1NdqqM5R6P781A23JY,9183
+botocore/data/cloud9/2017-09-23/paginators-1.json,sha256=lET7E3FWErLA8In260otKfr3_9oVSr5OTO1zcrBi28w,380
+botocore/data/cloud9/2017-09-23/service-2.json.gz,sha256=VkNtXR2LePPSanBgM2homGiaTj4PgZ58_rangOWQumM,5927
+botocore/data/cloudcontrol/2021-09-30/endpoint-rule-set-1.json.gz,sha256=LQ8BGgHsIkBtHjX9rffJDlBwmNT4VjGHXbOgkOz6zYo,1153
+botocore/data/cloudcontrol/2021-09-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudcontrol/2021-09-30/paginators-1.json,sha256=Xh6wJghPx6VpGNTTEdpRQIsrJuVeyY5FQNpNLpUdkhc,392
+botocore/data/cloudcontrol/2021-09-30/paginators-1.sdk-extras.json,sha256=9NbQ8xHg5ztdpvYFDl15_74F30ZNPFnSFDxismgvSMg,143
+botocore/data/cloudcontrol/2021-09-30/service-2.json.gz,sha256=8OMjZ-DRnEEqHiLCMCW95HBQFMSG7LerJHFu4xGud5I,5896
+botocore/data/cloudcontrol/2021-09-30/waiters-2.json,sha256=US_tyuvbMcXS6IrVB8D817Gg3pGKdCuooDJKz4Ta56U,738
+botocore/data/clouddirectory/2016-05-10/endpoint-rule-set-1.json.gz,sha256=7p1WcakHkdW2MYyXcs06Dq59C0kuVwMH_h3FnpxrfgY,1398
+botocore/data/clouddirectory/2016-05-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/clouddirectory/2016-05-10/paginators-1.json,sha256=y8GPuHURJmdagJ3QAI5mxkAzKvdCZwcnfYt3Z-qwgAU,2808
+botocore/data/clouddirectory/2016-05-10/service-2.json.gz,sha256=4vn33AHqZ1eho5Dhu_TsxBex2a9UPUNONO03Oq4yUzU,22958
+botocore/data/clouddirectory/2017-01-11/endpoint-rule-set-1.json.gz,sha256=UbiH4gvn5ywEOfTWTc-D-gutloQcHMnRlz9DMt0XwgA,1243
+botocore/data/clouddirectory/2017-01-11/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/clouddirectory/2017-01-11/paginators-1.json,sha256=gIqmeqo-8lsyEDEVDFvc1RJfd0T7c9xN6SdMnxGvSpw,3342
+botocore/data/clouddirectory/2017-01-11/service-2.json.gz,sha256=ji-W8evIkr2oQlwZlpY9MXmg6Xl3Lgaq5n3Tvqmq0NA,23874
+botocore/data/cloudformation/2010-05-15/endpoint-rule-set-1.json.gz,sha256=Ob48VL66jR0sI02UQVPDFawXMbO9KsrFa7I4ovfY7tE,1239
+botocore/data/cloudformation/2010-05-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudformation/2010-05-15/paginators-1.json,sha256=WwkLBQlV5ds8AUfNC72XUOIijNZmRwxfHKWjCF7ewWs,3318
+botocore/data/cloudformation/2010-05-15/service-2.json.gz,sha256=BCZcRpvNagzft65IpZFbYBRsyx6YSnLXuR6b8wiHMo0,75599
+botocore/data/cloudformation/2010-05-15/waiters-2.json,sha256=BM3U5p4j7iNbZ9UWiiiDU9OZfMsXS-oIGsUClUUKdkk,9500
+botocore/data/cloudfront-keyvaluestore/2022-07-26/endpoint-rule-set-1.json.gz,sha256=3vy1vDnrophy0cUBAOQUKj0C9O4-CPqKVHXi-oXuloE,2213
+botocore/data/cloudfront-keyvaluestore/2022-07-26/paginators-1.json,sha256=2wyrpgvniacM8xlFDnHQiCR0KVEAVJxBEpWFBcrB4Z0,180
+botocore/data/cloudfront-keyvaluestore/2022-07-26/service-2.json.gz,sha256=j0iCQQtnDzn5bDP8yjVsNwInIjegnW_XuvqXGvVHI2A,2220
+botocore/data/cloudfront/2014-05-31/endpoint-rule-set-1.json.gz,sha256=UnUSVAwVn-2UqEbQvYAnvU75CRPd_vz3RQ7CAoUHvcw,1839
+botocore/data/cloudfront/2014-05-31/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2014-05-31/service-2.json.gz,sha256=Uvr4VkDmlk8gry1rWs5jejsE7WrTWyq0mTtUWlKAqEM,15298
+botocore/data/cloudfront/2014-05-31/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2014-10-21/endpoint-rule-set-1.json.gz,sha256=UnUSVAwVn-2UqEbQvYAnvU75CRPd_vz3RQ7CAoUHvcw,1839
+botocore/data/cloudfront/2014-10-21/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2014-10-21/service-2.json.gz,sha256=Q7Kwhn-M9REeAT01ChVQQ7hnN4GY00BEu84jgZJumyI,15887
+botocore/data/cloudfront/2014-10-21/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2014-11-06/endpoint-rule-set-1.json.gz,sha256=UnUSVAwVn-2UqEbQvYAnvU75CRPd_vz3RQ7CAoUHvcw,1839
+botocore/data/cloudfront/2014-11-06/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2014-11-06/service-2.json.gz,sha256=hs8SV-trIrBBWlMlP26EKwZjo1S69EAESE5WZewaZew,15959
+botocore/data/cloudfront/2014-11-06/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2015-04-17/endpoint-rule-set-1.json.gz,sha256=UnUSVAwVn-2UqEbQvYAnvU75CRPd_vz3RQ7CAoUHvcw,1839
+botocore/data/cloudfront/2015-04-17/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2015-04-17/service-2.json.gz,sha256=P3-aYyISjIdmCVOplCbSMgAGKl_GY2a_UrI-bfDOcVs,16213
+botocore/data/cloudfront/2015-04-17/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2015-07-27/endpoint-rule-set-1.json.gz,sha256=UnUSVAwVn-2UqEbQvYAnvU75CRPd_vz3RQ7CAoUHvcw,1839
+botocore/data/cloudfront/2015-07-27/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2015-07-27/service-2.json.gz,sha256=SjSGi1wce9t1AN3xsJUjNZjC-g-b6cIFN5asPpdOXrc,16702
+botocore/data/cloudfront/2015-07-27/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2015-09-17/endpoint-rule-set-1.json.gz,sha256=UnUSVAwVn-2UqEbQvYAnvU75CRPd_vz3RQ7CAoUHvcw,1839
+botocore/data/cloudfront/2015-09-17/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2015-09-17/service-2.json.gz,sha256=TYvCQ3kALRWHstvPdGNSDzlC36bUgcVnitSuuyO80_U,15890
+botocore/data/cloudfront/2015-09-17/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2016-01-13/endpoint-rule-set-1.json.gz,sha256=UnUSVAwVn-2UqEbQvYAnvU75CRPd_vz3RQ7CAoUHvcw,1839
+botocore/data/cloudfront/2016-01-13/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2016-01-13/service-2.json.gz,sha256=aQ2HrIJw0MiNGq3sNMAMIik_ql1txgVLPMw9aEwSBB8,16358
+botocore/data/cloudfront/2016-01-13/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2016-01-28/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2016-01-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2016-01-28/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2016-01-28/service-2.json.gz,sha256=VpZ1E62ruLOJz8vkFyaHeRT_ylIRWBuaGSBvZMgRe00,16279
+botocore/data/cloudfront/2016-01-28/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2016-08-01/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2016-08-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2016-08-01/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2016-08-01/service-2.json.gz,sha256=RY815uRVBX62Nxc0A239dmJX26aczPfWiCngyT7qXZY,17725
+botocore/data/cloudfront/2016-08-01/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2016-08-20/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2016-08-20/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2016-08-20/service-2.json.gz,sha256=AtQWTiyduOzoLDhgI5SDRSSeFPwb6o7Z2PBqtLR5W14,18123
+botocore/data/cloudfront/2016-08-20/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2016-09-07/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2016-09-07/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2016-09-07/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2016-09-07/service-2.json.gz,sha256=H9Fw53Sem1jDew9H3kzxUuZTdyJXfiITv98Ept0t890,18444
+botocore/data/cloudfront/2016-09-07/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2016-09-29/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2016-09-29/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2016-09-29/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2016-09-29/service-2.json.gz,sha256=2JkFpJzQKUAXh0xBrg4Pt89mpWk06R8t8ieCMiU3prE,27522
+botocore/data/cloudfront/2016-09-29/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2016-11-25/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2016-11-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2016-11-25/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2016-11-25/service-2.json.gz,sha256=80lMn5dgZ5SHh6eS3y_MnCOSyiCJ1ZxYRk7zuU3Jwas,27955
+botocore/data/cloudfront/2016-11-25/waiters-2.json,sha256=jzREqDxfIg2KbmPYOmDoYgDvy8mWAEK0w_NmEoCqhHI,1184
+botocore/data/cloudfront/2017-03-25/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2017-03-25/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2017-03-25/service-2.json.gz,sha256=2d3D3sEA31eNFTgsGtMdgBI8qabGZ62cvDuCXYr93Yk,29088
+botocore/data/cloudfront/2017-03-25/waiters-2.json,sha256=JboqzXjlni8p-wiVKBz1jRj-mFpkryqueCgI1hD7WPA,1184
+botocore/data/cloudfront/2017-10-30/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2017-10-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2017-10-30/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2017-10-30/service-2.json.gz,sha256=vMZ57vj132pFmDvX6ucht6lz2fdU5acbTLOVb5okUi4,34767
+botocore/data/cloudfront/2017-10-30/waiters-2.json,sha256=JboqzXjlni8p-wiVKBz1jRj-mFpkryqueCgI1hD7WPA,1184
+botocore/data/cloudfront/2018-06-18/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2018-06-18/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2018-06-18/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2018-06-18/service-2.json.gz,sha256=aB65qJZNaArJxbRQDJgrKWUZ2NjYURHOmdsT2QhFIuU,35482
+botocore/data/cloudfront/2018-06-18/waiters-2.json,sha256=JboqzXjlni8p-wiVKBz1jRj-mFpkryqueCgI1hD7WPA,1184
+botocore/data/cloudfront/2018-11-05/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2018-11-05/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2018-11-05/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2018-11-05/service-2.json.gz,sha256=bIl4-Cpz7aSUgTTSPlp5wfOCcZS6Lq2OoNqqgPMvlBE,36144
+botocore/data/cloudfront/2018-11-05/waiters-2.json,sha256=JboqzXjlni8p-wiVKBz1jRj-mFpkryqueCgI1hD7WPA,1184
+botocore/data/cloudfront/2019-03-26/endpoint-rule-set-1.json.gz,sha256=V1SXuzdlogLdezVpg65MMtkuVlUAWWUC_aXRdqTz_PY,1574
+botocore/data/cloudfront/2019-03-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2019-03-26/paginators-1.json,sha256=I7u4h1MFflBvFJemcrLHSn7uOrEeDFc7ecWGqwDxGF8,1126
+botocore/data/cloudfront/2019-03-26/service-2.json.gz,sha256=OPrajDC7oV11v5eEFuAFAtSC5w1JTLSqpYklZP2_vhk,37652
+botocore/data/cloudfront/2019-03-26/waiters-2.json,sha256=qt7oBhQ-B52-397Q88q0EJoFpDWuOZM7CZpaFhX1xgM,1184
+botocore/data/cloudfront/2020-05-31/endpoint-rule-set-1.json.gz,sha256=YCIjSlLg0A2dnLlrO4euUpgqtuxA817PY9wGzma8_zQ,1408
+botocore/data/cloudfront/2020-05-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudfront/2020-05-31/paginators-1.json,sha256=F-GvV1ja3FID22KPpBnvQ2a7QMGxtP_kIdXRd4UWb7s,1323
+botocore/data/cloudfront/2020-05-31/service-2.json.gz,sha256=Db8s-vmMYtLYhKsctnR-5qh__QjUxPRdI9k_AFUpjgQ,69639
+botocore/data/cloudfront/2020-05-31/waiters-2.json,sha256=qt7oBhQ-B52-397Q88q0EJoFpDWuOZM7CZpaFhX1xgM,1184
+botocore/data/cloudhsm/2014-05-30/endpoint-rule-set-1.json.gz,sha256=WkUtusz-EVKRgIsShHISyI4PytSqEXr423t48lhPTh8,1148
+botocore/data/cloudhsm/2014-05-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudhsm/2014-05-30/paginators-1.json,sha256=pe-X06JkfqlENEk-25nE_w_q3QQXkdMnQ5cOG1NPi6E,409
+botocore/data/cloudhsm/2014-05-30/service-2.json.gz,sha256=U-yPLdyprVjBGgtdpqwdS3h-5boCvQnb4tbDsCJE0AI,5556
+botocore/data/cloudhsmv2/2017-04-28/endpoint-rule-set-1.json.gz,sha256=H5aw-UY8WAqDL_bTlqQPTRp90EMUPTamUtRYXho8tVk,1244
+botocore/data/cloudhsmv2/2017-04-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudhsmv2/2017-04-28/paginators-1.json,sha256=VvCnjrdoGz3Lb-gi5YSOAhhAHzB50i0vIks0GaA2nS0,512
+botocore/data/cloudhsmv2/2017-04-28/service-2.json.gz,sha256=Trc1OSBvErkjsnsFcY39SGGVu2DuqWbMAffGuFqyoyc,7582
+botocore/data/cloudsearch/2011-02-01/endpoint-rule-set-1.json.gz,sha256=DSYTW1gDruIp1T8dAjPmZzTNT0Tce0z5RP0MiWXIcV8,1149
+botocore/data/cloudsearch/2011-02-01/service-2.json.gz,sha256=52w-ByW2U36rLPdVWCkav9Mxu-qaBoK-a91Ymb2wHbg,9599
+botocore/data/cloudsearch/2013-01-01/endpoint-rule-set-1.json.gz,sha256=on9NfnCutX7tzaCBB_MARr3Kr-YEoe6TnrXbwUIGfh0,1150
+botocore/data/cloudsearch/2013-01-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudsearch/2013-01-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/cloudsearch/2013-01-01/service-2.json.gz,sha256=69B2CPtr0_swnQn9L3WHaTUDo2IqjEe6Xz12rbU2fqY,12084
+botocore/data/cloudsearchdomain/2013-01-01/endpoint-rule-set-1.json.gz,sha256=iU76yvMQCMvMdCQlQDGLOuRMSJiM0GjEUOkN5G0o92g,1154
+botocore/data/cloudsearchdomain/2013-01-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudsearchdomain/2013-01-01/service-2.json.gz,sha256=WUtA7-6Xcyxk91EZQVpDIF0DlLdnUKIZhSDcz6YJsfg,9099
+botocore/data/cloudtrail-data/2021-08-11/endpoint-rule-set-1.json.gz,sha256=Kf20gQj_fAgNuQH526l7iQBZ62lTNZM4-W8QZ--MjLc,1295
+botocore/data/cloudtrail-data/2021-08-11/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/cloudtrail-data/2021-08-11/service-2.json.gz,sha256=TWI2v9V51jc7de1BFR4RfPHFa1vn1XIisCoj9jK9jrk,2165
+botocore/data/cloudtrail/2013-11-01/endpoint-rule-set-1.json.gz,sha256=mGs78CpHvCZ-6kWgbgeVHhiQg40TBKqLqLT_NY_hQoA,1235
+botocore/data/cloudtrail/2013-11-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudtrail/2013-11-01/paginators-1.json,sha256=YF379KBXA_FzpvoX8iPsNIUPHrhwyGGX4nICIjqs15o,906
+botocore/data/cloudtrail/2013-11-01/service-2.json.gz,sha256=7FIsog-nJMgPK18eIVN9k8wEjT6G3ZbJiNJkjemff-c,39618
+botocore/data/cloudwatch/2010-08-01/endpoint-rule-set-1.json.gz,sha256=8ozRyCTMNKeMQWp8XXoQPnMx7lRZajNzCIcpTrtlfxE,1237
+botocore/data/cloudwatch/2010-08-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cloudwatch/2010-08-01/paginators-1.json,sha256=OfAocfP12RM8pfP6Fh2EUikcL00nN2vRMCW3O4wsjHo,1122
+botocore/data/cloudwatch/2010-08-01/service-2.json.gz,sha256=YTCkNUWuAvwo3fLNu3xFHl_AQPCv1JaGpP5gJPUZnpU,38673
+botocore/data/cloudwatch/2010-08-01/waiters-2.json,sha256=MloXSzqs1ZkzyWAP2NrkVyNkIE63Hbk24II7PCuUxl0,644
+botocore/data/codeartifact/2018-09-22/endpoint-rule-set-1.json.gz,sha256=pa1_oGn9lXu6kxUfbYEfbtJM1MW5bds_Nw_jiiVRbMI,1153
+botocore/data/codeartifact/2018-09-22/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codeartifact/2018-09-22/paginators-1.json,sha256=I3MlPdEGK-hCFxJnNpPrpWkJSBaM9dhFiZ4uo0AoR8o,1747
+botocore/data/codeartifact/2018-09-22/paginators-1.sdk-extras.json,sha256=kNVDIOe3C5yL0xTWSrW2xDchpno4Xozz60DY53uxNEA,444
+botocore/data/codeartifact/2018-09-22/service-2.json.gz,sha256=CNsewKZwsmgqEM2t7UAS6aXQHMMiTPp5NOrbXcipGQc,23241
+botocore/data/codebuild/2016-10-06/endpoint-rule-set-1.json.gz,sha256=eB1E6ymQVEjyaIx1JVmLrdJcupybOd8jq8nM7uFg6fc,1151
+botocore/data/codebuild/2016-10-06/examples-1.json,sha256=_-tVq2XM1YDuzv78VwIj_WjyXHu-yrIPyxzTtTbdFJ8,9778
+botocore/data/codebuild/2016-10-06/paginators-1.json,sha256=bbKaGCdRO-JDOzUHKIVi1sEU9h8xDj6Yso9CNiszoRA,1932
+botocore/data/codebuild/2016-10-06/service-2.json.gz,sha256=WADASWbJVMpruUCJckJxHIjfQdlVT4ySSfgSlDYGYms,40929
+botocore/data/codecatalyst/2022-09-28/endpoint-rule-set-1.json.gz,sha256=4Jv9wyq_gpMqO3SgI2h6zf6SPZP7eLyxBUnRFjFWLDY,851
+botocore/data/codecatalyst/2022-09-28/paginators-1.json,sha256=TuEQ6NVw_F_LgmG-TurtBCvFTRFRP8DWHseWdO8DNRk,1637
+botocore/data/codecatalyst/2022-09-28/service-2.json.gz,sha256=_IqABg7Uw--fUHMpTK8gZVHYPqN5iWYZ0A3mrr_MPr8,13989
+botocore/data/codecatalyst/2022-09-28/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/codecommit/2015-04-13/endpoint-rule-set-1.json.gz,sha256=7mBPi45xjZ-QKZ_CrPPARiNXCFxHGzWB-LD1V42lcLI,1152
+botocore/data/codecommit/2015-04-13/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codecommit/2015-04-13/paginators-1.json,sha256=2w92BpzUce0gSVEaZH0la2r8ZT_MDtxoLc6RG-dpln4,1206
+botocore/data/codecommit/2015-04-13/service-2.json.gz,sha256=gcQRvWgt5pZblJWcPTj2EmSbhXw6pUo20qTMZGgg5Z8,40956
+botocore/data/codeconnections/2023-12-01/endpoint-rule-set-1.json.gz,sha256=PK8tteymwLryIKgQx81JiqHqILX7hhc0FHSwbPmoRN4,1304
+botocore/data/codeconnections/2023-12-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/codeconnections/2023-12-01/service-2.json.gz,sha256=BePqGFlYgj2MdCr0pAh5q8hnxmPY0GZXGgBkR_oc-OA,9868
+botocore/data/codedeploy/2014-10-06/endpoint-rule-set-1.json.gz,sha256=4DoYX7YUHdWNgSFcZixBejSXkyBpN15ydDdCxN-9bxo,1152
+botocore/data/codedeploy/2014-10-06/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codedeploy/2014-10-06/paginators-1.json,sha256=riyMuhePXvzjx3lAoHiIaOi0U6v2lCVd65qX4UWPoxo,1313
+botocore/data/codedeploy/2014-10-06/service-2.json.gz,sha256=qT-2DDYv8xjQ-yCm529Ob0QUgcV3YxQT1NM1H1QaEik,31904
+botocore/data/codedeploy/2014-10-06/waiters-2.json,sha256=OARBxBeZTRUui1WztkVtUn7Q2lAh3-Bemczgk455MGQ,662
+botocore/data/codeguru-reviewer/2019-09-19/endpoint-rule-set-1.json.gz,sha256=QH9nTi0dGvg9VjAa6Z7P3Blc_5cc5KNGuIqic7aBMeY,1156
+botocore/data/codeguru-reviewer/2019-09-19/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codeguru-reviewer/2019-09-19/paginators-1.json,sha256=0bkbq9IDAtNTQOShBQuJVNtb8xgFFUYNdzOcl3ri_DM,223
+botocore/data/codeguru-reviewer/2019-09-19/service-2.json.gz,sha256=WQEmzw7tLv5qWm98e6OSNgWFSfOGypreaJzXcZHv0us,11762
+botocore/data/codeguru-reviewer/2019-09-19/waiters-2.json,sha256=0jf0N7KHQV4qYAOPKBKNdiExhxEvojmGQ2Jzrc9lYR4,1733
+botocore/data/codeguru-security/2018-05-10/endpoint-rule-set-1.json.gz,sha256=3VEYeX6zR9n8htURZ_Bo6rImXurBWN85Oa0DKOoegcU,1308
+botocore/data/codeguru-security/2018-05-10/paginators-1.json,sha256=nwCp854x7Q4pjInZgk9mpYoj9BiFf09ekRTXObmU4GQ,522
+botocore/data/codeguru-security/2018-05-10/service-2.json.gz,sha256=FXsYrljfx9e_sz-qwZaoXPTAu2IHowHU-ezQw7UBEeQ,7914
+botocore/data/codeguruprofiler/2019-07-18/endpoint-rule-set-1.json.gz,sha256=rwkzdMXXp6XpxH9KXjxJU9EwGNGMaMrdCnsCEQMnpoE,1157
+botocore/data/codeguruprofiler/2019-07-18/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codeguruprofiler/2019-07-18/paginators-1.json,sha256=d7DXbQ-GmZLDQRjjpAO-vzvm7OEA-pNKfPUyA9rgaag,195
+botocore/data/codeguruprofiler/2019-07-18/service-2.json.gz,sha256=0ha6XMSurV2Q10S-7xdKESEzrPHUvibrx7IkJEy8A-o,14592
+botocore/data/codepipeline/2015-07-09/endpoint-rule-set-1.json.gz,sha256=HqiTNfl-DYGJ4O0uE66KGlLIvaJhhZIDmC7zs4temQ8,1153
+botocore/data/codepipeline/2015-07-09/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codepipeline/2015-07-09/paginators-1.json,sha256=O-qHwmtC4PDM7s41yA5e63oR2xWmIwbHZ40YuOf3bYo,1205
+botocore/data/codepipeline/2015-07-09/service-2.json.gz,sha256=qBVyxmwWhE3XOhPdZcJCnTXAZLzRxDRDTXeH4EHzXwA,34061
+botocore/data/codestar-connections/2019-12-01/endpoint-rule-set-1.json.gz,sha256=KLT1f4lLEG15i7uhQ-slhH5W_V3iA_KyG0vQz57q6kk,1156
+botocore/data/codestar-connections/2019-12-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codestar-connections/2019-12-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/codestar-connections/2019-12-01/service-2.json.gz,sha256=G_p87WtKJPz_zsvzNec0-OxbfFwodBMRmFIiUkL-5LU,9877
+botocore/data/codestar-notifications/2019-10-15/endpoint-rule-set-1.json.gz,sha256=lId4ag-kCYoJswXaYKzylGIY1nTAjfpPkAwlVsg7aRI,1156
+botocore/data/codestar-notifications/2019-10-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codestar-notifications/2019-10-15/paginators-1.json,sha256=bD6rBB54kEd5ns5mM8KWWE2Gfs6rNkRWTLyvKHai9OA,531
+botocore/data/codestar-notifications/2019-10-15/service-2.json.gz,sha256=cJpX4s1-EXTm3TErEpaewcG2S_KeEQrtk34mNPutQcU,5528
+botocore/data/codestar/2017-04-19/endpoint-rule-set-1.json.gz,sha256=qnIznzyppqZN-gZl__Pg8DvTe9M6djOK-sPald2mhlg,1147
+botocore/data/codestar/2017-04-19/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/codestar/2017-04-19/paginators-1.json,sha256=3r-icSh_oPAVyAdyLoPtkZehAQXuKwEJJan-PFrl6N4,689
+botocore/data/codestar/2017-04-19/service-2.json.gz,sha256=zg6b_dDHWTwbhY2bgQjI0lVo1ZmSFNmvvS3a_VYM14k,7065
+botocore/data/cognito-identity/2014-06-30/endpoint-rule-set-1.json.gz,sha256=5FZR7cfxZL70TrN5R5VIO8o_hL5_NAQSg735AqRmJv8,1158
+botocore/data/cognito-identity/2014-06-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cognito-identity/2014-06-30/paginators-1.json,sha256=iRnVNYNjXj4riBW6sjwmAF2p9fSX2MkfoM5W_Y9_tkE,197
+botocore/data/cognito-identity/2014-06-30/service-2.json.gz,sha256=30_2nELXwqQQyu3-5ySaFUhYhzy78Ynl1vm0e1MOzLc,10097
+botocore/data/cognito-idp/2016-04-18/endpoint-rule-set-1.json.gz,sha256=ESJjtKYSCvkA9kHD_ixRJcAnEAHZo-umGZpwQkvgV10,1153
+botocore/data/cognito-idp/2016-04-18/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cognito-idp/2016-04-18/paginators-1.json,sha256=RdlZ6K9kobwkGd7v3X15eeU_1MTpNNWwHD35RwB4keA,1527
+botocore/data/cognito-idp/2016-04-18/service-2.json.gz,sha256=Gm2MriixElKoRYl2UHWs1izMjr2DAE6nGtC25wsd18s,81131
+botocore/data/cognito-sync/2014-06-30/endpoint-rule-set-1.json.gz,sha256=EgX4e8ojQPhl1bJ3ofQLnheVuwlkcTjnfGTiHMOrG58,1151
+botocore/data/cognito-sync/2014-06-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/cognito-sync/2014-06-30/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/cognito-sync/2014-06-30/service-2.json.gz,sha256=mr0DIZtUwcaAZ3iLil4g2CNVDyBpJ9POshI5GTFbx4w,7316
+botocore/data/comprehend/2017-11-27/endpoint-rule-set-1.json.gz,sha256=yu3D6YlkZvjoMkyb7YcXaPROCu0wFmnFf6XEE_wF-TI,1151
+botocore/data/comprehend/2017-11-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/comprehend/2017-11-27/paginators-1.json,sha256=aCozRajzUb4wblnxzb_bTJlztnDFC3PnwItAMek2WtY,2033
+botocore/data/comprehend/2017-11-27/service-2.json.gz,sha256=hYACKGIhuzJbF_LY5alFMRniUaO4AtqNgvowIeGiyMI,43031
+botocore/data/comprehendmedical/2018-10-30/endpoint-rule-set-1.json.gz,sha256=2Pgo3EakGVw1EjRlAd-Y9YNm-vBlWlpvg2SNQg9YIjw,1153
+botocore/data/comprehendmedical/2018-10-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/comprehendmedical/2018-10-30/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/comprehendmedical/2018-10-30/service-2.json.gz,sha256=aNdkooH8rLvD3eXGYF3yr4VUFptVyCFXEcaXJzuDxB4,10270
+botocore/data/compute-optimizer/2019-11-01/endpoint-rule-set-1.json.gz,sha256=LfWsmA0Wgda3EHlV-X5MEjHYWJ-5j8l3fRnpYL97T3I,1157
+botocore/data/compute-optimizer/2019-11-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/compute-optimizer/2019-11-01/paginators-1.json,sha256=FBFfvnKfuzo8mWExsEqu3Gy9-nKsBwTxjoRuqT-_oU0,1022
+botocore/data/compute-optimizer/2019-11-01/service-2.json.gz,sha256=waylrijxjohAsrfHaI1GM6dVfBjEG7msnDxUIpzsejA,37894
+botocore/data/config/2014-11-12/endpoint-rule-set-1.json.gz,sha256=F2ybxRypTlKmD1uKqhwqEzIn-KJyw2QgqtBGTz7dmGA,1233
+botocore/data/config/2014-11-12/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/config/2014-11-12/paginators-1.json,sha256=YDfVkCIT6qMFYl43FqCdg7Sg6RjnSfx9w8V1QkU1SqQ,6011
+botocore/data/config/2014-11-12/service-2.json.gz,sha256=W4Dpro4Yy_-9sd7oNklzGrFr6a7fAej2FWGSxcUrxro,60175
+botocore/data/connect-contact-lens/2020-08-21/endpoint-rule-set-1.json.gz,sha256=jnr5_4ZuW8UBNHcar6KRqpM3jchylCEez6DFQKNv09o,1153
+botocore/data/connect-contact-lens/2020-08-21/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/connect-contact-lens/2020-08-21/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/connect-contact-lens/2020-08-21/service-2.json.gz,sha256=VIwpJhEm2CBPmQCDKDdtaMat15DOuR_ZMAz7fGqS0vU,3170
+botocore/data/connect/2017-08-08/endpoint-rule-set-1.json.gz,sha256=W7FWi9V19kfd2SeJGbJ8B-8or2_klMDgUz2awMfMF4o,1233
+botocore/data/connect/2017-08-08/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/connect/2017-08-08/paginators-1.json,sha256=WUS3yQLV70kA4rrMAEfDDixSIgAVkWp2Nr6Hu9VbFtc,11983
+botocore/data/connect/2017-08-08/service-2.json.gz,sha256=mbXq_votQ4L3kBsipOFsVmDM59WMHE-Mbl7Zg51r4-s,120042
+botocore/data/connectcampaigns/2021-01-30/endpoint-rule-set-1.json.gz,sha256=HhF3m-0SaJKFnQm9-juNPld1w8nxJnJZPTCCJFgtzwk,1158
+botocore/data/connectcampaigns/2021-01-30/paginators-1.json,sha256=0u4LcBZFpshvXnakuryTCgfVdLeSI-dpWmlZds4eVWs,199
+botocore/data/connectcampaigns/2021-01-30/service-2.json.gz,sha256=LrF3cIvAXMYBc0KMUn-n93pnrE6AM2voXFfp_P2lyNA,5276
+botocore/data/connectcases/2022-10-03/endpoint-rule-set-1.json.gz,sha256=eKSkV-oookU8yOp7xC9p6lvqRRhz4G5HgQiROZlfXWo,1296
+botocore/data/connectcases/2022-10-03/paginators-1.json,sha256=M0kWmC60l-5J1fP-wyuoYdvPMzmuPYOAdGqEDIErYWI,355
+botocore/data/connectcases/2022-10-03/service-2.json.gz,sha256=BacGj_cGb7YSfmwTweopg1-Ev7P6yvQOLee1lRm10_M,12821
+botocore/data/connectparticipant/2018-09-07/endpoint-rule-set-1.json.gz,sha256=wVmUdsmt9c2Ltm_EJUV6ha_56dXqw375t8pC69dlK-4,1240
+botocore/data/connectparticipant/2018-09-07/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/connectparticipant/2018-09-07/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/connectparticipant/2018-09-07/service-2.json.gz,sha256=Ug5IlKNSvnEEYaIzvSyzIge8vR6QumzpXUGapuZeXPA,6769
+botocore/data/controlcatalog/2018-05-10/endpoint-rule-set-1.json.gz,sha256=62Q4YhRitKcX4G_NaLtv2b5j3m14vhZMfaNgI1NV0ek,1304
+botocore/data/controlcatalog/2018-05-10/paginators-1.json,sha256=XI3p1FA1O6zZYNjHJPStJTsy8jgkshzuWsvAhkmGpiY,687
+botocore/data/controlcatalog/2018-05-10/service-2.json.gz,sha256=vR-CtRAj3w_WK7BF82QDWw3h7wtfFvfXrmd0SnfBAK8,4082
+botocore/data/controlcatalog/2018-05-10/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/controltower/2018-05-10/endpoint-rule-set-1.json.gz,sha256=7CEmOV75BC05kIkQDMbvVbL0oGqQI_I9CUzEitzxQyY,1153
+botocore/data/controltower/2018-05-10/paginators-1.json,sha256=fnUyaumVMU4LxD6VRfutlQ549Lr8SBKeWce0mqgA0uM,1081
+botocore/data/controltower/2018-05-10/service-2.json.gz,sha256=9kFgs5EtTLx93wsCbcqxo32pGa2Yf5K_9s5fv-Wtrto,11976
+botocore/data/cost-optimization-hub/2022-07-26/endpoint-rule-set-1.json.gz,sha256=HUDyJJyyuZepbeQIg6IWKoOOqDIu6EZVOj3CXPgxFGY,1311
+botocore/data/cost-optimization-hub/2022-07-26/paginators-1.json,sha256=UTQXBj6oqxK8QwJmb157yVSB10S_7MhbTh4tT1a5RAU,534
+botocore/data/cost-optimization-hub/2022-07-26/paginators-1.sdk-extras.json,sha256=O4h78RRWkbXvEDne3NyxM_npO-yI2KU8QL1jnJnfiwE,382
+botocore/data/cost-optimization-hub/2022-07-26/service-2.json.gz,sha256=ehbhZqNgIwuoKKhJ6Tmq0Xhm4BMyT4iN1i1HzrZgAUQ,7894
+botocore/data/cost-optimization-hub/2022-07-26/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/cur/2017-01-06/endpoint-rule-set-1.json.gz,sha256=9ijsKWZ2rUoRO36YD32z1lM97zG-RyZ0W9UfXp624Rg,1147
+botocore/data/cur/2017-01-06/examples-1.json,sha256=NyOJJuDWe_rnuUTIp9cdvnw0GfJCK2aaDMW8Qkyf2Mg,2874
+botocore/data/cur/2017-01-06/paginators-1.json,sha256=svrnnDA-WDB_TSjNDhx_3bXmieM10GBn4TRFNlZNPHg,209
+botocore/data/cur/2017-01-06/service-2.json.gz,sha256=81eAZ97D7TJQm6nCSDe1mzfzoG3a2SH2O0fiv9ex5Vg,3835
+botocore/data/customer-profiles/2020-08-15/endpoint-rule-set-1.json.gz,sha256=Hy2GjAAPL6zEjvk5ukrJZdc3_SOuCrwTKJ-EUOqCBi8,1150
+botocore/data/customer-profiles/2020-08-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/customer-profiles/2020-08-15/paginators-1.json,sha256=-OlroN0mtsUy6CJXtf2X8go_5y0XBjG_W-BLdTRd-LE,188
+botocore/data/customer-profiles/2020-08-15/service-2.json.gz,sha256=odpfRygvYebuVLMkeXAqvwfilD76fxeJyUEemwLKIKQ,30653
+botocore/data/databrew/2017-07-25/endpoint-rule-set-1.json.gz,sha256=OZDuL8j3Tt_nfmVrQ0kiMOkYy5dO0ZV8cEtcZf5SS38,1210
+botocore/data/databrew/2017-07-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/databrew/2017-07-25/paginators-1.json,sha256=i_5ZTxjwAyOvq_e_Etz8L97TB_O2FHjLsAkKFbGJf8U,1316
+botocore/data/databrew/2017-07-25/service-2.json.gz,sha256=SjIYt1U9XZZtpFuBz-b0XuRtLEhL1kh_lCxnGY0vb2Y,20270
+botocore/data/dataexchange/2017-07-25/endpoint-rule-set-1.json.gz,sha256=JTzOGIhSwKXWKxQSl9UiCkNTSKdPJkfymxmnetLfwYw,1154
+botocore/data/dataexchange/2017-07-25/paginators-1.json,sha256=UykSh3IGMDfXWMvEmuyXdyETgPwFDGUHULkPZ7kwmxE,848
+botocore/data/dataexchange/2017-07-25/service-2.json.gz,sha256=CRiMXZ6CIZnWf30AI1Ef2wiG97vEV5GFirgiQHTDkac,14876
+botocore/data/dataexchange/2017-07-25/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/datapipeline/2012-10-29/endpoint-rule-set-1.json.gz,sha256=ccppRd3mUqzv-Kl88CPV96SV2Zsn6L1kST7s575gELk,1150
+botocore/data/datapipeline/2012-10-29/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/datapipeline/2012-10-29/paginators-1.json,sha256=JdrA68aI3fnPWh2_ecOxC5DtcFz4OkiO8GvsBkzOgUw,554
+botocore/data/datapipeline/2012-10-29/service-2.json.gz,sha256=JIl-P2m7LAyMKNTLlvTPdgHRBJ-WIiXbrpk1rxi3-cU,9587
+botocore/data/datasync/2018-11-09/endpoint-rule-set-1.json.gz,sha256=yXl5-PVp9VCSWj_za7-9mtaG99zjHZJWIODKYD9kcRo,1151
+botocore/data/datasync/2018-11-09/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/datasync/2018-11-09/paginators-1.json,sha256=xyjK7EJvThNXPS7riG3yqLl5wo8ufwRqt1z1A8MjJos,1373
+botocore/data/datasync/2018-11-09/service-2.json.gz,sha256=UnKbWVnR3m_NODuJE8d2BSTBOJ-3cwopSdPq2AaJoJY,39287
+botocore/data/datazone/2018-05-10/endpoint-rule-set-1.json.gz,sha256=YjxuPtiXXdBpCq68NF5k4W2Dx1frUCHnFwOEfQgJmhU,1127
+botocore/data/datazone/2018-05-10/paginators-1.json,sha256=Wa9-0SGc3nhDhv_FB8-oJPZ3VqW8SBYYWBBeGWSrraI,4534
+botocore/data/datazone/2018-05-10/paginators-1.sdk-extras.json,sha256=PDLX-xnxEfPjAUjKyltSi_A-UwPDEaAYefhbLvCSFDo,368
+botocore/data/datazone/2018-05-10/service-2.json.gz,sha256=iuYtEW2BldzUvwRkrQrJJzhze64dX8g_37uJ24uk23c,47647
+botocore/data/dax/2017-04-19/endpoint-rule-set-1.json.gz,sha256=VbZ9LinOZXB4iTuwaraOhIY5K2H5jIOYgVWeW-HZOFk,1144
+botocore/data/dax/2017-04-19/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/dax/2017-04-19/paginators-1.json,sha256=OOhBXs1nXQbwQO2dybisWoE6M5Z7WrPyQUCAyGgfEiA,1175
+botocore/data/dax/2017-04-19/service-2.json.gz,sha256=3iD4bf6tP5JbXsOLpFliSGSn90lUmfQtoYnL5gE8Khs,9746
+botocore/data/deadline/2023-10-12/endpoint-rule-set-1.json.gz,sha256=QMatSgObnsgoGw0m6FV5uev_3rL5WW8-U5DO4fFrG5I,1298
+botocore/data/deadline/2023-10-12/paginators-1.json,sha256=NZIEiv_et3adOXZ7RBdzz0lyoRXNtDswN0Foka13scY,4399
+botocore/data/deadline/2023-10-12/paginators-1.sdk-extras.json,sha256=hFPg_wsYLg1pXtVqU63jGPKL7Fc9mW9vLLloldy_b04,218
+botocore/data/deadline/2023-10-12/service-2.json.gz,sha256=uMhvcwUq4USsMn2OnYci1pfD9G8HSHPj9g43bWCmmdE,37843
+botocore/data/deadline/2023-10-12/waiters-2.json,sha256=ilz5tYuLAYJ-xcGbQ_1BnNlq18wUFR-LooqfXhO21fE,3894
+botocore/data/detective/2018-10-26/endpoint-rule-set-1.json.gz,sha256=qNRw-nKRY89G5Zm473hZ1crI2nV8gEVaF38JtEXjCQQ,1153
+botocore/data/detective/2018-10-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/detective/2018-10-26/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/detective/2018-10-26/service-2.json.gz,sha256=F30LuCmzxw3BM3i6cyGGDLt5Ww4lhDf5J-kN38rs0po,13154
+botocore/data/devicefarm/2015-06-23/endpoint-rule-set-1.json.gz,sha256=NJd9Zl4FMQC4SbPN7XbXOv62CJc7QeTD6lWjR8rlwns,1152
+botocore/data/devicefarm/2015-06-23/examples-1.json,sha256=ph2IehoxWkjr60w1Itx_H2XRMVKQ9J1WHbDDdS2-i6Q,42721
+botocore/data/devicefarm/2015-06-23/paginators-1.json,sha256=dsBpWrsUYvlphjtWSswDS3BYoWFzpq3sqwpOK4ER5vA,2870
+botocore/data/devicefarm/2015-06-23/service-2.json.gz,sha256=Ndk-J026V1C0KJNV8HGYOXlXmPJ1NvWTJK420r7COrk,31160
+botocore/data/devops-guru/2020-12-01/endpoint-rule-set-1.json.gz,sha256=TRgr7g5mE0AOyILvkAzKumdw3hGJZZYOpaP--RES1HM,1153
+botocore/data/devops-guru/2020-12-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/devops-guru/2020-12-01/paginators-1.json,sha256=L8a_Vi9F4QUZiw34P5LPuf6ELhTE3_rKfSJTiF-Jsrw,3043
+botocore/data/devops-guru/2020-12-01/service-2.json.gz,sha256=8fWzGxAng6assppgeD9Z0xsiReUqYBBQQqKhqxIRZt8,25040
+botocore/data/directconnect/2012-10-25/endpoint-rule-set-1.json.gz,sha256=HOfvxuFDn-AuQOfga04JO_tl9r8JXX2VedCWVVCen9k,1155
+botocore/data/directconnect/2012-10-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/directconnect/2012-10-25/paginators-1.json,sha256=xeMiI713ZrL0L4eTYXOT8iXsmsiguus1SZdRE7OWYCo,643
+botocore/data/directconnect/2012-10-25/service-2.json.gz,sha256=yrZ_yqTIl5mgoB9XyZBvVl8u_uN8z_bCiLYyvF142kk,20166
+botocore/data/discovery/2015-11-01/endpoint-rule-set-1.json.gz,sha256=dtlwMDixUGX8-ekg0XMNVomgiryp9OXBl9lQZvj9Bww,1151
+botocore/data/discovery/2015-11-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/discovery/2015-11-01/paginators-1.json,sha256=9TAcWsEEH768Rt1ArlrAzFDXYkp82xhdZ5Kh5LVrkmw,1221
+botocore/data/discovery/2015-11-01/service-2.json.gz,sha256=w1s_izM8z6rX_gnjLygIQecKUbUL1NSFejot0E4vav4,18793
+botocore/data/dlm/2018-01-12/endpoint-rule-set-1.json.gz,sha256=cuQzRqq2JG2emqF5bI-F3CGpRUcw02UViSmpB7e7XzI,1232
+botocore/data/dlm/2018-01-12/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/dlm/2018-01-12/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/dlm/2018-01-12/service-2.json.gz,sha256=Lsh1gHGzcc60PTO7Hm5xe-s-quppOhwyA6oZw-VS_oc,12071
+botocore/data/dms/2016-01-01/endpoint-rule-set-1.json.gz,sha256=JuaX89rsGm8vPLqj3rW9DH20ncwZR7_qFY4vK6qWTVc,1302
+botocore/data/dms/2016-01-01/examples-1.json,sha256=vV_0L6caRIbPqk4IOCZVqNc0xcbN77GsWwY3KaK0SA0,35747
+botocore/data/dms/2016-01-01/paginators-1.json,sha256=Y3SZaT-h8ftXIrqxEa-ITfC6Bin1V9vBGh6xMm3zXXQ,2332
+botocore/data/dms/2016-01-01/service-2.json.gz,sha256=j4GBPHCxZJX-KJVkcbOV-G_Ff2IEOA_8t5Ya5SES2HA,75988
+botocore/data/dms/2016-01-01/waiters-2.json,sha256=q_cVn5QLry8e5ZZquSwUs7tJo5LQnnQfswzEpsF45F8,11781
+botocore/data/docdb-elastic/2022-11-28/endpoint-rule-set-1.json.gz,sha256=PD0kzaPQEAJnfxdY2Odkx7KA1U33RpG2nnRphcZ8l7A,1304
+botocore/data/docdb-elastic/2022-11-28/paginators-1.json,sha256=9cdvHJPwLW6YNYyFzwyXh6EdyQFNv5_L5n-ZkzdhYm0,358
+botocore/data/docdb-elastic/2022-11-28/service-2.json.gz,sha256=oiu5h-iC_L3ND8wRsuSzhUYAA_5sqI1iJdSz-2OFrQ8,6430
+botocore/data/docdb/2014-10-31/endpoint-rule-set-1.json.gz,sha256=D2eqYU0CLHapqhwnDyTYkUIkgPbCVgsDwp5V9d-Z2Pg,1232
+botocore/data/docdb/2014-10-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/docdb/2014-10-31/paginators-1.json,sha256=Lc8FwQvudtu-XOnFfOh-qM6pOrsnlRajew2PKY6ZtZk,2318
+botocore/data/docdb/2014-10-31/service-2.json.gz,sha256=k0q2OrpHmTORUX5SR7eFNpgjV4NsIolN-1FuHl4K0Fg,32347
+botocore/data/docdb/2014-10-31/service-2.sdk-extras.json,sha256=U_PgxwtPhWl8ZwLlxYiXD4ZQ4iy605x4miYT38nMvnM,561
+botocore/data/docdb/2014-10-31/waiters-2.json,sha256=8bYoMOMz2Tb0aGdtlPhvlMel075q1n7BRnCpQ-Bcc1c,2398
+botocore/data/drs/2020-02-26/endpoint-rule-set-1.json.gz,sha256=cwrFofaYh9fw8rixyuKXNP_Y5eebNyKCongNGStO4Ik,1146
+botocore/data/drs/2020-02-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/drs/2020-02-26/paginators-1.json,sha256=j1Nq2iBDgHjtNTzLW5JGDB5BfwGLcqOX3kewE_mNNIM,1909
+botocore/data/drs/2020-02-26/service-2.json.gz,sha256=V-q9mYfUQ79hu3rmNDohwq-iB9f7wV2G71BwAwK-Jqg,21053
+botocore/data/ds/2015-04-16/endpoint-rule-set-1.json.gz,sha256=EllZ-_iUD1Exu7ACDalCWQN4QfuEkpErTTjiqG3E9gQ,1146
+botocore/data/ds/2015-04-16/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ds/2015-04-16/paginators-1.json,sha256=4ho4Q2kTYsj5kSg6PXMy--xzeeVlJYvK0Ha1c1L7yJ0,2390
+botocore/data/ds/2015-04-16/service-2.json.gz,sha256=aFs_KcNUgmMbeON3FMNDvvHAoNW987rVmYLDYfdIuLw,25563
+botocore/data/dynamodb/2011-12-05/endpoint-rule-set-1.json.gz,sha256=g6iJCQKGnzY_vRlWbWeUz4kvDt3pRw59vFKWaOS9WXc,1343
+botocore/data/dynamodb/2011-12-05/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/dynamodb/2012-08-10/endpoint-rule-set-1.json.gz,sha256=IXaeZ7bhzU2cLmNAU_WN6KSTOr8bJ_W5D-EN1_RMFm8,1342
+botocore/data/dynamodb/2012-08-10/examples-1.json,sha256=cZ5PBzQtSA9b1ZN39RffvUM54Tqf_h5-AQA7zSBVK4Q,16947
+botocore/data/dynamodb/2012-08-10/paginators-1.json,sha256=U84oi-heJVXxjHM1enODt6qI5J117zh0YoM4BHwZZ18,1103
+botocore/data/dynamodb/2012-08-10/service-2.json.gz,sha256=i5Mk0tnI3W_tDjhXsYc70iBTM3frWGYHnArhePTdeh0,78473
+botocore/data/dynamodb/2012-08-10/waiters-2.json,sha256=G_iaXR3xZP3M8lpMR1olm2p-EvK6InTidNZnUUqPL70,727
+botocore/data/dynamodbstreams/2012-08-10/endpoint-rule-set-1.json.gz,sha256=4ukei33-usw8zpkOR9FtvRU6T-Su0hvbAvcUsaQYQsQ,1671
+botocore/data/dynamodbstreams/2012-08-10/examples-1.json,sha256=LF2m4pmyTs0G8NR6AhmybL0E2F9WHfnbxz5q31DtjAg,7693
+botocore/data/dynamodbstreams/2012-08-10/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/dynamodbstreams/2012-08-10/service-2.json.gz,sha256=_dR7Z-UD62-O7DCGcXZyCzuh6ELgnqrKdxfTF7xy9Hs,6799
+botocore/data/ebs/2019-11-02/endpoint-rule-set-1.json.gz,sha256=K69DkOLjWSFPrEmgFEHXM1KyZFeDcW8pTDlZWYpy_6Y,1144
+botocore/data/ebs/2019-11-02/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ebs/2019-11-02/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/ebs/2019-11-02/service-2.json.gz,sha256=m_q4Cq1DQzvuttIYTHDACxEwQynNqGrSo5YWbTx_WfU,6371
+botocore/data/ec2-instance-connect/2018-04-02/endpoint-rule-set-1.json.gz,sha256=3qrNk74oLr2I2kSi3Yu0A-LAA8eWwzYoM_3RrQr91Ng,1162
+botocore/data/ec2-instance-connect/2018-04-02/examples-1.json,sha256=Qnm4-ldcu-2O38JTe_w17UJWdblMaRBfIc8HyJ62DYU,1712
+botocore/data/ec2-instance-connect/2018-04-02/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/ec2-instance-connect/2018-04-02/service-2.json.gz,sha256=6jO7pRW29hrhPiCBtWbjE1swtqGl4ObQluwcfVaCm1M,2282
+botocore/data/ec2/2014-09-01/endpoint-rule-set-1.json.gz,sha256=DucHo-mKCAXFWEu1LWUMc1I-avB3Rw2PQZd8cit0XOg,1237
+botocore/data/ec2/2014-09-01/paginators-1.json,sha256=XpA8TZvmBGGraKlRGE-U-YeLIBN1ZvbcyE8Wh8uuIDM,1271
+botocore/data/ec2/2014-09-01/service-2.json.gz,sha256=qcITZgxgqWXu0WcdOlpeq6bZ0YcXaA1PEC0iCuQfqmQ,71841
+botocore/data/ec2/2014-09-01/waiters-2.json,sha256=HG1xDu-8ICfvY1n_YV9i0ylufepFUYmDd0dLkQxwKuY,8548
+botocore/data/ec2/2014-10-01/endpoint-rule-set-1.json.gz,sha256=DucHo-mKCAXFWEu1LWUMc1I-avB3Rw2PQZd8cit0XOg,1237
+botocore/data/ec2/2014-10-01/paginators-1.json,sha256=Uns0O6V6ZIXI09iZdCY77w-CBHbes_siW5vFU-bpE1w,1439
+botocore/data/ec2/2014-10-01/service-2.json.gz,sha256=prYQ-qEYTNvV3wna2NHtvgNnJ3dTn2Gvp94q1kZekhU,75362
+botocore/data/ec2/2014-10-01/waiters-2.json,sha256=UDhKYGIrItEq2e56vKMh6yLdn_YfsfTYsmankCjsR3k,11040
+botocore/data/ec2/2015-03-01/endpoint-rule-set-1.json.gz,sha256=DucHo-mKCAXFWEu1LWUMc1I-avB3Rw2PQZd8cit0XOg,1237
+botocore/data/ec2/2015-03-01/paginators-1.json,sha256=Uns0O6V6ZIXI09iZdCY77w-CBHbes_siW5vFU-bpE1w,1439
+botocore/data/ec2/2015-03-01/service-2.json.gz,sha256=W9zSFGpp9-hOIWU2kKOPIMqr9XCm10SrXAioJQg-wOE,77885
+botocore/data/ec2/2015-03-01/waiters-2.json,sha256=UDhKYGIrItEq2e56vKMh6yLdn_YfsfTYsmankCjsR3k,11040
+botocore/data/ec2/2015-04-15/endpoint-rule-set-1.json.gz,sha256=DucHo-mKCAXFWEu1LWUMc1I-avB3Rw2PQZd8cit0XOg,1237
+botocore/data/ec2/2015-04-15/paginators-1.json,sha256=Uns0O6V6ZIXI09iZdCY77w-CBHbes_siW5vFU-bpE1w,1439
+botocore/data/ec2/2015-04-15/service-2.json.gz,sha256=O0Einy6jXWFHZBNigZ71InDG_DR8vIQMdrPcuu37MMI,90171
+botocore/data/ec2/2015-04-15/waiters-2.json,sha256=1iUHJTDrTvb5_HbDMbVVzC4Ex1S97GZl-tnP70MaDEY,11546
+botocore/data/ec2/2015-10-01/endpoint-rule-set-1.json.gz,sha256=s4VdI91m1NK6uQxZnxy5CI4HlR8j-HuLTpIXx0XS3TQ,1391
+botocore/data/ec2/2015-10-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ec2/2015-10-01/paginators-1.json,sha256=Vom5HeCc0UgDyEyYKw3piztolJ3IIxz_tIhEX61TvM8,1793
+botocore/data/ec2/2015-10-01/service-2.json.gz,sha256=RXTNqhLX4PETDxMKv2z5zxJjtzHz2OQhKn85eNFaa9A,107913
+botocore/data/ec2/2015-10-01/waiters-2.json,sha256=8sXo9xWtm1IZMKcm9Ne42ha-9XDTVP_fZUejgA1tw3E,14823
+botocore/data/ec2/2016-04-01/endpoint-rule-set-1.json.gz,sha256=s4VdI91m1NK6uQxZnxy5CI4HlR8j-HuLTpIXx0XS3TQ,1391
+botocore/data/ec2/2016-04-01/examples-1.json,sha256=0xdUoNVzXNn5ZMmA_aiPwiQC68adrXjBJPhw3AzQC8M,109914
+botocore/data/ec2/2016-04-01/paginators-1.json,sha256=Vom5HeCc0UgDyEyYKw3piztolJ3IIxz_tIhEX61TvM8,1793
+botocore/data/ec2/2016-04-01/service-2.json.gz,sha256=KsArixXrEiOKmj3CDyxDoYgKhAAqF7TjgNZ3KxZzslk,112481
+botocore/data/ec2/2016-04-01/waiters-2.json,sha256=ZjSjdDS-pisO_MoRjsulXMshrcU5qNJd4m1bOBQ9mKQ,15259
+botocore/data/ec2/2016-09-15/endpoint-rule-set-1.json.gz,sha256=s4VdI91m1NK6uQxZnxy5CI4HlR8j-HuLTpIXx0XS3TQ,1391
+botocore/data/ec2/2016-09-15/examples-1.json,sha256=Dv18Ql8faOeBMQlenC7HBzlgrNQXNeokvLsyFf6Q_yY,110174
+botocore/data/ec2/2016-09-15/paginators-1.json,sha256=Vom5HeCc0UgDyEyYKw3piztolJ3IIxz_tIhEX61TvM8,1793
+botocore/data/ec2/2016-09-15/service-2.json.gz,sha256=t0yPTweS-M8PSJpi076T_YDVvDwkO3s1r9cVX_84XDc,114400
+botocore/data/ec2/2016-09-15/waiters-2.json,sha256=1ZtptOEInU4p-4ZQFXbC5lxZ8XNsseki72qxLO2dX4M,14875
+botocore/data/ec2/2016-11-15/endpoint-rule-set-1.json.gz,sha256=3ez3G5WGwH05r3U_4grZlX6fHjwVGY2FyubNAQTGnc4,1233
+botocore/data/ec2/2016-11-15/examples-1.json,sha256=gB8-MuMSl9N4ic1oBYCv02B_YplxOdnKsfS7g5pY7hk,147949
+botocore/data/ec2/2016-11-15/paginators-1.json,sha256=tzeIKCNu5EaD41EvXklVBJSgH_rcdvDiCNqvzgeVPtw,26822
+botocore/data/ec2/2016-11-15/paginators-1.sdk-extras.json,sha256=s-xAN9v51q2N4UE-PQ_I-wK9PDbrSnwQlKx0yA_rmSk,249
+botocore/data/ec2/2016-11-15/service-2.json.gz,sha256=RSpFWpnt6i6irSC3dvEQjIlcdF-1pf0U9wMD7w6wEM4,382647
+botocore/data/ec2/2016-11-15/waiters-2.json,sha256=4kAaAuL0ulzVcgZclPE2104MSuov-oDQdeylOucoBSM,18443
+botocore/data/ecr-public/2020-10-30/endpoint-rule-set-1.json.gz,sha256=qHFONv9oz1pKwqLi2QphfyGbjbb-IR20QEEObIhziug,1152
+botocore/data/ecr-public/2020-10-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ecr-public/2020-10-30/paginators-1.json,sha256=EEmON1DSCdAARd-o_S_RiZ6rXcWO8AZbYlx4UMyZEGE,711
+botocore/data/ecr-public/2020-10-30/service-2.json.gz,sha256=a_OqNxbCgYCrFQcXQ-10rur616m_GYJFTdVMnoTcFQ8,10694
+botocore/data/ecr/2015-09-21/endpoint-rule-set-1.json.gz,sha256=WwuQ99ls9iXsGV_XTk4_E1It_L496GibHnmD0uR-XPI,1262
+botocore/data/ecr/2015-09-21/examples-1.json,sha256=cFx-qAY3SfNXEHCMe7I9RTWxV-Jtlo8moRHGDZ5UCAM,6603
+botocore/data/ecr/2015-09-21/paginators-1.json,sha256=jTAyTM5a36H94lthRLaf_MOoFKa0_9YW0wqzaMoyiNM,1736
+botocore/data/ecr/2015-09-21/service-2.json.gz,sha256=hnqgccddKpsGGBK9Js6k15ounPjeo5Nlp27JTOnV9UQ,23724
+botocore/data/ecr/2015-09-21/waiters-2.json,sha256=j4QQUhn_PYN87gWoaY1j1RR-lv7KjzPItwwn1WMYkB8,1482
+botocore/data/ecs/2014-11-13/endpoint-rule-set-1.json.gz,sha256=KXEyTQrNRFrcrXNIuWDm9TWLo3m3c4fl76QBj5UVLLE,1146
+botocore/data/ecs/2014-11-13/examples-1.json,sha256=Qp-rrnSHaDiVv4ESeJkTGfC1-guCjRc9B9LfiwjrMjg,36519
+botocore/data/ecs/2014-11-13/paginators-1.json,sha256=Y_nqEkKUMY3UhZ5D6DJ2QqxBHfnLkqM6FsOxPp5JUVE,1565
+botocore/data/ecs/2014-11-13/service-2.json.gz,sha256=dfM9ZRjtCp3Tx6fDDXdysVFGK1zCk-cPCIfAjdreKl8,95194
+botocore/data/ecs/2014-11-13/waiters-2.json,sha256=F4d_a7_xVQIib5MpmSitTQBxupfL0Z9NqxOibIA6Igs,2246
+botocore/data/efs/2015-02-01/endpoint-rule-set-1.json.gz,sha256=6YSjzhE5OfvbRTckkMrNRe4IoEMQz9TcbCLFISx0uZQ,1157
+botocore/data/efs/2015-02-01/examples-1.json,sha256=0EFBCHNGLNS0ftGQqjngkhfTFYpw6E-7lnuAh-d6YKU,8825
+botocore/data/efs/2015-02-01/paginators-1.json,sha256=SKRuOWm1E5Nvvzppzjn-IeS1Lj0I3qSqvc9t9XtKpA4,878
+botocore/data/efs/2015-02-01/service-2.json.gz,sha256=5K0ZJ65I-9UTUdPE6hahQMISOqHAGOhuDTOnLP_suoA,23040
+botocore/data/eks-auth/2023-11-26/endpoint-rule-set-1.json.gz,sha256=NIqLIIL81kRleI0f-hLEf0sjvcSJ91n05G0rAtKRgAQ,1127
+botocore/data/eks-auth/2023-11-26/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/eks-auth/2023-11-26/service-2.json.gz,sha256=nzUtqCYiscGUFo4AyZXxYCbb_u9tsZPitfIRK9hDOFU,2343
+botocore/data/eks-auth/2023-11-26/waiters-2.json,sha256=tj1ZnaqhwmJkUEQlwH7wm1SqY3lg1BvZDfzfPaIgNrY,38
+botocore/data/eks/2017-11-01/endpoint-rule-set-1.json.gz,sha256=6nCVNn6AEryB3dheWe_dSobtvyaR_acvY-HAY0uNmD0,1267
+botocore/data/eks/2017-11-01/examples-1.json,sha256=vCT3MFB7D3tNzqaIdxd8nyDbt7hevsAvDE4RQTQcEKg,5021
+botocore/data/eks/2017-11-01/paginators-1.json,sha256=O8AjzL_WxvPulPVk4eLVY43BRSSmQpoV_Qo043qnAF0,2365
+botocore/data/eks/2017-11-01/service-2.json.gz,sha256=YRx9cs8UwX6PvuD5sHyXL9DhjZqNZkAzLYa6HdVuNPs,40696
+botocore/data/eks/2017-11-01/service-2.sdk-extras.json,sha256=pmn0V8Su5NiqW8Y3X-IBtzD1Bz_JANtKgU4fsr-i_bM,107
+botocore/data/eks/2017-11-01/waiters-2.json,sha256=j-ZLRcYn34oHDZY9xth7Vrz7q1eCNn_fzC1bK1WVVwo,4198
+botocore/data/elastic-inference/2017-07-25/endpoint-rule-set-1.json.gz,sha256=L5EZXSAC2gq5G84gnw_PUDikgSYB0TWOQWOKUBCCKz8,1159
+botocore/data/elastic-inference/2017-07-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/elastic-inference/2017-07-25/paginators-1.json,sha256=t1OswldbiUvR3fUJS_-AbIukdJ-LmbdPeYOPJ4m8jIw,201
+botocore/data/elastic-inference/2017-07-25/service-2.json.gz,sha256=kmUc3XMy5VluhQlXIUWgQb0RqEiG8WrAUjPxUdXz_rA,3025
+botocore/data/elasticache/2014-09-30/endpoint-rule-set-1.json.gz,sha256=Agzg3m6hZXC9-KdHlKTeJHpFYpPJZZmQ0c8SYRnrVgw,1241
+botocore/data/elasticache/2014-09-30/paginators-1.json,sha256=YkZxwpICpidoDrIimyr0yFGYg_T0emkSfhlNfPOfVMA,2171
+botocore/data/elasticache/2014-09-30/service-2.json.gz,sha256=byEArR_V3Dof63JH2bgBcr0KvnfI6LmCAiPWLzd_dTk,22920
+botocore/data/elasticache/2014-09-30/waiters-2.json,sha256=mIVMN9SNrvDJ2iW_uXAA-N5ptxGmDw964Sv89zKAs-g,3719
+botocore/data/elasticache/2015-02-02/endpoint-rule-set-1.json.gz,sha256=WcEues7m8F2Yst959w2c9PYWPEjV1FQmBtEF82C1D9k,1238
+botocore/data/elasticache/2015-02-02/examples-1.json,sha256=iWpOlje8s2EFHlnYNgjHX2DpC7teIKmeA7f6e51u00I,111590
+botocore/data/elasticache/2015-02-02/paginators-1.json,sha256=XrsOWe2fflZLszEuZYsZjeXPNAAj5IjpOdfsse_Peg8,3401
+botocore/data/elasticache/2015-02-02/service-2.json.gz,sha256=BdgKzVu84qwD17U1Lkqhpd_x_cruSt5p9CWUrBxIKmU,56009
+botocore/data/elasticache/2015-02-02/waiters-2.json,sha256=N6NTYHqUoktWaIjapl3RDepPknxNlIbb8a0wnS0HB_E,5118
+botocore/data/elasticbeanstalk/2010-12-01/endpoint-rule-set-1.json.gz,sha256=bUFlLHwxqml9KS_XcwGIWXy1oCKNKhtCYwKyvXWF50Y,1242
+botocore/data/elasticbeanstalk/2010-12-01/examples-1.json,sha256=EuEpZEobhGxWPfRosGTFNWYs8zRFVtkQtLXD8M_5fm0,37449
+botocore/data/elasticbeanstalk/2010-12-01/paginators-1.json,sha256=qM8N07fmdTtnZBXFiyFeW31EjqjmDWb-viwc19UyF5o,934
+botocore/data/elasticbeanstalk/2010-12-01/service-2.json.gz,sha256=-eO4R35Fhuq22Zt-EOMnZaNglYvRCc5AlZd0MthGhho,27770
+botocore/data/elasticbeanstalk/2010-12-01/waiters-2.json,sha256=nS1qW0cVQpjnVhpONryvuFWWW4JwJYSW82ooLigmCu0,1463
+botocore/data/elastictranscoder/2012-09-25/endpoint-rule-set-1.json.gz,sha256=W7d9YD8F6uKTZu_hfr0kR-8IlUoX-pIrvBYMRUWJn_0,1157
+botocore/data/elastictranscoder/2012-09-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/elastictranscoder/2012-09-25/paginators-1.json,sha256=xHyxPQTUGKK7Vj_z_1E46xAI6BwZC8IvDFuZ3DXD4BY,559
+botocore/data/elastictranscoder/2012-09-25/service-2.json.gz,sha256=wvL0SqUDGOXRF_7-yZM_bVkmKddHUCjxyp6RM9sieBQ,35481
+botocore/data/elastictranscoder/2012-09-25/waiters-2.json,sha256=ePD8qEyUXJMnroVmvrubritF3re95gdBAETq6do-Uh8,613
+botocore/data/elb/2012-06-01/endpoint-rule-set-1.json.gz,sha256=p3i_glOQoFTPuGNWjkqT9ExUp6o6rx9gtA-QXTLd4_g,1244
+botocore/data/elb/2012-06-01/examples-1.json,sha256=NE6HcGypE87pOfvGkxKi_QD-UJ_qWHG2_Q9ynk6V9xA,30446
+botocore/data/elb/2012-06-01/paginators-1.json,sha256=udADJnjh3b-REUTKNlC9yYaRI6aOiXfx3demJA1Msxg,373
+botocore/data/elb/2012-06-01/service-2.json.gz,sha256=3W6jUANTReynEA5ircBW5ESQKmI77cjFtDqAjDVSVeA,13226
+botocore/data/elb/2012-06-01/waiters-2.json,sha256=9NjB-6qbZ5pHxElH90T-4YPEBdXHCA9QHdcF96gTbP0,1527
+botocore/data/elbv2/2015-12-01/endpoint-rule-set-1.json.gz,sha256=p3i_glOQoFTPuGNWjkqT9ExUp6o6rx9gtA-QXTLd4_g,1244
+botocore/data/elbv2/2015-12-01/examples-1.json,sha256=4Qxoz28hEDW8u1O7iGLKnH9NNb7Po5qybLFQtvtR7ss,44281
+botocore/data/elbv2/2015-12-01/paginators-1.json,sha256=wtIfS6A6vl7MQPq0zkaEk9BUn8YRov0XE-FywxEhMuE,1198
+botocore/data/elbv2/2015-12-01/service-2.json.gz,sha256=puQb8BEZPXPzsd2b8gZmquZM4YswAL8W0YX_md8n7BQ,28114
+botocore/data/elbv2/2015-12-01/waiters-2.json,sha256=k-g2ypXqfbW4ktwuK1iVKpApIncFhOPemhbs7pf7cW8,2371
+botocore/data/emr-containers/2020-10-01/endpoint-rule-set-1.json.gz,sha256=5D8fpXpyQmMzEKEIgy7VHcOu35n8Pdg2ze8IJ9NRO2I,1155
+botocore/data/emr-containers/2020-10-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/emr-containers/2020-10-01/paginators-1.json,sha256=H-qB-RVfZ-v6uivpkok6jdL9RsL9yHZmDhdG4hdiPtU,889
+botocore/data/emr-containers/2020-10-01/service-2.json.gz,sha256=hkNp3qd4EQDa4-gJWtOhJqmzEtKANpvtuudhtUp79o8,10957
+botocore/data/emr-serverless/2021-07-13/endpoint-rule-set-1.json.gz,sha256=cZfJpAUQxikQ09LVbCQr6T_7AZI46jChKVEMZ5XN57c,1154
+botocore/data/emr-serverless/2021-07-13/paginators-1.json,sha256=X_bd8HxYUcjMp19q-YBGDOq_AGDlUYT1vP3yTfn6l8E,529
+botocore/data/emr-serverless/2021-07-13/service-2.json.gz,sha256=Yvj6rZCqU73zVqr2EiE4hfDSdCdC1DqzynD8geca5Pc,10951
+botocore/data/emr/2009-03-31/endpoint-rule-set-1.json.gz,sha256=0OT7J4uv7Ne9mviy9ml9pobMm-m3IXBK-YwXt6ZJmKQ,1241
+botocore/data/emr/2009-03-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/emr/2009-03-31/paginators-1.json,sha256=4EzVWE6TiQt5Mklp197KH8t17OiwaOVgVnBGK4y_HiQ,1357
+botocore/data/emr/2009-03-31/service-2.json.gz,sha256=VcI_d1sSumPWzJiNG_3Qs7Anchb7B0IADWg6tA-a35A,45142
+botocore/data/emr/2009-03-31/waiters-2.json,sha256=pMh5RSVHgFU-DlrH0dSf4IibHo9Hddmg9DvaR4a0Z90,2073
+botocore/data/endpoints.json,sha256=GzmaqB_NsHUsAVR6Ol7qH0219S-bD4lOC0u4AUNBILk,916938
+botocore/data/entityresolution/2018-05-10/endpoint-rule-set-1.json.gz,sha256=mUxli_dJ8crQN9M6diYWb2_-KmsrOpkJ5wZSuoaK6UU,1305
+botocore/data/entityresolution/2018-05-10/paginators-1.json,sha256=SRcdwInaqBXq7gpYBftOPb7OMmwgOTUQUTrGaY4594g,1245
+botocore/data/entityresolution/2018-05-10/service-2.json.gz,sha256=zpoRJNvd-j-SOv_6q3G4b8b30WtM-0RhF-GQjeIHlpU,15105
+botocore/data/es/2015-01-01/endpoint-rule-set-1.json.gz,sha256=YKq9_0pMitUiMNTSiSzMdrY1PT35768Vw6PF-dHIHKw,1313
+botocore/data/es/2015-01-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/es/2015-01-01/paginators-1.json,sha256=sbfve7QYejJgHClHTY4PgdwH4A-PJlY2y0XZ0qRCq9Q,1022
+botocore/data/es/2015-01-01/service-2.json.gz,sha256=12GdvicO1nv2isWYBe1LcQgZYITvaSg_WraG6b57mgA,29501
+botocore/data/events/2014-02-03/endpoint-rule-set-1.json.gz,sha256=DUeqI-HYtO14bS94zGF5DmETjE4N-5vn_ylgev2q6xw,1856
+botocore/data/events/2014-02-03/service-2.json.gz,sha256=kl2ADDmwuVyYtTIuOm-FsaPIceDtSD9Pbmyo71Gncnk,5254
+botocore/data/events/2015-10-07/endpoint-rule-set-1.json.gz,sha256=ib5pq988DYFb6sm3veir97P8snu3ZVVQVEe7wqdibuQ,1741
+botocore/data/events/2015-10-07/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/events/2015-10-07/paginators-1.json,sha256=A4gA5VY4LAnP_3iCOI-P0-c5nVH5ntM9hOh3gytyGco,504
+botocore/data/events/2015-10-07/service-2.json.gz,sha256=Hwi4vnlQPGZjeV0sy4XN-GA-vvGB1F254t4BRa7wqtE,34736
+botocore/data/evidently/2021-02-01/endpoint-rule-set-1.json.gz,sha256=3YJuSuXHymLlwZ6lNf2TagPcW7uUTnYmTPZ0EIZokaI,1148
+botocore/data/evidently/2021-02-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/evidently/2021-02-01/paginators-1.json,sha256=dzsz3rOFQc5MqVrha2K97L1ooI2e1kt8Om55efyV-tI,1016
+botocore/data/evidently/2021-02-01/service-2.json.gz,sha256=ftHdPjffI29G5bgVdTRWRfQ4Ojijgx6KOiYvHV9nZ7M,20415
+botocore/data/finspace-data/2020-07-13/endpoint-rule-set-1.json.gz,sha256=YEQXW-WLbVmCOLhVFRmF5b9AKFHccCQBNAHCBE42BVA,1153
+botocore/data/finspace-data/2020-07-13/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/finspace-data/2020-07-13/paginators-1.json,sha256=2RzSHda8vNoQX1L1pkYSwHfCF6Us0IKOrXdsXe-ZHkU,851
+botocore/data/finspace-data/2020-07-13/service-2.json.gz,sha256=bEZipKpmQCASuwavozSZsmL2_5sDl-Kdw2LdINiHkZ4,14476
+botocore/data/finspace/2021-03-12/endpoint-rule-set-1.json.gz,sha256=Gq4Ap6d_puH5mhd_SbwVW_TiuzDhzt4mcsF98AUwK_Y,1150
+botocore/data/finspace/2021-03-12/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/finspace/2021-03-12/paginators-1.json,sha256=S_FGEtC07GgFCRSKmv_l4RhRBCFmOEmIsQl7QfDI678,197
+botocore/data/finspace/2021-03-12/service-2.json.gz,sha256=G0elBvpRbodSxtIV8M4Qiw3M4vQWSrxEMOPtekV-Qo4,30353
+botocore/data/firehose/2015-08-04/endpoint-rule-set-1.json.gz,sha256=bxJMDLjW5wUbrn0X6B8HDOJs9AUbpgmBhDbu2ClM6Cg,1150
+botocore/data/firehose/2015-08-04/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/firehose/2015-08-04/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/firehose/2015-08-04/service-2.json.gz,sha256=FWLYgEb0IGjPKEtoB7tbsiYfM12Cy0qE2-nU8JedwQE,32777
+botocore/data/fis/2020-12-01/endpoint-rule-set-1.json.gz,sha256=nEpxI1dxOcMh02LNUMI3zOhXvffTbNar3dNqGaIzoDU,1232
+botocore/data/fis/2020-12-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/fis/2020-12-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/fis/2020-12-01/service-2.json.gz,sha256=NvHWQuGcEsJmB3mcOKPR2uifHsqY37NPDSZXD2ynaOs,8533
+botocore/data/fms/2018-01-01/endpoint-rule-set-1.json.gz,sha256=bALeWnTgR82-3Ysp_4XoY-w_F2BHYCc-C5jKdGOMu3w,1147
+botocore/data/fms/2018-01-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/fms/2018-01-01/paginators-1.json,sha256=Nv9OHpCiWQyuj5sj_Pz-0TjbnmtiMCR0tuySMApzYjM,1470
+botocore/data/fms/2018-01-01/service-2.json.gz,sha256=60LVre2DNGVOtvI6162M5tgRXq2K8FhnXGciCd2pTDQ,34376
+botocore/data/forecast/2018-06-26/endpoint-rule-set-1.json.gz,sha256=YPEo2Ju6N7K2B4R5mWy5nqj2JxS0L3WyfSZ7KbFtWAc,1147
+botocore/data/forecast/2018-06-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/forecast/2018-06-26/paginators-1.json,sha256=uwjfu4LU_nDuv9woqU_mcL_58oVcFi8QfUSAtQycpA8,2508
+botocore/data/forecast/2018-06-26/service-2.json.gz,sha256=d9iEsP6I_gWVS1rsKAniWk-eCi3KhD8o7ld40rDwado,40055
+botocore/data/forecastquery/2018-06-26/endpoint-rule-set-1.json.gz,sha256=IN1-wLcZWNuW4OCCbWmlXBcl4CAA3jQZUiXyOUOI01k,1151
+botocore/data/forecastquery/2018-06-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/forecastquery/2018-06-26/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/forecastquery/2018-06-26/service-2.json.gz,sha256=fY6bj6tXH03FnbhzVOQ0ukNZDqcFdcPN0_7tXUcyCX0,2160
+botocore/data/frauddetector/2019-11-15/endpoint-rule-set-1.json.gz,sha256=ydJV41CqXOH38eFeb1HVIDRzda5tjX2a4WU5FOhUmD4,1151
+botocore/data/frauddetector/2019-11-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/frauddetector/2019-11-15/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/frauddetector/2019-11-15/service-2.json.gz,sha256=9YV7JNvk5PDFKha-80f57JOoF1SKqYRjptNrJ_ygETM,24330
+botocore/data/freetier/2023-09-07/endpoint-rule-set-1.json.gz,sha256=EsmogQjJ2iqP4BZzi6Grx5DL7RMFf_84bjXtYX7sIKA,1422
+botocore/data/freetier/2023-09-07/paginators-1.json,sha256=3Gxmktm90Wak1Jk06fQ2wTZgX1ago6yInZZwNCk4S34,197
+botocore/data/freetier/2023-09-07/service-2.json.gz,sha256=biO9uBteobdFvKqH4QFuYElTGT9h8qukN3dTRtcnENo,2803
+botocore/data/fsx/2018-03-01/endpoint-rule-set-1.json.gz,sha256=WgCwrzFjGnPBWlsBClJIbT15nqhLmBtKBCOe6e0TG00,1147
+botocore/data/fsx/2018-03-01/examples-1.json,sha256=Ys4PS4GcrfV3F5Lg4hkaZgyemGgNKNLYSm-uepLDkR4,14242
+botocore/data/fsx/2018-03-01/paginators-1.json,sha256=6BwGoMkBZ7b2Gmata3ZEM1Sgvsnbcr3h2G-e6622ssA,884
+botocore/data/fsx/2018-03-01/service-2.json.gz,sha256=aMApNwq_t1d5h5iSak-FuI_tnT87VtHZmufo665zCAI,74075
+botocore/data/gamelift/2015-10-01/endpoint-rule-set-1.json.gz,sha256=b0yhP7wPN18DsIMqp1tV7IkV9vGDiAJhOjP5l6VexaA,1150
+botocore/data/gamelift/2015-10-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/gamelift/2015-10-01/paginators-1.json,sha256=PZ_MV_FNJ-B5eGrP-E3WCkcvr_hlqWzgT7G4XEAlQ80,3919
+botocore/data/gamelift/2015-10-01/service-2.json.gz,sha256=51lub-5evw19qBkn8L6lbCD1KyaQly7TQ4y9BfgYyP8,99984
+botocore/data/glacier/2012-06-01/endpoint-rule-set-1.json.gz,sha256=NluYe3OGOInKJJDSkIrYWink2SfG5v4wHpScDdzczfk,1393
+botocore/data/glacier/2012-06-01/examples-1.json,sha256=hR-1NmWo9lL0Cdqnr6x95Ywu_VfJucv0T4OveUp-S4o,27536
+botocore/data/glacier/2012-06-01/paginators-1.json,sha256=RAeqGFOs4GRiC-DuphMOBHWljwDfqBQINYf1qA2LbNA,628
+botocore/data/glacier/2012-06-01/service-2.json.gz,sha256=EeslSftlobNyLEwt0WUkyg8C676EwPTSTcjqsqRE4Vc,20913
+botocore/data/glacier/2012-06-01/waiters-2.json,sha256=hzoyJJT1wJh9lq1_z4MK2ZBj98TGRhroii0kbeFXnJw,785
+botocore/data/globalaccelerator/2018-08-08/endpoint-rule-set-1.json.gz,sha256=TrMqDfzQhrqoPWocPkmXcTzZ5nPDxH8T5je1Amn9gK8,1156
+botocore/data/globalaccelerator/2018-08-08/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/globalaccelerator/2018-08-08/paginators-1.json,sha256=Exal9Oqocr6pKQ_T5yEFYLXwm0BSxPYcuZTjZL2_8x8,2016
+botocore/data/globalaccelerator/2018-08-08/service-2.json.gz,sha256=Wo9zIs2IaeAl1FeCvVRlJ_sMIxmtiCOutY5KffpTCNc,21919
+botocore/data/glue/2017-03-31/endpoint-rule-set-1.json.gz,sha256=OHsXIrR1Aif1Sh2dl3N5ItHvltkKQOE_oRx4MdlVHAA,1147
+botocore/data/glue/2017-03-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/glue/2017-03-31/paginators-1.json,sha256=Trc47SNc0BdYEQFKtrJ4gaCKvMWHgLyaQHwpWYcyGn8,4201
+botocore/data/glue/2017-03-31/service-2.json.gz,sha256=rTLJ0zATMkDYnVNxg_RCPFa1ErYIjasPXz2x37kMmKA,135412
+botocore/data/grafana/2020-08-18/endpoint-rule-set-1.json.gz,sha256=DzOfytyZG6taxfmE_yO5sE4g4icyFMVyFhp7d4oASUY,1150
+botocore/data/grafana/2020-08-18/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/grafana/2020-08-18/paginators-1.json,sha256=1w34xYXn5nUies34W7BC_lzVPnbnhggKcWKUug4ckRc,908
+botocore/data/grafana/2020-08-18/paginators-1.sdk-extras.json,sha256=_g8panv1mpml0x69Y013wHvb22Sy63dKgVhK5oRqbwE,329
+botocore/data/grafana/2020-08-18/service-2.json.gz,sha256=q-kUhEksOHavB61sCaZwRkgnCp7i62curcZTAlRoAgk,15023
+botocore/data/greengrass/2017-06-07/endpoint-rule-set-1.json.gz,sha256=1wnBRvNUqfEfLBcfSMCSi76OnqQswoZsNwt9cmTAcoA,1356
+botocore/data/greengrass/2017-06-07/paginators-1.json,sha256=LphzapxioJkdlNs-zU4IVmg_pjswwy8RuDPq79sbW64,3366
+botocore/data/greengrass/2017-06-07/service-2.json.gz,sha256=D2KcSp1ojJAMXPCj2CKmZlS62Qdx0j6J9l5peo3MoZc,17141
+botocore/data/greengrassv2/2020-11-30/endpoint-rule-set-1.json.gz,sha256=U6mKCKXeHi6X7IbKWDpO-2PwhHA3ucRt5RQbHELd1ek,1364
+botocore/data/greengrassv2/2020-11-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/greengrassv2/2020-11-30/paginators-1.json,sha256=geNY9pksg1eDuJ9mpqk1iee_t8zQuFBrBG_O6eaZ7GU,1283
+botocore/data/greengrassv2/2020-11-30/service-2.json.gz,sha256=xkipS3HSVeFFdKMw1xf1pfZZ7ab6PxE-ijCJUR4_BKM,19925
+botocore/data/groundstation/2019-05-23/endpoint-rule-set-1.json.gz,sha256=jK3bRLr7elBDzSRCPdHww9Mpp60p5RIXvwExcuEEd1Q,1153
+botocore/data/groundstation/2019-05-23/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/groundstation/2019-05-23/paginators-1.json,sha256=4_ogVwU_XXx--s-8FB9fXMd5kIjdEXBdN6iBd04Kmlk,1236
+botocore/data/groundstation/2019-05-23/service-2.json.gz,sha256=Zbd-tlUp1-eNxAVfJRRITmwDJFIzedYvMYeSeQPnc1E,13952
+botocore/data/groundstation/2019-05-23/waiters-2.json,sha256=fuayBSt0gQV3HjjFxrqZgUCLSo6DxBG5qb-ASxS3oKE,534
+botocore/data/guardduty/2017-11-28/endpoint-rule-set-1.json.gz,sha256=kx75iWo78yBRaGYslsEjhbUZ7n5fRGJeSMtuQYMrUF4,1237
+botocore/data/guardduty/2017-11-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/guardduty/2017-11-28/paginators-1.json,sha256=cwDvPmlbwnBGCv5y3JIbAjS7BjRfPSAYO_ImStsJM-A,1699
+botocore/data/guardduty/2017-11-28/service-2.json.gz,sha256=YAzb6vry4oau0dvN87VMMXgjwTfn-w8apNh01kbxFfc,45763
+botocore/data/health/2016-08-04/endpoint-rule-set-1.json.gz,sha256=rRQAVsT7KvNc6olvN-YQRU4qfsl9c0OJrlElgNcvlho,1297
+botocore/data/health/2016-08-04/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/health/2016-08-04/paginators-1.json,sha256=yiHNcdPNOcqngUnAvp1BUD8e9oWSgqGS-T0Esl6r8vI,1397
+botocore/data/health/2016-08-04/service-2.json.gz,sha256=a6rp07eWYmmuLpnSx-2nSAoclkEip1CNzbvd_tq3AC4,10158
+botocore/data/healthlake/2017-07-01/endpoint-rule-set-1.json.gz,sha256=Cz-ntKwue6U-KL5qW2IhdOJGM8XEWd-Jg4k86usgoZ0,1153
+botocore/data/healthlake/2017-07-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/healthlake/2017-07-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/healthlake/2017-07-01/service-2.json.gz,sha256=k03IwbDPWMkHbFclzcfT1TA4PcH1ZzZxYRTAKmWoZCU,6428
+botocore/data/iam/2010-05-08/endpoint-rule-set-1.json.gz,sha256=or2A0IqsTew4pwjDot_TwysGdhHVL_X7ml3xLS7O3lw,1720
+botocore/data/iam/2010-05-08/examples-1.json,sha256=T5EqrFFZBiVlL9dsN-T5DnigU1UnMSXfVVwBK00AWrU,48537
+botocore/data/iam/2010-05-08/paginators-1.json,sha256=Mrjh9WIhO3YlPK04LELNlBGOWlr4EOWDPV22S4XlPM4,7036
+botocore/data/iam/2010-05-08/service-2.json.gz,sha256=_f8FWXuFxLZ3wfCWXmfqSivJVC3cOP9nX22vql8WH3g,69751
+botocore/data/iam/2010-05-08/waiters-2.json,sha256=sC6nS5oxMDEinb4z8GAMfZvFfPVWBzL_j1chnAT_z4k,1462
+botocore/data/identitystore/2020-06-15/endpoint-rule-set-1.json.gz,sha256=kfQVpKf7dmg0jVrcC-yQh8M2tvW6G-nRHJItO8Meq7Y,1242
+botocore/data/identitystore/2020-06-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/identitystore/2020-06-15/paginators-1.json,sha256=lpGJQxUC8FqJ_JuWaDSHw3cMW677pwZDQpoWRcBvA0M,704
+botocore/data/identitystore/2020-06-15/service-2.json.gz,sha256=TaOPtlthzriAvvQJ599Wbx33YWOikypR7JlVihkb8-w,7351
+botocore/data/imagebuilder/2019-12-02/endpoint-rule-set-1.json.gz,sha256=_nR0SbINwi8YlG009SJXKIyK43RnlzPRgWz81lIVvwo,1239
+botocore/data/imagebuilder/2019-12-02/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/imagebuilder/2019-12-02/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/imagebuilder/2019-12-02/service-2.json.gz,sha256=oH8tw-nm__kc5Owdx5xL3AegZWirjzlIl6DDoUGngzs,39365
+botocore/data/importexport/2010-06-01/endpoint-rule-set-1.json.gz,sha256=sRaKQSHmxXW_Cy7hCxGPNftGbsIct8tcRYz9aXoFFs0,1599
+botocore/data/importexport/2010-06-01/paginators-1.json,sha256=Etmobek-KI_4Gx8vLRBQsy6nYiRvog88hJCCXuRESZQ,215
+botocore/data/importexport/2010-06-01/service-2.json.gz,sha256=zdu0328CIt_bPe8dxxbgXwP-TXx9Jf5cNaznbrhCudc,4733
+botocore/data/inspector-scan/2023-08-08/endpoint-rule-set-1.json.gz,sha256=vh2zMw65YrdWxRyMM6u1DLj5oaZr_tNb0Qr8RTR6oFE,1305
+botocore/data/inspector-scan/2023-08-08/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/inspector-scan/2023-08-08/service-2.json.gz,sha256=XuVPDSjtTwV0mODux1LqM9z2B3nYKJLY-kQAg5zo_1w,1460
+botocore/data/inspector/2015-08-18/endpoint-rule-set-1.json.gz,sha256=Z33MWKXrUKUGVOwqkmY1G5aiFawiNlhfW8xbN0zQmIo,1147
+botocore/data/inspector/2015-08-18/service-2.json.gz,sha256=Kfa9Ct6iTXFilaDTPyaEyW6rBfBnwbGifvJzylQZC3M,8021
+botocore/data/inspector/2016-02-16/endpoint-rule-set-1.json.gz,sha256=0eNrLY3-sJsgZQBpHzQCSoq9panQdBLKIJ_AEHUkXw4,1148
+botocore/data/inspector/2016-02-16/examples-1.json,sha256=EoIoRt_vSBIFaQ8UnXLRGL2W5H50CW9rscWvZ012w-g,36903
+botocore/data/inspector/2016-02-16/paginators-1.json,sha256=weo6-A-gbXJmE6B8bFERy0jQdJHvIDANiZLITbP_9ZQ,1610
+botocore/data/inspector/2016-02-16/service-2.json.gz,sha256=uXEQuCxBorSmeFNg29hIsWV1h_e3U6coKqCZuKBLWP0,14137
+botocore/data/inspector2/2020-06-08/endpoint-rule-set-1.json.gz,sha256=JpxeY3ZDVUQ5G65h5z7qQftoI6Q7YvGYJhqkMU-PFsY,1153
+botocore/data/inspector2/2020-06-08/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/inspector2/2020-06-08/paginators-1.json,sha256=lNYohDMSFPrMf-Uv3BvJvSlbAhZEdISdYxGpZlJ5iCo,2601
+botocore/data/inspector2/2020-06-08/paginators-1.sdk-extras.json,sha256=WXkFBTPQczZBVGrBAb2IoUJRliU1uNg-m8znDFawOOA,287
+botocore/data/inspector2/2020-06-08/service-2.json.gz,sha256=Q_c01a8TcqNMN5GKzN-TuYUtnaemuWlyvArYZAa40Cw,36322
+botocore/data/internetmonitor/2021-06-03/endpoint-rule-set-1.json.gz,sha256=EU2BjA0Lob5zI0gfiNAAeQ5VuwqUoKIKpkdXegUGzww,1157
+botocore/data/internetmonitor/2021-06-03/paginators-1.json,sha256=5eozwrH81SIJWEZD5zsaAs9rr8CvRMMoXvYnOj1IxTs,531
+botocore/data/internetmonitor/2021-06-03/service-2.json.gz,sha256=Q7nzfiNgQCmz60wAphFocUegxYEr24VUVoDl1cXtv9U,13670
+botocore/data/internetmonitor/2021-06-03/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/iot-data/2015-05-28/endpoint-rule-set-1.json.gz,sha256=qEblAX7yDWT5z5xujfqWN9TPlEpTNf8t4YqmklI3JbQ,1487
+botocore/data/iot-data/2015-05-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iot-data/2015-05-28/paginators-1.json,sha256=FCM_y5QY56bw4TOgH3_OTBsnKj2PjI3ObCOOnKtsq80,201
+botocore/data/iot-data/2015-05-28/service-2.json.gz,sha256=CIuEDP5nmSfKWO5mczPFLEe-tw1PXPgOntlyJ4BJL_E,4271
+botocore/data/iot-jobs-data/2017-09-29/endpoint-rule-set-1.json.gz,sha256=gzfdIqsMuYF1M-L38wtNbodGV2w4tqMXoteeR9trpqM,1152
+botocore/data/iot-jobs-data/2017-09-29/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iot-jobs-data/2017-09-29/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/iot-jobs-data/2017-09-29/service-2.json.gz,sha256=vbOYEK3S7F42hwzbw7039qkB3pigiFOgtfyM_R-CKnI,3451
+botocore/data/iot/2015-05-28/endpoint-rule-set-1.json.gz,sha256=eHqOmVQr6OSNB0gBY0lLLEbqJRnPnr7YFs2DGpkHVwo,1268
+botocore/data/iot/2015-05-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iot/2015-05-28/paginators-1.json,sha256=qx5Q_h0GeGePfyVFlpaAHq-3_gedN2xmolXIa6rZhoQ,10330
+botocore/data/iot/2015-05-28/service-2.json.gz,sha256=iOtYWGEB5UrHUqHMOKMnwwkUp7OfXNoocij534kYsts,110273
+botocore/data/iot1click-devices/2018-05-14/endpoint-rule-set-1.json.gz,sha256=LqfF0r6zA5B437RPHVq20Q8KRorCScggKjKkdCcx5tw,1158
+botocore/data/iot1click-devices/2018-05-14/paginators-1.json,sha256=tZrEjZru_lPLHHvNWfoSGdewQrMSASM4QoteB9gmBuQ,349
+botocore/data/iot1click-devices/2018-05-14/service-2.json.gz,sha256=G3J9N00TqPEIwjZETPLznClA6PqypM2XPlE6e0vkSg8,3814
+botocore/data/iot1click-projects/2018-05-14/endpoint-rule-set-1.json.gz,sha256=aJEv3ljuF1KRxKuWNj-U65DCHfO4Pdap5pJrd1blqpw,1159
+botocore/data/iot1click-projects/2018-05-14/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iot1click-projects/2018-05-14/paginators-1.json,sha256=pPCk6aIAl86n6twV40lkG4ktlf2QMoD0hsOjjiUMwHc,353
+botocore/data/iot1click-projects/2018-05-14/service-2.json.gz,sha256=WZv1ZSIrRN4oVPxmeL3etdlkh8sPUEZh2RT_SZ9u1EU,4255
+botocore/data/iotanalytics/2017-11-27/endpoint-rule-set-1.json.gz,sha256=a_geNtLIng9OdEO34-I6K1K7xNDpNBHDPw6PIVSzcvo,1151
+botocore/data/iotanalytics/2017-11-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotanalytics/2017-11-27/paginators-1.json,sha256=X_pDdHN034STvHt8ULopV8fu0e5gyFt8Z1dj17AfZQY,895
+botocore/data/iotanalytics/2017-11-27/service-2.json.gz,sha256=NzAHzR8h2_GJoM4tysncXn1aiqRLrKpEqjPgehBW4jI,18162
+botocore/data/iotdeviceadvisor/2020-09-18/endpoint-rule-set-1.json.gz,sha256=zBgR23kqLNrOZZFaAwpNDjBEW7nDKngzvtxlBDsnIqU,1156
+botocore/data/iotdeviceadvisor/2020-09-18/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotdeviceadvisor/2020-09-18/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/iotdeviceadvisor/2020-09-18/service-2.json.gz,sha256=KL7ACtBEkEb9jex-dTnlioKWA5pvgakjTnag7yNs57c,5170
+botocore/data/iotevents-data/2018-10-23/endpoint-rule-set-1.json.gz,sha256=SJC-0gL9Z1s0qoiTdmFA7eBwN6OSLtgF-9mGem3nyqI,1151
+botocore/data/iotevents-data/2018-10-23/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotevents-data/2018-10-23/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/iotevents-data/2018-10-23/service-2.json.gz,sha256=gLMpiP3nepqAldEcRnlmUjcL53FoH899thJt4NwasxM,6392
+botocore/data/iotevents/2018-07-27/endpoint-rule-set-1.json.gz,sha256=e-IcR1v97bk5tO_Z6X8zLpO82FQgx4eOp37yMwMbHM8,1150
+botocore/data/iotevents/2018-07-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotevents/2018-07-27/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/iotevents/2018-07-27/service-2.json.gz,sha256=VhuT5qNzbh9DjPgBnigg4dz6DBEGPpfEsoXtDmeF40g,16090
+botocore/data/iotfleethub/2020-11-03/endpoint-rule-set-1.json.gz,sha256=U06Zu0UcD5tbfywoZSDbx2AgTaCUk087eBRPP7L46xU,1156
+botocore/data/iotfleethub/2020-11-03/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotfleethub/2020-11-03/paginators-1.json,sha256=E1zXLzaqEyOgzwC0sWIIHboXro29efEvDgF1aA3ZaB8,170
+botocore/data/iotfleethub/2020-11-03/service-2.json.gz,sha256=RGUmr0Eud2IjHJRR93uLFshzD9mbR1Q4R4mAoFlOJe4,2835
+botocore/data/iotfleetwise/2021-06-17/endpoint-rule-set-1.json.gz,sha256=HUKyxfgtL3xb-1u9YYC-DimoUQhSaLSlXisYPKzFfNw,1153
+botocore/data/iotfleetwise/2021-06-17/paginators-1.json,sha256=kxpQ4LWY9KdLE_GD46e2BB70WMq6A8kw0BPiy3G_irc,2261
+botocore/data/iotfleetwise/2021-06-17/service-2.json.gz,sha256=QiFnryrroI0qpRKOurArsfH463wIHkBIZQTCogIOFCw,23894
+botocore/data/iotfleetwise/2021-06-17/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/iotsecuretunneling/2018-10-05/endpoint-rule-set-1.json.gz,sha256=7f7aTuLkkjZtU6tGAkqkru5dSpO2QLz8mBqsEbayD84,1157
+botocore/data/iotsecuretunneling/2018-10-05/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotsecuretunneling/2018-10-05/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/iotsecuretunneling/2018-10-05/service-2.json.gz,sha256=JaN2Ch7xjd9KJAUku_FX4dqj9y_6D1v4bhsrXhVNZuM,3409
+botocore/data/iotsitewise/2019-12-02/endpoint-rule-set-1.json.gz,sha256=NUHQ1c5fW2IwNMGpFcOwgfA7O-j2SPlI1vWbP2BirwE,1152
+botocore/data/iotsitewise/2019-12-02/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotsitewise/2019-12-02/paginators-1.json,sha256=gjbjBAp-uwTGWdnjWLKkIdB50irwsCcdCa7cn60GBG0,3804
+botocore/data/iotsitewise/2019-12-02/paginators-1.sdk-extras.json,sha256=YRdxHylWCPUlQDFxU2BHajclulJZBfY-NpWldEBwzEU,159
+botocore/data/iotsitewise/2019-12-02/service-2.json.gz,sha256=u28J7ZetrAUY4OZqz6iLSiaLhkmZbQme3yPkW9pRN0o,45709
+botocore/data/iotsitewise/2019-12-02/waiters-2.json,sha256=qVN5Ie90YeUrNZqZKgckPkyTBYdKjgEbbrlsx-3RXUw,2237
+botocore/data/iotthingsgraph/2018-09-06/endpoint-rule-set-1.json.gz,sha256=OuslxvNlEkw221wL9UwcWvkUsBKR7XFWSbKVvcYfULg,1218
+botocore/data/iotthingsgraph/2018-09-06/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotthingsgraph/2018-09-06/paginators-1.json,sha256=3329WY0CXoFVg2osoDFw4kPWYxWK559asARwgffXvbw,1730
+botocore/data/iotthingsgraph/2018-09-06/service-2.json.gz,sha256=Gnbk5ZFZJ5HsRz1J6A-3elmRCIaLqtmRq0ordT4N6u8,10349
+botocore/data/iottwinmaker/2021-11-29/endpoint-rule-set-1.json.gz,sha256=yY9BminO9w506O9_kAhtDPqac7KBFto0e0vXtYH4wNg,1154
+botocore/data/iottwinmaker/2021-11-29/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iottwinmaker/2021-11-29/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/iottwinmaker/2021-11-29/service-2.json.gz,sha256=SIeY_U5YM7KXUqTORdroFOIM4QlwGASxJl2kFIZjrjI,16782
+botocore/data/iottwinmaker/2021-11-29/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/iotwireless/2020-11-22/endpoint-rule-set-1.json.gz,sha256=Z1t5wIxQY2Va7xGXw4nhDyUKAc5viRnrjc1GIjiZClM,1155
+botocore/data/iotwireless/2020-11-22/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/iotwireless/2020-11-22/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/iotwireless/2020-11-22/service-2.json.gz,sha256=RPZ0LhBvQkzALgcA1bqzr1yg_RM-N4-S_B2_kplyiog,35918
+botocore/data/ivs-realtime/2020-07-14/endpoint-rule-set-1.json.gz,sha256=-1-53pE6hEnkGteXebmvmqqwOXH1MIfv1VEwe07cGmM,1302
+botocore/data/ivs-realtime/2020-07-14/paginators-1.json,sha256=v-d7IzRxLzq3GmKHOxqORpsg4NHaBqJYyFrJn46xA34,191
+botocore/data/ivs-realtime/2020-07-14/service-2.json.gz,sha256=9y22j_6fj0Gv5BOjaARjfOe2v5U68LkfnwvCX7fbOGc,12513
+botocore/data/ivs-realtime/2020-07-14/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/ivs/2020-07-14/endpoint-rule-set-1.json.gz,sha256=nWw54ZGuuTmacp44ndwTVXE2S3BbuDzvEMb6vaydOAU,1147
+botocore/data/ivs/2020-07-14/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ivs/2020-07-14/paginators-1.json,sha256=QibJ2axvh2Gp9C80kOHE6Ac5RxI-El9k6jxWbVtHyqw,875
+botocore/data/ivs/2020-07-14/service-2.json.gz,sha256=Ze0fyktYatId07GBm4gtGRk6A2tPLgCxlN8fonqNhbA,14674
+botocore/data/ivschat/2020-07-14/endpoint-rule-set-1.json.gz,sha256=BZTCGv70N7Dw3qckrVyRitNB0fKbQ5nBUf2JTyE5Saw,1150
+botocore/data/ivschat/2020-07-14/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ivschat/2020-07-14/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/ivschat/2020-07-14/service-2.json.gz,sha256=QeS9-PmzUDq7zyl6lA5Ibxr9tBG1XJMmYzo8irOyqtU,8268
+botocore/data/ivschat/2020-07-14/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/kafka/2018-11-14/endpoint-rule-set-1.json.gz,sha256=CD4owp6MI3UiX3WcIIVUkV9ZcWAWymHjGCm-aYdH6tM,1234
+botocore/data/kafka/2018-11-14/paginators-1.json,sha256=0xDGScsW7MBEMgFda8Lbrq3aSp_3GKm7souf4etaC0M,2126
+botocore/data/kafka/2018-11-14/service-2.json.gz,sha256=56x7l8-RMfoDIPaLNFMdhIve4w9TX_Mj2e2DYwhBMJk,21629
+botocore/data/kafkaconnect/2021-09-14/endpoint-rule-set-1.json.gz,sha256=3CluJw0w1y_0nbqN408vrVFutuRTwQUPPjZ4aJeDfVU,1154
+botocore/data/kafkaconnect/2021-09-14/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kafkaconnect/2021-09-14/paginators-1.json,sha256=b81jbZwqWp7FLdXNKd7Hitfvr2h4gEGKDrX3vvki85o,549
+botocore/data/kafkaconnect/2021-09-14/service-2.json.gz,sha256=2nLQhWm_YTQZshhYUlS5SBilxCet00Q6--ebL7QkWeQ,7175
+botocore/data/kendra-ranking/2022-10-19/endpoint-rule-set-1.json.gz,sha256=63pvONYkqDWmRJV57KvrVk24uEDdDjEbKVzHVaLtRPI,1148
+botocore/data/kendra-ranking/2022-10-19/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/kendra-ranking/2022-10-19/service-2.json.gz,sha256=c-XYdne4MelitI89GQSUbWEzKKJf6Ng0Hjhl2lAPF7A,4362
+botocore/data/kendra/2019-02-03/endpoint-rule-set-1.json.gz,sha256=ksLEmo-dznblH8K4tF_Voe6APnxoFYryZ8_JioqalV4,1149
+botocore/data/kendra/2019-02-03/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kendra/2019-02-03/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/kendra/2019-02-03/service-2.json.gz,sha256=PrX8dXC5FNmuTiDIc7ABrwrWAr6EVFk4e0g3ABrkAEA,70109
+botocore/data/keyspaces/2022-02-10/endpoint-rule-set-1.json.gz,sha256=i3Kf7CynksebtXB0WGrlxBB7mfFucj7HM_6LzmqaXIE,1237
+botocore/data/keyspaces/2022-02-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/keyspaces/2022-02-10/paginators-1.json,sha256=T5FqYEgKvH1hv2kAVQ7ezCkLlhi0fngH-gv76NdlHGU,512
+botocore/data/keyspaces/2022-02-10/service-2.json.gz,sha256=tUu4AoF-1zaerPhTPKzh9GmuZt7rSs4ughLJKUeURTc,11242
+botocore/data/keyspaces/2022-02-10/waiters-2.json,sha256=tj1ZnaqhwmJkUEQlwH7wm1SqY3lg1BvZDfzfPaIgNrY,38
+botocore/data/kinesis-video-archived-media/2017-09-30/endpoint-rule-set-1.json.gz,sha256=pTnJW07NrFP5llbDI4pf4rHifuWHYQ2wgikwshGOosI,1153
+botocore/data/kinesis-video-archived-media/2017-09-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kinesis-video-archived-media/2017-09-30/paginators-1.json,sha256=2QyELet6SZ2S2nDPmoKrNlJ9kQyJyMlMTkrUh1FHeh0,346
+botocore/data/kinesis-video-archived-media/2017-09-30/service-2.json.gz,sha256=hqpf47PFyfOSml2HKtUch8GMaGE4FjiX_fr3hHlwlRE,13517
+botocore/data/kinesis-video-media/2017-09-30/endpoint-rule-set-1.json.gz,sha256=-kgOwX-ImoeCLFXgVm6zzKdJenxLf1OzKcxC7jss4G8,1150
+botocore/data/kinesis-video-media/2017-09-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kinesis-video-media/2017-09-30/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/kinesis-video-media/2017-09-30/service-2.json.gz,sha256=JN6Qg8JSmx5mPwfuNFl83qu3irIa351PHQQvlKAIYeg,3434
+botocore/data/kinesis-video-signaling/2019-12-04/endpoint-rule-set-1.json.gz,sha256=-kgOwX-ImoeCLFXgVm6zzKdJenxLf1OzKcxC7jss4G8,1150
+botocore/data/kinesis-video-signaling/2019-12-04/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kinesis-video-signaling/2019-12-04/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/kinesis-video-signaling/2019-12-04/service-2.json.gz,sha256=o240JupYAoDV9L5k-2p3CVBSgyxI5i1AZPArS5gve4g,2439
+botocore/data/kinesis-video-webrtc-storage/2018-05-10/endpoint-rule-set-1.json.gz,sha256=dqZQSgTwDiJk6g6s1-t4rcfJ8Bfd8o7gZE5kQihCLOY,1303
+botocore/data/kinesis-video-webrtc-storage/2018-05-10/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/kinesis-video-webrtc-storage/2018-05-10/service-2.json.gz,sha256=2KoQAIiI9MWOEH_Qemkp14L6Jhv5SKZKJhth6jkUUHc,2094
+botocore/data/kinesis/2013-12-02/endpoint-rule-set-1.json.gz,sha256=Blj5XQS8KmvN788lBnn4gCi0GfgkA3Dt1XWHG1h4Rpo,5445
+botocore/data/kinesis/2013-12-02/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kinesis/2013-12-02/paginators-1.json,sha256=qSFJYsvx9QiXPFHa-xy00L9bJWbtmRbGUfaVCF9VzNE,1257
+botocore/data/kinesis/2013-12-02/service-2.json.gz,sha256=lYFCBGMreYQz86dtihBmiib5Uq5dYCQevzD5Lcp9urM,23836
+botocore/data/kinesis/2013-12-02/waiters-2.json,sha256=O09l7u4uKnojQ0nCnGvABSm0pUXaLj8vvi2Y7sfH_9w,615
+botocore/data/kinesisanalytics/2015-08-14/endpoint-rule-set-1.json.gz,sha256=U4RSkCQSQWibti2ju5NB7fRsKjaLsGMgsRkQAvCm14A,1156
+botocore/data/kinesisanalytics/2015-08-14/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kinesisanalytics/2015-08-14/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/kinesisanalytics/2015-08-14/service-2.json.gz,sha256=bSAOz1ibWF5BhZSKz8bZT0VweiJUEOKdzvZp4jt2zuI,14039
+botocore/data/kinesisanalyticsv2/2018-05-23/endpoint-rule-set-1.json.gz,sha256=nwshP9fU_ZzBPRBBpCVsD4L5sdZTU9WIY_ibYdLeHpw,1159
+botocore/data/kinesisanalyticsv2/2018-05-23/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kinesisanalyticsv2/2018-05-23/paginators-1.json,sha256=4ttV2ZxNQIdY8Gfsw2atQYWigRj6V6b8bLI70CA4vKs,753
+botocore/data/kinesisanalyticsv2/2018-05-23/service-2.json.gz,sha256=syiHdLAeR0k2CzTo9oWH79S7LdPkINU7HKS9NiEg4E0,25182
+botocore/data/kinesisvideo/2017-09-30/endpoint-rule-set-1.json.gz,sha256=pTnJW07NrFP5llbDI4pf4rHifuWHYQ2wgikwshGOosI,1153
+botocore/data/kinesisvideo/2017-09-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/kinesisvideo/2017-09-30/paginators-1.json,sha256=u4Avq0nOOLDcxZR6MF_lKdBBqtPAxO96JsSaq9zIdqQ,758
+botocore/data/kinesisvideo/2017-09-30/service-2.json.gz,sha256=GPyfswXrk0Z9ZvWoOJhJpHUEYxkfKADNxBngfULE2uY,14715
+botocore/data/kms/2014-11-01/endpoint-rule-set-1.json.gz,sha256=l8QjYOg_tLQTyulw52dbVX5dUJcWKqXUVJbOVLyd3NY,1147
+botocore/data/kms/2014-11-01/examples-1.json,sha256=TgahTl1uBYiHIxv63mxyaCc-5c9xQKobWHPhDio4x3c,77655
+botocore/data/kms/2014-11-01/paginators-1.json,sha256=pUo1LF_2xGXf1-sM8xJiafSISwL8m8ygUKravuO-Iv0,1565
+botocore/data/kms/2014-11-01/service-2.json.gz,sha256=7aj5v1M560Y4kGb9dB5n8fseqhT9lm0udu1Xb3CUot4,69574
+botocore/data/lakeformation/2017-03-31/endpoint-rule-set-1.json.gz,sha256=aJtWfG8mjyguZ18x5BEFF3ARNFNUG4My2vUpGVJtuxg,1153
+botocore/data/lakeformation/2017-03-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lakeformation/2017-03-31/paginators-1.json,sha256=tuq1PJ1_gAHGMUrR39bxotCADjIe9KICvNd-_dnGssI,874
+botocore/data/lakeformation/2017-03-31/paginators-1.sdk-extras.json,sha256=C6kS_EfPl5yTSl_zGXUU30Tp5Z82gPH2KKPi-u1IbOo,159
+botocore/data/lakeformation/2017-03-31/service-2.json.gz,sha256=X7-A_UEN0Ef2pD3CoZAPGBWRypW0vqS1DdNiODTEiU4,22395
+botocore/data/lambda/2014-11-11/endpoint-rule-set-1.json.gz,sha256=VYTHIM6XIPEsfPUyfdOjRj1BXVwGWQaBK7Tziof8VKQ,1288
+botocore/data/lambda/2014-11-11/service-2.json.gz,sha256=qm18iRAK3rbnKMbSH5w1m_fYnvXjf7jkJNVCIUeWm7A,5528
+botocore/data/lambda/2015-03-31/endpoint-rule-set-1.json.gz,sha256=cUy8CGn1hQRJ8o-6z1ulYU7YpjssLdpvwlGwsbmbP50,1149
+botocore/data/lambda/2015-03-31/examples-1.json,sha256=_TOXptTVZUFkSxrkaq_JpIKLxUYjRcK_TpC_0itGHLg,52811
+botocore/data/lambda/2015-03-31/paginators-1.json,sha256=q90Wka2nn9mxNQrh--dvPTyb5J5qednqJXyTZIB3itk,1943
+botocore/data/lambda/2015-03-31/service-2.json.gz,sha256=V-RQ-T0XLRazqQLWMP1EuPziad1JTQroF4jXNivAhOc,44937
+botocore/data/lambda/2015-03-31/waiters-2.json,sha256=xhjngYpK1QSq2PLy7ofZoa94iSQItpBk9gOYC5FwFY4,4267
+botocore/data/launch-wizard/2018-05-10/endpoint-rule-set-1.json.gz,sha256=6RuCEX_SyMttuCxDi3T5je7e_qrLKRb1P7AbWJoD0zs,1304
+botocore/data/launch-wizard/2018-05-10/paginators-1.json,sha256=_qhTYa40h1ckIfS0xEC6DCUnO-0OPlclJSK9zAxC8D4,733
+botocore/data/launch-wizard/2018-05-10/service-2.json.gz,sha256=5TJabt7qswFhsBuYqkIz5TFYGYbolVuCJjPQMFAJZcw,4452
+botocore/data/lex-models/2017-04-19/endpoint-rule-set-1.json.gz,sha256=O40pSzqxgYgvCNW4HEyplNeKjZytet_3ohw-e3uCSM8,1337
+botocore/data/lex-models/2017-04-19/examples-1.json,sha256=bOPm5nP9H4YSzKIpuI2sCPe4agTMgdenNLtxDAWIat4,23898
+botocore/data/lex-models/2017-04-19/paginators-1.json,sha256=NmghgFUthvQgC3SqXuZBn-6vnUJ5ey3MZYBpRF7YMqI,1686
+botocore/data/lex-models/2017-04-19/service-2.json.gz,sha256=iVkUbGHLhYV92AHbpSphPzPSiGV4dr-1Rb2QBpwuhX8,29495
+botocore/data/lex-runtime/2016-11-28/endpoint-rule-set-1.json.gz,sha256=TEPhrF2_EADDgeBoAjTurlubLIgW_cCY-mpofcmybHk,1268
+botocore/data/lex-runtime/2016-11-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lex-runtime/2016-11-28/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/lex-runtime/2016-11-28/service-2.json.gz,sha256=XsZwZFGr9_ilnR4F9ZEcwcCNIs41oAK06xCWHP3JO-g,11790
+botocore/data/lexv2-models/2020-08-07/endpoint-rule-set-1.json.gz,sha256=lllLg9sBAJq5tai1gJAQLqL4gWtIjcXIglAYUp-Q-Zo,1155
+botocore/data/lexv2-models/2020-08-07/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lexv2-models/2020-08-07/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/lexv2-models/2020-08-07/service-2.json.gz,sha256=82UZ9ZmtmCoS-JZyN2WfGkXYDMya2HY5hgjVobaZasU,75417
+botocore/data/lexv2-models/2020-08-07/waiters-2.json,sha256=Kj-OzJdHpbEuK2Og-0ok3E17irFQKjDwk2KfOj_xKcQ,7231
+botocore/data/lexv2-runtime/2020-08-07/endpoint-rule-set-1.json.gz,sha256=7mVToPWFj2ukw1Bi4zKxSE_Iz7DH5h6yH8RvNpPCKX4,1156
+botocore/data/lexv2-runtime/2020-08-07/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lexv2-runtime/2020-08-07/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/lexv2-runtime/2020-08-07/service-2.json.gz,sha256=3yWIBN9p-y2LzBfDEE0u6i80ZyLMMFHoYU6CKd5Rj2I,12882
+botocore/data/license-manager-linux-subscriptions/2018-05-10/endpoint-rule-set-1.json.gz,sha256=bval2EwGTIxvXZjG-Q12EteLzRI2dlLJTow8yjC2VPk,1320
+botocore/data/license-manager-linux-subscriptions/2018-05-10/paginators-1.json,sha256=9hH87MXwn0OiJQlRwCyyof-Pe9Esid1WmRA32IvfLKU,591
+botocore/data/license-manager-linux-subscriptions/2018-05-10/service-2.json.gz,sha256=SDr2Gh6A-yDfXjNBev7YKzsHrghIM9mgoJIS39y_014,4790
+botocore/data/license-manager-user-subscriptions/2018-05-10/endpoint-rule-set-1.json.gz,sha256=3PnI0BkIuy1V0TFgv_UlZkC2SMwswcol3ofhK7pADTM,1167
+botocore/data/license-manager-user-subscriptions/2018-05-10/paginators-1.json,sha256=lHVPYYVbHro2t3PMgbBI_ikPC6qDTFQlhYISCC7xb3Y,754
+botocore/data/license-manager-user-subscriptions/2018-05-10/service-2.json.gz,sha256=DMKoJQTTrBGSXb45PJ7l3eR8LFoxoLVD7yPVg-CGVOQ,3612
+botocore/data/license-manager/2018-08-01/endpoint-rule-set-1.json.gz,sha256=bo9C5qOBmCQqJrYQzDmmidfMgNuNHZ-1YPQAiKUXgIE,1156
+botocore/data/license-manager/2018-08-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/license-manager/2018-08-01/paginators-1.json,sha256=u83kulrKizQ1RsV1wfSx_UjFbm72dCbztJd3m2qKZwc,1012
+botocore/data/license-manager/2018-08-01/service-2.json.gz,sha256=66ad3Ou8qD0WkNxQcO6hRze65YS4hywUnW1s0Y6K0tM,16546
+botocore/data/lightsail/2016-11-28/endpoint-rule-set-1.json.gz,sha256=a9IGmkLAUH8uDc6SfnmGcic3iFNpF2h481IVUTd_NCw,1152
+botocore/data/lightsail/2016-11-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lightsail/2016-11-28/paginators-1.json,sha256=9EaLlqeMLm1cO4A5z-uPznc4OgcKMLV3tbvMLdSjZF4,2925
+botocore/data/lightsail/2016-11-28/service-2.json.gz,sha256=EsJFiDGahXtr_M-cJSeYskYhkkSE45QCVwGbjLIFLHc,86413
+botocore/data/location/2020-11-19/endpoint-rule-set-1.json.gz,sha256=sOCkOO5AwEbnMd7OCjdGyyCRFfAX6_3nP59xIz0h-Ws,1146
+botocore/data/location/2020-11-19/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/location/2020-11-19/paginators-1.json,sha256=voiPL9-aOzRI3yqf4kvw2pNzmkLpxD5QoFZZaOYaQoQ,1871
+botocore/data/location/2020-11-19/paginators-1.sdk-extras.json,sha256=RkjEzGF7VMsfK3VpqDSV5a3Ol5XSjUAn_udGxYz4uyA,197
+botocore/data/location/2020-11-19/service-2.json.gz,sha256=EOjmEE6tZmgYdQUxiK1ZZSgXQAM4iA6k15rX4NrUFsw,40080
+botocore/data/logs/2014-03-28/endpoint-rule-set-1.json.gz,sha256=pzNFRoQpDxpZb1u75Fm6Yf4NNehzdEP9HeTPXTGacRE,1232
+botocore/data/logs/2014-03-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/logs/2014-03-28/paginators-1.json,sha256=x3W8wr6UWfFxluXvx0DZaC0KLS6oJoyNU7uTQYAvsOY,2450
+botocore/data/logs/2014-03-28/service-2.json.gz,sha256=4dzq9J9DyyaaXxkb1h4LWtBelEoRQ_zu6cqIdi0kfoM,41511
+botocore/data/lookoutequipment/2020-12-15/endpoint-rule-set-1.json.gz,sha256=0djDW5WFp2W5jd4bHwPea5r2XiGbSizskBzkR1z3KRA,1158
+botocore/data/lookoutequipment/2020-12-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lookoutequipment/2020-12-15/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/lookoutequipment/2020-12-15/service-2.json.gz,sha256=raO4qxJ0pU45UfAjMloehDzzKkd83LqGRuZCiuTMf6Y,23211
+botocore/data/lookoutmetrics/2017-07-25/endpoint-rule-set-1.json.gz,sha256=uGlAKpONNlfPzAoiyZvuHTjrzXlAAgo58e1l1hYKvG8,1152
+botocore/data/lookoutmetrics/2017-07-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lookoutmetrics/2017-07-25/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/lookoutmetrics/2017-07-25/service-2.json.gz,sha256=ZvPsXK_RiP8dkZnBCjCMtSCsWEZUa54OiQQQP_VOks8,12970
+botocore/data/lookoutvision/2020-11-20/endpoint-rule-set-1.json.gz,sha256=Coha-9t0BW1NHjEteSHAHr77TQdykKnB57Yaz24iTUY,1152
+botocore/data/lookoutvision/2020-11-20/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/lookoutvision/2020-11-20/paginators-1.json,sha256=YN-rEb9H11mcKTYPX3d73TBWHz4UTiibNk_5fVrKUQs,701
+botocore/data/lookoutvision/2020-11-20/service-2.json.gz,sha256=qTXp94_QtpOwgdBvI78U8i4g_ZyH5Ko14EfLfyilRqs,13268
+botocore/data/m2/2021-04-28/endpoint-rule-set-1.json.gz,sha256=gupEfnNIS4FGlHcV8MHyxOanYWoPCb04XxOEPfgCyh0,1147
+botocore/data/m2/2021-04-28/paginators-1.json,sha256=1Ozjz1tfgEcpWywPezS7twNglIV14eZv7AuudCzv-SI,1603
+botocore/data/m2/2021-04-28/service-2.json.gz,sha256=cBnEHKFrUp15syb2VZvDVZbaLtq4s6Aj-TQhLjpBVOg,15425
+botocore/data/machinelearning/2014-12-12/endpoint-rule-set-1.json.gz,sha256=BCrPbuV2r-qiDdl8MOg_uqODLNzruDsWpDWMzOd1mMg,1156
+botocore/data/machinelearning/2014-12-12/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/machinelearning/2014-12-12/paginators-1.json,sha256=80ddAOlwUPt-mXpDtk3eJqnm7lB95-DjTW6-G6eqmJc,679
+botocore/data/machinelearning/2014-12-12/service-2.json.gz,sha256=TrbO9SyYbTHgEszx27_WGt5IuLxaL8VD6gBI6woADXQ,21283
+botocore/data/machinelearning/2014-12-12/waiters-2.json,sha256=_tyML4Sw4VQBk8fUWh1bUQjlcooL1hgRpvkqxKxEeCY,1902
+botocore/data/macie2/2020-01-01/endpoint-rule-set-1.json.gz,sha256=j84SjuzYtdzxMgCXgNn-lbBDoD0Z9KlZO6BxGNDbwlM,1149
+botocore/data/macie2/2020-01-01/paginators-1.json,sha256=QNpyggmzK1vrlEP4LHAy0qKzLTJNNoV9x3y8nqJkj3o,2959
+botocore/data/macie2/2020-01-01/service-2.json.gz,sha256=b3tB5xbRG48nioUJZWiO3jfnQ88VHiA8E8ILst_FT3k,59011
+botocore/data/macie2/2020-01-01/waiters-2.json,sha256=YjTydOnsawe754SLZZxzxMgFaq0M88fq5jOu-UQvAWE,553
+botocore/data/mailmanager/2023-10-17/endpoint-rule-set-1.json.gz,sha256=XHhccrzC-202GtIhe1rIpJV9dk4bAKF5Z5bK_v2bRIM,1303
+botocore/data/mailmanager/2023-10-17/paginators-1.json,sha256=ziHOOfwzIu3CPqf5toY8N3uNWfrZafM7M45P1SHza9s,1529
+botocore/data/mailmanager/2023-10-17/service-2.json.gz,sha256=-urbQ5NDCEohkqfA8V0CzxysNMzRj4oQ39458AXLxJs,17048
+botocore/data/managedblockchain-query/2023-05-04/endpoint-rule-set-1.json.gz,sha256=St0V58Xs-xQQ2xYgF6k-FXDmBZbyEJTHALqIsY8V3_w,1313
+botocore/data/managedblockchain-query/2023-05-04/paginators-1.json,sha256=aLhFDqzj7KQVTC4MVIRoQAo6tDKInqtlsOkFmvln-7o,882
+botocore/data/managedblockchain-query/2023-05-04/service-2.json.gz,sha256=1FF2vYVKXqZ632kE1X_-hzGhyS7MfrrUDh9MYIhAuJg,7186
+botocore/data/managedblockchain-query/2023-05-04/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/managedblockchain/2018-09-24/endpoint-rule-set-1.json.gz,sha256=ngj6FaFrxw4z8Z9oUhQEvQ5uFJFEMR7AT7gT0NiRXcQ,1160
+botocore/data/managedblockchain/2018-09-24/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/managedblockchain/2018-09-24/paginators-1.json,sha256=zAjmRcrAx6dDwoJVM-7ceZ1U04fGfxMgQsREvvVcIeI,189
+botocore/data/managedblockchain/2018-09-24/service-2.json.gz,sha256=W_DFDTx-WNDvgJRiZGoDwkL-WyjCvQVAsjH7oxzlwck,13864
+botocore/data/marketplace-agreement/2020-03-01/endpoint-rule-set-1.json.gz,sha256=I9byGkiYEaIdrgUNxUYSruvT_d7cYxI8HoIa0JtGdDU,1311
+botocore/data/marketplace-agreement/2020-03-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/marketplace-agreement/2020-03-01/service-2.json.gz,sha256=W0-FD8TOQRX-Kkyc7zLnz3xgOCttS3shkmYS7h9-k80,8181
+botocore/data/marketplace-catalog/2018-09-17/endpoint-rule-set-1.json.gz,sha256=r-EmdZ7890KusJy9xUyI8DpruRRVkXR6bHwFYlhWPXo,1159
+botocore/data/marketplace-catalog/2018-09-17/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/marketplace-catalog/2018-09-17/paginators-1.json,sha256=JbO7iSHFp-U7kJIRHTRxPClYMlBkenux5Ow534JGcyQ,372
+botocore/data/marketplace-catalog/2018-09-17/service-2.json.gz,sha256=O9LCklNfWn5MDeeaBh4Od7yO-xWzxmZYrz7A3TcnKx4,12227
+botocore/data/marketplace-deployment/2023-01-25/endpoint-rule-set-1.json.gz,sha256=k06mE6y791Afi9TLA8e28iEjvvlsAeqtsP3yLNL8MWA,1312
+botocore/data/marketplace-deployment/2023-01-25/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/marketplace-deployment/2023-01-25/service-2.json.gz,sha256=j0T3ILL8hVqqlJezCMuhwrK5GGrmq9EAXJLWayQOnGQ,2553
+botocore/data/marketplace-entitlement/2017-01-11/endpoint-rule-set-1.json.gz,sha256=gZDHKIr2S2GZW6Rgg7CM3OYEomw2OxDKHFdyKVLkXSU,1274
+botocore/data/marketplace-entitlement/2017-01-11/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/marketplace-entitlement/2017-01-11/paginators-1.json,sha256=xFY_-BU5Ho7OPWDGn_aX-WwguHOeDyE1N4F-7nlw2KA,194
+botocore/data/marketplace-entitlement/2017-01-11/service-2.json.gz,sha256=Fbf9-wYjE54duhSdlbCha-v6klAb_XlrLcR73wwLFfM,2104
+botocore/data/marketplacecommerceanalytics/2015-07-01/endpoint-rule-set-1.json.gz,sha256=QByRBMTUXwlcz6-rkd_Lwev1t_8d8umGuLebmLK_7H8,1167
+botocore/data/marketplacecommerceanalytics/2015-07-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/marketplacecommerceanalytics/2015-07-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/marketplacecommerceanalytics/2015-07-01/service-2.json.gz,sha256=2XxZT57WcIpU0Nnr23IJmJ7mCm7-bn2cSGcm47FQIWQ,3248
+botocore/data/mediaconnect/2018-11-14/endpoint-rule-set-1.json.gz,sha256=Kqn4EG06iX0hezyJ5Q5jjJ4KMk1boO2Gwt30DMIvJHM,1153
+botocore/data/mediaconnect/2018-11-14/paginators-1.json,sha256=FTRCyBm6AFLee9VE5l8oTSJUqDd8pucnSTB0RgXh0Gw,1178
+botocore/data/mediaconnect/2018-11-14/service-2.json.gz,sha256=-XgrBMMuJn7soXMsHnxLFI2IYCLug_kfJ-NG32YSgc0,25493
+botocore/data/mediaconnect/2018-11-14/waiters-2.json,sha256=bZzKt8OYBuvnYAP3OV9R2sBvTqOtVyOo5-MBYv6BWis,2679
+botocore/data/mediaconvert/2017-08-29/endpoint-rule-set-1.json.gz,sha256=CVIH3kK2R3r5OVvew2tEgLhYAm_YFLHFm0IsopVfzIA,1301
+botocore/data/mediaconvert/2017-08-29/paginators-1.json,sha256=0mUvv1xkKTZlgR8HmDHnhzTXvJ2H3Y-2A2843lCq5vc,991
+botocore/data/mediaconvert/2017-08-29/service-2.json.gz,sha256=aSrLfVZ6XufvUjEcthZHrNKaUl6U0xTA62K1SaXEvWo,144819
+botocore/data/medialive/2017-10-14/endpoint-rule-set-1.json.gz,sha256=f1Q1B1LUv4r9bwooZ86IDRDe6vJByJwJl4lHEt93vHs,1151
+botocore/data/medialive/2017-10-14/paginators-1.json,sha256=0jFRfboC_lGEOGcl64KRvkt3OC1y7TJghhz6MMYn2HY,2702
+botocore/data/medialive/2017-10-14/service-2.json.gz,sha256=5gdg7SC-gmukv6rRTyP2gazkczJs10VofmGpy7vtHv4,102592
+botocore/data/medialive/2017-10-14/waiters-2.json,sha256=bE3mAiAeXm-NP7P_RnatMQZ5Ls88kqRfrzk57M0dYec,10479
+botocore/data/mediapackage-vod/2018-11-07/endpoint-rule-set-1.json.gz,sha256=RzEMDVfxE6VP2ocewMAKTKuDyTMkNixDRmI4dGo5MbA,1156
+botocore/data/mediapackage-vod/2018-11-07/paginators-1.json,sha256=uyOY7MfVXvY7qil_RhqS9KThRg9A3_8LB6C8en49Z3k,551
+botocore/data/mediapackage-vod/2018-11-07/service-2.json.gz,sha256=I1FgBnH5JToh8U7iXTCFpvy7lSvDZovu2rb4y_wpZK0,7182
+botocore/data/mediapackage/2017-10-12/endpoint-rule-set-1.json.gz,sha256=VA-KE2L178owN9QxphorbxlDRjUuOaA9ASSitkQTMAw,1153
+botocore/data/mediapackage/2017-10-12/paginators-1.json,sha256=Hkze_cyn0q7t1o4PHpf079W6jE_g7l8tGQf7x-t3ocs,531
+botocore/data/mediapackage/2017-10-12/service-2.json.gz,sha256=LZmcFqvK0pg8L0JYNdznwVBRI60EueE69b1RfzT1yBk,9854
+botocore/data/mediapackagev2/2022-12-25/endpoint-rule-set-1.json.gz,sha256=auw-tnETFu2D-LimQrccEPhvrXwQdlek8G4j_mLVkv4,1306
+botocore/data/mediapackagev2/2022-12-25/paginators-1.json,sha256=jgysD72mvdA7sWLF6onkRCQ6KxKlVCpK7N0Pnq_Q77M,514
+botocore/data/mediapackagev2/2022-12-25/service-2.json.gz,sha256=wbtz2hpCou20BVzqsWeRSqacUDtFCAcM7Hnq5hJB8vE,15540
+botocore/data/mediapackagev2/2022-12-25/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/mediastore-data/2017-09-01/endpoint-rule-set-1.json.gz,sha256=TdOJ-Qe-t7q0uFf5CJ0kYn3UYaHwatO2yyMo329p_Hg,1152
+botocore/data/mediastore-data/2017-09-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/mediastore-data/2017-09-01/paginators-1.json,sha256=iGhEIo_9ydhnm5jAD4K6mIgNGZ51FKUA4AlfMlG0sao,181
+botocore/data/mediastore-data/2017-09-01/service-2.json.gz,sha256=qMaizgwpsvdy_2kCb9OyylKF7S5voq5PfYfGZHrOpjo,3757
+botocore/data/mediastore/2017-09-01/endpoint-rule-set-1.json.gz,sha256=Cma8kB4uUiab4PHn8cCEM-ClJZhlBVQY54B0aXK8Ih8,1148
+botocore/data/mediastore/2017-09-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/mediastore/2017-09-01/paginators-1.json,sha256=0XO8tEPJl9J7qprTHPQQt6dC7GrjIoqoCn4AcAbjiyM,191
+botocore/data/mediastore/2017-09-01/service-2.json.gz,sha256=k3vS9IQ4EHVP1FiuAu_psrjKzdx9GTfn5XFwKLFpYsM,7045
+botocore/data/mediatailor/2018-04-23/endpoint-rule-set-1.json.gz,sha256=WL2xYZGCDw5M1S-iPPaNHOTZ97pNAcxcoD_ybCnN2K4,1155
+botocore/data/mediatailor/2018-04-23/paginators-1.json,sha256=AxqBHJot9wpawiVdBaiwALEkmIwfz6mhJsXIo7qDvlw,1336
+botocore/data/mediatailor/2018-04-23/service-2.json.gz,sha256=k3ZKj3vvPoGfeJs_fKtL3wUFdT1JVC4fG2W5bLOQ5is,20706
+botocore/data/medical-imaging/2023-07-19/endpoint-rule-set-1.json.gz,sha256=2GPyi8XBY6pPqBmcjRv6qftO3_gpaD1Em15rSFspIwI,1306
+botocore/data/medical-imaging/2023-07-19/paginators-1.json,sha256=Zdv-t-Mpi7RENFkReFlaQ40h5arjqt4t0EDliR_8VOs,739
+botocore/data/medical-imaging/2023-07-19/paginators-1.sdk-extras.json,sha256=SH5DkCGoc9NqpV_7FKFpREPdZP8dt8uz6TDVCQZmeCI,159
+botocore/data/medical-imaging/2023-07-19/service-2.json.gz,sha256=J3wLWMD30k_8hJYB_yGOGpQR9dX065xhJ-HJTU448Y8,9518
+botocore/data/medical-imaging/2023-07-19/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/memorydb/2021-01-01/endpoint-rule-set-1.json.gz,sha256=1N6MDqgptwWzSWU2YXWasYkbHaquEDb5eeXPu5K6heM,1266
+botocore/data/memorydb/2021-01-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/memorydb/2021-01-01/paginators-1.json,sha256=gZ2X2pjloGYnUZ5dwXvDrKS10-LqHJj4VdsNHDPqwAA,2089
+botocore/data/memorydb/2021-01-01/service-2.json.gz,sha256=1Nrh8ORbC9MK6n6wCgGP6dJQJMN2rBPeWXc0unKBKOk,16163
+botocore/data/meteringmarketplace/2016-01-14/endpoint-rule-set-1.json.gz,sha256=ZAUoLX6GFBYnStYiUbB-PsKnAyr12Z-BT2wsZ7NZ8iQ,1243
+botocore/data/meteringmarketplace/2016-01-14/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/meteringmarketplace/2016-01-14/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/meteringmarketplace/2016-01-14/service-2.json.gz,sha256=-CXqEE_chOE3CtZmXTl_DG5o_M-j80H_x9wq98bPrqs,6179
+botocore/data/mgh/2017-05-31/endpoint-rule-set-1.json.gz,sha256=DRZ1iwJbMiMeFQsfsXX7VlPsVywpMz7otQ4lBdkkPdI,1144
+botocore/data/mgh/2017-05-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/mgh/2017-05-31/paginators-1.json,sha256=c6aCKzKnKp8Z0d-UNEY7VdDUCTIDdQhspIghXobWm5o,958
+botocore/data/mgh/2017-05-31/service-2.json.gz,sha256=60lhBgzibxz5ZJeXYbdqtF4dBMT3FioACCR69BmUrdY,7186
+botocore/data/mgn/2020-02-26/endpoint-rule-set-1.json.gz,sha256=7zGYMbDwmS6CaaIYSZAT-HcpAvQtZpYOJ29k_ayqLOw,1147
+botocore/data/mgn/2020-02-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/mgn/2020-02-26/paginators-1.json,sha256=zJ9gqjYlOC0wR5m9M1J-VB79ZFXJcrL78WvqPplRE8M,2682
+botocore/data/mgn/2020-02-26/service-2.json.gz,sha256=VHXJXaxoBkfgEqmpAFX4KIsIv18dyGLNEVXYHFCqEFc,19748
+botocore/data/migration-hub-refactor-spaces/2021-10-26/endpoint-rule-set-1.json.gz,sha256=A_dEljMon4xmQ7CxQ4ZFN-o783TCCFW5hwrAAaiKlb4,1153
+botocore/data/migration-hub-refactor-spaces/2021-10-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/migration-hub-refactor-spaces/2021-10-26/paginators-1.json,sha256=OZ6GIc0aC4ikn9W96M2UbxWeBdIy3QA7ksZ2Ec7t1e8,904
+botocore/data/migration-hub-refactor-spaces/2021-10-26/service-2.json.gz,sha256=QCwZOvhIisZkz6ntjv70s_-Hau1Ht9lXiojZBqEYfLA,12478
+botocore/data/migrationhub-config/2019-06-30/endpoint-rule-set-1.json.gz,sha256=SAOlKXyMO94IjnVvmcf92os4KMXWI1fduINPf3ZNzss,1158
+botocore/data/migrationhub-config/2019-06-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/migrationhub-config/2019-06-30/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/migrationhub-config/2019-06-30/service-2.json.gz,sha256=yjsK4DOQP5CMxnr3YVAxApPUdXPdEvB0s5TmgIsy9bQ,2714
+botocore/data/migrationhuborchestrator/2021-08-28/endpoint-rule-set-1.json.gz,sha256=dMIv6oWJu9SHCY47HdGitFij_Xsv_pAmerbkVPZ7XnI,1310
+botocore/data/migrationhuborchestrator/2021-08-28/paginators-1.json,sha256=K3BSaAaX302rt-fuD-8ewfuAaO1cXLwfwPxQmgs4gLw,1272
+botocore/data/migrationhuborchestrator/2021-08-28/service-2.json.gz,sha256=YIlGgPQyvpgUXTiVBZud4zm2TctYQnTdAlTfTi1Isy4,8432
+botocore/data/migrationhuborchestrator/2021-08-28/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/migrationhubstrategy/2020-02-19/endpoint-rule-set-1.json.gz,sha256=Xk1CIDXzr1HgudVkZPJnyJ0yyKK3EiEq8lEkPWRzKQ4,1158
+botocore/data/migrationhubstrategy/2020-02-19/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/migrationhubstrategy/2020-02-19/paginators-1.json,sha256=1kU7uoqpjQDozh9dBNVWf7QyZDxK2PBkajg_gfz7dxY,1076
+botocore/data/migrationhubstrategy/2020-02-19/paginators-1.sdk-extras.json,sha256=x686VmA6fsdUSIKSMZbp5ZF280pREQ7HpnPkgQTZ730,220
+botocore/data/migrationhubstrategy/2020-02-19/service-2.json.gz,sha256=Ti7qjhi1VIehoD30SNoG8qBwLh15byX7jj4AiAxQpJk,13363
+botocore/data/mq/2017-11-27/endpoint-rule-set-1.json.gz,sha256=5FCP9C6NnFRgU-p6PO1RAv3v5bfhEZkob46V4Iic5lg,1146
+botocore/data/mq/2017-11-27/paginators-1.json,sha256=JZRhf6w_8oFT1nPyeTQNU09bR1-xrJn09KOtiOPO2Rg,193
+botocore/data/mq/2017-11-27/service-2.json.gz,sha256=B_QzIOZAhEoKXsxw5uJH1LRfZSDOZ_s_OhHPqQaGeYg,14441
+botocore/data/mturk/2017-01-17/endpoint-rule-set-1.json.gz,sha256=5Rsih1PzlbXP-vEJpegXCpY-socAyUD6hEbR8deqOMo,1214
+botocore/data/mturk/2017-01-17/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/mturk/2017-01-17/paginators-1.json,sha256=NFfGwUHHAX0lwKOB92RJHnfVkFP5IvDCtM1FnTJ-A0g,1591
+botocore/data/mturk/2017-01-17/service-2.json.gz,sha256=_0bUL76izQVTfwWMTQr7IjE-EU787WZnShiPDJHheZg,19736
+botocore/data/mwaa/2020-07-01/endpoint-rule-set-1.json.gz,sha256=QDTuDgU3UgNSzOfXTxUEIq_LZ6CeKnQW3ztcI4bddYk,1150
+botocore/data/mwaa/2020-07-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/mwaa/2020-07-01/paginators-1.json,sha256=ggep_PmvO9S8tClL3v6oAmOMYV4qZcZt06URX5M9658,195
+botocore/data/mwaa/2020-07-01/service-2.json.gz,sha256=oqQ9Jak50ydseyJaxMRuP_aoJvfaxb5AXyuj0jh_hws,9691
+botocore/data/neptune-graph/2023-11-29/endpoint-rule-set-1.json.gz,sha256=TEnujnzMshkLZ3HY6jGmLtKyzZtEtoRowWY3hYTw4oY,1410
+botocore/data/neptune-graph/2023-11-29/paginators-1.json,sha256=HKEeF_fwQ4_3HH3nJR4jLaDxri9pobfvSXtJHe4hjks,707
+botocore/data/neptune-graph/2023-11-29/service-2.json.gz,sha256=kwBgsUQ-HXD505isx0TxCxfPbnn8xYs6iJ_3u65spgI,12674
+botocore/data/neptune-graph/2023-11-29/waiters-2.json,sha256=A89UBqmXLgIByXFHrjSkwREul09ls4B8Xrmj2Ny3Aow,4614
+botocore/data/neptune/2014-10-31/endpoint-rule-set-1.json.gz,sha256=Xe8mtmk20q8znQJ0Uj9r1yy5EveZaanYh6ZhR-jeEfg,1232
+botocore/data/neptune/2014-10-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/neptune/2014-10-31/paginators-1.json,sha256=66ojR4_WsS4k5APEI8fjU0mCJTn7B15KuG8mPLcqFk8,2881
+botocore/data/neptune/2014-10-31/service-2.json.gz,sha256=n5w8mdb8ENh-Lv9_y8yL7GZrlPewQ0sO6YPDi8e5GUg,43568
+botocore/data/neptune/2014-10-31/service-2.sdk-extras.json,sha256=U_PgxwtPhWl8ZwLlxYiXD4ZQ4iy605x4miYT38nMvnM,561
+botocore/data/neptune/2014-10-31/waiters-2.json,sha256=8bYoMOMz2Tb0aGdtlPhvlMel075q1n7BRnCpQ-Bcc1c,2398
+botocore/data/neptunedata/2023-08-01/endpoint-rule-set-1.json.gz,sha256=iBI9dKhJRHeVKkhrV-5y8MlocZYpFVhrOuOK3XerMbo,1300
+botocore/data/neptunedata/2023-08-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/neptunedata/2023-08-01/service-2.json.gz,sha256=sax3y273Ipe81jAFizYy9r8rp8PdQLOdWoA6XHGkBNI,23510
+botocore/data/network-firewall/2020-11-12/endpoint-rule-set-1.json.gz,sha256=S0O24GS8IJzB8RsS0zgeniNQtBjebGVPjHe0zVD8scU,1156
+botocore/data/network-firewall/2020-11-12/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/network-firewall/2020-11-12/paginators-1.json,sha256=rc4lbYxQbxyaVvByJxQ2KE9FIfgW_HkNLVnqNpKGoVU,898
+botocore/data/network-firewall/2020-11-12/service-2.json.gz,sha256=Zivvu_zDGeAZRGEAZGJVqNAHH2i9nWGnrV7E7Dax3Po,32532
+botocore/data/networkmanager/2019-07-05/endpoint-rule-set-1.json.gz,sha256=FtdJGbETYXEwYlr4cMNH0xg6IcxEyig_OUZTDf-7Oac,1371
+botocore/data/networkmanager/2019-07-05/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/networkmanager/2019-07-05/paginators-1.json,sha256=F-D4AkI3mG-HlMew8cQ0qNkUkV042pE_2Wl1rIvA-PY,3791
+botocore/data/networkmanager/2019-07-05/service-2.json.gz,sha256=Qt6wM7TdxBjnAtqlpRC_1zIh-rKBsCd6CYpAIf8jXF8,23289
+botocore/data/networkmonitor/2023-08-01/endpoint-rule-set-1.json.gz,sha256=yx0NQEP8rFpZE9J7Og-So32dknrK0wLFHwPLKtrKK8E,1305
+botocore/data/networkmonitor/2023-08-01/paginators-1.json,sha256=nHQ47DVYXQU7zjhe4CUO3-J0OdqR2OjaTaQ4c8vcMW0,187
+botocore/data/networkmonitor/2023-08-01/service-2.json.gz,sha256=unABG7ykHHOMD3Rcb8CCyPbdDSznDZ4VF9oL-1xlSxE,5147
+botocore/data/networkmonitor/2023-08-01/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/nimble/2020-08-01/endpoint-rule-set-1.json.gz,sha256=oav3SbOpx6inGgO2w_Lq8eyuD65XpeC-lx65BIrNZZc,1147
+botocore/data/nimble/2020-08-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/nimble/2020-08-01/paginators-1.json,sha256=fQoPzBzZycrd2bpBOHW87wjkP_q69fE8MunvHwQQEB8,1549
+botocore/data/nimble/2020-08-01/service-2.json.gz,sha256=Rk3RntMG-k4SnfzKf6xPi2VIZoRWvF8jN7_sMWVzWww,19823
+botocore/data/nimble/2020-08-01/waiters-2.json,sha256=HoBJRV8BoYfDxR_P369ryNfegpItljNv_8L2RprmSj0,7400
+botocore/data/oam/2022-06-10/endpoint-rule-set-1.json.gz,sha256=kNvOuypVbq8UpoefHUssDpk5kevEv_9egNlT-jTM-8A,1297
+botocore/data/oam/2022-06-10/paginators-1.json,sha256=O-yiC1jmUubOdoY_nq_BvS2UBfskjOM7cgJ547VWO3U,501
+botocore/data/oam/2022-06-10/service-2.json.gz,sha256=lMdkhoAfjj5Otek4TQB45KIbcqv8W8Y5__rlGo1F6Fc,6548
+botocore/data/omics/2022-11-28/endpoint-rule-set-1.json.gz,sha256=3FyCKtbkgfLlqv5smMoqUq3KsFXfB01Gs9PPdnoEOCc,1298
+botocore/data/omics/2022-11-28/paginators-1.json,sha256=XgnHw-zWg4kjhELV1tVUeNxVQQwLcCg1tEbVP_gjOtQ,3466
+botocore/data/omics/2022-11-28/service-2.json.gz,sha256=xZLFGoJ2Yoc-snB4h4I2xvOHLZiM5My30zLdUpHevQg,26229
+botocore/data/omics/2022-11-28/waiters-2.json,sha256=ojk083awKnugjRQUH5s0ltjk2CLaNzuTAt6p1wSjGro,14692
+botocore/data/opensearch/2021-01-01/endpoint-rule-set-1.json.gz,sha256=YKq9_0pMitUiMNTSiSzMdrY1PT35768Vw6PF-dHIHKw,1313
+botocore/data/opensearch/2021-01-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/opensearch/2021-01-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/opensearch/2021-01-01/service-2.json.gz,sha256=mHE8pl6bhvZC9AfALV6kiLzNL0AC3pG3I9L6mt-F-VI,39874
+botocore/data/opensearchserverless/2021-11-01/endpoint-rule-set-1.json.gz,sha256=Fyru7cdxw5X7aCkW3fX9XJ6_tz95-DUiQuN826HEFuc,1297
+botocore/data/opensearchserverless/2021-11-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/opensearchserverless/2021-11-01/service-2.json.gz,sha256=j_hxQ2mkEgai7cKHQA31pGfx6JkrmGTJ1WDfEV8W6NA,10369
+botocore/data/opsworks/2013-02-18/endpoint-rule-set-1.json.gz,sha256=qtpY6QQDqtIouTezkJjkTpieY53i4BrT2HWQIYpShwY,1151
+botocore/data/opsworks/2013-02-18/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/opsworks/2013-02-18/paginators-1.json,sha256=Z7xh6Z3rU23wP3DvH7dA_6rESCx1av8E7ABInwpMRY4,197
+botocore/data/opsworks/2013-02-18/service-2.json.gz,sha256=73CwF1wAm8IO95s0jx5yg1qXM7HwnP7EWywJPrmq1dc,37925
+botocore/data/opsworks/2013-02-18/waiters-2.json,sha256=2crmFuAdFm1n1gXfrbBHg_w-b0aaCPtBiXxHzp1N-LI,7578
+botocore/data/opsworkscm/2016-11-01/endpoint-rule-set-1.json.gz,sha256=atV3fqpZ05tYmBkxXsf-nltbr_6xrkKCErQwRFEEMD8,1151
+botocore/data/opsworkscm/2016-11-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/opsworkscm/2016-11-01/paginators-1.json,sha256=rozfOtYjgHVB4_nRCx9wz3_FEi0EYUK0v5gbhAN2oQQ,686
+botocore/data/opsworkscm/2016-11-01/service-2.json.gz,sha256=MjeF2u0s1oWOP5HCP6xte_gawh5dTFI0GawcigwuMCE,13908
+botocore/data/opsworkscm/2016-11-01/waiters-2.json,sha256=nTnFtemD7H4YJ99PqmULXNfZeNb18T1sxitQXDntJ2o,582
+botocore/data/organizations/2016-11-28/endpoint-rule-set-1.json.gz,sha256=evJgCfkw1xZhTV6agLpLoONtNMAyucxFjmVL2t5Nr8k,1553
+botocore/data/organizations/2016-11-28/examples-1.json,sha256=H-s8eMAzogFkvDj193d_NweczAUFsyrDfjFEE_77BFQ,50009
+botocore/data/organizations/2016-11-28/paginators-1.json,sha256=q7RjxA1l-62dDheys7Z3_Ayp04TpUyeTHIW5z2DWb1s,2789
+botocore/data/organizations/2016-11-28/service-2.json.gz,sha256=Rph6LbdckaQ42TdcQNnaX5EY-XLWJ-V4uocSh0_iq8c,35258
+botocore/data/osis/2022-01-01/endpoint-rule-set-1.json.gz,sha256=vLcPhMOxjV8_3wVOytkcqIEKHHPsPRGs3N74SRWnM5g,1297
+botocore/data/osis/2022-01-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/osis/2022-01-01/service-2.json.gz,sha256=UaaLtzjtt1-80lYEVALVP1L2F3F6mGPYWcViY4SU3cM,6216
+botocore/data/outposts/2019-12-03/endpoint-rule-set-1.json.gz,sha256=rRS_dVDd11_SMINjYxvorbTvimJzM-S2SfDj6TgatDs,1236
+botocore/data/outposts/2019-12-03/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/outposts/2019-12-03/paginators-1.json,sha256=urgKFuFk-hWZGyWoP0MZo43zBpupLfos7cJYPbY8A10,1366
+botocore/data/outposts/2019-12-03/paginators-1.sdk-extras.json,sha256=iIpnnvGVs9_NYgNSgHxbwXLP6VZ8Fh2kP2txOOkn3Tw,196
+botocore/data/outposts/2019-12-03/service-2.json.gz,sha256=7Y8hZ2t0bCg9bGcdqkNk2zGpgpZgMOU29wZ3ly_w34k,12137
+botocore/data/panorama/2019-07-24/endpoint-rule-set-1.json.gz,sha256=oUSlDJnF-0V_jrBIOAxqipnvnO03RrbVE0w9xPoiqzI,1148
+botocore/data/panorama/2019-07-24/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/panorama/2019-07-24/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/panorama/2019-07-24/service-2.json.gz,sha256=xPKvawgUNg9KrVKUqF38eFA6t-D06jZjFmBmbGL3e-w,11986
+botocore/data/partitions.json,sha256=f1qtc1XQ9nR3_6DFNP00RPhtitN6Hsh1VkEql-RI5tE,5843
+botocore/data/payment-cryptography-data/2022-02-03/endpoint-rule-set-1.json.gz,sha256=WAv98p6OxmP17TxwD5R7Bv5e2bJct5148KIa1YjBXOQ,1319
+botocore/data/payment-cryptography-data/2022-02-03/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/payment-cryptography-data/2022-02-03/service-2.json.gz,sha256=GoGFwNh3FVd2DKtDIH3_Rm-A_YkwMCVdJLTJbxbz710,12554
+botocore/data/payment-cryptography-data/2022-02-03/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/payment-cryptography/2021-09-14/endpoint-rule-set-1.json.gz,sha256=YYEcOmkxOQJBovrv-JJaholuKcDboO_veRlJjw2J4_M,1319
+botocore/data/payment-cryptography/2021-09-14/paginators-1.json,sha256=Q3nZHuUZ53pNZpShnEVxB2Z6ec8thvlIx-hPXFVBNM8,504
+botocore/data/payment-cryptography/2021-09-14/service-2.json.gz,sha256=ypse1aikYG9JHjSTwEqfjrKIFRAwozdDU61hmcghYkQ,15130
+botocore/data/payment-cryptography/2021-09-14/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/pca-connector-ad/2018-05-10/endpoint-rule-set-1.json.gz,sha256=liCq5ZE7fB9LILdoJFtKzYF_lyOtl_LBN8YW8XeeBq0,1296
+botocore/data/pca-connector-ad/2018-05-10/paginators-1.json,sha256=AS3R0cOqXrf6ALY1Ar4Z_HdXbvrA4SwPve_YSeqtIFc,932
+botocore/data/pca-connector-ad/2018-05-10/service-2.json.gz,sha256=DJQmAhjxVuk0WvfZ5po9m-w3NBMCl-mktyEmC1641kc,13160
+botocore/data/pca-connector-scep/2018-05-10/endpoint-rule-set-1.json.gz,sha256=VqYrmMM4AK_qcWpGD1P2jWIQDaWKFfI4lJNk1GnTpeI,1308
+botocore/data/pca-connector-scep/2018-05-10/paginators-1.json,sha256=-TAE2EG4hET8i1kSBmb5SkQbT8NEQ_peQNskuUSs0Ug,364
+botocore/data/pca-connector-scep/2018-05-10/service-2.json.gz,sha256=AaZk4jo90dCHeOhnT6krKTDWDyJBd1J1pHHM3XRYWzA,5833
+botocore/data/pca-connector-scep/2018-05-10/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/personalize-events/2018-03-22/endpoint-rule-set-1.json.gz,sha256=faPnQqkDxZBCxBDDPRMkaDHzuKd8Of9Tyj5A_R_XEy4,1159
+botocore/data/personalize-events/2018-03-22/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/personalize-events/2018-03-22/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/personalize-events/2018-03-22/service-2.json.gz,sha256=9eZXB8wYxScgfxOmByDrt7RQ-mWVfzG-MLWzqXKLLRQ,3870
+botocore/data/personalize-runtime/2018-05-22/endpoint-rule-set-1.json.gz,sha256=eLj4B73sQ9n_OBR_Z0TjqXtEKqvvvAoNdwo-VAACgww,1160
+botocore/data/personalize-runtime/2018-05-22/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/personalize-runtime/2018-05-22/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/personalize-runtime/2018-05-22/service-2.json.gz,sha256=1WY5N6uondPZGUYa4OqfrW6TiqelfDCqpXXkG18KeZg,3827
+botocore/data/personalize/2018-05-22/endpoint-rule-set-1.json.gz,sha256=I_C0Odb4fUOK29DcGERnFUJgdCxWHeaYIbg-GSjEYWI,1154
+botocore/data/personalize/2018-05-22/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/personalize/2018-05-22/paginators-1.json,sha256=PfTPE03jTLANh2F51b68_GALtAUqFWJp2R0o2Xl5u0A,2766
+botocore/data/personalize/2018-05-22/service-2.json.gz,sha256=v0dzHTCL-jQBM9j7EGuay8UZbn3WMr2O167U8jorA6U,29341
+botocore/data/pi/2018-02-27/endpoint-rule-set-1.json.gz,sha256=dnTXzDdr_toRdx71cORD5xjp7hWFbc3UeHn7awiWGtQ,1146
+botocore/data/pi/2018-02-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/pi/2018-02-27/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/pi/2018-02-27/service-2.json.gz,sha256=clbuM93z_MBxJVqIGpaBdJT6ywrzrkL0sxHK0l8BJLM,11588
+botocore/data/pinpoint-email/2018-07-26/endpoint-rule-set-1.json.gz,sha256=aJ3_uI4re88G7ggmUd0wkq_AvR8ZnKqknIZ2r_-xi84,1145
+botocore/data/pinpoint-email/2018-07-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/pinpoint-email/2018-07-26/paginators-1.json,sha256=G74a7tI3gD77zuNQfj6bfDHtriSA2qhAWh6Su9Tw6Bc,914
+botocore/data/pinpoint-email/2018-07-26/service-2.json.gz,sha256=k0asNuTT-r4ALVmXJtc12K8zUfj4SbW_eyWJPNeMBSA,23610
+botocore/data/pinpoint-sms-voice-v2/2022-03-31/endpoint-rule-set-1.json.gz,sha256=G8_FJob0mRrXsItcfygMzHiMNoMrSY_eIsNnbr3HYbo,1152
+botocore/data/pinpoint-sms-voice-v2/2022-03-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/pinpoint-sms-voice-v2/2022-03-31/paginators-1.json,sha256=57vZEjynGFiP5IUlHQvUklhlHYMvZo1WyfLVWGhAAPQ,3919
+botocore/data/pinpoint-sms-voice-v2/2022-03-31/paginators-1.sdk-extras.json,sha256=PemFH9N5xIhFFsL0NeaK_OIvlCHX1f7yYTr4LMpajd4,1242
+botocore/data/pinpoint-sms-voice-v2/2022-03-31/service-2.json.gz,sha256=x9ytzhsIrOtE2BN7ryMs6Tt2UK--kTblO8wrfKJUtq4,34661
+botocore/data/pinpoint-sms-voice-v2/2022-03-31/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/pinpoint-sms-voice/2018-09-05/endpoint-rule-set-1.json.gz,sha256=3YZ344VsgxU_0Wp05ptQJbrfpVoqmrW4lxieIeaz2L4,1112
+botocore/data/pinpoint-sms-voice/2018-09-05/service-2.json.gz,sha256=dKD73rI5QhZ8Tjcb4HmJ9xjV9WuU6_vN0KfKOlG0Cpk,2998
+botocore/data/pinpoint/2016-12-01/endpoint-rule-set-1.json.gz,sha256=98LzxO-4-kv9hyyyd_rbKY0M6PQWz4bcAQwR1jLcogQ,1315
+botocore/data/pinpoint/2016-12-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/pinpoint/2016-12-01/service-2.json.gz,sha256=-gT_yp-JAfmlR2PuR1I13pO2KHZlZBa3CoD15f7cNhU,70366
+botocore/data/pipes/2015-10-07/endpoint-rule-set-1.json.gz,sha256=iJ6o75apnhHA-xFUY_PhegSh72kOqlgAamIuX41qgS4,1296
+botocore/data/pipes/2015-10-07/paginators-1.json,sha256=a_b-W2Fj-9dt3XIXqHzXHKGRz8elOX8p9h2pI3wg5ls,176
+botocore/data/pipes/2015-10-07/service-2.json.gz,sha256=H0sKDrEo0IhbfQ03czvR-xLiLWgP33JPKDf-8A4Loo0,22600
+botocore/data/pipes/2015-10-07/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/polly/2016-06-10/endpoint-rule-set-1.json.gz,sha256=RRvza6C_UI-97Zn-ViqmrwZih_QfOncY-cGGKhyGCrQ,1149
+botocore/data/polly/2016-06-10/examples-1.json,sha256=-uFGLZQ6nTWN0cCt3DVe5TWPh8TbmBGwBTOpcW1Uq00,5102
+botocore/data/polly/2016-06-10/paginators-1.json,sha256=IJnO61fPCtuJPYshmxGjm9ZzkXfOxEvsL0acyUPG55E,463
+botocore/data/polly/2016-06-10/service-2.json.gz,sha256=DzOKJojvRc6BpAon-76vCZcMGjbR8z420sS1qPppa3c,8145
+botocore/data/pricing/2017-10-15/endpoint-rule-set-1.json.gz,sha256=6aIpRnyKHimqVsZnU7P00o7c070r4H1lEUtBNqRWy8A,1219
+botocore/data/pricing/2017-10-15/examples-1.json,sha256=LX0A-kHCd3N64FsP7EdT6IV-Sej2qNX9ygW6n6jBucs,4263
+botocore/data/pricing/2017-10-15/paginators-1.json,sha256=rizUQ-J932MNyVUTMjrRSVOm-tmzWnvnYhWoIMGxuuM,820
+botocore/data/pricing/2017-10-15/service-2.json.gz,sha256=Rgt_meSuhwn29I87Sw20qm1yoe3RK29_rd7B_uO9v44,4255
+botocore/data/pricing/2017-10-15/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/privatenetworks/2021-12-03/endpoint-rule-set-1.json.gz,sha256=C7gJ5fwCZF3gPERZvVZX2FcH9eKaZFqahFTe3t1_n24,1156
+botocore/data/privatenetworks/2021-12-03/paginators-1.json,sha256=2t0Vnl787IXWCNFDMXckoCvxqZR8HCwSdkrODId7u_U,878
+botocore/data/privatenetworks/2021-12-03/service-2.json.gz,sha256=YO-ZQDgz-BA-uZWk1rnfGvMm3udBonWrA8Gk-ENr1uk,8927
+botocore/data/proton/2020-07-20/endpoint-rule-set-1.json.gz,sha256=FDOeZdJ_ha6m5C1bmpD6lNvtn9E1PMnHHeI2mDoutMo,1146
+botocore/data/proton/2020-07-20/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/proton/2020-07-20/paginators-1.json,sha256=oioU0xuoNT12gWLZPvkd8rPQIM9gk8AOnNiZxDJybLs,3501
+botocore/data/proton/2020-07-20/service-2.json.gz,sha256=XaVERtz4BaUpm5MNobveTYzWY7SSoaWHRKVpKmHMM1k,28066
+botocore/data/proton/2020-07-20/waiters-2.json,sha256=sGpaiRnx46CfHQh_T__IIByVlrchRRjseWa3NCdIqdI,6872
+botocore/data/qapps/2023-11-27/endpoint-rule-set-1.json.gz,sha256=VLHA-Efp3ZI9vW3E1ulrypWTOwSZINo2mFhr5pvLwqk,1301
+botocore/data/qapps/2023-11-27/paginators-1.json,sha256=McjYxb7TrO3PLEXrYryPjrdMEbmbSYts2zAu2s0zbnY,340
+botocore/data/qapps/2023-11-27/service-2.json.gz,sha256=_XE9e_gyEiFeXxRajh3vfejv0EfwKOu7sJg65GFzdrg,9695
+botocore/data/qapps/2023-11-27/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/qbusiness/2023-11-27/endpoint-rule-set-1.json.gz,sha256=yMaBudZv7DT1wCi2feNd3SmYEnh80zhH9oyLXjlExNo,1128
+botocore/data/qbusiness/2023-11-27/paginators-1.json,sha256=Qr-2G_TbbrcPxQBgvp9QG4e29RLJgpe8EwY1QRTPFVs,2047
+botocore/data/qbusiness/2023-11-27/paginators-1.sdk-extras.json,sha256=FScSk94iMkNFBB-PDJNX3R0wgwjoQbejjy-WrxO7Ii0,318
+botocore/data/qbusiness/2023-11-27/service-2.json.gz,sha256=gY3Veg8Jq1tajsPIoY9z8N9JUuWczf5bPDfUsVDAWcs,32281
+botocore/data/qbusiness/2023-11-27/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/qconnect/2020-10-19/endpoint-rule-set-1.json.gz,sha256=zkCroadVoYwRtCjcozqPJaUCSbmgjcKVN8kqh0qTUSQ,1299
+botocore/data/qconnect/2020-10-19/paginators-1.json,sha256=N-vzFMreD5xilV7stPpITamDPKnMqTg2j6-dsEZGQuQ,1970
+botocore/data/qconnect/2020-10-19/service-2.json.gz,sha256=wMMarqq5muADK0c_2QYLNBh96LbtfxX8EyHPWxwC2A8,20742
+botocore/data/qldb-session/2019-07-11/endpoint-rule-set-1.json.gz,sha256=lG78XzF0arYwkikfC867cfXD-h9EGeAT1YeA0OcBvOY,1151
+botocore/data/qldb-session/2019-07-11/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/qldb-session/2019-07-11/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/qldb-session/2019-07-11/service-2.json.gz,sha256=oBL9_dPTExrOxZJ9rWOXP_bhEwUjWHtPWpfHercGU4I,3035
+botocore/data/qldb/2019-01-02/endpoint-rule-set-1.json.gz,sha256=hhX0hfz5eq8ioUhTlkuhKU8hW3FgDEP24FZ0QbW6oaY,1148
+botocore/data/qldb/2019-01-02/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/qldb/2019-01-02/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/qldb/2019-01-02/service-2.json.gz,sha256=32Vfpad1IPSUpgCJe2MtLHFGZSQ_RbWKAlzf6KvhZsU,11016
+botocore/data/quicksight/2018-04-01/endpoint-rule-set-1.json.gz,sha256=87Sx_a2ROo_X2XzsZuWSDBvVkiFaUfIdeLs3-7YbK6g,1153
+botocore/data/quicksight/2018-04-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/quicksight/2018-04-01/paginators-1.json,sha256=W360YRVmZloMdg1jyt2nusy8L3ZVJZu8n5IgnCtdMOY,5477
+botocore/data/quicksight/2018-04-01/paginators-1.sdk-extras.json,sha256=bcn5gSjB7MwLOFRq5359e4JjeiRrBkwBiDOiHVa1rsI,3927
+botocore/data/quicksight/2018-04-01/service-2.json.gz,sha256=yO3gdYdwL_1tfpSq2aN37wwv6qwimQ8J4YHimkw6Lh0,164025
+botocore/data/ram/2018-01-04/endpoint-rule-set-1.json.gz,sha256=myM5DboJNuLc1W9Pn3w3k5LfJpUX5zDo5huLJ9wmrSw,1234
+botocore/data/ram/2018-01-04/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ram/2018-01-04/paginators-1.json,sha256=68WO6NwCy0OQL3rko-MRoZ0l1F2vhih8z8F3sse3R3g,1085
+botocore/data/ram/2018-01-04/service-2.json.gz,sha256=x2Hx_-IY0yc8dDn5gmIIU5zO_5w8xcLnmHrT8OGhjjs,17997
+botocore/data/rbin/2021-06-15/endpoint-rule-set-1.json.gz,sha256=zS_MQgsMqC5ubN7uDkNPUjOcOaqHh5CQOkZXcWjTqnE,1147
+botocore/data/rbin/2021-06-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/rbin/2021-06-15/paginators-1.json,sha256=LB-X6UiLpJdFPrOCSc0raKGabdXiY9PhtS7nzQJbMts,181
+botocore/data/rbin/2021-06-15/service-2.json.gz,sha256=wNCj4YOCCJS5OMTPOSj002Xs2tT0Mghlkkwe6DJbs3c,4330
+botocore/data/rds-data/2018-08-01/endpoint-rule-set-1.json.gz,sha256=uRu7rNEdTeF1JfRllHpJHfuUdHRtFWmPSjmmn1r_RDE,1151
+botocore/data/rds-data/2018-08-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/rds-data/2018-08-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/rds-data/2018-08-01/service-2.json.gz,sha256=8DHr_uesNr2G5jD7E-1I4Tn21khFxZ8pjJSmZbgKUOY,6401
+botocore/data/rds/2014-09-01/endpoint-rule-set-1.json.gz,sha256=baK05kX8IymxYzS0D_6n7eWT9jpTRaWH3MGCbX93rMs,1234
+botocore/data/rds/2014-09-01/paginators-1.json,sha256=CKMhQjYqNQB1hiHNi4vCNIVtQvu29SM_ySRhqxTKfOQ,3095
+botocore/data/rds/2014-09-01/service-2.json.gz,sha256=nl54dJX14fX9rQPN6uaKmeziVx_47bCFnv2A4yoNGLw,37839
+botocore/data/rds/2014-09-01/waiters-2.json,sha256=9BpCCotIHKKeyJHD5Bo1fdRi6EnHK6jyJJx_9wswzCQ,2645
+botocore/data/rds/2014-10-31/endpoint-rule-set-1.json.gz,sha256=Xe8mtmk20q8znQJ0Uj9r1yy5EveZaanYh6ZhR-jeEfg,1232
+botocore/data/rds/2014-10-31/examples-1.json,sha256=Pa_Dpbo8pg0O9rZRPEuFXsgnzT6XUqIfwHpXauQnc0M,57903
+botocore/data/rds/2014-10-31/paginators-1.json,sha256=lGkdOcAY7evx-HcHdGw-Gc6f_jJpJRCWPnA5OFVe0n8,7216
+botocore/data/rds/2014-10-31/paginators-1.sdk-extras.json,sha256=S21buVoyp0LlykSD0lYWlVIRbOqJB4qpVw7mt2GFprQ,192
+botocore/data/rds/2014-10-31/service-2.json.gz,sha256=TEfMjXlqzPGnxqM_2FerLA_Txy6r0k5_pfTt1JAYwZ4,155036
+botocore/data/rds/2014-10-31/service-2.sdk-extras.json,sha256=NWqAyPauBSLTPFOO_wMu4XZ7VTkw7nY8QjCorphUpTM,1345
+botocore/data/rds/2014-10-31/waiters-2.json,sha256=DaJxFaWQOJpx0aNV4rEHX8yDWHBfTWHNkA3u6NgDAOk,10970
+botocore/data/redshift-data/2019-12-20/endpoint-rule-set-1.json.gz,sha256=k2gXz6yJE28xhUGxwNyxMtwO5JbYu4ljo5QnlsMxk3A,1151
+botocore/data/redshift-data/2019-12-20/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/redshift-data/2019-12-20/paginators-1.json,sha256=pTodaTjP4vyeMJ1vi2z6dXlhZ8bzMbG5tszVa5ecXNI,972
+botocore/data/redshift-data/2019-12-20/paginators-1.sdk-extras.json,sha256=UZq2Z0iNiqOovkDKsQDRL7mLznTmcTB6uWAHDosl3tk,296
+botocore/data/redshift-data/2019-12-20/service-2.json.gz,sha256=Y60lLRePUh1-oNONkSnDdKv4WhGGaFYi9Mq3azOMK1w,7105
+botocore/data/redshift-serverless/2021-04-21/endpoint-rule-set-1.json.gz,sha256=Uupi47NPDrZ1iJCAlpIshz1WQnA3AX253VIV8M3lDAE,1158
+botocore/data/redshift-serverless/2021-04-21/paginators-1.json,sha256=riPpaR2zgPb8A-kYrawb_5IowSJVsHP0_An24IwnK3U,1774
+botocore/data/redshift-serverless/2021-04-21/service-2.json.gz,sha256=x_LQxuJi9s--kydNkilEYyF0VoRXq4zpugU7rU_6lSM,17462
+botocore/data/redshift/2012-12-01/endpoint-rule-set-1.json.gz,sha256=S9J0ExwDRiq2c7qhDcejlu01SQrO_Gkud513YXKZsFE,1236
+botocore/data/redshift/2012-12-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/redshift/2012-12-01/paginators-1.json,sha256=JgBec9dWZlYhfmcCTdzFt4ZfoKi01UrIwECO6LICkOc,6533
+botocore/data/redshift/2012-12-01/service-2.json.gz,sha256=Syz-daO_VBcfSCzTruTQqHGeRyRcMiiscSvkphtq4i8,71581
+botocore/data/redshift/2012-12-01/waiters-2.json,sha256=mvax_COD6X10xa7Toxa2DsrarFdKFg9kOWbIKRLahS4,2344
+botocore/data/rekognition/2016-06-27/endpoint-rule-set-1.json.gz,sha256=mqSq2jSXeM3UAmQIFwrkYScSeIcSG8ss0Ss8QDBPnf8,1152
+botocore/data/rekognition/2016-06-27/examples-1.json,sha256=pEUj6cF9yKB10eaE3lAAObBMc4nV3Jak105Ro2A3ZMc,20327
+botocore/data/rekognition/2016-06-27/paginators-1.json,sha256=mDoU6wXUCCgHeOrcvnEqTpQ18yV5otpEqZt5TsFarFA,1699
+botocore/data/rekognition/2016-06-27/service-2.json.gz,sha256=dCJqN4DoYSib9RZyg-iKCQ3cl0eB8bvvS43hPIB64gs,70528
+botocore/data/rekognition/2016-06-27/waiters-2.json,sha256=KRKVzu37WzZwVdazhDURGYo_qTbgIDDIhBTPyvTt1lg,1542
+botocore/data/repostspace/2022-05-13/endpoint-rule-set-1.json.gz,sha256=05cXmmAB66BLgAhj2TfPZISnoGV0IsNRVjkGXoRnhEM,1302
+botocore/data/repostspace/2022-05-13/paginators-1.json,sha256=l2K-MFT1bld9enr5-e6GITswfFbKf-R8DEfZaI6KPc8,183
+botocore/data/repostspace/2022-05-13/service-2.json.gz,sha256=Rmr92pNHezPh5d_c4aRcSpFfHJphI-tAmRR23armfQU,4335
+botocore/data/resiliencehub/2020-04-30/endpoint-rule-set-1.json.gz,sha256=bIKjqszaxPoREIZwA2-EfO_R9p4t-3CT3773NjknlUA,1154
+botocore/data/resiliencehub/2020-04-30/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/resiliencehub/2020-04-30/paginators-1.json,sha256=_X2qEKeQsOkiZ9K8DBQGlXrwfnTzaJLXmBTUnwc-sKs,412
+botocore/data/resiliencehub/2020-04-30/service-2.json.gz,sha256=jGOPi3GMMSzo-YFOeMF3nK9q9r5VsvoaM0qQHx2G_Eo,28152
+botocore/data/resource-explorer-2/2022-07-28/endpoint-rule-set-1.json.gz,sha256=-swLbYnZoTjPKFg70IbQi51DOgoNIAgWHgLDcqcfbTM,1159
+botocore/data/resource-explorer-2/2022-07-28/paginators-1.json,sha256=bWLY1D7sMpwKr2es_i6LGbiISopcVrqak2MOv5gbeHc,849
+botocore/data/resource-explorer-2/2022-07-28/paginators-1.sdk-extras.json,sha256=J3Mpshl1o5fjl1TsHoZoS9TRUOIsww_irE_qtAMNUf4,172
+botocore/data/resource-explorer-2/2022-07-28/service-2.json.gz,sha256=416n58jdWbZE59dFipfpCIDY2WxqwNCo6ooI1J1f5Ok,11928
+botocore/data/resource-groups/2017-11-27/endpoint-rule-set-1.json.gz,sha256=7B0G2xiTJK61EeGNVjMuuPxmLApP4ykmLPhmMcXSkwE,1241
+botocore/data/resource-groups/2017-11-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/resource-groups/2017-11-27/paginators-1.json,sha256=k1XV_MZrDEKU1boAc5CsYD_1DRfpjPfB1D1cnFLZuOE,623
+botocore/data/resource-groups/2017-11-27/service-2.json.gz,sha256=Nnleru4_xVc-ZeFrfMgsRTldjeW0XrTL0HFPwMNlv48,10896
+botocore/data/resourcegroupstaggingapi/2017-01-26/endpoint-rule-set-1.json.gz,sha256=H5k0mKsauACZg1fUdeL9jxMbx0X3m0cYj3LLOoFxKug,1147
+botocore/data/resourcegroupstaggingapi/2017-01-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/resourcegroupstaggingapi/2017-01-26/paginators-1.json,sha256=_NdoJ81VYpnX6AtSk2EBR7zAQf0S5WSbVxXq33ZXghU,684
+botocore/data/resourcegroupstaggingapi/2017-01-26/service-2.json.gz,sha256=iqEcB0Sgf3apC57yilTiP4nmWxRhR5sS82MNX-84q_E,7721
+botocore/data/robomaker/2018-06-29/endpoint-rule-set-1.json.gz,sha256=kPofUAuGhmE6Lm6EWcCaefaszl7pa_KwtfpwkMedNIM,1148
+botocore/data/robomaker/2018-06-29/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/robomaker/2018-06-29/paginators-1.json,sha256=zHASRcWF0d1i1iWFCA6xa2VkSgv-WXj48_kEUGAlhqQ,2001
+botocore/data/robomaker/2018-06-29/service-2.json.gz,sha256=Jt0q5-MJeVKSU-f6v9TaLA7YahTGiDaMqYLEREqqPyg,24335
+botocore/data/rolesanywhere/2018-05-10/endpoint-rule-set-1.json.gz,sha256=es9xMSMxJchvk_xLDN5rMEDv1CHRTXkoKDECTeXMfSw,1154
+botocore/data/rolesanywhere/2018-05-10/paginators-1.json,sha256=IaF8k8b_3R6qbXcxbFkIQqN0DTaCim4eQhIiEanVZkc,541
+botocore/data/rolesanywhere/2018-05-10/service-2.json.gz,sha256=KOJSr6LOTGkURZwKGYWhMWDZHYW9KqeK5z5isH3XvcY,7193
+botocore/data/route53-recovery-cluster/2019-12-02/endpoint-rule-set-1.json.gz,sha256=TGySliWcqrmWgelFQOV40xNoNZm1BIz825HCtSActCY,1169
+botocore/data/route53-recovery-cluster/2019-12-02/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/route53-recovery-cluster/2019-12-02/paginators-1.json,sha256=UhH6MsunbcB3w057DvJoHxEWGweOpch31kCr68-51eQ,201
+botocore/data/route53-recovery-cluster/2019-12-02/service-2.json.gz,sha256=CQyzxk73WOROAQwkp2ztLhJYoWYpNtU89ApGFUqqN10,4022
+botocore/data/route53-recovery-control-config/2020-11-02/endpoint-rule-set-1.json.gz,sha256=eCdyQ_mB0ageyYGt4Ful9cfktBY2SMiUz2miwqN7gVQ,1286
+botocore/data/route53-recovery-control-config/2020-11-02/paginators-1.json,sha256=eDByeUTgAtdsrqJD0NiWUp5AfuXhqM2q0oa-5MCgt38,892
+botocore/data/route53-recovery-control-config/2020-11-02/service-2.json.gz,sha256=D-mIcPuHJT09AdznLSLtrq2qgcCaQmAPWO_chw8DRPs,8198
+botocore/data/route53-recovery-control-config/2020-11-02/waiters-2.json,sha256=iw6vHr5XZ7c87aPCP4ejk0EHpOVt-ZT2ioC0asbgGJA,3674
+botocore/data/route53-recovery-readiness/2019-12-02/endpoint-rule-set-1.json.gz,sha256=vrintmyoPeXOmtzKd9iy5S85FNyozEzK4Gwq_nbSolY,1166
+botocore/data/route53-recovery-readiness/2019-12-02/paginators-1.json,sha256=bkbDR1VU1mtDe84KapiLM8rWUPHKj-aEpn7TLzqFeW0,2032
+botocore/data/route53-recovery-readiness/2019-12-02/service-2.json.gz,sha256=RMqWCwFG6J6pUi8xVcHxVtAhoNU_ObyRlS9oYIYwG_0,7313
+botocore/data/route53/2013-04-01/endpoint-rule-set-1.json.gz,sha256=bjFLPMnrAJi96UDmW4CGMjZU3FbF6_6fCB0vjh5bceE,1705
+botocore/data/route53/2013-04-01/examples-1.json,sha256=C3c7hhO4Y2jbpqrTEGNc7x007deldIJsNVDxdhaH_T8,29631
+botocore/data/route53/2013-04-01/paginators-1.json,sha256=-nS2WnQKiOUbqyQRXiMxCbqHwZ7xJQXVS98-vYEjiuI,1734
+botocore/data/route53/2013-04-01/service-2.json.gz,sha256=cARZ2uqKIaa6AST9Bt5aAIRgcPxZPd-qsH_Ajc5uk1E,62972
+botocore/data/route53/2013-04-01/waiters-2.json,sha256=s6BzW8AQ9pEM5yCsRa64E7lfUvhX5vxNARuiAtZwjsU,338
+botocore/data/route53domains/2014-05-15/endpoint-rule-set-1.json.gz,sha256=tg_-dOtqnTT7VxeKAGDIwKCKMzikiFeJc6Kw1pP34lk,1157
+botocore/data/route53domains/2014-05-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/route53domains/2014-05-15/paginators-1.json,sha256=VN49BhgZ_VxpcqSi9W0aIr8bv4iFv9QnrVoUerrFwoI,696
+botocore/data/route53domains/2014-05-15/service-2.json.gz,sha256=jSRedpDPyTp9athvSGGcPsBDpi5InOAwQpZELSA1Jus,21256
+botocore/data/route53profiles/2018-05-10/endpoint-rule-set-1.json.gz,sha256=aSl2hfPdnMey1sVw9p3nlT_Plk8FE_Zs0OYOllsYG4Q,1308
+botocore/data/route53profiles/2018-05-10/paginators-1.json,sha256=-QdEoZNxlj37dlhmE8U5hCOev_UGR-5Nv-_bCti2HtE,579
+botocore/data/route53profiles/2018-05-10/service-2.json.gz,sha256=Fcyrmv7auyWGwdevD3on4w5gksTWefFLqLLqYmTaioQ,4822
+botocore/data/route53resolver/2018-04-01/endpoint-rule-set-1.json.gz,sha256=gTXRgWjeghdqGxn0vauxt97mhBIK5LsyOaczB9HEXbQ,1242
+botocore/data/route53resolver/2018-04-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/route53resolver/2018-04-01/paginators-1.json,sha256=dZl4mnbzBj99_gIPlVRqF3YSMSG98HW1xVD1Kh1C_-k,2954
+botocore/data/route53resolver/2018-04-01/paginators-1.sdk-extras.json,sha256=3XJ5UEbB_NT-xjx41jRgxgoNKMWuUL-bcLPzf9n1o9I,806
+botocore/data/route53resolver/2018-04-01/service-2.json.gz,sha256=GSHHchAt62qzNleojaw4KqAF4vTA34DBlicGduLqVp4,30620
+botocore/data/rum/2018-05-10/endpoint-rule-set-1.json.gz,sha256=M6jXjR2ocdULJV_j0CTXege03gnL229niwoVEAL-cAc,1147
+botocore/data/rum/2018-05-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/rum/2018-05-10/paginators-1.json,sha256=eiv4iOnLCb9wVy6VijmIS8FeKbt7SfSmIY3M4qv3wIs,733
+botocore/data/rum/2018-05-10/service-2.json.gz,sha256=G7lwy1lxmuwzU3sKbkaY1yQosuKFGyd_oVCHbpLCv3Q,13304
+botocore/data/s3/2006-03-01/endpoint-rule-set-1.json.gz,sha256=8x3XUkuiLGA2xzN7Zp-bdR4G5nUwkyOVZz17nd_L90Q,17607
+botocore/data/s3/2006-03-01/examples-1.json,sha256=bGw9MrbmwHRES_w7kwW-Hr31-Js7JGP_oxoE4Tw21b4,57596
+botocore/data/s3/2006-03-01/paginators-1.json,sha256=sCuUQpM7lam7gE_27Js9PhAZ9gaz0L6CIoyeK07U3Tw,1837
+botocore/data/s3/2006-03-01/paginators-1.sdk-extras.json,sha256=FPn-BDMcfWtZLzHdRPCoL_HUvXpuZjCzoTvxXnpiceM,754
+botocore/data/s3/2006-03-01/service-2.json.gz,sha256=6Peh_2vNAGqBdH5_pcJtiBJBqFlqd5ICeB0D7TmNn_8,139162
+botocore/data/s3/2006-03-01/waiters-2.json,sha256=m0RJIxnJW7u6emLjY1201rmfeKxgz1f7VDU7qKJOI4c,1436
+botocore/data/s3control/2018-08-20/endpoint-rule-set-1.json.gz,sha256=7ACzXKfXiw4LGNOkJpGCQ0eWVrbIeZ3b9SPA7j8_uPs,7594
+botocore/data/s3control/2018-08-20/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/s3control/2018-08-20/paginators-1.json,sha256=mQbakYLFsRf-PwUykRHaPqhJ5lmg7TydWKz85l_Wv0g,225
+botocore/data/s3control/2018-08-20/service-2.json.gz,sha256=pZAhuNVsdAMPdZWlghoei-I5cPe_xPuX6dAYQJCf9eU,58854
+botocore/data/s3outposts/2017-07-25/endpoint-rule-set-1.json.gz,sha256=Veyr2dKZ0yyXQo-rcQ9MY6w1ziGn-02UabF-7JOwj6c,1154
+botocore/data/s3outposts/2017-07-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/s3outposts/2017-07-25/paginators-1.json,sha256=MNhAyDW1gAXK_msh5EL1QpaFjXE7KCnk2xzMuUZUiT4,527
+botocore/data/s3outposts/2017-07-25/service-2.json.gz,sha256=SqURqVCWEQ9vb67hml-86dghCRbjWRw5I12lWmaP4yE,3453
+botocore/data/sagemaker-a2i-runtime/2019-11-07/endpoint-rule-set-1.json.gz,sha256=RUOCHKLMbMf0mL0MXfOdlK-i3qIR_A-gMGlbxM_Vzks,1160
+botocore/data/sagemaker-a2i-runtime/2019-11-07/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sagemaker-a2i-runtime/2019-11-07/paginators-1.json,sha256=X0gq-uz_QUVGPACQxWwKf6n-ZZ-MsaXi3huDYMOu10o,199
+botocore/data/sagemaker-a2i-runtime/2019-11-07/service-2.json.gz,sha256=muYMw94kalsbk1ERkjjjelLO1dvIcY1ycenEkqhf1Wo,3777
+botocore/data/sagemaker-edge/2020-09-23/endpoint-rule-set-1.json.gz,sha256=MPzAUyTptezUMCyLtbhZPlZWbB-1EYdkUpfLMtPJMzA,1152
+botocore/data/sagemaker-edge/2020-09-23/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sagemaker-edge/2020-09-23/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/sagemaker-edge/2020-09-23/service-2.json.gz,sha256=UXsl4CWnZ1lSGyOcENEL96eLpYhjkeYXtOjjlXVlTK0,2219
+botocore/data/sagemaker-featurestore-runtime/2020-07-01/endpoint-rule-set-1.json.gz,sha256=SdYh_pPp354q8Qu8xNXOTMoEzD-bUOrZVH617DCVQ3E,1170
+botocore/data/sagemaker-featurestore-runtime/2020-07-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sagemaker-featurestore-runtime/2020-07-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/sagemaker-featurestore-runtime/2020-07-01/service-2.json.gz,sha256=uOCbFTbdbfUObikWlsOWs2TGHXPxO5lidFZcC4-M4W0,4127
+botocore/data/sagemaker-geospatial/2020-05-27/endpoint-rule-set-1.json.gz,sha256=reSoWDo8pY0GBONpxHzWRC-CdBhZOimJGdhr09Mfesw,1299
+botocore/data/sagemaker-geospatial/2020-05-27/paginators-1.json,sha256=F6o4MlbqixSACzxItwWHBiMmvvc3VqdxdWlY9NRKy6E,609
+botocore/data/sagemaker-geospatial/2020-05-27/service-2.json.gz,sha256=ZSe-XioOIGwPkc9LYwkBeILg-RkP34RSeVB9Q8dbDwg,11920
+botocore/data/sagemaker-metrics/2022-09-30/endpoint-rule-set-1.json.gz,sha256=iirT6h0ibv58BKqZ4X1EDYO3NkEn5C9UH_Oiy0i7TB0,1296
+botocore/data/sagemaker-metrics/2022-09-30/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/sagemaker-metrics/2022-09-30/service-2.json.gz,sha256=CdQHpLUocEt8G0nFCScgDbjCVX0AfBtPeruCYHKMzus,1359
+botocore/data/sagemaker-runtime/2017-05-13/endpoint-rule-set-1.json.gz,sha256=NI4aK0nJsZ9zcEmVTu7HHomWm9FjVLgLQWO75uIsZx4,1274
+botocore/data/sagemaker-runtime/2017-05-13/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sagemaker-runtime/2017-05-13/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/sagemaker-runtime/2017-05-13/service-2.json.gz,sha256=kGzeEdPjg7V_OmPrUcRriVygCKzL7PYg2Mdr795120g,5152
+botocore/data/sagemaker/2017-07-24/endpoint-rule-set-1.json.gz,sha256=YCnvd87CBzc4AAHb30BOeHgxSUzRs9DrucR7pOrUJt8,1271
+botocore/data/sagemaker/2017-07-24/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sagemaker/2017-07-24/paginators-1.json,sha256=iVx3JQ7Sda2A09XGaVtIw1pM32DGTVRmbw0IBUbDfCU,13371
+botocore/data/sagemaker/2017-07-24/service-2.json.gz,sha256=Pz5kQozinFpnIGc6yWmW21s55jWIIolnJc0jyoGzaLU,287570
+botocore/data/sagemaker/2017-07-24/waiters-2.json,sha256=s0kutyNgTzNXRyNRBkjUpqnkcauHlnwBcgpfxVvA0bw,7559
+botocore/data/savingsplans/2019-06-28/endpoint-rule-set-1.json.gz,sha256=fAOKu1wxRaZqMj9CYG9tzDQC0BaKzicX3o1scAywGh4,1308
+botocore/data/savingsplans/2019-06-28/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/savingsplans/2019-06-28/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/savingsplans/2019-06-28/service-2.json.gz,sha256=Pnz2Tr7qLMVdaJM21JhBLGcUsnOSjklLlfX-rpTtzTs,4275
+botocore/data/scheduler/2021-06-30/endpoint-rule-set-1.json.gz,sha256=S1q9iFzAAvmWVWOn9AaaItQrWcARbZS675bNEAhjT5Q,1290
+botocore/data/scheduler/2021-06-30/paginators-1.json,sha256=VH5c3yVo1Un4lL_GVN-D3A5GNOTWMmnqAQ0QZAOMJOo,363
+botocore/data/scheduler/2021-06-30/service-2.json.gz,sha256=mrKUY66I8cYtFJ50e0zSAGY53EqWgekZ-fM8U2UPIes,9349
+botocore/data/schemas/2019-12-02/endpoint-rule-set-1.json.gz,sha256=bYyPk4_7xP9MtgJMb0el3nYywfLSj-QuoSZAEdr7dlU,1147
+botocore/data/schemas/2019-12-02/paginators-1.json,sha256=JG7VhSHU5MW5ZSEzWuvc0fcOMdYngtguHEeVk1fPoro,830
+botocore/data/schemas/2019-12-02/service-2.json.gz,sha256=BgdEzwz2cQE8QaGeqG0op8yY3byGw8u3M3xlss5ODxA,5698
+botocore/data/schemas/2019-12-02/waiters-2.json,sha256=t1IowU2djOrDdhK7r7dmmVfVARz1Zp31Dl3MPtnqy5I,824
+botocore/data/sdb/2009-04-15/endpoint-rule-set-1.json.gz,sha256=fsYOg3PqDv5qyUwFQC0y_a9qO9q5wWKsUntF-NXIq_U,1198
+botocore/data/sdb/2009-04-15/paginators-1.json,sha256=3KF7ZF879CPbTIZ8drlqnq5S3aFHdubXunwekE3ARG4,317
+botocore/data/sdb/2009-04-15/service-2.json.gz,sha256=1Jv6V7k1LSib4vGUWxkhYSaaFTLWqqtCNT0ohiGEhVk,6036
+botocore/data/sdk-default-configuration.json,sha256=LlmdeqSk0HQAKMCGNgPsFO1K6dJXQdjzq8Ad3wRs7g8,4135
+botocore/data/secretsmanager/2017-10-17/endpoint-rule-set-1.json.gz,sha256=VuAqOqgEfLojufBuG48gikDWV7BDHyIncqjl7KbyXNY,1354
+botocore/data/secretsmanager/2017-10-17/examples-1.json,sha256=3LKYx_uc48qXDFx7m8cU2l8XByq1wu28h5fOggrmDCI,22410
+botocore/data/secretsmanager/2017-10-17/paginators-1.json,sha256=wFoEW6m_jRSAAt8D1r54a9XXWnZerkFn83sHj413-ww,188
+botocore/data/secretsmanager/2017-10-17/service-2.json.gz,sha256=NYbZXqU-e6j16RTel-dUenMlxImA2qoPRpyo4FbiWDw,21257
+botocore/data/secretsmanager/2017-10-17/service-2.sdk-extras.json,sha256=IEA3uxtjPY8I1on-q2W9-tozHHIVmneQyB6gCTcYTro,120
+botocore/data/securityhub/2018-10-26/endpoint-rule-set-1.json.gz,sha256=g81QG2cjGX1UbBfwdUrn5AfArFTX1cVa3x8eMdpL_n4,1152
+botocore/data/securityhub/2018-10-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/securityhub/2018-10-26/paginators-1.json,sha256=v4BqR8RUobtNQil4RRAKgXo1GXA8mtXS2QZFQqIWBDk,3084
+botocore/data/securityhub/2018-10-26/service-2.json.gz,sha256=2HnMO23l5wcaQkEcjwNOuT69608UEgQBwYe6Qyk_zmg,151267
+botocore/data/securitylake/2018-05-10/endpoint-rule-set-1.json.gz,sha256=k8KrC4_cvI2fH_bbkbXOsCsi5A0k2_ICfx3ykb1HJg4,1303
+botocore/data/securitylake/2018-05-10/paginators-1.json,sha256=aw_RlW6BEfqxgzWUOJF6ZrCTf49mvjJ9uAmhefV_2kg,705
+botocore/data/securitylake/2018-05-10/paginators-1.sdk-extras.json,sha256=v0jKSsBUrC-WdKoMQzNm6hfXLmDajUWqKZtLDn1TA9k,169
+botocore/data/securitylake/2018-05-10/service-2.json.gz,sha256=3Y0Yc4On5frBrjVNxchZ5XPqaMoa09apbv8clUspY9M,13912
+botocore/data/serverlessrepo/2017-09-08/endpoint-rule-set-1.json.gz,sha256=x2VwcgW5-U-oONvw0nX4JWBlC96506KZNbE_DPMEgf4,1242
+botocore/data/serverlessrepo/2017-09-08/paginators-1.json,sha256=6mp7kgpraGJSmfK8vEcMsz_LdDUfQN9dI4kjn83wRhY,543
+botocore/data/serverlessrepo/2017-09-08/service-2.json.gz,sha256=o7sKKHW3QhmKZPl0M2Fo2LJnwygmSfxKqQukX0qHP9I,9555
+botocore/data/service-quotas/2019-06-24/endpoint-rule-set-1.json.gz,sha256=Vfy42cL02ho76mOhlxxE_jyCCZcnk9FOndVAN7uml1U,1242
+botocore/data/service-quotas/2019-06-24/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/service-quotas/2019-06-24/paginators-1.json,sha256=e9hZphztzUJpLy1e7mpXUhwobjRsYyLMWkY1DYQfRpw,1149
+botocore/data/service-quotas/2019-06-24/service-2.json.gz,sha256=ClAOGKikH4u85Boa2gnOLhG8V1phYtw14x-PEqP625Q,6411
+botocore/data/servicecatalog-appregistry/2020-06-24/endpoint-rule-set-1.json.gz,sha256=AX9GiYmm6GHK8N3ttFYTHC2uioQgYfKfoSM-feN6xaI,1248
+botocore/data/servicecatalog-appregistry/2020-06-24/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/servicecatalog-appregistry/2020-06-24/paginators-1.json,sha256=2lclqrEMyRSrGV8L1DZoppkiLhUGI1VcinKImELBFi0,928
+botocore/data/servicecatalog-appregistry/2020-06-24/service-2.json.gz,sha256=hiUZepll4dfuWQ_hZhQUdj0Gv9SEgxjyo5VpZ3yzL_s,7841
+botocore/data/servicecatalog/2015-12-10/endpoint-rule-set-1.json.gz,sha256=EB8_l4vLST8_zxAnSmgp5n4JcfCptEvQleUkAVctPz0,1154
+botocore/data/servicecatalog/2015-12-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/servicecatalog/2015-12-10/paginators-1.json,sha256=ghdoop27a-JBxcfHGVvA0vMp9y_Z-MY1R7TuRQCrmP4,2996
+botocore/data/servicecatalog/2015-12-10/service-2.json.gz,sha256=O_CihXg0TnBOJ7pZkztDQyR0fXV6lzmlmeSdaOwrPCM,37370
+botocore/data/servicediscovery/2017-03-14/endpoint-rule-set-1.json.gz,sha256=ewrM9a35sNmVOr5RzHTU6gdcy3BKK7fZ0TXYO6ttrbE,1308
+botocore/data/servicediscovery/2017-03-14/examples-1.json,sha256=iJqJB_1uy_oppRbcXbl5SmCA2yLLVdSdj674nZ7dSQQ,18861
+botocore/data/servicediscovery/2017-03-14/paginators-1.json,sha256=sKu-j-WBHT8KpiemY4vgLiQkV1Ub2GtqLbYiUxdkjjE,683
+botocore/data/servicediscovery/2017-03-14/service-2.json.gz,sha256=e70T2COCg5YBYWb33MFiyT2cU3aOtxwKd6b6cnkwoNg,19082
+botocore/data/ses/2010-12-01/endpoint-rule-set-1.json.gz,sha256=MyJPDzWSQyP4xvOPGHtooaGRA59aVQtwGa8jECVBax8,1148
+botocore/data/ses/2010-12-01/examples-1.json,sha256=LdOG9qOcWahQ6xYBc3_UEV-teA96yJJSesbf0fNI8Bw,28834
+botocore/data/ses/2010-12-01/paginators-1.json,sha256=G_7q2KFDP0LwwEUoCgd9qikwYlHoaFwDjQ_3CtWBVPw,883
+botocore/data/ses/2010-12-01/service-2.json.gz,sha256=DpvCX-rkTomotQk3xtW5Tbf-0WadtE5wjEemQByrfTo,35606
+botocore/data/ses/2010-12-01/waiters-2.json,sha256=4GF4zY3Tg43WiGAVWSJeabII8bSEU7_ElsMj_G3Bt68,380
+botocore/data/sesv2/2019-09-27/endpoint-rule-set-1.json.gz,sha256=MyJPDzWSQyP4xvOPGHtooaGRA59aVQtwGa8jECVBax8,1148
+botocore/data/sesv2/2019-09-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sesv2/2019-09-27/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/sesv2/2019-09-27/service-2.json.gz,sha256=N8pAxipT-XpS9W4Lx5qaQLAeH1LGK1sa4HzuMZqfiAQ,52338
+botocore/data/shield/2016-06-02/endpoint-rule-set-1.json.gz,sha256=SXQudsydRtYZWYdoMpPW31CCt1hNqqsQRlmuNf5VM70,1347
+botocore/data/shield/2016-06-02/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/shield/2016-06-02/paginators-1.json,sha256=MRQd38Sw6vMYcdoF_zRIXAdMaDQHs_indt6OtJxi0BE,361
+botocore/data/shield/2016-06-02/service-2.json.gz,sha256=Jg1u0UIwL7lz9APwQgn7KEog5APoL4V4Cq8rymUy-Xs,15250
+botocore/data/signer/2017-08-25/endpoint-rule-set-1.json.gz,sha256=Liv0UaPnsJ_jcB_-ZGHRxjf1kfqDug01tl7gzKLowt8,1148
+botocore/data/signer/2017-08-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/signer/2017-08-25/paginators-1.json,sha256=vjItW2pdi1KsZB_HwJEZqWIDJNHlrKbyxSuN6x8LHkU,526
+botocore/data/signer/2017-08-25/service-2.json.gz,sha256=tOClDnaySKFI-c0OWi9LVAo8VKupYaxQk6ra-MJt94M,10289
+botocore/data/signer/2017-08-25/waiters-2.json,sha256=ZvZgSYJd2QhWkeR1jaM1ECQ8295slZ6oDEFLtA2tYRE,607
+botocore/data/simspaceweaver/2022-10-28/endpoint-rule-set-1.json.gz,sha256=n9ZAHBskbHjsOijlcMkahyBOiHvz2P_x6DbPDjchpIQ,1294
+botocore/data/simspaceweaver/2022-10-28/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/simspaceweaver/2022-10-28/service-2.json.gz,sha256=BEl9TPV1MVdU51tNlL-cd_yE3YOPeEmR2LZS1G2Hxvo,6915
+botocore/data/sms-voice/2018-09-05/endpoint-rule-set-1.json.gz,sha256=3YZ344VsgxU_0Wp05ptQJbrfpVoqmrW4lxieIeaz2L4,1112
+botocore/data/sms-voice/2018-09-05/service-2.json.gz,sha256=U7iapqmzo61WAnE2Jskwt0tgMUqwY7LuLL9WAJFy8r4,3324
+botocore/data/sms/2016-10-24/endpoint-rule-set-1.json.gz,sha256=w1bBvJOCVB4iDh_L4d_bfXBK-wUP1X5w71Xreaepl0U,1286
+botocore/data/sms/2016-10-24/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sms/2016-10-24/paginators-1.json,sha256=-hjimmtmqb13Nn5a5N7IfTXJO31BBcEXxMabaXv-2Rs,865
+botocore/data/sms/2016-10-24/service-2.json.gz,sha256=rgJyPI7xLs271t69AqJ1JCCDE-P4JdUy52deoW7bCao,9676
+botocore/data/snow-device-management/2021-08-04/endpoint-rule-set-1.json.gz,sha256=HMtkL1Rd2CiFRjaPln8OOg9aeO7ZGywxHphNIDbFyZc,1161
+botocore/data/snow-device-management/2021-08-04/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/snow-device-management/2021-08-04/paginators-1.json,sha256=rNmRDBuxcetGirFRJQJA1vFXHeMY-sFLZ8BSld7BkFw,677
+botocore/data/snow-device-management/2021-08-04/service-2.json.gz,sha256=mP-JzyNUNhvqXzFK7f7u4hWbx10y6K8_GWk0HN_eQaw,5955
+botocore/data/snowball/2016-06-30/endpoint-rule-set-1.json.gz,sha256=6-0iAyn-SQKDsMbLQ1pLgn6va4mhzSNJ6kTihW_6q5c,1151
+botocore/data/snowball/2016-06-30/examples-1.json,sha256=c2uRhH8SNSzMSPVVlezBwPPoxWxhOl2QxkdNc0A37q4,18099
+botocore/data/snowball/2016-06-30/paginators-1.json,sha256=vMdXg3dD9a7r3ifpM8lAmkBfXJBVz66l-6uUq_4OJjo,1061
+botocore/data/snowball/2016-06-30/service-2.json.gz,sha256=PLX4DN6ERAjNX5VEhtLgu-NmDIxfuQucKTS8eaQv8f8,17017
+botocore/data/sns/2010-03-31/endpoint-rule-set-1.json.gz,sha256=ZRl8HKR0aYdGpAlXemjVfdebfatLsIv_54x0D8uvJCE,1230
+botocore/data/sns/2010-03-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sns/2010-03-31/paginators-1.json,sha256=a5cU7i3ZYF5D-u8S4oYs5kDUAAeav2kcWeG21u8RjPg,1241
+botocore/data/sns/2010-03-31/service-2.json.gz,sha256=roz-nlBjeSdVcNXp2NYAkTykHtbHEr2BqQH3GGE3QUU,24965
+botocore/data/sqs/2012-11-05/endpoint-rule-set-1.json.gz,sha256=RsoIteUCggf1wfHWpfHAMGsFmC2c4EWm_6N8XofkRI0,1232
+botocore/data/sqs/2012-11-05/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sqs/2012-11-05/paginators-1.json,sha256=fwyodl-UMt13laxQWAtCY9DEgncIy3mWPV-tS9M5m50,363
+botocore/data/sqs/2012-11-05/service-2.json.gz,sha256=JDsECVZTLCpjIUWb8og5NnQmtsBp8rO4kB3FlKNvvtY,23239
+botocore/data/ssm-contacts/2021-05-03/endpoint-rule-set-1.json.gz,sha256=AlXOLiB_FZcLeIpzjYDC7Hc2BQcyPCKKVmMvsyivz5I,1151
+botocore/data/ssm-contacts/2021-05-03/examples-1.json,sha256=DgD8jM1qr-3c2rDYBCXlsWUyaA_3S4VTwUogOr5KX0s,28860
+botocore/data/ssm-contacts/2021-05-03/paginators-1.json,sha256=Zvq8EuioTe0ZGvZrNX07bNJzAplhIUTDre4-HOhKrsc,1872
+botocore/data/ssm-contacts/2021-05-03/service-2.json.gz,sha256=oFCo3nhPwMm7h4mAG4C5QDnvxIqRuKC9g6vttpvn6K0,12756
+botocore/data/ssm-incidents/2018-05-10/endpoint-rule-set-1.json.gz,sha256=aHFBdc0_mvkNonVxfBV8ZskC3_tg_2w-IXhBWcoshN0,1156
+botocore/data/ssm-incidents/2018-05-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ssm-incidents/2018-05-10/paginators-1.json,sha256=4qlmECBX9jmRprL7ROo4h4MHrfWWjH2gGPLr9sjuV3o,1259
+botocore/data/ssm-incidents/2018-05-10/service-2.json.gz,sha256=11T8QKkBICp3b_7AxuPmPECg1vGp8L3LmqzHcZwsti8,14417
+botocore/data/ssm-incidents/2018-05-10/waiters-2.json,sha256=1xhj2BSaBj_CCZlCG7wTLL4ZB0e8_Uuq97DXjf7rADI,1465
+botocore/data/ssm-quicksetup/2018-05-10/endpoint-rule-set-1.json.gz,sha256=QYlEkghR-C8XueyOomeLh4PBWSKagFwh-A_xksybATg,1305
+botocore/data/ssm-quicksetup/2018-05-10/paginators-1.json,sha256=sAkarDqDeguVfyUTh4_sljvB0nkCoJTRg5uaBYevfo0,219
+botocore/data/ssm-quicksetup/2018-05-10/service-2.json.gz,sha256=EUf1okMTOthVoGppt0Qe-NnYDfP4KYC8-agWlw30S4s,6946
+botocore/data/ssm-sap/2018-05-10/endpoint-rule-set-1.json.gz,sha256=a92xRGlXWavJAA22MVDRinQ68nHjfRJ23YmLgjEHlhg,1300
+botocore/data/ssm-sap/2018-05-10/paginators-1.json,sha256=UXliUwyoXCGnGn6T4C_YyE6kIr6oteN9If7usz66I7E,867
+botocore/data/ssm-sap/2018-05-10/service-2.json.gz,sha256=lEvX8dkuxyH7Xv6vqBx4z6NV4ZGqv1K-gN_AEyT0Zr8,7533
+botocore/data/ssm/2014-11-06/endpoint-rule-set-1.json.gz,sha256=k2DzJ-Su0c6Xupiyr1Gl-YrOJA0v9Ld5vRNYawq0V2E,1232
+botocore/data/ssm/2014-11-06/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/ssm/2014-11-06/paginators-1.json,sha256=llZKzzY_33z-x5UUqVOdqv9jPcNbv2jei4XS7XniTdU,8798
+botocore/data/ssm/2014-11-06/service-2.json.gz,sha256=-jHUAmZFBQftIjDFMdeumpnXwVLVnl0pKe76nf_g2EY,125804
+botocore/data/ssm/2014-11-06/waiters-2.json,sha256=eTUBQgvIuYcA9hhUZZ3mY4KqLap6FbcReyPUqdPYduc,1457
+botocore/data/sso-admin/2020-07-20/endpoint-rule-set-1.json.gz,sha256=6C0VEE9f_cC2dwTHdyE_1DIrP7XHd1bWQPPZwhRlt-o,1232
+botocore/data/sso-admin/2020-07-20/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sso-admin/2020-07-20/paginators-1.json,sha256=blhgKthRgscSeZRmLa2ASHICJ4LeT3IVnlIJ0FAlpdA,3714
+botocore/data/sso-admin/2020-07-20/service-2.json.gz,sha256=RwHf0gd5q4A9dLVHHq_KD0BNHNAxU01ReB6DmsCVxi0,19790
+botocore/data/sso-oidc/2019-06-10/endpoint-rule-set-1.json.gz,sha256=QDuYgvVoByzPQl7IkAjUSbCurKnihmQg1mtlxLq0Uyc,1233
+botocore/data/sso-oidc/2019-06-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sso-oidc/2019-06-10/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/sso-oidc/2019-06-10/service-2.json.gz,sha256=mfGBsnSwO4XFpho1b6fdB8XvyGIrVyd3LNWxHl5ivbw,5438
+botocore/data/sso/2019-06-10/endpoint-rule-set-1.json.gz,sha256=b7S5dOzl3w0kOuEQktFNxoru-2vnhP5P5r1BH2wR0qc,1239
+botocore/data/sso/2019-06-10/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/sso/2019-06-10/paginators-1.json,sha256=IScw_JafDnQ5pGRs-y61MtR0d4glhFcZR5D-8KLn2-Y,356
+botocore/data/sso/2019-06-10/service-2.json.gz,sha256=1k8mrh3UF8Z4ubb_yCMxm56UYT5M-vkPUeiDHVgWXjI,2913
+botocore/data/stepfunctions/2016-11-23/endpoint-rule-set-1.json.gz,sha256=vny5Ju-ka12bQ96ULz7wcnVwtvmjsOtTJponY6MWZQI,1210
+botocore/data/stepfunctions/2016-11-23/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/stepfunctions/2016-11-23/paginators-1.json,sha256=2p0xo5UgVh-6RA8-svDfT8HBM_Cf2d05upRi3VIOnuY,856
+botocore/data/stepfunctions/2016-11-23/service-2.json.gz,sha256=DvheoLm5f_uw3CblBYJ3kwzt5_N_5OdJlDJ0R3g_uH0,28646
+botocore/data/storagegateway/2013-06-30/endpoint-rule-set-1.json.gz,sha256=0S6-8oKIH4rqnk8LrpUVLXUwFLzMrASBWzQxQukBqUk,1155
+botocore/data/storagegateway/2013-06-30/examples-1.json,sha256=2-mBPJqbSFv2f3t6KqdtrU5dW0Z49zylBvFGmoQEAk8,49947
+botocore/data/storagegateway/2013-06-30/paginators-1.json,sha256=xinZcEJUcO4hsTa3TxMl6HAggFplRjfyCPtZr8wCsBc,1967
+botocore/data/storagegateway/2013-06-30/service-2.json.gz,sha256=QRyfXvuRx0mHDE1lHx5UESt4qLQEaaYCmh0G5y0xjvk,50405
+botocore/data/sts/2011-06-15/endpoint-rule-set-1.json.gz,sha256=mGPgClqGlCDvpIRHzo-4yp7QgejoSUQsjWTODZVmVlU,1778
+botocore/data/sts/2011-06-15/examples-1.json,sha256=yD_CcHN2f9t9PlGQ5NzOJaCYccexGPoonbBW2T4OMck,11885
+botocore/data/sts/2011-06-15/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/sts/2011-06-15/service-2.json.gz,sha256=_keZzzBHQNc8QQPtaLGBsBe2ogkcaoLRi61vrH2K3qM,16795
+botocore/data/supplychain/2024-01-01/endpoint-rule-set-1.json.gz,sha256=YHTl1RvDvnWbmJIRdNbJ36JDmiM1A0Bf6wcCdj-NX8Q,1296
+botocore/data/supplychain/2024-01-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/supplychain/2024-01-01/service-2.json.gz,sha256=uDPvzyMekn9WmoUygRZqP2-ivjzvhCKR3WyrmntM2z8,2603
+botocore/data/support-app/2021-08-20/endpoint-rule-set-1.json.gz,sha256=fJU_CymmAkxRzO9aURIagOgmLQ_ER-w_NFQFsl8zsvs,1147
+botocore/data/support-app/2021-08-20/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/support-app/2021-08-20/service-2.json.gz,sha256=ZmiHQsQq0G7nmsXJDcl3JUM7hjY40QbpqLQuyzlNiTU,4271
+botocore/data/support/2013-04-15/endpoint-rule-set-1.json.gz,sha256=wJIQ9vYEIZ1YqNKMLa3--of-txCRwk43u0AI1BqmOvE,1557
+botocore/data/support/2013-04-15/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/support/2013-04-15/paginators-1.json,sha256=b74jGAMdiNu8tKXAfyVILd2DpHqQx91qieo1BlSXpK8,363
+botocore/data/support/2013-04-15/service-2.json.gz,sha256=jIsJqO-urzxlwv6QmSnxIwyM0kDoH9_c8FO_eypbPC0,11900
+botocore/data/swf/2012-01-25/endpoint-rule-set-1.json.gz,sha256=KQ0i9eHj8u46cTBRk1EmG6TLt6wJnRLEgPjeeOnUeLI,1232
+botocore/data/swf/2012-01-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/swf/2012-01-25/paginators-1.json,sha256=tOiP-8y-iuvOSJM35cQg6qCE0ai5dd5IWenCE1BH_yk,1496
+botocore/data/swf/2012-01-25/service-2.json.gz,sha256=FU2_U1rPBKKm2Z5IPbzw0MX2HTSY5vUxtcW0OiUQySw,34463
+botocore/data/synthetics/2017-10-11/endpoint-rule-set-1.json.gz,sha256=X--oh62IsFgt5SoZD1DJ-3V0bhBtAmlylmjiKZ3LMMg,1149
+botocore/data/synthetics/2017-10-11/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/synthetics/2017-10-11/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/synthetics/2017-10-11/service-2.json.gz,sha256=oVHIDSpTKpea5-JmcwuiBsxn1bcdhTqEUugGBz7LE4k,13497
+botocore/data/taxsettings/2018-05-10/endpoint-rule-set-1.json.gz,sha256=vmJ3TEuS0KNqWZL59SxzJ8GwMl81EJR9OLg0md1PYOQ,1379
+botocore/data/taxsettings/2018-05-10/paginators-1.json,sha256=ywnoe6Hwi6IWFC0r8psxayJa-8yD220KHuSs1wgKu3Y,201
+botocore/data/taxsettings/2018-05-10/service-2.json.gz,sha256=2ZLXsx8sZyHB4U7j00T8Mbt43yTnfum9SvqIWNG6JTI,10014
+botocore/data/textract/2018-06-27/endpoint-rule-set-1.json.gz,sha256=pXM4l_6ifTlLnGnuaeSIB1uipHK69MYWdPoZW5iENqg,1147
+botocore/data/textract/2018-06-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/textract/2018-06-27/paginators-1.json,sha256=IQfBMdVD87vGqQnApoCTQrnbboZ3niS3DPFSlfrqh80,363
+botocore/data/textract/2018-06-27/service-2.json.gz,sha256=NT2OpT7iT3qGqJoUAZCf1a7CIkV-Q9zKhqFlSfFkXwA,21988
+botocore/data/timestream-influxdb/2023-01-27/endpoint-rule-set-1.json.gz,sha256=2LefLaYREropESXnJmS35LIFwl9rfGzE_L09gDL-C2A,1309
+botocore/data/timestream-influxdb/2023-01-27/paginators-1.json,sha256=IQSiFn17Yy9851zXkEICeo0Zr9uKaHvoUo3Yj9ZfkHM,355
+botocore/data/timestream-influxdb/2023-01-27/service-2.json.gz,sha256=1YhykK5z9rYWJtZ8_-K0Ja7xyRbIiEo-3t3s-aUPm9M,5581
+botocore/data/timestream-query/2018-11-01/endpoint-rule-set-1.json.gz,sha256=8unBIxOF64sjjm-xUks2b9p5yZLcIqsuvpxF8XuP-w0,1157
+botocore/data/timestream-query/2018-11-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/timestream-query/2018-11-01/paginators-1.json,sha256=GLU3XW2DRbhxpVzfVKj8-jHmRFvPqo-aadH3YwO8TUg,618
+botocore/data/timestream-query/2018-11-01/service-2.json.gz,sha256=55SXtIMxWaMbf0xpjIx9Gd5f_RZ98xC_01OeM1L-D5E,11336
+botocore/data/timestream-write/2018-11-01/endpoint-rule-set-1.json.gz,sha256=y-ZWV__1I_TDGOtsF7c5wK5epoxn5tOwokYK1RJn38s,1246
+botocore/data/timestream-write/2018-11-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/timestream-write/2018-11-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/timestream-write/2018-11-01/service-2.json.gz,sha256=MPGnKtJIzDZHQrxZLxTVBOqdh4STufOx2CsEKRStXds,12121
+botocore/data/tnb/2008-10-21/endpoint-rule-set-1.json.gz,sha256=KlymqOE5-BLs9st-UP4TgqvFdrwgnejgCDwiAkBi_Yc,1296
+botocore/data/tnb/2008-10-21/paginators-1.json,sha256=oz2uxUX8r9w5q6IjSx3zIxuNl3_jtJnCGLbFF1j0okw,932
+botocore/data/tnb/2008-10-21/service-2.json.gz,sha256=-J4mACkdsS5kHiINRzX7v3aLmfvIPIxgJR027YlPKJQ,10024
+botocore/data/transcribe/2017-10-26/endpoint-rule-set-1.json.gz,sha256=3SMfn1xmfNUaLm-Uk8wrdqo4ekBOVca2fWNRAeHtVkU,1340
+botocore/data/transcribe/2017-10-26/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/transcribe/2017-10-26/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/transcribe/2017-10-26/service-2.json.gz,sha256=wQBKKerdSGo1CjIkjbzWKarSPo-JyAavge80pFysmTg,33498
+botocore/data/transfer/2018-11-05/endpoint-rule-set-1.json.gz,sha256=8rKwNDI7ctzWQSfc4T3ETiRqwfQRm3dASnt5Z4vP1eU,1150
+botocore/data/transfer/2018-11-05/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/transfer/2018-11-05/paginators-1.json,sha256=fc0uh3xy9JmnDld4Amuh-tLtbs7M8LmkLCi-EsGkHx4,2072
+botocore/data/transfer/2018-11-05/service-2.json.gz,sha256=L9VZkxbPaxkV3TJlhZ09YMj5PyqFMniM1bdyuv7EIps,50963
+botocore/data/transfer/2018-11-05/waiters-2.json,sha256=hVdSZ-CDADnA9zRgSm0tK-qrrIGLUKXug5j6Ave1F-Q,868
+botocore/data/translate/2017-07-01/endpoint-rule-set-1.json.gz,sha256=-x-pyK9FHWDutwxAXYo7lGCB_wbllAsAvoxC3s2PvMU,1151
+botocore/data/translate/2017-07-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/translate/2017-07-01/paginators-1.json,sha256=eE-1ycW-V5DQ_8t4NsRrfJYKhUnAaS7d5OyOimdaOaA,209
+botocore/data/translate/2017-07-01/service-2.json.gz,sha256=CLwef_X-pkMc_st7vh0BXHut8rAQLwOa8uJ88KKokmk,13002
+botocore/data/trustedadvisor/2022-09-15/endpoint-rule-set-1.json.gz,sha256=CZeauGnIAHUKbQfP5NfpUuHY7or-4L_meaegU5HtBYg,1304
+botocore/data/trustedadvisor/2022-09-15/paginators-1.json,sha256=eM9ClOnA5h4jNM-0Xgyq-ZplOH89DYmmJzOJ59FieIY,1226
+botocore/data/trustedadvisor/2022-09-15/service-2.json.gz,sha256=UkI0e4fETPNEvwat49elb--6ie8nBO-q6ZpsTVoluEM,5193
+botocore/data/verifiedpermissions/2021-12-01/endpoint-rule-set-1.json.gz,sha256=EuH2bqKx1MavoKzLzU_YWPmBfhzTtGUDlYXMPUHe-Y0,1308
+botocore/data/verifiedpermissions/2021-12-01/paginators-1.json,sha256=4cQu2IKJA_8dQUylEDfAsxkN5ZxnoXrjv9rRdWg3rsk,709
+botocore/data/verifiedpermissions/2021-12-01/service-2.json.gz,sha256=jvkjUySkKGLSJAdGbAt7D2AmoVbT2FDNHUOVqwRxoO8,22665
+botocore/data/verifiedpermissions/2021-12-01/waiters-2.json,sha256=fsA0_mwCl57UFPiqxJUWLb9AE7gd9kpBT4x0_6Q7dww,39
+botocore/data/voice-id/2021-09-27/endpoint-rule-set-1.json.gz,sha256=JKQCJ6uiaBg82etOwsYuDd4dsnpwOa7w_N59CrfEdyE,1147
+botocore/data/voice-id/2021-09-27/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/voice-id/2021-09-27/paginators-1.json,sha256=mgfNZB61NZhxJAtDiZ1WOqHTvwaWmArbDCHTAkdf520,1073
+botocore/data/voice-id/2021-09-27/service-2.json.gz,sha256=rVXjjNitnXfNcRxcZywH76HJ__Em5DRkA05rFsUQ99Y,11875
+botocore/data/vpc-lattice/2022-11-30/endpoint-rule-set-1.json.gz,sha256=Oz715mc2Vj3pguO2ULNItJIfaNajMyG_TNqHCezNIuo,1303
+botocore/data/vpc-lattice/2022-11-30/paginators-1.json,sha256=8-TntKPqApRrjCwKlln2wyr8Y80DAHuzhNPsALvh__4,1524
+botocore/data/vpc-lattice/2022-11-30/service-2.json.gz,sha256=IGgdBXLDldWwzNKvSjvJhLtoU9aOO1a_sDyeG_-mOko,17141
+botocore/data/waf-regional/2016-11-28/endpoint-rule-set-1.json.gz,sha256=rrIht5f6Z4pTSuCzWkfw1HfuWhzuov2GycLLGwp6huw,1151
+botocore/data/waf-regional/2016-11-28/examples-1.json,sha256=6OPuCnLynJIfGO-Vxhb9ZZV9ktEKhpByvf2jSwAg-DY,29749
+botocore/data/waf-regional/2016-11-28/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/waf-regional/2016-11-28/service-2.json.gz,sha256=FQJA-RLSqmX0ziSS6NOdUP97wS9RUwE4mEck8MQG6rI,42886
+botocore/data/waf/2015-08-24/endpoint-rule-set-1.json.gz,sha256=AdtjV5EPcG3ZooF_u-BmNJCUp7R6BqqsdgOl5U30OC8,1342
+botocore/data/waf/2015-08-24/examples-1.json,sha256=6OPuCnLynJIfGO-Vxhb9ZZV9ktEKhpByvf2jSwAg-DY,29749
+botocore/data/waf/2015-08-24/paginators-1.json,sha256=ulE-ztimMiePJZAVUJkWb57N9b2OKV7xz_GIOHCw7PM,2717
+botocore/data/waf/2015-08-24/service-2.json.gz,sha256=NEX3C6CoF0U54K8xTOe8lofGKuojB5kzZdAPK4sUwtU,41724
+botocore/data/wafv2/2019-07-29/endpoint-rule-set-1.json.gz,sha256=Q2XrCC0AeUjEIgKQhFgsBwbW3LBn0RKD7RZ8CdWm2bY,1150
+botocore/data/wafv2/2019-07-29/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/wafv2/2019-07-29/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/wafv2/2019-07-29/service-2.json.gz,sha256=XDQafROwihWx1OVuQuihOmJDGjcr8gHhAk93SLWo9p0,71440
+botocore/data/wellarchitected/2020-03-31/endpoint-rule-set-1.json.gz,sha256=DJHPGMOJFIKu_55AbFHV0M57uPAQ04upiH5GIUQ8OGk,1156
+botocore/data/wellarchitected/2020-03-31/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/wellarchitected/2020-03-31/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/wellarchitected/2020-03-31/service-2.json.gz,sha256=_D8Tfvj6OzFUT1Y0rZNJMAsdw5Y9NFesiPN_f7bs8Jw,21125
+botocore/data/wisdom/2020-10-19/endpoint-rule-set-1.json.gz,sha256=PtKrr0VOWiKT8-xANejj6ZyE1ws1Pf5OO6fDlOpBTHI,1149
+botocore/data/wisdom/2020-10-19/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/wisdom/2020-10-19/paginators-1.json,sha256=Mrm9rH5_xIiJTb4YXQUznBuP7k3tAPM5lVtE0HpFnow,1778
+botocore/data/wisdom/2020-10-19/service-2.json.gz,sha256=3F2iFFVq-t6fUrjJthKALe0M4B4D_k55Am-CmnF04ik,17064
+botocore/data/workdocs/2016-05-01/endpoint-rule-set-1.json.gz,sha256=WrFsFaJvC3CGcfe9kncHQS4u4FLSDVoUwQSCYsFjlAg,1148
+botocore/data/workdocs/2016-05-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/workdocs/2016-05-01/paginators-1.json,sha256=PERmz7nK6Ur9t877K2ivamloNl6knySKpwgvjbOcUe0,1666
+botocore/data/workdocs/2016-05-01/service-2.json.gz,sha256=bCSSgGcKeCKkK2fa3TFjAemst1VD3j6H9Mn0LFOPnjo,16223
+botocore/data/worklink/2018-09-25/endpoint-rule-set-1.json.gz,sha256=_JbzUbiyoe98mBwb0hhPoVxwsYPb7JAR7FeeGKfTQZw,1148
+botocore/data/worklink/2018-09-25/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/worklink/2018-09-25/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/worklink/2018-09-25/service-2.json.gz,sha256=lPwmEr5YuWXQt9ExxlVt1boZF95ubYug5rELTLRI_90,6085
+botocore/data/workmail/2017-10-01/endpoint-rule-set-1.json.gz,sha256=J4z0desfuv2Gga_MKr3MbLsQRjksF757AI7JgklYcIg,1148
+botocore/data/workmail/2017-10-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/workmail/2017-10-01/paginators-1.json,sha256=cifXQJ4GwrUAiLzlD6767FJIzTi5ByaVVBEFAHLqCR0,1553
+botocore/data/workmail/2017-10-01/service-2.json.gz,sha256=mb6B2J1j3OluqfOFG87Pw8APg04SaZ_yr7ibpEdGeLk,24872
+botocore/data/workmailmessageflow/2019-05-01/endpoint-rule-set-1.json.gz,sha256=7Pk9aD_C8g62idtCfkZJBgD0Y3_juuGFQKKJtyVI_BI,1157
+botocore/data/workmailmessageflow/2019-05-01/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/workmailmessageflow/2019-05-01/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/workmailmessageflow/2019-05-01/service-2.json.gz,sha256=I4uPPXwt1OAiLmsrewjcKlUfnNrSK3p8lnX73toPf7Y,2272
+botocore/data/workspaces-thin-client/2023-08-22/endpoint-rule-set-1.json.gz,sha256=XILF6_jAyB-tyqtq7JCNaxtB1LF4br3tbnpSb52m-jc,1300
+botocore/data/workspaces-thin-client/2023-08-22/paginators-1.json,sha256=eoHZHYlG1VP49fqQ29q3I58cojJxkZ8AQQg_xOyd10Y,525
+botocore/data/workspaces-thin-client/2023-08-22/service-2.json.gz,sha256=aP4uS3dD_fXagCgZZisdjSZ-si2Lg8TG4oR_MVlkhGc,6427
+botocore/data/workspaces-web/2020-07-08/endpoint-rule-set-1.json.gz,sha256=JCUIf_fJ-9q2Yj-GryhlG04Hh1dGVTSEz1I-Opk8XVg,1156
+botocore/data/workspaces-web/2020-07-08/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/workspaces-web/2020-07-08/paginators-1.json,sha256=hIQ7AbLBsY4fPSNLVMg0dS45m6cjZKFTjbp3ZLh4zj8,23
+botocore/data/workspaces-web/2020-07-08/service-2.json.gz,sha256=N2NtdURYiqE3U6IfdDj_0ox1FkKaapONChaGxfU5Acg,12694
+botocore/data/workspaces/2015-04-08/endpoint-rule-set-1.json.gz,sha256=a4FRGt6czwpe2DczvMAAb-bmoCAcl5iSiaA68izaVK4,1152
+botocore/data/workspaces/2015-04-08/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/workspaces/2015-04-08/paginators-1.json,sha256=RiG7XOsbS-HVzRL_pYhhradEGVQnhRgv41nSgAAzABI,1504
+botocore/data/workspaces/2015-04-08/service-2.json.gz,sha256=xDClljG_zK_92QSygwJI_nNwWh0IdTrPQjzlPnK1t3Q,37597
+botocore/data/xray/2016-04-12/endpoint-rule-set-1.json.gz,sha256=MyoWa-n00nQFN7VrLbXBZnoEVG4beshn6XSsB6BBKu0,1145
+botocore/data/xray/2016-04-12/examples-1.json,sha256=K3b6mgYkitvcecSlJT-iV_EQATmvOySs66iKJI5qx0g,44
+botocore/data/xray/2016-04-12/paginators-1.json,sha256=2BXVUlpR51GRav7g4-ML3Fr7U9pBDqXax4lZYeJnwZU,1785
+botocore/data/xray/2016-04-12/service-2.json.gz,sha256=1Y1nLaFB4K79xP3CGb6BcQQoRf7HhJUcmG9gQ6N3zZU,17737
+botocore/discovery.py,sha256=uZNiWnwdh2t2sK8cdSNtqSJEl8GUeQ_P2whSHWsWh8U,11051
+botocore/docs/__init__.py,sha256=Mxx6eiy76-SxPpUsPMUPoHS-Wjy8Mj1gDfrowCu0S4U,2027
+botocore/docs/__pycache__/__init__.cpython-312.pyc,,
+botocore/docs/__pycache__/client.cpython-312.pyc,,
+botocore/docs/__pycache__/docstring.cpython-312.pyc,,
+botocore/docs/__pycache__/example.cpython-312.pyc,,
+botocore/docs/__pycache__/method.cpython-312.pyc,,
+botocore/docs/__pycache__/paginator.cpython-312.pyc,,
+botocore/docs/__pycache__/params.cpython-312.pyc,,
+botocore/docs/__pycache__/service.cpython-312.pyc,,
+botocore/docs/__pycache__/shape.cpython-312.pyc,,
+botocore/docs/__pycache__/sharedexample.cpython-312.pyc,,
+botocore/docs/__pycache__/translator.cpython-312.pyc,,
+botocore/docs/__pycache__/utils.cpython-312.pyc,,
+botocore/docs/__pycache__/waiter.cpython-312.pyc,,
+botocore/docs/bcdoc/__init__.py,sha256=V2g87AefB2DOD9_3xIF5k9Nv5ttb4_gNJOVvSF0Mp3s,588
+botocore/docs/bcdoc/__pycache__/__init__.cpython-312.pyc,,
+botocore/docs/bcdoc/__pycache__/docstringparser.cpython-312.pyc,,
+botocore/docs/bcdoc/__pycache__/restdoc.cpython-312.pyc,,
+botocore/docs/bcdoc/__pycache__/style.cpython-312.pyc,,
+botocore/docs/bcdoc/docstringparser.py,sha256=a-TefXFJysEog9AdTgTwa8Pzik9sxcFlbhQ83jGwFR0,10197
+botocore/docs/bcdoc/restdoc.py,sha256=DVu7-ttBmiugDXOeQkATgv4rAHZPU-sy5ZvmbaEx_Ao,9698
+botocore/docs/bcdoc/style.py,sha256=bNfbjquLLfEt9F6HH_g7FwMeCU9ZZJLNXN-spELATvY,13120
+botocore/docs/client.py,sha256=Ur2X-uoT6OMQ9WhxyZPmCi0OAe8ZFjjbj4GDeCqINX4,17321
+botocore/docs/docstring.py,sha256=Jo9lA4ZFPq75cNCUfpz7zWiXlDB-Cn3bP62cZvBntfA,3648
+botocore/docs/example.py,sha256=ZOCQpJ8irhZ-0Urf7VJkd0SwDeJ1WLvyJc-TMBCZ4Ho,8945
+botocore/docs/method.py,sha256=_GiyG_2GV1uNOCq2e5MEEc300Wk3rACROLUAAxxay7s,12058
+botocore/docs/paginator.py,sha256=QlDyldRiPT8JyNpeUIyx9rsZoy7GsCYGVY-o16oCGkY,8988
+botocore/docs/params.py,sha256=44R0OTnlWoCHZmcfhtyQ5ifTYD7q2SaqfVoCkw6BRhU,11750
+botocore/docs/service.py,sha256=oSPLoXn08cSsFDToEuFp4DDOl_COuSYQJ7q7OEf2lfg,4990
+botocore/docs/shape.py,sha256=EZze3L3AhPNnx_iHvRtn2Z-04TbMHTZ2_okdpAmwPOc,5198
+botocore/docs/sharedexample.py,sha256=WrAklim6mYWOgqcm9qmm5ajXpr2FqIGNuXnmlQrmNiU,9202
+botocore/docs/translator.py,sha256=v9ZTifRrwmfxBHCBaRPoZqufvpHI31pdVMny1wcVi-4,2331
+botocore/docs/utils.py,sha256=Ukqkmy8ncFE8JjwDaj4cLyQ4-yroos6RSixykh3bW5w,7301
+botocore/docs/waiter.py,sha256=vSTYQBfP7OuAfObuS9wXrkHuYYnN0Hk6-exVyQw0DB4,6548
+botocore/endpoint.py,sha256=3tdIYsKLzb6v7yQipH-2P61sKIRCU4HhGoxIqSTePd0,16367
+botocore/endpoint_provider.py,sha256=VIlGc0ESVEEsZQy3EV7YtNqCeUVRCXZLJgoMtaK47tY,22933
+botocore/errorfactory.py,sha256=hdrxsOw0ihpT540ukWPbWqc7D-Dxe_la5H-ZcXgdLJ4,3722
+botocore/eventstream.py,sha256=rBj3nrNTFIMXTMdYRMTf1aSl7NKK9HrzJTpaSL9u5v4,20275
+botocore/exceptions.py,sha256=_tSLf2bnoeWR3GEpImMxwuKpJ9AxCF1BkzeUBuTFD9A,22804
+botocore/handlers.py,sha256=TenG6N9nF-OHB9tjLtKsEm-dOcCz5Qsp-TlftQnI2sg,55254
+botocore/history.py,sha256=QR1WnpJYTo02Rz3GqWt45sF6wzu6EQrM_kS3FPH58t4,1744
+botocore/hooks.py,sha256=tbfLatufIrqqq_aR92ikHOrqt9xTa4Ev7qVODdfhTSg,25050
+botocore/httpchecksum.py,sha256=uvgk0WGRImqXDuAt-eiQEWpVYkKj-pt9p8WoHG9Rs5k,16239
+botocore/httpsession.py,sha256=8OC5Zk5a9j-3YoAMy60XcI6PnNl6DKjCD67pq4pHFX4,18582
+botocore/loaders.py,sha256=80fw6XvffeFplFwugoqB-3yeAum55FpunxRY2pohk9I,18834
+botocore/model.py,sha256=cdEgIwcykvRbeXn08Eo5rSATkz9iN6iJ32RyCsjh3i4,30620
+botocore/monitoring.py,sha256=mJ_IWoqSjjaUElVTkbQ62OrNl8RGspgO6C7OUerz3vU,20597
+botocore/paginate.py,sha256=q0FXPXV9AtsPCKMOVOudNJMV1MKEHdgl3tMAO6JUmcw,27382
+botocore/parsers.py,sha256=1Zl9sgx76_zC3Y6VQGzgkZbjiY_Nqw3QaDHDSCctYSU,45583
+botocore/regions.py,sha256=GK1e3wGRRGf3qHnQRVH39VXYsEsC5wJQ_NP_GTmO-c8,32399
+botocore/response.py,sha256=atPfv1M3_H6i0bLrtfyrDNWJ7HMd0snserEP8ggo6wA,7227
+botocore/retries/__init__.py,sha256=YaZ6AwMRyuDBs5fOvl-PAvxQxZE2RBlcad2JmLOMo8k,121
+botocore/retries/__pycache__/__init__.cpython-312.pyc,,
+botocore/retries/__pycache__/adaptive.cpython-312.pyc,,
+botocore/retries/__pycache__/base.cpython-312.pyc,,
+botocore/retries/__pycache__/bucket.cpython-312.pyc,,
+botocore/retries/__pycache__/quota.cpython-312.pyc,,
+botocore/retries/__pycache__/special.cpython-312.pyc,,
+botocore/retries/__pycache__/standard.cpython-312.pyc,,
+botocore/retries/__pycache__/throttling.cpython-312.pyc,,
+botocore/retries/adaptive.py,sha256=0Y0QjSgK0sGS1nbWZV7wiBZgR82a-nA-vL5HjQadLOs,4207
+botocore/retries/base.py,sha256=rGJYVZEXLGSQ2BnaIT-W9ccGtSbIMvU-wzmV78d-Ccg,797
+botocore/retries/bucket.py,sha256=sZ5TGGMCyIh3qcRI5p5csNXPvtXZ2nC-sH6u5wJiIac,3994
+botocore/retries/quota.py,sha256=bijUNfy2fejuma0OB43sIn57OdRDhLSdFV_xCmcuTv4,1937
+botocore/retries/special.py,sha256=hePuqKytb0BmjKmfXbMf4fGrwiArbqu_HBepROpmi7U,1664
+botocore/retries/standard.py,sha256=Fu8JHmH-sCvS2aSMf1QjLXcoX6CQ3zUQoNt-AUXcx70,19971
+botocore/retries/throttling.py,sha256=x8pU_jMyapr0YODg8mtyYoXa8MzDAf0e-bWg9EfkFos,1779
+botocore/retryhandler.py,sha256=0cmoBCE3aIo60-co7idqO9FA2pIcyoI3NcDD_EHv0Yw,14702
+botocore/serialize.py,sha256=JIBkf2XpvVhozXT7wxJgBef0VIACy6Oz-pN1jTN-lz4,33113
+botocore/session.py,sha256=FmfjrZsBl4xOx0fcjhoDobIK69NJwucpcUWZxOqsk58,49180
+botocore/signers.py,sha256=RNLL21hMlPR3FmeS9r1okDHf__H9yPxvcbhv4YoSLP8,30881
+botocore/stub.py,sha256=fXBb5dVqCRiO-MWWE1mCjY73XVdivcSdBBdDDOR8t-Y,15242
+botocore/tokens.py,sha256=jIPsJFroiCxQ6Qwxjq0AJ9_aySiv3acq9mEFXfOjbrc,10910
+botocore/translate.py,sha256=UfKIIWr_BAcwvMScHuqrLtSD5yuXecl7Rs0Et3jfREc,3406
+botocore/useragent.py,sha256=_6wsPJO_fDknh1Ff6sOyO9w_NMzphh_0lwNGPpGHPuE,18306
+botocore/utils.py,sha256=qPcgWvYmEAnkSXHu-6_-K-ZwmoRjQ6zXPOI_xpPmhC4,134776
+botocore/validate.py,sha256=wV9gWmqHWy8j-34WeyET_baWgSDy-V7GU7Y_AyU8xZw,13765
+botocore/vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+botocore/vendored/__pycache__/__init__.cpython-312.pyc,,
+botocore/vendored/__pycache__/six.cpython-312.pyc,,
+botocore/vendored/requests/__init__.py,sha256=Pu8JNWAMzj9l8E0Qs4rU7clTOfxVIA6OuUKJkJDmvvc,227
+botocore/vendored/requests/__pycache__/__init__.cpython-312.pyc,,
+botocore/vendored/requests/__pycache__/exceptions.cpython-312.pyc,,
+botocore/vendored/requests/exceptions.py,sha256=zZhHieXgR1teqbvuo_9OrwDMHnrvRtulW97VfzumQv4,2517
+botocore/vendored/requests/packages/__init__.py,sha256=aXkbNCjM_WhryRBocE4AaA_p7-CTxL5LOutY7XzKm4s,62
+botocore/vendored/requests/packages/__pycache__/__init__.cpython-312.pyc,,
+botocore/vendored/requests/packages/urllib3/__init__.py,sha256=Nrq2HJOk0McF4saJ5zySsjVKGPV6j05iAFTJwkKEzOI,184
+botocore/vendored/requests/packages/urllib3/__pycache__/__init__.cpython-312.pyc,,
+botocore/vendored/requests/packages/urllib3/__pycache__/exceptions.cpython-312.pyc,,
+botocore/vendored/requests/packages/urllib3/exceptions.py,sha256=za-cEwBqxBKOqqKTaIVAMdH3j1nDRqi-MtdojdpU4Wc,4374
+botocore/vendored/six.py,sha256=TOOfQi7nFGfMrIvtdr6wX4wyHH8M7aknmuLfo2cBBrM,34549
+botocore/waiter.py,sha256=swukYbfJ2zCuZ1SAgb0i39oACfuaiysaFTx7miwxSBU,14517
diff --git a/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/WHEEL b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/WHEEL
new file mode 100644
index 0000000..5bad85f
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/top_level.txt
new file mode 100644
index 0000000..c5b9e12
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore-1.34.162.dist-info/top_level.txt
@@ -0,0 +1 @@
+botocore
diff --git a/venv/lib/python3.12/site-packages/botocore/__init__.py b/venv/lib/python3.12/site-packages/botocore/__init__.py
new file mode 100644
index 0000000..46e292e
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore/__init__.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
+# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+
+import logging
+import os
+import re
+
+__version__ = '1.34.162'
+
+
+class NullHandler(logging.Handler):
+ def emit(self, record):
+ pass
+
+
+# Configure default logger to do nothing
+log = logging.getLogger('botocore')
+log.addHandler(NullHandler())
+
+_INITIALIZERS = []
+
+_first_cap_regex = re.compile('(.)([A-Z][a-z]+)')
+_end_cap_regex = re.compile('([a-z0-9])([A-Z])')
+# The regex below handles the special case where some acronym
+# name is pluralized, e.g GatewayARNs, ListWebACLs, SomeCNAMEs.
+_special_case_transform = re.compile('[A-Z]{2,}s$')
+# Prepopulate the cache with special cases that don't match
+# our regular transformation.
+_xform_cache = {
+ ('CreateCachediSCSIVolume', '_'): 'create_cached_iscsi_volume',
+ ('CreateCachediSCSIVolume', '-'): 'create-cached-iscsi-volume',
+ ('DescribeCachediSCSIVolumes', '_'): 'describe_cached_iscsi_volumes',
+ ('DescribeCachediSCSIVolumes', '-'): 'describe-cached-iscsi-volumes',
+ ('DescribeStorediSCSIVolumes', '_'): 'describe_stored_iscsi_volumes',
+ ('DescribeStorediSCSIVolumes', '-'): 'describe-stored-iscsi-volumes',
+ ('CreateStorediSCSIVolume', '_'): 'create_stored_iscsi_volume',
+ ('CreateStorediSCSIVolume', '-'): 'create-stored-iscsi-volume',
+ ('ListHITsForQualificationType', '_'): 'list_hits_for_qualification_type',
+ ('ListHITsForQualificationType', '-'): 'list-hits-for-qualification-type',
+ ('ExecutePartiQLStatement', '_'): 'execute_partiql_statement',
+ ('ExecutePartiQLStatement', '-'): 'execute-partiql-statement',
+ ('ExecutePartiQLTransaction', '_'): 'execute_partiql_transaction',
+ ('ExecutePartiQLTransaction', '-'): 'execute-partiql-transaction',
+ ('ExecutePartiQLBatch', '_'): 'execute_partiql_batch',
+ ('ExecutePartiQLBatch', '-'): 'execute-partiql-batch',
+}
+# The items in this dict represent partial renames to apply globally to all
+# services which might have a matching argument or operation. This way a
+# common mis-translation can be fixed without having to call out each
+# individual case.
+ScalarTypes = ('string', 'integer', 'boolean', 'timestamp', 'float', 'double')
+
+BOTOCORE_ROOT = os.path.dirname(os.path.abspath(__file__))
+
+
+# Used to specify anonymous (unsigned) request signature
+class UNSIGNED:
+ def __copy__(self):
+ return self
+
+ def __deepcopy__(self, memodict):
+ return self
+
+
+UNSIGNED = UNSIGNED()
+
+
+def xform_name(name, sep='_', _xform_cache=_xform_cache):
+ """Convert camel case to a "pythonic" name.
+
+ If the name contains the ``sep`` character, then it is
+ returned unchanged.
+
+ """
+ if sep in name:
+ # If the sep is in the name, assume that it's already
+ # transformed and return the string unchanged.
+ return name
+ key = (name, sep)
+ if key not in _xform_cache:
+ if _special_case_transform.search(name) is not None:
+ is_special = _special_case_transform.search(name)
+ matched = is_special.group()
+ # Replace something like ARNs, ACLs with _arns, _acls.
+ name = f"{name[: -len(matched)]}{sep}{matched.lower()}"
+ s1 = _first_cap_regex.sub(r'\1' + sep + r'\2', name)
+ transformed = _end_cap_regex.sub(r'\1' + sep + r'\2', s1).lower()
+ _xform_cache[key] = transformed
+ return _xform_cache[key]
+
+
+def register_initializer(callback):
+ """Register an initializer function for session creation.
+
+ This initializer function will be invoked whenever a new
+ `botocore.session.Session` is instantiated.
+
+ :type callback: callable
+ :param callback: A callable that accepts a single argument
+ of type `botocore.session.Session`.
+
+ """
+ _INITIALIZERS.append(callback)
+
+
+def unregister_initializer(callback):
+ """Unregister an initializer function.
+
+ :type callback: callable
+ :param callback: A callable that was previously registered
+ with `botocore.register_initializer`.
+
+ :raises ValueError: If a callback is provided that is not currently
+ registered as an initializer.
+
+ """
+ _INITIALIZERS.remove(callback)
+
+
+def invoke_initializers(session):
+ """Invoke all initializers for a session.
+
+ :type session: botocore.session.Session
+ :param session: The session to initialize.
+
+ """
+ for initializer in _INITIALIZERS:
+ initializer(session)
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..50fda3f
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/__init__.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/args.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/args.cpython-312.pyc
new file mode 100644
index 0000000..dcc6430
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/args.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/auth.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/auth.cpython-312.pyc
new file mode 100644
index 0000000..28e2312
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/auth.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/awsrequest.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/awsrequest.cpython-312.pyc
new file mode 100644
index 0000000..6ad0adb
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/awsrequest.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/client.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/client.cpython-312.pyc
new file mode 100644
index 0000000..d447633
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/client.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/compat.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/compat.cpython-312.pyc
new file mode 100644
index 0000000..7fd16cd
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/compat.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/compress.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/compress.cpython-312.pyc
new file mode 100644
index 0000000..56c9bf5
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/compress.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/config.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/config.cpython-312.pyc
new file mode 100644
index 0000000..5a38017
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/config.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/configloader.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/configloader.cpython-312.pyc
new file mode 100644
index 0000000..ae92fc2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/configloader.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/configprovider.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/configprovider.cpython-312.pyc
new file mode 100644
index 0000000..814018a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/configprovider.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/credentials.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/credentials.cpython-312.pyc
new file mode 100644
index 0000000..71e5632
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/credentials.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/discovery.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/discovery.cpython-312.pyc
new file mode 100644
index 0000000..17561b3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/discovery.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/endpoint.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/endpoint.cpython-312.pyc
new file mode 100644
index 0000000..2643dc3
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/endpoint.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/endpoint_provider.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/endpoint_provider.cpython-312.pyc
new file mode 100644
index 0000000..fdf984b
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/endpoint_provider.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/errorfactory.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/errorfactory.cpython-312.pyc
new file mode 100644
index 0000000..c1a201c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/errorfactory.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/eventstream.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/eventstream.cpython-312.pyc
new file mode 100644
index 0000000..7523511
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/eventstream.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/exceptions.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/exceptions.cpython-312.pyc
new file mode 100644
index 0000000..19460e2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/exceptions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/handlers.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/handlers.cpython-312.pyc
new file mode 100644
index 0000000..f7e3dfc
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/handlers.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/history.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/history.cpython-312.pyc
new file mode 100644
index 0000000..9987803
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/history.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/hooks.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/hooks.cpython-312.pyc
new file mode 100644
index 0000000..5295452
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/hooks.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/httpchecksum.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/httpchecksum.cpython-312.pyc
new file mode 100644
index 0000000..101e2ad
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/httpchecksum.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/httpsession.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/httpsession.cpython-312.pyc
new file mode 100644
index 0000000..943e7be
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/httpsession.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/loaders.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/loaders.cpython-312.pyc
new file mode 100644
index 0000000..330310a
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/loaders.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/model.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/model.cpython-312.pyc
new file mode 100644
index 0000000..30984d6
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/model.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/monitoring.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/monitoring.cpython-312.pyc
new file mode 100644
index 0000000..0baacc8
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/monitoring.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/paginate.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/paginate.cpython-312.pyc
new file mode 100644
index 0000000..88cadab
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/paginate.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/parsers.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/parsers.cpython-312.pyc
new file mode 100644
index 0000000..98c1b74
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/parsers.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/regions.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/regions.cpython-312.pyc
new file mode 100644
index 0000000..54a9c07
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/regions.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/response.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/response.cpython-312.pyc
new file mode 100644
index 0000000..4731204
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/response.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/retryhandler.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/retryhandler.cpython-312.pyc
new file mode 100644
index 0000000..6a35943
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/retryhandler.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/serialize.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/serialize.cpython-312.pyc
new file mode 100644
index 0000000..28d63a2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/serialize.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/session.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/session.cpython-312.pyc
new file mode 100644
index 0000000..0e37b72
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/session.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/signers.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/signers.cpython-312.pyc
new file mode 100644
index 0000000..00a6986
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/signers.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/stub.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/stub.cpython-312.pyc
new file mode 100644
index 0000000..21565ef
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/stub.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/tokens.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/tokens.cpython-312.pyc
new file mode 100644
index 0000000..9f989dc
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/tokens.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/translate.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/translate.cpython-312.pyc
new file mode 100644
index 0000000..bf6b5ba
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/translate.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/useragent.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/useragent.cpython-312.pyc
new file mode 100644
index 0000000..07723ae
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/useragent.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/utils.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/utils.cpython-312.pyc
new file mode 100644
index 0000000..258e3d2
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/utils.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/validate.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/validate.cpython-312.pyc
new file mode 100644
index 0000000..a7bf936
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/validate.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/__pycache__/waiter.cpython-312.pyc b/venv/lib/python3.12/site-packages/botocore/__pycache__/waiter.cpython-312.pyc
new file mode 100644
index 0000000..1a5cc6c
Binary files /dev/null and b/venv/lib/python3.12/site-packages/botocore/__pycache__/waiter.cpython-312.pyc differ
diff --git a/venv/lib/python3.12/site-packages/botocore/args.py b/venv/lib/python3.12/site-packages/botocore/args.py
new file mode 100644
index 0000000..758a3c3
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore/args.py
@@ -0,0 +1,770 @@
+# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+"""Internal module to help with normalizing botocore client args.
+
+This module (and all function/classes within this module) should be
+considered internal, and *not* a public API.
+
+"""
+
+import copy
+import logging
+import socket
+
+import botocore.exceptions
+import botocore.parsers
+import botocore.serialize
+from botocore.config import Config
+from botocore.endpoint import EndpointCreator
+from botocore.regions import EndpointResolverBuiltins as EPRBuiltins
+from botocore.regions import EndpointRulesetResolver
+from botocore.signers import RequestSigner
+from botocore.useragent import UserAgentString
+from botocore.utils import ensure_boolean, is_s3_accelerate_url
+
+logger = logging.getLogger(__name__)
+
+
+VALID_REGIONAL_ENDPOINTS_CONFIG = [
+ 'legacy',
+ 'regional',
+]
+LEGACY_GLOBAL_STS_REGIONS = [
+ 'ap-northeast-1',
+ 'ap-south-1',
+ 'ap-southeast-1',
+ 'ap-southeast-2',
+ 'aws-global',
+ 'ca-central-1',
+ 'eu-central-1',
+ 'eu-north-1',
+ 'eu-west-1',
+ 'eu-west-2',
+ 'eu-west-3',
+ 'sa-east-1',
+ 'us-east-1',
+ 'us-east-2',
+ 'us-west-1',
+ 'us-west-2',
+]
+# Maximum allowed length of the ``user_agent_appid`` config field. Longer
+# values result in a warning-level log message.
+USERAGENT_APPID_MAXLEN = 50
+
+
+class ClientArgsCreator:
+ def __init__(
+ self,
+ event_emitter,
+ user_agent,
+ response_parser_factory,
+ loader,
+ exceptions_factory,
+ config_store,
+ user_agent_creator=None,
+ ):
+ self._event_emitter = event_emitter
+ self._response_parser_factory = response_parser_factory
+ self._loader = loader
+ self._exceptions_factory = exceptions_factory
+ self._config_store = config_store
+ if user_agent_creator is None:
+ self._session_ua_creator = UserAgentString.from_environment()
+ else:
+ self._session_ua_creator = user_agent_creator
+
+ def get_client_args(
+ self,
+ service_model,
+ region_name,
+ is_secure,
+ endpoint_url,
+ verify,
+ credentials,
+ scoped_config,
+ client_config,
+ endpoint_bridge,
+ auth_token=None,
+ endpoints_ruleset_data=None,
+ partition_data=None,
+ ):
+ final_args = self.compute_client_args(
+ service_model,
+ client_config,
+ endpoint_bridge,
+ region_name,
+ endpoint_url,
+ is_secure,
+ scoped_config,
+ )
+
+ service_name = final_args['service_name'] # noqa
+ parameter_validation = final_args['parameter_validation']
+ endpoint_config = final_args['endpoint_config']
+ protocol = final_args['protocol']
+ config_kwargs = final_args['config_kwargs']
+ s3_config = final_args['s3_config']
+ partition = endpoint_config['metadata'].get('partition', None)
+ socket_options = final_args['socket_options']
+ configured_endpoint_url = final_args['configured_endpoint_url']
+ signing_region = endpoint_config['signing_region']
+ endpoint_region_name = endpoint_config['region_name']
+
+ event_emitter = copy.copy(self._event_emitter)
+ signer = RequestSigner(
+ service_model.service_id,
+ signing_region,
+ endpoint_config['signing_name'],
+ endpoint_config['signature_version'],
+ credentials,
+ event_emitter,
+ auth_token,
+ )
+
+ config_kwargs['s3'] = s3_config
+ new_config = Config(**config_kwargs)
+ endpoint_creator = EndpointCreator(event_emitter)
+
+ endpoint = endpoint_creator.create_endpoint(
+ service_model,
+ region_name=endpoint_region_name,
+ endpoint_url=endpoint_config['endpoint_url'],
+ verify=verify,
+ response_parser_factory=self._response_parser_factory,
+ max_pool_connections=new_config.max_pool_connections,
+ proxies=new_config.proxies,
+ timeout=(new_config.connect_timeout, new_config.read_timeout),
+ socket_options=socket_options,
+ client_cert=new_config.client_cert,
+ proxies_config=new_config.proxies_config,
+ )
+
+ serializer = botocore.serialize.create_serializer(
+ protocol, parameter_validation
+ )
+ response_parser = botocore.parsers.create_parser(protocol)
+
+ ruleset_resolver = self._build_endpoint_resolver(
+ endpoints_ruleset_data,
+ partition_data,
+ client_config,
+ service_model,
+ endpoint_region_name,
+ region_name,
+ configured_endpoint_url,
+ endpoint,
+ is_secure,
+ endpoint_bridge,
+ event_emitter,
+ )
+
+ # Copy the session's user agent factory and adds client configuration.
+ client_ua_creator = self._session_ua_creator.with_client_config(
+ new_config
+ )
+ supplied_ua = client_config.user_agent if client_config else None
+ new_config._supplied_user_agent = supplied_ua
+
+ return {
+ 'serializer': serializer,
+ 'endpoint': endpoint,
+ 'response_parser': response_parser,
+ 'event_emitter': event_emitter,
+ 'request_signer': signer,
+ 'service_model': service_model,
+ 'loader': self._loader,
+ 'client_config': new_config,
+ 'partition': partition,
+ 'exceptions_factory': self._exceptions_factory,
+ 'endpoint_ruleset_resolver': ruleset_resolver,
+ 'user_agent_creator': client_ua_creator,
+ }
+
+ def compute_client_args(
+ self,
+ service_model,
+ client_config,
+ endpoint_bridge,
+ region_name,
+ endpoint_url,
+ is_secure,
+ scoped_config,
+ ):
+ service_name = service_model.endpoint_prefix
+ protocol = service_model.metadata['protocol']
+ parameter_validation = True
+ if client_config and not client_config.parameter_validation:
+ parameter_validation = False
+ elif scoped_config:
+ raw_value = scoped_config.get('parameter_validation')
+ if raw_value is not None:
+ parameter_validation = ensure_boolean(raw_value)
+
+ s3_config = self.compute_s3_config(client_config)
+
+ configured_endpoint_url = self._compute_configured_endpoint_url(
+ client_config=client_config,
+ endpoint_url=endpoint_url,
+ )
+
+ endpoint_config = self._compute_endpoint_config(
+ service_name=service_name,
+ region_name=region_name,
+ endpoint_url=configured_endpoint_url,
+ is_secure=is_secure,
+ endpoint_bridge=endpoint_bridge,
+ s3_config=s3_config,
+ )
+ endpoint_variant_tags = endpoint_config['metadata'].get('tags', [])
+
+ # Some third-party libraries expect the final user-agent string in
+ # ``client.meta.config.user_agent``. To maintain backwards
+ # compatibility, the preliminary user-agent string (before any Config
+ # object modifications and without request-specific user-agent
+ # components) is stored in the new Config object's ``user_agent``
+ # property but not used by Botocore itself.
+ preliminary_ua_string = self._session_ua_creator.with_client_config(
+ client_config
+ ).to_string()
+ # Create a new client config to be passed to the client based
+ # on the final values. We do not want the user to be able
+ # to try to modify an existing client with a client config.
+ config_kwargs = dict(
+ region_name=endpoint_config['region_name'],
+ signature_version=endpoint_config['signature_version'],
+ user_agent=preliminary_ua_string,
+ )
+ if 'dualstack' in endpoint_variant_tags:
+ config_kwargs.update(use_dualstack_endpoint=True)
+ if 'fips' in endpoint_variant_tags:
+ config_kwargs.update(use_fips_endpoint=True)
+ if client_config is not None:
+ config_kwargs.update(
+ connect_timeout=client_config.connect_timeout,
+ read_timeout=client_config.read_timeout,
+ max_pool_connections=client_config.max_pool_connections,
+ proxies=client_config.proxies,
+ proxies_config=client_config.proxies_config,
+ retries=client_config.retries,
+ client_cert=client_config.client_cert,
+ inject_host_prefix=client_config.inject_host_prefix,
+ tcp_keepalive=client_config.tcp_keepalive,
+ user_agent_extra=client_config.user_agent_extra,
+ user_agent_appid=client_config.user_agent_appid,
+ request_min_compression_size_bytes=(
+ client_config.request_min_compression_size_bytes
+ ),
+ disable_request_compression=(
+ client_config.disable_request_compression
+ ),
+ client_context_params=client_config.client_context_params,
+ )
+ self._compute_retry_config(config_kwargs)
+ self._compute_connect_timeout(config_kwargs)
+ self._compute_user_agent_appid_config(config_kwargs)
+ self._compute_request_compression_config(config_kwargs)
+ s3_config = self.compute_s3_config(client_config)
+
+ is_s3_service = self._is_s3_service(service_name)
+
+ if is_s3_service and 'dualstack' in endpoint_variant_tags:
+ if s3_config is None:
+ s3_config = {}
+ s3_config['use_dualstack_endpoint'] = True
+
+ return {
+ 'service_name': service_name,
+ 'parameter_validation': parameter_validation,
+ 'configured_endpoint_url': configured_endpoint_url,
+ 'endpoint_config': endpoint_config,
+ 'protocol': protocol,
+ 'config_kwargs': config_kwargs,
+ 's3_config': s3_config,
+ 'socket_options': self._compute_socket_options(
+ scoped_config, client_config
+ ),
+ }
+
+ def _compute_configured_endpoint_url(self, client_config, endpoint_url):
+ if endpoint_url is not None:
+ return endpoint_url
+
+ if self._ignore_configured_endpoint_urls(client_config):
+ logger.debug("Ignoring configured endpoint URLs.")
+ return endpoint_url
+
+ return self._config_store.get_config_variable('endpoint_url')
+
+ def _ignore_configured_endpoint_urls(self, client_config):
+ if (
+ client_config
+ and client_config.ignore_configured_endpoint_urls is not None
+ ):
+ return client_config.ignore_configured_endpoint_urls
+
+ return self._config_store.get_config_variable(
+ 'ignore_configured_endpoint_urls'
+ )
+
+ def compute_s3_config(self, client_config):
+ s3_configuration = self._config_store.get_config_variable('s3')
+
+ # Next specific client config values takes precedence over
+ # specific values in the scoped config.
+ if client_config is not None:
+ if client_config.s3 is not None:
+ if s3_configuration is None:
+ s3_configuration = client_config.s3
+ else:
+ # The current s3_configuration dictionary may be
+ # from a source that only should be read from so
+ # we want to be safe and just make a copy of it to modify
+ # before it actually gets updated.
+ s3_configuration = s3_configuration.copy()
+ s3_configuration.update(client_config.s3)
+
+ return s3_configuration
+
+ def _is_s3_service(self, service_name):
+ """Whether the service is S3 or S3 Control.
+
+ Note that throughout this class, service_name refers to the endpoint
+ prefix, not the folder name of the service in botocore/data. For
+ S3 Control, the folder name is 's3control' but the endpoint prefix is
+ 's3-control'.
+ """
+ return service_name in ['s3', 's3-control']
+
+ def _compute_endpoint_config(
+ self,
+ service_name,
+ region_name,
+ endpoint_url,
+ is_secure,
+ endpoint_bridge,
+ s3_config,
+ ):
+ resolve_endpoint_kwargs = {
+ 'service_name': service_name,
+ 'region_name': region_name,
+ 'endpoint_url': endpoint_url,
+ 'is_secure': is_secure,
+ 'endpoint_bridge': endpoint_bridge,
+ }
+ if service_name == 's3':
+ return self._compute_s3_endpoint_config(
+ s3_config=s3_config, **resolve_endpoint_kwargs
+ )
+ if service_name == 'sts':
+ return self._compute_sts_endpoint_config(**resolve_endpoint_kwargs)
+ return self._resolve_endpoint(**resolve_endpoint_kwargs)
+
+ def _compute_s3_endpoint_config(
+ self, s3_config, **resolve_endpoint_kwargs
+ ):
+ force_s3_global = self._should_force_s3_global(
+ resolve_endpoint_kwargs['region_name'], s3_config
+ )
+ if force_s3_global:
+ resolve_endpoint_kwargs['region_name'] = None
+ endpoint_config = self._resolve_endpoint(**resolve_endpoint_kwargs)
+ self._set_region_if_custom_s3_endpoint(
+ endpoint_config, resolve_endpoint_kwargs['endpoint_bridge']
+ )
+ # For backwards compatibility reasons, we want to make sure the
+ # client.meta.region_name will remain us-east-1 if we forced the
+ # endpoint to be the global region. Specifically, if this value
+ # changes to aws-global, it breaks logic where a user is checking
+ # for us-east-1 as the global endpoint such as in creating buckets.
+ if force_s3_global and endpoint_config['region_name'] == 'aws-global':
+ endpoint_config['region_name'] = 'us-east-1'
+ return endpoint_config
+
+ def _should_force_s3_global(self, region_name, s3_config):
+ s3_regional_config = 'legacy'
+ if s3_config and 'us_east_1_regional_endpoint' in s3_config:
+ s3_regional_config = s3_config['us_east_1_regional_endpoint']
+ self._validate_s3_regional_config(s3_regional_config)
+
+ is_global_region = region_name in ('us-east-1', None)
+ return s3_regional_config == 'legacy' and is_global_region
+
+ def _validate_s3_regional_config(self, config_val):
+ if config_val not in VALID_REGIONAL_ENDPOINTS_CONFIG:
+ raise botocore.exceptions.InvalidS3UsEast1RegionalEndpointConfigError(
+ s3_us_east_1_regional_endpoint_config=config_val
+ )
+
+ def _set_region_if_custom_s3_endpoint(
+ self, endpoint_config, endpoint_bridge
+ ):
+ # If a user is providing a custom URL, the endpoint resolver will
+ # refuse to infer a signing region. If we want to default to s3v4,
+ # we have to account for this.
+ if (
+ endpoint_config['signing_region'] is None
+ and endpoint_config['region_name'] is None
+ ):
+ endpoint = endpoint_bridge.resolve('s3')
+ endpoint_config['signing_region'] = endpoint['signing_region']
+ endpoint_config['region_name'] = endpoint['region_name']
+
+ def _compute_sts_endpoint_config(self, **resolve_endpoint_kwargs):
+ endpoint_config = self._resolve_endpoint(**resolve_endpoint_kwargs)
+ if self._should_set_global_sts_endpoint(
+ resolve_endpoint_kwargs['region_name'],
+ resolve_endpoint_kwargs['endpoint_url'],
+ endpoint_config,
+ ):
+ self._set_global_sts_endpoint(
+ endpoint_config, resolve_endpoint_kwargs['is_secure']
+ )
+ return endpoint_config
+
+ def _should_set_global_sts_endpoint(
+ self, region_name, endpoint_url, endpoint_config
+ ):
+ has_variant_tags = endpoint_config and endpoint_config.get(
+ 'metadata', {}
+ ).get('tags')
+ if endpoint_url or has_variant_tags:
+ return False
+ return (
+ self._get_sts_regional_endpoints_config() == 'legacy'
+ and region_name in LEGACY_GLOBAL_STS_REGIONS
+ )
+
+ def _get_sts_regional_endpoints_config(self):
+ sts_regional_endpoints_config = self._config_store.get_config_variable(
+ 'sts_regional_endpoints'
+ )
+ if not sts_regional_endpoints_config:
+ sts_regional_endpoints_config = 'legacy'
+ if (
+ sts_regional_endpoints_config
+ not in VALID_REGIONAL_ENDPOINTS_CONFIG
+ ):
+ raise botocore.exceptions.InvalidSTSRegionalEndpointsConfigError(
+ sts_regional_endpoints_config=sts_regional_endpoints_config
+ )
+ return sts_regional_endpoints_config
+
+ def _set_global_sts_endpoint(self, endpoint_config, is_secure):
+ scheme = 'https' if is_secure else 'http'
+ endpoint_config['endpoint_url'] = f'{scheme}://sts.amazonaws.com'
+ endpoint_config['signing_region'] = 'us-east-1'
+
+ def _resolve_endpoint(
+ self,
+ service_name,
+ region_name,
+ endpoint_url,
+ is_secure,
+ endpoint_bridge,
+ ):
+ return endpoint_bridge.resolve(
+ service_name, region_name, endpoint_url, is_secure
+ )
+
+ def _compute_socket_options(self, scoped_config, client_config=None):
+ # This disables Nagle's algorithm and is the default socket options
+ # in urllib3.
+ socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
+ client_keepalive = client_config and client_config.tcp_keepalive
+ scoped_keepalive = scoped_config and self._ensure_boolean(
+ scoped_config.get("tcp_keepalive", False)
+ )
+ # Enables TCP Keepalive if specified in client config object or shared config file.
+ if client_keepalive or scoped_keepalive:
+ socket_options.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
+ return socket_options
+
+ def _compute_retry_config(self, config_kwargs):
+ self._compute_retry_max_attempts(config_kwargs)
+ self._compute_retry_mode(config_kwargs)
+
+ def _compute_retry_max_attempts(self, config_kwargs):
+ # There's a pre-existing max_attempts client config value that actually
+ # means max *retry* attempts. There's also a `max_attempts` we pull
+ # from the config store that means *total attempts*, which includes the
+ # intitial request. We can't change what `max_attempts` means in
+ # client config so we try to normalize everything to a new
+ # "total_max_attempts" variable. We ensure that after this, the only
+ # configuration for "max attempts" is the 'total_max_attempts' key.
+ # An explicitly provided max_attempts in the client config
+ # overrides everything.
+ retries = config_kwargs.get('retries')
+ if retries is not None:
+ if 'total_max_attempts' in retries:
+ retries.pop('max_attempts', None)
+ return
+ if 'max_attempts' in retries:
+ value = retries.pop('max_attempts')
+ # client config max_attempts means total retries so we
+ # have to add one for 'total_max_attempts' to account
+ # for the initial request.
+ retries['total_max_attempts'] = value + 1
+ return
+ # Otherwise we'll check the config store which checks env vars,
+ # config files, etc. There is no default value for max_attempts
+ # so if this returns None and we don't set a default value here.
+ max_attempts = self._config_store.get_config_variable('max_attempts')
+ if max_attempts is not None:
+ if retries is None:
+ retries = {}
+ config_kwargs['retries'] = retries
+ retries['total_max_attempts'] = max_attempts
+
+ def _compute_retry_mode(self, config_kwargs):
+ retries = config_kwargs.get('retries')
+ if retries is None:
+ retries = {}
+ config_kwargs['retries'] = retries
+ elif 'mode' in retries:
+ # If there's a retry mode explicitly set in the client config
+ # that overrides everything.
+ return
+ retry_mode = self._config_store.get_config_variable('retry_mode')
+ if retry_mode is None:
+ retry_mode = 'legacy'
+ retries['mode'] = retry_mode
+
+ def _compute_connect_timeout(self, config_kwargs):
+ # Checking if connect_timeout is set on the client config.
+ # If it is not, we check the config_store in case a
+ # non legacy default mode has been configured.
+ connect_timeout = config_kwargs.get('connect_timeout')
+ if connect_timeout is not None:
+ return
+ connect_timeout = self._config_store.get_config_variable(
+ 'connect_timeout'
+ )
+ if connect_timeout:
+ config_kwargs['connect_timeout'] = connect_timeout
+
+ def _compute_request_compression_config(self, config_kwargs):
+ min_size = config_kwargs.get('request_min_compression_size_bytes')
+ disabled = config_kwargs.get('disable_request_compression')
+ if min_size is None:
+ min_size = self._config_store.get_config_variable(
+ 'request_min_compression_size_bytes'
+ )
+ # conversion func is skipped so input validation must be done here
+ # regardless if the value is coming from the config store or the
+ # config object
+ min_size = self._validate_min_compression_size(min_size)
+ config_kwargs['request_min_compression_size_bytes'] = min_size
+
+ if disabled is None:
+ disabled = self._config_store.get_config_variable(
+ 'disable_request_compression'
+ )
+ else:
+ # if the user provided a value we must check if it's a boolean
+ disabled = ensure_boolean(disabled)
+ config_kwargs['disable_request_compression'] = disabled
+
+ def _validate_min_compression_size(self, min_size):
+ min_allowed_min_size = 1
+ max_allowed_min_size = 1048576
+ if min_size is not None:
+ error_msg_base = (
+ f'Invalid value "{min_size}" for '
+ 'request_min_compression_size_bytes.'
+ )
+ try:
+ min_size = int(min_size)
+ except (ValueError, TypeError):
+ msg = (
+ f'{error_msg_base} Value must be an integer. '
+ f'Received {type(min_size)} instead.'
+ )
+ raise botocore.exceptions.InvalidConfigError(error_msg=msg)
+ if not min_allowed_min_size <= min_size <= max_allowed_min_size:
+ msg = (
+ f'{error_msg_base} Value must be between '
+ f'{min_allowed_min_size} and {max_allowed_min_size}.'
+ )
+ raise botocore.exceptions.InvalidConfigError(error_msg=msg)
+
+ return min_size
+
+ def _ensure_boolean(self, val):
+ if isinstance(val, bool):
+ return val
+ else:
+ return val.lower() == 'true'
+
+ def _build_endpoint_resolver(
+ self,
+ endpoints_ruleset_data,
+ partition_data,
+ client_config,
+ service_model,
+ endpoint_region_name,
+ region_name,
+ endpoint_url,
+ endpoint,
+ is_secure,
+ endpoint_bridge,
+ event_emitter,
+ ):
+ if endpoints_ruleset_data is None:
+ return None
+
+ # The legacy EndpointResolver is global to the session, but
+ # EndpointRulesetResolver is service-specific. Builtins for
+ # EndpointRulesetResolver must not be derived from the legacy
+ # endpoint resolver's output, including final_args, s3_config,
+ # etc.
+ s3_config_raw = self.compute_s3_config(client_config) or {}
+ service_name_raw = service_model.endpoint_prefix
+ # Maintain complex logic for s3 and sts endpoints for backwards
+ # compatibility.
+ if service_name_raw in ['s3', 'sts'] or region_name is None:
+ eprv2_region_name = endpoint_region_name
+ else:
+ eprv2_region_name = region_name
+ resolver_builtins = self.compute_endpoint_resolver_builtin_defaults(
+ region_name=eprv2_region_name,
+ service_name=service_name_raw,
+ s3_config=s3_config_raw,
+ endpoint_bridge=endpoint_bridge,
+ client_endpoint_url=endpoint_url,
+ legacy_endpoint_url=endpoint.host,
+ )
+ # Client context params for s3 conflict with the available settings
+ # in the `s3` parameter on the `Config` object. If the same parameter
+ # is set in both places, the value in the `s3` parameter takes priority.
+ if client_config is not None:
+ client_context = client_config.client_context_params or {}
+ else:
+ client_context = {}
+ if self._is_s3_service(service_name_raw):
+ client_context.update(s3_config_raw)
+
+ sig_version = (
+ client_config.signature_version
+ if client_config is not None
+ else None
+ )
+ return EndpointRulesetResolver(
+ endpoint_ruleset_data=endpoints_ruleset_data,
+ partition_data=partition_data,
+ service_model=service_model,
+ builtins=resolver_builtins,
+ client_context=client_context,
+ event_emitter=event_emitter,
+ use_ssl=is_secure,
+ requested_auth_scheme=sig_version,
+ )
+
+ def compute_endpoint_resolver_builtin_defaults(
+ self,
+ region_name,
+ service_name,
+ s3_config,
+ endpoint_bridge,
+ client_endpoint_url,
+ legacy_endpoint_url,
+ ):
+ # EndpointRulesetResolver rulesets may accept an "SDK::Endpoint" as
+ # input. If the endpoint_url argument of create_client() is set, it
+ # always takes priority.
+ if client_endpoint_url:
+ given_endpoint = client_endpoint_url
+ # If an endpoints.json data file other than the one bundled within
+ # the botocore/data directory is used, the output of legacy
+ # endpoint resolution is provided to EndpointRulesetResolver.
+ elif not endpoint_bridge.resolver_uses_builtin_data():
+ given_endpoint = legacy_endpoint_url
+ else:
+ given_endpoint = None
+
+ # The endpoint rulesets differ from legacy botocore behavior in whether
+ # forcing path style addressing in incompatible situations raises an
+ # exception or silently ignores the config setting. The
+ # AWS_S3_FORCE_PATH_STYLE parameter is adjusted both here and for each
+ # operation so that the ruleset behavior is backwards compatible.
+ if s3_config.get('use_accelerate_endpoint', False):
+ force_path_style = False
+ elif client_endpoint_url is not None and not is_s3_accelerate_url(
+ client_endpoint_url
+ ):
+ force_path_style = s3_config.get('addressing_style') != 'virtual'
+ else:
+ force_path_style = s3_config.get('addressing_style') == 'path'
+
+ return {
+ EPRBuiltins.AWS_REGION: region_name,
+ EPRBuiltins.AWS_USE_FIPS: (
+ # SDK_ENDPOINT cannot be combined with AWS_USE_FIPS
+ given_endpoint is None
+ # use legacy resolver's _resolve_endpoint_variant_config_var()
+ # or default to False if it returns None
+ and endpoint_bridge._resolve_endpoint_variant_config_var(
+ 'use_fips_endpoint'
+ )
+ or False
+ ),
+ EPRBuiltins.AWS_USE_DUALSTACK: (
+ # SDK_ENDPOINT cannot be combined with AWS_USE_DUALSTACK
+ given_endpoint is None
+ # use legacy resolver's _resolve_use_dualstack_endpoint() and
+ # or default to False if it returns None
+ and endpoint_bridge._resolve_use_dualstack_endpoint(
+ service_name
+ )
+ or False
+ ),
+ EPRBuiltins.AWS_STS_USE_GLOBAL_ENDPOINT: (
+ self._should_set_global_sts_endpoint(
+ region_name=region_name,
+ endpoint_url=None,
+ endpoint_config=None,
+ )
+ ),
+ EPRBuiltins.AWS_S3_USE_GLOBAL_ENDPOINT: (
+ self._should_force_s3_global(region_name, s3_config)
+ ),
+ EPRBuiltins.AWS_S3_ACCELERATE: s3_config.get(
+ 'use_accelerate_endpoint', False
+ ),
+ EPRBuiltins.AWS_S3_FORCE_PATH_STYLE: force_path_style,
+ EPRBuiltins.AWS_S3_USE_ARN_REGION: s3_config.get(
+ 'use_arn_region', True
+ ),
+ EPRBuiltins.AWS_S3CONTROL_USE_ARN_REGION: s3_config.get(
+ 'use_arn_region', False
+ ),
+ EPRBuiltins.AWS_S3_DISABLE_MRAP: s3_config.get(
+ 's3_disable_multiregion_access_points', False
+ ),
+ EPRBuiltins.SDK_ENDPOINT: given_endpoint,
+ }
+
+ def _compute_user_agent_appid_config(self, config_kwargs):
+ user_agent_appid = config_kwargs.get('user_agent_appid')
+ if user_agent_appid is None:
+ user_agent_appid = self._config_store.get_config_variable(
+ 'user_agent_appid'
+ )
+ if (
+ user_agent_appid is not None
+ and len(user_agent_appid) > USERAGENT_APPID_MAXLEN
+ ):
+ logger.warning(
+ 'The configured value for user_agent_appid exceeds the '
+ f'maximum length of {USERAGENT_APPID_MAXLEN} characters.'
+ )
+ config_kwargs['user_agent_appid'] = user_agent_appid
diff --git a/venv/lib/python3.12/site-packages/botocore/auth.py b/venv/lib/python3.12/site-packages/botocore/auth.py
new file mode 100644
index 0000000..6b296cf
--- /dev/null
+++ b/venv/lib/python3.12/site-packages/botocore/auth.py
@@ -0,0 +1,1162 @@
+# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
+# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file 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.
+import base64
+import calendar
+import datetime
+import functools
+import hmac
+import json
+import logging
+import time
+from collections.abc import Mapping
+from email.utils import formatdate
+from hashlib import sha1, sha256
+from operator import itemgetter
+
+from botocore.compat import (
+ HAS_CRT,
+ HTTPHeaders,
+ encodebytes,
+ ensure_unicode,
+ parse_qs,
+ quote,
+ unquote,
+ urlsplit,
+ urlunsplit,
+)
+from botocore.exceptions import NoAuthTokenError, NoCredentialsError
+from botocore.utils import (
+ is_valid_ipv6_endpoint_url,
+ normalize_url_path,
+ percent_encode_sequence,
+)
+
+# Imports for backwards compatibility
+from botocore.compat import MD5_AVAILABLE # noqa
+
+
+logger = logging.getLogger(__name__)
+
+
+EMPTY_SHA256_HASH = (
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
+)
+# This is the buffer size used when calculating sha256 checksums.
+# Experimenting with various buffer sizes showed that this value generally
+# gave the best result (in terms of performance).
+PAYLOAD_BUFFER = 1024 * 1024
+ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
+SIGV4_TIMESTAMP = '%Y%m%dT%H%M%SZ'
+SIGNED_HEADERS_BLACKLIST = [
+ 'expect',
+ 'user-agent',
+ 'x-amzn-trace-id',
+]
+UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD'
+STREAMING_UNSIGNED_PAYLOAD_TRAILER = 'STREAMING-UNSIGNED-PAYLOAD-TRAILER'
+
+
+def _host_from_url(url):
+ # Given URL, derive value for host header. Ensure that value:
+ # 1) is lowercase
+ # 2) excludes port, if it was the default port
+ # 3) excludes userinfo
+ url_parts = urlsplit(url)
+ host = url_parts.hostname # urlsplit's hostname is always lowercase
+ if is_valid_ipv6_endpoint_url(url):
+ host = f'[{host}]'
+ default_ports = {
+ 'http': 80,
+ 'https': 443,
+ }
+ if url_parts.port is not None:
+ if url_parts.port != default_ports.get(url_parts.scheme):
+ host = '%s:%d' % (host, url_parts.port)
+ return host
+
+
+def _get_body_as_dict(request):
+ # For query services, request.data is form-encoded and is already a
+ # dict, but for other services such as rest-json it could be a json
+ # string or bytes. In those cases we attempt to load the data as a
+ # dict.
+ data = request.data
+ if isinstance(data, bytes):
+ data = json.loads(data.decode('utf-8'))
+ elif isinstance(data, str):
+ data = json.loads(data)
+ return data
+
+
+class BaseSigner:
+ REQUIRES_REGION = False
+ REQUIRES_TOKEN = False
+
+ def add_auth(self, request):
+ raise NotImplementedError("add_auth")
+
+
+class TokenSigner(BaseSigner):
+ REQUIRES_TOKEN = True
+ """
+ Signers that expect an authorization token to perform the authorization
+ """
+
+ def __init__(self, auth_token):
+ self.auth_token = auth_token
+
+
+class SigV2Auth(BaseSigner):
+ """
+ Sign a request with Signature V2.
+ """
+
+ def __init__(self, credentials):
+ self.credentials = credentials
+
+ def calc_signature(self, request, params):
+ logger.debug("Calculating signature using v2 auth.")
+ split = urlsplit(request.url)
+ path = split.path
+ if len(path) == 0:
+ path = '/'
+ string_to_sign = f"{request.method}\n{split.netloc}\n{path}\n"
+ lhmac = hmac.new(
+ self.credentials.secret_key.encode("utf-8"), digestmod=sha256
+ )
+ pairs = []
+ for key in sorted(params):
+ # Any previous signature should not be a part of this
+ # one, so we skip that particular key. This prevents
+ # issues during retries.
+ if key == 'Signature':
+ continue
+ value = str(params[key])
+ quoted_key = quote(key.encode('utf-8'), safe='')
+ quoted_value = quote(value.encode('utf-8'), safe='-_~')
+ pairs.append(f'{quoted_key}={quoted_value}')
+ qs = '&'.join(pairs)
+ string_to_sign += qs
+ logger.debug('String to sign: %s', string_to_sign)
+ lhmac.update(string_to_sign.encode('utf-8'))
+ b64 = base64.b64encode(lhmac.digest()).strip().decode('utf-8')
+ return (qs, b64)
+
+ def add_auth(self, request):
+ # The auth handler is the last thing called in the
+ # preparation phase of a prepared request.
+ # Because of this we have to parse the query params
+ # from the request body so we can update them with
+ # the sigv2 auth params.
+ if self.credentials is None:
+ raise NoCredentialsError()
+ if request.data:
+ # POST
+ params = request.data
+ else:
+ # GET
+ params = request.params
+ params['AWSAccessKeyId'] = self.credentials.access_key
+ params['SignatureVersion'] = '2'
+ params['SignatureMethod'] = 'HmacSHA256'
+ params['Timestamp'] = time.strftime(ISO8601, time.gmtime())
+ if self.credentials.token:
+ params['SecurityToken'] = self.credentials.token
+ qs, signature = self.calc_signature(request, params)
+ params['Signature'] = signature
+ return request
+
+
+class SigV3Auth(BaseSigner):
+ def __init__(self, credentials):
+ self.credentials = credentials
+
+ def add_auth(self, request):
+ if self.credentials is None:
+ raise NoCredentialsError()
+ if 'Date' in request.headers:
+ del request.headers['Date']
+ request.headers['Date'] = formatdate(usegmt=True)
+ if self.credentials.token:
+ if 'X-Amz-Security-Token' in request.headers:
+ del request.headers['X-Amz-Security-Token']
+ request.headers['X-Amz-Security-Token'] = self.credentials.token
+ new_hmac = hmac.new(
+ self.credentials.secret_key.encode('utf-8'), digestmod=sha256
+ )
+ new_hmac.update(request.headers['Date'].encode('utf-8'))
+ encoded_signature = encodebytes(new_hmac.digest()).strip()
+ signature = (
+ f"AWS3-HTTPS AWSAccessKeyId={self.credentials.access_key},"
+ f"Algorithm=HmacSHA256,Signature={encoded_signature.decode('utf-8')}"
+ )
+ if 'X-Amzn-Authorization' in request.headers:
+ del request.headers['X-Amzn-Authorization']
+ request.headers['X-Amzn-Authorization'] = signature
+
+
+class SigV4Auth(BaseSigner):
+ """
+ Sign a request with Signature V4.
+ """
+
+ REQUIRES_REGION = True
+
+ def __init__(self, credentials, service_name, region_name):
+ self.credentials = credentials
+ # We initialize these value here so the unit tests can have
+ # valid values. But these will get overriden in ``add_auth``
+ # later for real requests.
+ self._region_name = region_name
+ self._service_name = service_name
+
+ def _sign(self, key, msg, hex=False):
+ if hex:
+ sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
+ else:
+ sig = hmac.new(key, msg.encode('utf-8'), sha256).digest()
+ return sig
+
+ def headers_to_sign(self, request):
+ """
+ Select the headers from the request that need to be included
+ in the StringToSign.
+ """
+ header_map = HTTPHeaders()
+ for name, value in request.headers.items():
+ lname = name.lower()
+ if lname not in SIGNED_HEADERS_BLACKLIST:
+ header_map[lname] = value
+ if 'host' not in header_map:
+ # TODO: We should set the host ourselves, instead of relying on our
+ # HTTP client to set it for us.
+ header_map['host'] = _host_from_url(request.url)
+ return header_map
+
+ def canonical_query_string(self, request):
+ # The query string can come from two parts. One is the
+ # params attribute of the request. The other is from the request
+ # url (in which case we have to re-split the url into its components
+ # and parse out the query string component).
+ if request.params:
+ return self._canonical_query_string_params(request.params)
+ else:
+ return self._canonical_query_string_url(urlsplit(request.url))
+
+ def _canonical_query_string_params(self, params):
+ # [(key, value), (key2, value2)]
+ key_val_pairs = []
+ if isinstance(params, Mapping):
+ params = params.items()
+ for key, value in params:
+ key_val_pairs.append(
+ (quote(key, safe='-_.~'), quote(str(value), safe='-_.~'))
+ )
+ sorted_key_vals = []
+ # Sort by the URI-encoded key names, and in the case of
+ # repeated keys, then sort by the value.
+ for key, value in sorted(key_val_pairs):
+ sorted_key_vals.append(f'{key}={value}')
+ canonical_query_string = '&'.join(sorted_key_vals)
+ return canonical_query_string
+
+ def _canonical_query_string_url(self, parts):
+ canonical_query_string = ''
+ if parts.query:
+ # [(key, value), (key2, value2)]
+ key_val_pairs = []
+ for pair in parts.query.split('&'):
+ key, _, value = pair.partition('=')
+ key_val_pairs.append((key, value))
+ sorted_key_vals = []
+ # Sort by the URI-encoded key names, and in the case of
+ # repeated keys, then sort by the value.
+ for key, value in sorted(key_val_pairs):
+ sorted_key_vals.append(f'{key}={value}')
+ canonical_query_string = '&'.join(sorted_key_vals)
+ return canonical_query_string
+
+ def canonical_headers(self, headers_to_sign):
+ """
+ Return the headers that need to be included in the StringToSign
+ in their canonical form by converting all header keys to lower
+ case, sorting them in alphabetical order and then joining
+ them into a string, separated by newlines.
+ """
+ headers = []
+ sorted_header_names = sorted(set(headers_to_sign))
+ for key in sorted_header_names:
+ value = ','.join(
+ self._header_value(v) for v in headers_to_sign.get_all(key)
+ )
+ headers.append(f'{key}:{ensure_unicode(value)}')
+ return '\n'.join(headers)
+
+ def _header_value(self, value):
+ # From the sigv4 docs:
+ # Lowercase(HeaderName) + ':' + Trimall(HeaderValue)
+ #
+ # The Trimall function removes excess white space before and after
+ # values, and converts sequential spaces to a single space.
+ return ' '.join(value.split())
+
+ def signed_headers(self, headers_to_sign):
+ headers = sorted(n.lower().strip() for n in set(headers_to_sign))
+ return ';'.join(headers)
+
+ def _is_streaming_checksum_payload(self, request):
+ checksum_context = request.context.get('checksum', {})
+ algorithm = checksum_context.get('request_algorithm')
+ return isinstance(algorithm, dict) and algorithm.get('in') == 'trailer'
+
+ def payload(self, request):
+ if self._is_streaming_checksum_payload(request):
+ return STREAMING_UNSIGNED_PAYLOAD_TRAILER
+ elif not self._should_sha256_sign_payload(request):
+ # When payload signing is disabled, we use this static string in
+ # place of the payload checksum.
+ return UNSIGNED_PAYLOAD
+ request_body = request.body
+ if request_body and hasattr(request_body, 'seek'):
+ position = request_body.tell()
+ read_chunksize = functools.partial(
+ request_body.read, PAYLOAD_BUFFER
+ )
+ checksum = sha256()
+ for chunk in iter(read_chunksize, b''):
+ checksum.update(chunk)
+ hex_checksum = checksum.hexdigest()
+ request_body.seek(position)
+ return hex_checksum
+ elif request_body:
+ # The request serialization has ensured that
+ # request.body is a bytes() type.
+ return sha256(request_body).hexdigest()
+ else:
+ return EMPTY_SHA256_HASH
+
+ def _should_sha256_sign_payload(self, request):
+ # Payloads will always be signed over insecure connections.
+ if not request.url.startswith('https'):
+ return True
+
+ # Certain operations may have payload signing disabled by default.
+ # Since we don't have access to the operation model, we pass in this
+ # bit of metadata through the request context.
+ return request.context.get('payload_signing_enabled', True)
+
+ def canonical_request(self, request):
+ cr = [request.method.upper()]
+ path = self._normalize_url_path(urlsplit(request.url).path)
+ cr.append(path)
+ cr.append(self.canonical_query_string(request))
+ headers_to_sign = self.headers_to_sign(request)
+ cr.append(self.canonical_headers(headers_to_sign) + '\n')
+ cr.append(self.signed_headers(headers_to_sign))
+ if 'X-Amz-Content-SHA256' in request.headers:
+ body_checksum = request.headers['X-Amz-Content-SHA256']
+ else:
+ body_checksum = self.payload(request)
+ cr.append(body_checksum)
+ return '\n'.join(cr)
+
+ def _normalize_url_path(self, path):
+ normalized_path = quote(normalize_url_path(path), safe='/~')
+ return normalized_path
+
+ def scope(self, request):
+ scope = [self.credentials.access_key]
+ scope.append(request.context['timestamp'][0:8])
+ scope.append(self._region_name)
+ scope.append(self._service_name)
+ scope.append('aws4_request')
+ return '/'.join(scope)
+
+ def credential_scope(self, request):
+ scope = []
+ scope.append(request.context['timestamp'][0:8])
+ scope.append(self._region_name)
+ scope.append(self._service_name)
+ scope.append('aws4_request')
+ return '/'.join(scope)
+
+ def string_to_sign(self, request, canonical_request):
+ """
+ Return the canonical StringToSign as well as a dict
+ containing the original version of all headers that
+ were included in the StringToSign.
+ """
+ sts = ['AWS4-HMAC-SHA256']
+ sts.append(request.context['timestamp'])
+ sts.append(self.credential_scope(request))
+ sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
+ return '\n'.join(sts)
+
+ def signature(self, string_to_sign, request):
+ key = self.credentials.secret_key
+ k_date = self._sign(
+ (f"AWS4{key}").encode(), request.context["timestamp"][0:8]
+ )
+ k_region = self._sign(k_date, self._region_name)
+ k_service = self._sign(k_region, self._service_name)
+ k_signing = self._sign(k_service, 'aws4_request')
+ return self._sign(k_signing, string_to_sign, hex=True)
+
+ def add_auth(self, request):
+ if self.credentials is None:
+ raise NoCredentialsError()
+ datetime_now = datetime.datetime.utcnow()
+ request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
+ # This could be a retry. Make sure the previous
+ # authorization header is removed first.
+ self._modify_request_before_signing(request)
+ canonical_request = self.canonical_request(request)
+ logger.debug("Calculating signature using v4 auth.")
+ logger.debug('CanonicalRequest:\n%s', canonical_request)
+ string_to_sign = self.string_to_sign(request, canonical_request)
+ logger.debug('StringToSign:\n%s', string_to_sign)
+ signature = self.signature(string_to_sign, request)
+ logger.debug('Signature:\n%s', signature)
+
+ self._inject_signature_to_request(request, signature)
+
+ def _inject_signature_to_request(self, request, signature):
+ auth_str = [f'AWS4-HMAC-SHA256 Credential={self.scope(request)}']
+ headers_to_sign = self.headers_to_sign(request)
+ auth_str.append(
+ f"SignedHeaders={self.signed_headers(headers_to_sign)}"
+ )
+ auth_str.append(f'Signature={signature}')
+ request.headers['Authorization'] = ', '.join(auth_str)
+ return request
+
+ def _modify_request_before_signing(self, request):
+ if 'Authorization' in request.headers:
+ del request.headers['Authorization']
+ self._set_necessary_date_headers(request)
+ if self.credentials.token:
+ if 'X-Amz-Security-Token' in request.headers:
+ del request.headers['X-Amz-Security-Token']
+ request.headers['X-Amz-Security-Token'] = self.credentials.token
+
+ if not request.context.get('payload_signing_enabled', True):
+ if 'X-Amz-Content-SHA256' in request.headers:
+ del request.headers['X-Amz-Content-SHA256']
+ request.headers['X-Amz-Content-SHA256'] = UNSIGNED_PAYLOAD
+
+ def _set_necessary_date_headers(self, request):
+ # The spec allows for either the Date _or_ the X-Amz-Date value to be
+ # used so we check both. If there's a Date header, we use the date
+ # header. Otherwise we use the X-Amz-Date header.
+ if 'Date' in request.headers:
+ del request.headers['Date']
+ datetime_timestamp = datetime.datetime.strptime(
+ request.context['timestamp'], SIGV4_TIMESTAMP
+ )
+ request.headers['Date'] = formatdate(
+ int(calendar.timegm(datetime_timestamp.timetuple()))
+ )
+ if 'X-Amz-Date' in request.headers:
+ del request.headers['X-Amz-Date']
+ else:
+ if 'X-Amz-Date' in request.headers:
+ del request.headers['X-Amz-Date']
+ request.headers['X-Amz-Date'] = request.context['timestamp']
+
+
+class S3SigV4Auth(SigV4Auth):
+ def _modify_request_before_signing(self, request):
+ super()._modify_request_before_signing(request)
+ if 'X-Amz-Content-SHA256' in request.headers:
+ del request.headers['X-Amz-Content-SHA256']
+
+ request.headers['X-Amz-Content-SHA256'] = self.payload(request)
+
+ def _should_sha256_sign_payload(self, request):
+ # S3 allows optional body signing, so to minimize the performance
+ # impact, we opt to not SHA256 sign the body on streaming uploads,
+ # provided that we're on https.
+ client_config = request.context.get('client_config')
+ s3_config = getattr(client_config, 's3', None)
+
+ # The config could be None if it isn't set, or if the customer sets it
+ # to None.
+ if s3_config is None:
+ s3_config = {}
+
+ # The explicit configuration takes precedence over any implicit
+ # configuration.
+ sign_payload = s3_config.get('payload_signing_enabled', None)
+ if sign_payload is not None:
+ return sign_payload
+
+ # We require that both a checksum be present and https be enabled
+ # to implicitly disable body signing. The combination of TLS and
+ # a checksum is sufficiently secure and durable for us to be
+ # confident in the request without body signing.
+ checksum_header = 'Content-MD5'
+ checksum_context = request.context.get('checksum', {})
+ algorithm = checksum_context.get('request_algorithm')
+ if isinstance(algorithm, dict) and algorithm.get('in') == 'header':
+ checksum_header = algorithm['name']
+ if (
+ not request.url.startswith("https")
+ or checksum_header not in request.headers
+ ):
+ return True
+
+ # If the input is streaming we disable body signing by default.
+ if request.context.get('has_streaming_input', False):
+ return False
+
+ # If the S3-specific checks had no results, delegate to the generic
+ # checks.
+ return super()._should_sha256_sign_payload(request)
+
+ def _normalize_url_path(self, path):
+ # For S3, we do not normalize the path.
+ return path
+
+
+class S3ExpressAuth(S3SigV4Auth):
+ REQUIRES_IDENTITY_CACHE = True
+
+ def __init__(
+ self, credentials, service_name, region_name, *, identity_cache
+ ):
+ super().__init__(credentials, service_name, region_name)
+ self._identity_cache = identity_cache
+
+ def add_auth(self, request):
+ super().add_auth(request)
+
+ def _modify_request_before_signing(self, request):
+ super()._modify_request_before_signing(request)
+ if 'x-amz-s3session-token' not in request.headers:
+ request.headers['x-amz-s3session-token'] = self.credentials.token
+ # S3Express does not support STS' X-Amz-Security-Token
+ if 'X-Amz-Security-Token' in request.headers:
+ del request.headers['X-Amz-Security-Token']
+
+
+class S3ExpressPostAuth(S3ExpressAuth):
+ REQUIRES_IDENTITY_CACHE = True
+
+ def add_auth(self, request):
+ datetime_now = datetime.datetime.utcnow()
+ request.context['timestamp'] = datetime_now.strftime(SIGV4_TIMESTAMP)
+
+ fields = {}
+ if request.context.get('s3-presign-post-fields', None) is not None:
+ fields = request.context['s3-presign-post-fields']
+
+ policy = {}
+ conditions = []
+ if request.context.get('s3-presign-post-policy', None) is not None:
+ policy = request.context['s3-presign-post-policy']
+ if policy.get('conditions', None) is not None:
+ conditions = policy['conditions']
+
+ policy['conditions'] = conditions
+
+ fields['x-amz-algorithm'] = 'AWS4-HMAC-SHA256'
+ fields['x-amz-credential'] = self.scope(request)
+ fields['x-amz-date'] = request.context['timestamp']
+
+ conditions.append({'x-amz-algorithm': 'AWS4-HMAC-SHA256'})
+ conditions.append({'x-amz-credential': self.scope(request)})
+ conditions.append({'x-amz-date': request.context['timestamp']})
+
+ if self.credentials.token is not None:
+ fields['X-Amz-S3session-Token'] = self.credentials.token
+ conditions.append(
+ {'X-Amz-S3session-Token': self.credentials.token}
+ )
+
+ # Dump the base64 encoded policy into the fields dictionary.
+ fields['policy'] = base64.b64encode(
+ json.dumps(policy).encode('utf-8')
+ ).decode('utf-8')
+
+ fields['x-amz-signature'] = self.signature(fields['policy'], request)
+
+ request.context['s3-presign-post-fields'] = fields
+ request.context['s3-presign-post-policy'] = policy
+
+
+class S3ExpressQueryAuth(S3ExpressAuth):
+ DEFAULT_EXPIRES = 300
+ REQUIRES_IDENTITY_CACHE = True
+
+ def __init__(
+ self,
+ credentials,
+ service_name,
+ region_name,
+ *,
+ identity_cache,
+ expires=DEFAULT_EXPIRES,
+ ):
+ super().__init__(
+ credentials,
+ service_name,
+ region_name,
+ identity_cache=identity_cache,
+ )
+ self._expires = expires
+
+ def _modify_request_before_signing(self, request):
+ # We automatically set this header, so if it's the auto-set value we
+ # want to get rid of it since it doesn't make sense for presigned urls.
+ content_type = request.headers.get('content-type')
+ blocklisted_content_type = (
+ 'application/x-www-form-urlencoded; charset=utf-8'
+ )
+ if content_type == blocklisted_content_type:
+ del request.headers['content-type']
+
+ # Note that we're not including X-Amz-Signature.
+ # From the docs: "The Canonical Query String must include all the query
+ # parameters from the preceding table except for X-Amz-Signature.
+ signed_headers = self.signed_headers(self.headers_to_sign(request))
+
+ auth_params = {
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-Credential': self.scope(request),
+ 'X-Amz-Date': request.context['timestamp'],
+ 'X-Amz-Expires': self._expires,
+ 'X-Amz-SignedHeaders': signed_headers,
+ }
+ if self.credentials.token is not None:
+ auth_params['X-Amz-S3session-Token'] = self.credentials.token
+ # Now parse the original query string to a dict, inject our new query
+ # params, and serialize back to a query string.
+ url_parts = urlsplit(request.url)
+ # parse_qs makes each value a list, but in our case we know we won't
+ # have repeated keys so we know we have single element lists which we
+ # can convert back to scalar values.
+ query_string_parts = parse_qs(url_parts.query, keep_blank_values=True)
+ query_dict = {k: v[0] for k, v in query_string_parts.items()}
+
+ if request.params:
+ query_dict.update(request.params)
+ request.params = {}
+ # The spec is particular about this. It *has* to be:
+ # https://?&
+ # You can't mix the two types of params together, i.e just keep doing
+ # new_query_params.update(op_params)
+ # new_query_params.update(auth_params)
+ # percent_encode_sequence(new_query_params)
+ operation_params = ''
+ if request.data:
+ # We also need to move the body params into the query string. To
+ # do this, we first have to convert it to a dict.
+ query_dict.update(_get_body_as_dict(request))
+ request.data = ''
+ if query_dict:
+ operation_params = percent_encode_sequence(query_dict) + '&'
+ new_query_string = (
+ f"{operation_params}{percent_encode_sequence(auth_params)}"
+ )
+ # url_parts is a tuple (and therefore immutable) so we need to create
+ # a new url_parts with the new query string.
+ # -
+ # scheme - 0
+ # netloc - 1
+ # path - 2
+ # query - 3 <-- we're replacing this.
+ # fragment - 4
+ p = url_parts
+ new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
+ request.url = urlunsplit(new_url_parts)
+
+ def _inject_signature_to_request(self, request, signature):
+ # Rather than calculating an "Authorization" header, for the query
+ # param quth, we just append an 'X-Amz-Signature' param to the end
+ # of the query string.
+ request.url += f'&X-Amz-Signature={signature}'
+
+ def _normalize_url_path(self, path):
+ # For S3, we do not normalize the path.
+ return path
+
+ def payload(self, request):
+ # From the doc link above:
+ # "You don't include a payload hash in the Canonical Request, because
+ # when you create a presigned URL, you don't know anything about the
+ # payload. Instead, you use a constant string "UNSIGNED-PAYLOAD".
+ return UNSIGNED_PAYLOAD
+
+
+class SigV4QueryAuth(SigV4Auth):
+ DEFAULT_EXPIRES = 3600
+
+ def __init__(
+ self, credentials, service_name, region_name, expires=DEFAULT_EXPIRES
+ ):
+ super().__init__(credentials, service_name, region_name)
+ self._expires = expires
+
+ def _modify_request_before_signing(self, request):
+ # We automatically set this header, so if it's the auto-set value we
+ # want to get rid of it since it doesn't make sense for presigned urls.
+ content_type = request.headers.get('content-type')
+ blacklisted_content_type = (
+ 'application/x-www-form-urlencoded; charset=utf-8'
+ )
+ if content_type == blacklisted_content_type:
+ del request.headers['content-type']
+
+ # Note that we're not including X-Amz-Signature.
+ # From the docs: "The Canonical Query String must include all the query
+ # parameters from the preceding table except for X-Amz-Signature.
+ signed_headers = self.signed_headers(self.headers_to_sign(request))
+
+ auth_params = {
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-Credential': self.scope(request),
+ 'X-Amz-Date': request.context['timestamp'],
+ 'X-Amz-Expires': self._expires,
+ 'X-Amz-SignedHeaders': signed_headers,
+ }
+ if self.credentials.token is not None:
+ auth_params['X-Amz-Security-Token'] = self.credentials.token
+ # Now parse the original query string to a dict, inject our new query
+ # params, and serialize back to a query string.
+ url_parts = urlsplit(request.url)
+ # parse_qs makes each value a list, but in our case we know we won't
+ # have repeated keys so we know we have single element lists which we
+ # can convert back to scalar values.
+ query_string_parts = parse_qs(url_parts.query, keep_blank_values=True)
+ query_dict = {k: v[0] for k, v in query_string_parts.items()}
+
+ if request.params:
+ query_dict.update(request.params)
+ request.params = {}
+ # The spec is particular about this. It *has* to be:
+ # https://?