configure docker

master
Sascha 2024-12-11 13:41:55 +07:00
parent 9adb3696c0
commit cdfdffc274
2992 changed files with 532618 additions and 11 deletions

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="finance.db" uuid="1ea8b50c-eb10-45e8-b4f2-da82d2ee1c53">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/finance.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
</component>
</project>

@ -0,0 +1,10 @@
FROM python:3.8-slim-buster
WORKDIR /python-docker
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]

@ -21,7 +21,7 @@ Session(app)
# Configure CS50 Library to use SQLite database
db = SQL("sqlite:///finance.db")
# db = SQL("postgresql://postgres:password@db/finance")
@app.after_request
def after_request(response):
@ -35,10 +35,9 @@ def after_request(response):
@app.route("/")
@login_required
def index():
# Get actual cash of the logged in user
# Get actual cash of the logged-in user
user = db.execute("SELECT * FROM users WHERE id = ?", session["user_id"])
transactions = db.execute(
"SELECT symbol, SUM(shares), SUM(costs) FROM transactions WHERE user_id = ? GROUP BY symbol", session["user_id"])
transactions = db.execute("SELECT symbol, SUM(shares), SUM(costs) FROM transactions WHERE user_id = ? GROUP BY symbol", session["user_id"])
# Calculations
stocks_total = 0.0
@ -111,10 +110,8 @@ def buy():
# Buy and save transaction
cash -= costs
db.execute("UPDATE users SET cash = ? WHERE id = ? ", cash, session["user_id"])
db.execute("INSERT INTO transactions (user_id, buy_sell, datetime, symbol, shares, costs) VALUES(?, ?, ?, ?, ?, ?)",
session["user_id"], 1, datetime.now(), symbol, shares, -costs)
db.execute("INSERT INTO transactions (user_id, buy_sell, datetime, symbol, shares, costs) VALUES(?, ?, ?, ?, ?, ?)",session["user_id"], 1, datetime.now(), symbol, shares, -costs)
# Show notification about bought
flash(f"Bought {shares} shares of {symbol} for {usd(costs)}!")
@ -131,8 +128,8 @@ def buy():
@login_required
def history():
# Show history of transactions
transactions = []
transactions = db.execute("SELECT * FROM transactions WHERE user_id = ?", session["user_id"])
if not transactions: transactions = []
return render_template("history.html", transactions=transactions)
@ -166,7 +163,6 @@ def login():
session["user_id"] = rows[0]["id"]
# Redirect user to home page
return apology("logged in succesfully", 200)
return redirect("/")
# User reached route via GET (as by clicking a link or via redirect)
@ -233,8 +229,8 @@ def register():
return apology("password and confirmation does not match", 400)
# Add user to db
hash = generate_password_hash(password)
db.execute("INSERT INTO users (username, hash) VALUES(?, ?)", username, hash)
password_hash = generate_password_hash(password)
db.execute("INSERT INTO users (username, hash) VALUES(?, ?)", username, password_hash)
# Redirect user to home page
return redirect("/")
@ -283,6 +279,7 @@ def sell():
return apology("symbol not found", 400)
# Check if enough shares available
old_shares = 0
for transaction in transactions:
if transaction['symbol'] == symbol:
old_shares = transaction['SUM(shares)']

@ -0,0 +1,25 @@
services:
flask:
build: .
ports:
- "5000:5000"
environment:
traefik.enable: true
traefik.http.routers.finance-web.entrypoints: web
traefik.http.routers.finance-web.rule: (Host(`finance.home`) && PathPrefix(`/web`))
traefik.http.routers.finance.entrypoints: websecure
traefik.http.routers.finance.rule: (Host(`finance.woitschetzki.eu`) && PathPrefix(`/web`))
traefik.http.routers.finance.tls: true
traefik.http..finance.tls.certresolver: http_resolver
traefik.http.routers.finance.service: finance
traefik.http.services.finance.loadbalancer.server.port: 5000
traefik.docker.network: proxy
traefik.http.routers.finance.middlewares: default@file
volumes:
- .:/app
networks:
- proxy
networks:
proxy:
external: true

@ -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"

@ -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 "/mnt/Daten/coding/cs50/psets/9/finance/env")
else
# use the path as-is
export VIRTUAL_ENV="/mnt/Daten/coding/cs50/psets/9/finance/env"
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="(env) ${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT="(env) "
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

@ -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 <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
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 "/mnt/Daten/coding/cs50/psets/9/finance/env"
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = "(env) $prompt"
setenv VIRTUAL_ENV_PROMPT "(env) "
endif
alias pydoc python -m pydoc
rehash

@ -0,0 +1,69 @@
# This file must be used with "source <venv>/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 "/mnt/Daten/coding/cs50/psets/9/finance/env"
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) "(env) " (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 "(env) "
end

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from automat._visualize import tool
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(tool())

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from flask.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from charset_normalizer.cli import cli_detect
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli_detect())

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- 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())

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- 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())

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- 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())

@ -0,0 +1 @@
/usr/bin/python

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from sqlparse.__main__ import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

@ -0,0 +1,8 @@
#!/mnt/Daten/coding/cs50/psets/9/finance/env/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from wheel.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

@ -0,0 +1,164 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#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 */

@ -0,0 +1,21 @@
Copyright (c) 2014
Rackspace
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.

@ -0,0 +1,199 @@
Metadata-Version: 2.1
Name: Automat
Version: 24.8.1
Summary: Self-service finite-state machines for the programmer on the go.
Author-email: Glyph <code@glyph.im>
License: Copyright (c) 2014
Rackspace
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.
Project-URL: Documentation, https://automat.readthedocs.io/
Project-URL: Source, https://github.com/glyph/automat/
Keywords: fsm,state machine,automata
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.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
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typing-extensions; python_version < "3.10"
Provides-Extra: visualize
Requires-Dist: graphviz>0.5.1; extra == "visualize"
Requires-Dist: Twisted>=16.1.1; extra == "visualize"
# Automat #
[![Documentation Status](https://readthedocs.org/projects/automat/badge/?version=latest)](http://automat.readthedocs.io/en/latest/)
[![Build Status](https://github.com/glyph/automat/actions/workflows/ci.yml/badge.svg?branch=trunk)](https://github.com/glyph/automat/actions/workflows/ci.yml?query=branch%3Atrunk)
[![Coverage Status](http://codecov.io/github/glyph/automat/coverage.svg?branch=trunk)](http://codecov.io/github/glyph/automat?branch=trunk)
## Self-service finite-state machines for the programmer on the go. ##
Automat is a library for concise, idiomatic Python expression of finite-state
automata (particularly deterministic finite-state transducers).
Read more here, or on [Read the Docs](https://automat.readthedocs.io/), or watch the following videos for an overview and presentation
### Why use state machines? ###
Sometimes you have to create an object whose behavior varies with its state,
but still wishes to present a consistent interface to its callers.
For example, let's say you're writing the software for a coffee machine. It
has a lid that can be opened or closed, a chamber for water, a chamber for
coffee beans, and a button for "brew".
There are a number of possible states for the coffee machine. It might or
might not have water. It might or might not have beans. The lid might be open
or closed. The "brew" button should only actually attempt to brew coffee in
one of these configurations, and the "open lid" button should only work if the
coffee is not, in fact, brewing.
With diligence and attention to detail, you can implement this correctly using
a collection of attributes on an object; `hasWater`, `hasBeans`, `isLidOpen`
and so on. However, you have to keep all these attributes consistent. As the
coffee maker becomes more complex - perhaps you add an additional chamber for
flavorings so you can make hazelnut coffee, for example - you have to keep
adding more and more checks and more and more reasoning about which
combinations of states are allowed.
Rather than adding tedious `if` checks to every single method to make sure that
each of these flags are exactly what you expect, you can use a state machine to
ensure that if your code runs at all, it will be run with all the required
values initialized, because they have to be called in the order you declare
them.
You can read about state machines and their advantages for Python programmers
in more detail [in this excellent article by Jean-Paul
Calderone](https://web.archive.org/web/20160507053658/https://clusterhq.com/2013/12/05/what-is-a-state-machine/).
### What makes Automat different? ###
There are
[dozens of libraries on PyPI implementing state machines](https://pypi.org/search/?q=finite+state+machine).
So it behooves me to say why yet another one would be a good idea.
Automat is designed around this principle: while organizing your code around
state machines is a good idea, your callers don't, and shouldn't have to, care
that you've done so. In Python, the "input" to a stateful system is a method
call; the "output" may be a method call, if you need to invoke a side effect,
or a return value, if you are just performing a computation in memory. Most
other state-machine libraries require you to explicitly create an input object,
provide that object to a generic "input" method, and then receive results,
sometimes in terms of that library's interfaces and sometimes in terms of
classes you define yourself.
For example, a snippet of the coffee-machine example above might be implemented
as follows in naive Python:
```python
class CoffeeMachine(object):
def brewButton(self) -> None:
if self.hasWater and self.hasBeans and not self.isLidOpen:
self.heatTheHeatingElement()
# ...
```
With Automat, you'd begin with a `typing.Protocol` that describes all of your
inputs:
```python
from typing import Protocol
class CoffeeBrewer(Protocol):
def brewButton(self) -> None:
"The user pressed the 'brew' button."
def putInBeans(self) -> None:
"The user put in some beans."
```
We'll then need a concrete class to contain the shared core of state shared
among the different states:
```python
from dataclasses import dataclass
@dataclass
class BrewerCore:
heatingElement: HeatingElement
```
Next, we need to describe our state machine, including all of our states. For
simplicity's sake let's say that the only two states are `noBeans` and
`haveBeans`:
```python
from automat import TypeMachineBuilder
builder = TypeMachineBuilder(CoffeeBrewer, BrewerCore)
noBeans = builder.state("noBeans")
haveBeans = builder.state("haveBeans")
```
Next we can describe a simple transition; when we put in beans, we move to the
`haveBeans` state, with no other behavior.
```python
# When we don't have beans, upon putting in beans, we will then have beans
noBeans.upon(CoffeeBrewer.putInBeans).to(haveBeans).returns(None)
```
And then another transition that we describe with a decorator, one that *does*
have some behavior, that needs to heat up the heating element to brew the
coffee:
```python
@haveBeans.upon(CoffeeBrewer.brewButton).to(noBeans)
def heatUp(inputs: CoffeeBrewer, core: BrewerCore) -> None:
"""
When we have beans, upon pressing the brew button, we will then not have
beans any more (as they have been entered into the brewing chamber) and
our output will be heating the heating element.
"""
print("Brewing the coffee...")
core.heatingElement.turnOn()
```
Then we finalize the state machine by building it, which gives us a callable
that takes a `BrewerCore` and returns a synthetic `CoffeeBrewer`
```python
newCoffeeMachine = builder.build()
```
```python
>>> coffee = newCoffeeMachine(BrewerCore(HeatingElement()))
>>> machine.putInBeans()
>>> machine.brewButton()
Brewing the coffee...
```
All of the *inputs* are provided by calling them like methods, all of the
*output behaviors* are automatically invoked when they are produced according
to the outputs specified to `upon` and all of the states are simply opaque
tokens.

@ -0,0 +1,40 @@
../../../bin/automat-visualize,sha256=Gyv_3d6TeKMpzxHJb64voO8imr3m8LPZReWuLMxsYwE,254
Automat-24.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
Automat-24.8.1.dist-info/LICENSE,sha256=siATAWeNCpN9k4VDgnyhNgcS6zTiPejuPzv_-9TA43Y,1053
Automat-24.8.1.dist-info/METADATA,sha256=XJIxL4Olb15WCoAdVEcORum39__wiCXQR6LNubERZ6M,8396
Automat-24.8.1.dist-info/RECORD,,
Automat-24.8.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
Automat-24.8.1.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
Automat-24.8.1.dist-info/entry_points.txt,sha256=D5Dc6byHpLWAktM443OHbx0ZGl1ZBZtf6rPNlGi7NbQ,62
Automat-24.8.1.dist-info/top_level.txt,sha256=vg4zAOyhP_3YCmpKZLNgFw1uMF3lC_b6TKsdz7jBSpI,8
automat/__init__.py,sha256=8yHAuqaxK0mdiOEXHbwe6WaNzaY01k2HMWD_RltPB-U,356
automat/__pycache__/__init__.cpython-312.pyc,,
automat/__pycache__/_core.cpython-312.pyc,,
automat/__pycache__/_discover.cpython-312.pyc,,
automat/__pycache__/_introspection.cpython-312.pyc,,
automat/__pycache__/_methodical.cpython-312.pyc,,
automat/__pycache__/_runtimeproto.cpython-312.pyc,,
automat/__pycache__/_typed.cpython-312.pyc,,
automat/__pycache__/_visualize.cpython-312.pyc,,
automat/_core.py,sha256=oe4QNlfvmgsnKe_8fyNiOsHsfz5xPArGuXWle9zePp8,6663
automat/_discover.py,sha256=KRbmm7kxpd-WReDQU4qe6hVKGUKmGBHUjYIkRneO4mc,5197
automat/_introspection.py,sha256=uF5ymY-GZckyRxvRs7UToPBV_oVV6xHmvlBVey9nv80,1441
automat/_methodical.py,sha256=YgZXraDe6dvK6w_Y9xfPa0kXCka4ZeGLfcb4IfK4bnI,17243
automat/_runtimeproto.py,sha256=mJ_4VuEGpLc1u7Ptm5cfaLUq7LGD7KfrvmsAa4LsyuU,1654
automat/_test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
automat/_test/__pycache__/__init__.cpython-312.pyc,,
automat/_test/__pycache__/test_core.cpython-312.pyc,,
automat/_test/__pycache__/test_discover.cpython-312.pyc,,
automat/_test/__pycache__/test_methodical.cpython-312.pyc,,
automat/_test/__pycache__/test_trace.cpython-312.pyc,,
automat/_test/__pycache__/test_type_based.cpython-312.pyc,,
automat/_test/__pycache__/test_visualize.cpython-312.pyc,,
automat/_test/test_core.py,sha256=PJHNvQ85i8vjH-oF6nPNKB84_noTyl2dQSv_iRl70J8,3481
automat/_test/test_discover.py,sha256=ROnW7eSLE4T76FVncY2UK05DIYcHZ3TSGGg7kQnbvtw,22067
automat/_test/test_methodical.py,sha256=SKMMbl-6bjH-QZ1hDFRItCOyKF4SstuA4QvExnVhn7w,20267
automat/_test/test_trace.py,sha256=tty7P_ctJtk38ZXnpmEy-J9Rn-Hh2AKb_ia6EmtXSQI,3299
automat/_test/test_type_based.py,sha256=8oZxz3T7zIvyMKg4THg7M85zaHtE4QU9nAegU_OUvko,17872
automat/_test/test_visualize.py,sha256=HXBPgMAD0OTz_l1Yq0lI3d1vJ_l3sHTH67Jauaf-2sk,14631
automat/_typed.py,sha256=lMzMgUfX713Xw_W4pr8iyPqcdpRSbu4rEpRlrXAOW2k,24204
automat/_visualize.py,sha256=DQYig2mBKX-LquPEEy89Y_qyj21tSutAUaFsF1F64ws,6512
automat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (72.2.0)
Root-Is-Purelib: true
Tag: py3-none-any

@ -0,0 +1,2 @@
[console_scripts]
automat-visualize = automat._visualize:tool

@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,92 @@
Metadata-Version: 2.1
Name: MarkupSafe
Version: 3.0.2
Summary: Safely add untrusted strings to HTML/XML markup.
Maintainer-email: Pallets <contact@palletsprojects.com>
License: Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
Project-URL: Source, https://github.com/pallets/markupsafe/
Project-URL: Chat, https://discord.gg/pallets
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Text Processing :: Markup :: HTML
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE.txt
# MarkupSafe
MarkupSafe implements a text object that escapes characters so it is
safe to use in HTML and XML. Characters that have special meanings are
replaced so that they display as the actual characters. This mitigates
injection attacks, meaning untrusted user input can safely be displayed
on a page.
## Examples
```pycon
>>> from markupsafe import Markup, escape
>>> # escape replaces special characters and wraps in Markup
>>> escape("<script>alert(document.cookie);</script>")
Markup('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')
>>> # wrap in Markup to mark text "safe" and prevent escaping
>>> Markup("<strong>Hello</strong>")
Markup('<strong>hello</strong>')
>>> escape(Markup("<strong>Hello</strong>"))
Markup('<strong>hello</strong>')
>>> # Markup is a str subclass
>>> # methods and operators escape their arguments
>>> template = Markup("Hello <em>{name}</em>")
>>> template.format(name='"World"')
Markup('Hello <em>&#34;World&#34;</em>')
```
## Donate
The Pallets organization develops and supports MarkupSafe and other
popular packages. In order to grow the community of contributors and
users, and allow the maintainers to devote more time to the projects,
[please donate today][].
[please donate today]: https://palletsprojects.com/donate

@ -0,0 +1,14 @@
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475
MarkupSafe-3.0.2.dist-info/METADATA,sha256=aAwbZhSmXdfFuMM-rEHpeiHRkBOGESyVLJIuwzHP-nw,3975
MarkupSafe-3.0.2.dist-info/RECORD,,
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=OVgtqZzfzIXXtylXP90gxCZ6CKBCwKYyHM8PpMEjN1M,151
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
markupsafe/__init__.py,sha256=sr-U6_27DfaSrj5jnHYxWN-pvhM27sjlDplMDPZKm7k,13214
markupsafe/__pycache__/__init__.cpython-312.pyc,,
markupsafe/__pycache__/_native.cpython-312.pyc,,
markupsafe/_native.py,sha256=hSLs8Jmz5aqayuengJJ3kdT5PwNpBWpKrmQSdipndC8,210
markupsafe/_speedups.c,sha256=O7XulmTo-epI6n2FtMVOrJXl8EAaIwD2iNYmBI5SEoQ,4149
markupsafe/_speedups.cpython-312-x86_64-linux-gnu.so,sha256=t1DBZlpsjFA30BOOvXfXfT1wvO_4cS16VbHz1-49q5U,43432
markupsafe/_speedups.pyi,sha256=ENd1bYe7gbBUf2ywyYWOGUpnXOHNJ-cgTNqetlW8h5k,41
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: setuptools (75.2.0)
Root-Is-Purelib: false
Tag: cp312-cp312-manylinux_2_17_x86_64
Tag: cp312-cp312-manylinux2014_x86_64

@ -0,0 +1,19 @@
Copyright 2005-2024 SQLAlchemy authors and contributors <see AUTHORS file>.
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.

@ -0,0 +1,243 @@
Metadata-Version: 2.1
Name: SQLAlchemy
Version: 2.0.36
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 :: 3.13
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 ; python_version < "3.13" and (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.10,!=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 <https://www.sqlalchemy.org/docs/intro.html#installation>`_.
Getting Help / Development / Bug reporting
------------------------------------------
Please refer to the `SQLAlchemy Community Guide <https://www.sqlalchemy.org/support.html>`_.
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 <https://www.sqlalchemy.org/codeofconduct.html>`_.
License
-------
SQLAlchemy is distributed under the `MIT license
<https://www.opensource.org/licenses/mit-license.php>`_.

@ -0,0 +1,530 @@
SQLAlchemy-2.0.36.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
SQLAlchemy-2.0.36.dist-info/LICENSE,sha256=PA9Zq4h9BB3mpOUv_j6e212VIt6Qn66abNettue-MpM,1100
SQLAlchemy-2.0.36.dist-info/METADATA,sha256=EZH514FydYtyOhgoZk_OF1ZQEtI4eTAEddlnUlRjzac,9692
SQLAlchemy-2.0.36.dist-info/RECORD,,
SQLAlchemy-2.0.36.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
SQLAlchemy-2.0.36.dist-info/WHEEL,sha256=7B4nnId14TToQHuAKpxbDLCJbNciqBsV-mvXE2hVLJc,151
SQLAlchemy-2.0.36.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
sqlalchemy/__init__.py,sha256=J2PsdiJiNW93Etxk6YN8o_C3TcpR1_DckU71r4LBcGE,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=Hq2bkXmG6-KO_RfCrwMqx4oGH-uH1Z1WWKqPWNjz8p4,6138
sqlalchemy/connectors/pyodbc.py,sha256=t7AjyxIOnaWg3CrlUEpBs4Y5l0HFdNt3P_cSSKhbi0Y,8501
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=ofziMIrcxCV-AGNBEvHL7QorRR2SPA9bkQj_k3WLD9E,1932256
sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571
sqlalchemy/cyextension/immutabledict.cpython-312-x86_64-linux-gnu.so,sha256=7SNuSRYPX4hzKqjAtbxJT2xLwa6Aegqtjfc9MYYZV0w,805632
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=hBeVC8lWoCgLmD5det5fcoL4V8LYT9Cu8TRLaAzWeW0,530680
sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792
sqlalchemy/cyextension/resultproxy.cpython-312-x86_64-linux-gnu.so,sha256=n6E3F0rPZFVUnpAm1pr3T-Rc8ZMqUcLjdRoOetVvJ8M,621328
sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725
sqlalchemy/cyextension/util.cpython-312-x86_64-linux-gnu.so,sha256=IbH9FP4ihbuGMhZ95oiRo_6F2wkSAlE2YDzegCVqErg,950928
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=msl_N_a_z8ali7Nthx55AGoV7b5wakCWvWu560BvH9o,132423
sqlalchemy/dialects/mssql/information_schema.py,sha256=HswjDc6y0mPXCf_x6VyylHlBdBa4PSY6Evxmmlch700,8084
sqlalchemy/dialects/mssql/json.py,sha256=evUACW2O62TAPq8B7QIPagz7jfc664ql9ms68JqiYzg,4816
sqlalchemy/dialects/mssql/provision.py,sha256=ZAtt6Div9NLIngMs8kyloxfphw0KDNMsnRCAVd7-esE,5593
sqlalchemy/dialects/mssql/pymssql.py,sha256=LAv43q4vBCB85OsAwHQItaQUYTYIO0QJ-jvzaBrswmY,4097
sqlalchemy/dialects/mssql/pyodbc.py,sha256=vwM-vBlmRwrqxOc73P0sFOrBSwn24wzc5IkEOpalbXQ,27056
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=-oMZnCqNsSki8mlQRTWIwiQPT1OVdZIuANkb90q8LAs,9999
sqlalchemy/dialects/mysql/asyncmy.py,sha256=YpuuOh8VknEeqHqUXQGfQ3jhfO3Xb-vZv78Jq5cscJ0,10067
sqlalchemy/dialects/mysql/base.py,sha256=giGlZNGrKsNMoSkbzY0PGgfamKjA9rOkSq1o5vKvno4,122755
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=_S1aV93kyP52Nvj7HR9weThML4oUvSLsLqiVFdoLR2o,8623
sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=oq3mtsNOMldUjs32JbJG2u3Hy3DObyVzUUMYfOkwkHg,5729
sqlalchemy/dialects/mysql/mysqldb.py,sha256=qUBbA6STeYGozutyTxHCo5p1W3p59QFFS2FwCgPrjBA,9503
sqlalchemy/dialects/mysql/provision.py,sha256=Jnk8UO9_Apd2odR2IQFLrscCfAmYxuBKcB8giS3bBog,3575
sqlalchemy/dialects/mysql/pymysql.py,sha256=GUnSHd2M2uKjmN46Hheymtm26g7phEgwYOXrX0zLY8M,4083
sqlalchemy/dialects/mysql/pyodbc.py,sha256=072crI4qVyPhajYvHnsfFeSrNjLFVPIjBQKo5uyz5yk,4297
sqlalchemy/dialects/mysql/reflection.py,sha256=3u34YwT1JJh3uThGZJZ3FKdnUcT7v08QB-tAl1r7VRk,22834
sqlalchemy/dialects/mysql/reserved_words.py,sha256=ucKX2p2c3UnMq2ayZuOHuf73eXhu7SKsOsTlIN1Q83I,9258
sqlalchemy/dialects/mysql/types.py,sha256=L5cTCsMT1pTedszNEM3jSxFNZEMcHQLprYCZ0vmfsnA,24343
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=zLMZedrr6j1LvJz4qYnoSjikI5RZY92YFeQHiZ_YvW0,119676
sqlalchemy/dialects/oracle/cx_oracle.py,sha256=q8Nyj15UZCE2TWOmxuWp5ZsxiCiGMzqfd_9UkmjIja0,55235
sqlalchemy/dialects/oracle/dictionary.py,sha256=7WMrbPkqo8ZdGjaEZyQr-5f2pajSOF1OTGb8P97z8-g,19519
sqlalchemy/dialects/oracle/oracledb.py,sha256=fZRKGqNIwW9LG4i8yDOXABrucbfzn_yC86Od-BJ3PcM,13619
sqlalchemy/dialects/oracle/provision.py,sha256=O9ZpF4OG6Cx4mMzLRfZwhs8dZjrJETWR402n9c7726A,8304
sqlalchemy/dialects/oracle/types.py,sha256=QK3hJvWzKnnCe3oD3rItwEEIwcoBze8qGg7VFOvVlIk,8231
sqlalchemy/dialects/postgresql/__init__.py,sha256=wwnNAq4wDQzrlPRzDNB06ayuq3L2HNO99nzeEvq-YcU,3892
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=bWcame7ntmI_Kx6gmBX0-chwADFdLHeCvaDQ4iX8id8,13734
sqlalchemy/dialects/postgresql/asyncpg.py,sha256=9P0Itn9eeSBu67kGSsHuzx8xd4YYwRKdiZ5m7bF5onU,41074
sqlalchemy/dialects/postgresql/base.py,sha256=dGPsaV3Esw6-AwE3QcgHF0Fray3Yw5-gLLgCvgdxvS0,179083
sqlalchemy/dialects/postgresql/dml.py,sha256=Pc69Le6qzmUHHb1FT5zeUSD31dWm6SBgdCAGW89cs3s,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=53rQWon9cUXd1yCjIvUpJjWwNyRSy3U7Kz0HV70ftrc,11618
sqlalchemy/dialects/postgresql/named_types.py,sha256=3IV1ufo7zJjKmX4VtGDEnoXE6xEqLJAtGG82IiqHXwY,17594
sqlalchemy/dialects/postgresql/operators.py,sha256=NsAaWun_tL3d_be0fs9YL6T4LPKK6crnmFxxIJHgyeY,2808
sqlalchemy/dialects/postgresql/pg8000.py,sha256=3yoekiWSF-xnaWMqG76XrYPMqerg-42TdmfsW_ivK9E,18640
sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=hY3NXEUHxTWD4umhd2aowNu3laC-61Q_qQ_pReyXTUM,9254
sqlalchemy/dialects/postgresql/provision.py,sha256=t6TZj0XaWG9zrpCjNr0oJRjAC_WQzaNdp3kaKJIbS8I,5770
sqlalchemy/dialects/postgresql/psycopg.py,sha256=Uwf45f9fInOtaExiEdwiP9xzRo7hw0XyZTkRtgdom44,23168
sqlalchemy/dialects/postgresql/psycopg2.py,sha256=kwEnflz5bAqJcuO_20eYiCtha_a4m_tg5_lppdDnaeU,31998
sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=M7wAYSL6Pvt-4nbfacAHGyyw4XMKJ_bQZ1tc1pBtIdg,1756
sqlalchemy/dialects/postgresql/ranges.py,sha256=6CgV7qkxEMJ9AQsiibo_XBLJYzGh-2ZxpG83sRaesVY,32949
sqlalchemy/dialects/postgresql/types.py,sha256=Jfxqw9JaKNOq29JRWBublywgb3lLMyzx8YZI7CXpS2s,7300
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=g3qGV6jmiXabWyb3282g_Nmxtj1jThxGSe9C9yalb-U,12345
sqlalchemy/dialects/sqlite/base.py,sha256=LcnW6hzxqTtPlDBOInHumvuDt8a31THA5Jnm4vFvdFI,97811
sqlalchemy/dialects/sqlite/dml.py,sha256=9GE55WvwoktKy2fHeT-Wbc9xPHgsbh5oBfd_fckMH5Q,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=aDp47n0J509kl2hDchoaBKXEQVZtkux54DwfKytUAe4,28068
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=frWSMmt3dlentYH4QNN3cijdGzp8NbunColUZwWsWgI,122958
sqlalchemy/engine/characteristics.py,sha256=N3kbvw_ApMh86wb5yAGnxtPYD4YRhYMWion1H_aVZBI,4765
sqlalchemy/engine/create.py,sha256=mYJtOG2ZKM8sgyfjpGpamW15RDU7JXi5s6iibbJHMIs,33206
sqlalchemy/engine/cursor.py,sha256=cFq61yrw76k-QR_xNUBWuL-Zeyb14ltG-6jo2Q2iuuw,76392
sqlalchemy/engine/default.py,sha256=2wwKKdsagb3QTajRSEw8Hl-EnQ-LmRxy822xOGyenHc,84648
sqlalchemy/engine/events.py,sha256=c0unNFFiHzTAvkUtXoJaxzMFMDwurBkHiiUhuN8qluc,37381
sqlalchemy/engine/interfaces.py,sha256=fcVHOmnMo7JZLHzgSKoK3QsdVHH7kJ_AmrDvwW9Ka3k,112936
sqlalchemy/engine/mock.py,sha256=yvpxgFmRw5G4QsHeF-ZwQGHKES-HqQOucTxFtN1uzdk,4179
sqlalchemy/engine/processors.py,sha256=XyfINKbo-2fjN-mW55YybvFyQMOil50_kVqsunahkNs,2379
sqlalchemy/engine/reflection.py,sha256=gwGs8y7x6py5z-ZWx3hQqQrwpHepMCTJyQcFwWJjPlw,75364
sqlalchemy/engine/result.py,sha256=NZEskTMAcDzK-vjE96Fw8VvBL58s5Y6rt9vXcmZdM4w,77651
sqlalchemy/engine/row.py,sha256=9AAQo9zYDL88GcZ3bjcQTwMT-YIcuGTSMAyTfmBJ_yM,12032
sqlalchemy/engine/strategies.py,sha256=DqFSWaXJPL-29Omot9O0aOcuGL8KmCGyOvnPGDkAJoE,442
sqlalchemy/engine/url.py,sha256=8eWkUaIUyDExOcJ2D4xJXRcn4OY1GQJ3Q2duSX6UGAg,30784
sqlalchemy/engine/util.py,sha256=bNirO8k1S8yOW61uNH-a9QrWtAJ9VGFgbiR0lk1lUQU,5682
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=DtDVgjKSorOfp9MGJ7fgMWrj4seC_hkwF4D8CW1RFZU,8226
sqlalchemy/event/attr.py,sha256=X8QeHGK4ioSYht1vkhc11f606_mq_t91jMNIT314ubs,20751
sqlalchemy/event/base.py,sha256=270OShTD17-bSFUFnPtKdVnB0NFJZ2AouYPo1wT0aJw,15127
sqlalchemy/event/legacy.py,sha256=teMPs00fO-4g8a_z2omcVKkYce5wj_1uvJO2n2MIeuo,8227
sqlalchemy/event/registry.py,sha256=nfTSSyhjZZXc5wseWB4sXn-YibSc0LKX8mg17XlWmAo,10835
sqlalchemy/events.py,sha256=k-ZD38aSPD29LYhED7CBqttp5MDVVx_YSaWC2-cu9ec,525
sqlalchemy/exc.py,sha256=M_8-O1hd8i6gbyx-TapV400p_Lxq2QqTGMXUAO-YgCc,23976
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=ZGc_ssGf7FC6eKrja1iTvnWEKLkFZQA8CiVAjR8iVRw,66062
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=fl7wxZD9KjgFiCtG3WXrYjHEvanamcsodCqq9pH9lOk,8905
sqlalchemy/ext/asyncio/engine.py,sha256=S_IRWX4QAjj2veLSu4Y3gKBIXkKQt7_2StJAK2_KUDY,48190
sqlalchemy/ext/asyncio/exc.py,sha256=8sII7VMXzs2TrhizhFQMzSfcroRtiesq8o3UwLfXSgQ,639
sqlalchemy/ext/asyncio/result.py,sha256=3rbVIY_wySi50JwaK3Kf2qa3c5Fc8W84FtUpt-9i9Vk,30477
sqlalchemy/ext/asyncio/scoping.py,sha256=UxHAFxtWKqA7TEozyN2h7MJyzSspTCrS-1SlgQLTExo,52608
sqlalchemy/ext/asyncio/session.py,sha256=QpXnqspwYnT28znD1EdpUIaVjQOO1BirtS0BJeBxeZk,63087
sqlalchemy/ext/automap.py,sha256=r0mUSyogNyqdBL4m9AA1NXbLiTLQmtvyQymsssNEipo,61581
sqlalchemy/ext/baked.py,sha256=H6T1il7GY84BhzPFj49UECSpZh_eBuiHomA-QIsYOYQ,17807
sqlalchemy/ext/compiler.py,sha256=6X6sZCAo9v-PQfLbwBSYQUK0-XH2xTE5Jm0Zg6Ka6eM,20877
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=wuwAPnHymln0unSBnyx-cpX0AfESKSsypaSQTYCvzDk,16750
sqlalchemy/ext/hybrid.py,sha256=IYkCaPZ29gm2cPKPg0cWMkLCEqMykD8-JJTvgacGbmc,52458
sqlalchemy/ext/indexable.py,sha256=UkTelbydKCdKelzbv3HWFFavoET9WocKaGRPGEOVfN8,11032
sqlalchemy/ext/instrumentation.py,sha256=sg8ghDjdHSODFXh_jAmpgemnNX1rxCeeXEG3-PMdrNk,15707
sqlalchemy/ext/mutable.py,sha256=L5ZkHBGYhMaqO75Xtyrk2DBR44RDk0g6Rz2HzHH0F8Q,37355
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=Aek_-XA1eXihT4attxhfE43yBKtCgsxBSb--qgZKUqc,10550
sqlalchemy/ext/mypy/decl_class.py,sha256=1vVJRII2apnLTUbc5HkJS6Z2GueaUv_eKvhbqh7Wik4,17384
sqlalchemy/ext/mypy/infer.py,sha256=KVnmLFEVS33Al8pUKI7MJbJQu3KeveBUMl78EluBORw,19369
sqlalchemy/ext/mypy/names.py,sha256=Q3ef8XQBgVm9WUwlItqlYCXDNi_kbV5DdLEgbtEMEI8,10479
sqlalchemy/ext/mypy/plugin.py,sha256=74ML8LI9xar0V86oCxnPFv5FQGEEfUzK64vOay4BKFs,9750
sqlalchemy/ext/mypy/util.py,sha256=DKRaurkXHI2lAMAAcEO5GLXbX_m2Xqy7l_juh8Byf5U,9960
sqlalchemy/ext/orderinglist.py,sha256=TGYbsGH72wEZcFNQDYDsZg9OSPuzf__P8YX8_2HtYUo,14384
sqlalchemy/ext/serializer.py,sha256=D0g4jMZkRk0Gjr0L-FZe81SR63h0Zs-9JzuWtT_SD7k,6140
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=MF-LE358wZDUEl1IH8-Uwt2HI65EsQpQW5o5udHkZwA,5063
sqlalchemy/log.py,sha256=8x9UR3nj0uFm6or6bQF-JWb4fYv2zOeQjG_w-0wOJFA,8607
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=8EQfYsDL2k_ev0eK-wxMl3algouczN38Gu43CrRlAlo,103434
sqlalchemy/orm/_typing.py,sha256=DVBfpHmDVK4x1zxaGJPY2GoTrAsyR6uexv20Lzf1afc,4973
sqlalchemy/orm/attributes.py,sha256=lorOHBJvJJYndOuafWJhHBbQ1pR6FAyimhqz-mErBRQ,92534
sqlalchemy/orm/base.py,sha256=FXkYTSCDUJFQSB5pcyPt2wG-dRctf5P6ySjyjVxQsX0,27502
sqlalchemy/orm/bulk_persistence.py,sha256=1FC23bRJKjpfbp2D5hYuV1qOVIKGSswu9XPXbbSJ5Mo,72663
sqlalchemy/orm/clsregistry.py,sha256=IjoDZwWpjG42ji59L4M1EZvjBEoXPZykzENDtKWxU8A,17974
sqlalchemy/orm/collections.py,sha256=WEKuUCRgLhDhJEIBhZ21UrE0pBOyRm2zxD20GvbgA9g,52243
sqlalchemy/orm/context.py,sha256=FMPyw07OA9OXWQ32RQx52AEa2xTLSkqdYgx9R_yN1x0,112955
sqlalchemy/orm/decl_api.py,sha256=_WPKQ_vSE5k2TLtNmkaxxYmvbhZvkRMrrvCeDxdqDQE,63998
sqlalchemy/orm/decl_base.py,sha256=8R7go5sULTYNRlhYiEjXIJkQ34oPp7DY_fC2nS5D5is,83343
sqlalchemy/orm/dependency.py,sha256=hgjksUWhgbmgHK5GdJdiDCBgDAIGQXIrY-Tj79tbL2k,47631
sqlalchemy/orm/descriptor_props.py,sha256=dR_h4Gvdtpcdp4sj_ZOR4P5Nng2J2vhsvFHouRLlntc,37244
sqlalchemy/orm/dynamic.py,sha256=rWAZ-nfAkREuNjt8e_FRdqYrvHDdbODn1CcfyP8Y18k,9816
sqlalchemy/orm/evaluator.py,sha256=tRETz4dNZ71VsEA8nG0hpefByB-W0zBt02IxcSR5H2g,12353
sqlalchemy/orm/events.py,sha256=1PiGT7JMUWTDAb3X1T79P02BMVDmcWEpatz1FwpLqoA,127777
sqlalchemy/orm/exc.py,sha256=IP40P-wOeXhkYk0YizuTC3wqm6W9cPTaQU08f5MMaQ0,7413
sqlalchemy/orm/identity.py,sha256=jHdCxCpCyda_8mFOfGmN_Pr0XZdKiU-2hFZshlNxbHs,9249
sqlalchemy/orm/instrumentation.py,sha256=M-kZmkUvHUxtf-0mCA8RIM5QmMH1hWlYR_pKMwaidjA,24321
sqlalchemy/orm/interfaces.py,sha256=7Lni4Cue41b1CsmN4VbeUyWwzuNMcKtkrpihc9U-WIw,48690
sqlalchemy/orm/loading.py,sha256=9RacpzFOWbuKgPRWHFmyIvD4fYCLAnkpwBFASyQ2CoI,58277
sqlalchemy/orm/mapped_collection.py,sha256=zK3d3iozORzDruBUrAmkVC0RR3Orj5szk-TSQ24xzIU,19682
sqlalchemy/orm/mapper.py,sha256=W-srpoEc3UIYv_6qTXTd_dG_TVeQcToG77VGrXt85PM,171738
sqlalchemy/orm/path_registry.py,sha256=sJZMv_WPqUpHfQtKWaX3WYFeKBcNJ8C3wOM2mkBGkTE,25920
sqlalchemy/orm/persistence.py,sha256=dzyB2JOXNwQgaCbN8kh0sEz00WFePr48qf8NWVCUZH8,61701
sqlalchemy/orm/properties.py,sha256=eDPFzxYUgdM3uWjHywnb1XW-i0tVKKyx7A2MCD31GQU,29306
sqlalchemy/orm/query.py,sha256=Cf0e94-u1XyoXJoOAmr4iFvtCwNY98kxUYyMPenaWTE,117708
sqlalchemy/orm/relationships.py,sha256=dS5SY0v1MiD7iCNnAQlHaI6prUQhL5EkXT7ijc8FR8E,128644
sqlalchemy/orm/scoping.py,sha256=rJVc7_Lic4V00HZ-UvYFWkVpXqdrMayRmIs4fIwH1UA,78688
sqlalchemy/orm/session.py,sha256=CZJTQ-wPwIy0c3AMFxgJnBgaft6eEf4JzcCLcaaCSjg,195979
sqlalchemy/orm/state.py,sha256=327-F4TG29s6mLC8oWRiO2PuvYIUZzY1MqUPjtUy7M4,37670
sqlalchemy/orm/state_changes.py,sha256=qKYg7NxwrDkuUY3EPygAztym6oAVUFcP2wXn7QD3Mz4,6815
sqlalchemy/orm/strategies.py,sha256=-tsBRsmEqkaxAAIn4t2F-U5SrRIPoPCyzpqFYGTAwNs,119866
sqlalchemy/orm/strategy_options.py,sha256=oeDl_rMDNAC_90N7ytsni-psXWAeQMhABQFyKBSmai0,85353
sqlalchemy/orm/sync.py,sha256=g7iZfSge1HgxMk9SKRgUgtHEbpbZ1kP_CBqOIdTOXqc,5779
sqlalchemy/orm/unitofwork.py,sha256=fiVaqcymbDDHRa1NjS90N9Z466nd5pkJOEi1dHO6QLY,27033
sqlalchemy/orm/util.py,sha256=5SC4MOVU0cPObexDjpMvXvetueiU5pze42raL94gj24,81021
sqlalchemy/orm/writeonly.py,sha256=SYu2sAaHZONk2pW4PmtE871LG-O0P_bjidvKzY1H_zI,22305
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=WF4az4ZKuzQGuKeSJeyexaYjmWZUvYdC6KIi8zTGodw,52236
sqlalchemy/pool/events.py,sha256=xGjkIUZl490ZDtCHqnQF9ZCwe2Jv93eGXmnQxftB11E,13147
sqlalchemy/pool/impl.py,sha256=JwpALSkH-pCoO_6oENbkHYY00Jx9nlttyoI61LivRNc,18944
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=833Flez92odZkE2Vy6SXK8LcoO1AwkfVzOnATJLWFsA,63168
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=wjE6HrLm9cR7bxvZXT8sFLUqT6t_J9G1XyQCnYmBDl0,18780
sqlalchemy/sql/_typing.py,sha256=oqwrYHVMtK-AuKGH9c4SgfiOEJUt5vjkzSEzzscMHkM,12771
sqlalchemy/sql/annotation.py,sha256=aqbbVz9kfbCT3_66CZ9GEirVN197Cukoqt8rq48FgkQ,18245
sqlalchemy/sql/base.py,sha256=M1b-Tg49ikUW2mnZv0aI38oASG6dgeo4jBNWDgJgAg8,73925
sqlalchemy/sql/cache_key.py,sha256=0Db8mR8IrpBgdzXs4TGTt98LOpL3c7KABd72MAPKUQQ,33668
sqlalchemy/sql/coercions.py,sha256=hAEou9Ycyswzu8yz_Q7QkwL2_c3nctzBJQS2oDEr4iE,40664
sqlalchemy/sql/compiler.py,sha256=hrTptbOKIgVIHapywj4Lk5OMwpXvHS-KGg3odFwlo-I,274687
sqlalchemy/sql/crud.py,sha256=HBX4QPtW_PYYJmIKfNr-wE8IdEr963N24WXzFBUZOo0,56514
sqlalchemy/sql/ddl.py,sha256=lKqvOigbcYrDG0euxd5F4tu9HbBi1kmp3eFPc45HH-8,45636
sqlalchemy/sql/default_comparator.py,sha256=utXWsZVGEjflhFfCT4ywa6RnhORc1Rryo87Hga71Rps,16707
sqlalchemy/sql/dml.py,sha256=pn0Lm1ofC5qVZzwGWFW73lPCiNba8OsTeemurJgwRyg,65614
sqlalchemy/sql/elements.py,sha256=YfccXzQc9DlgF8q15kDf-zKBUY_vpIe0FGaVDBPoic4,176544
sqlalchemy/sql/events.py,sha256=iC_Q1Htm1Aobt5tOYxWfHHqNpoytrULORmUKcusH_-E,18290
sqlalchemy/sql/expression.py,sha256=VMX-dLpsZYnVRJpYNDozDUgaj7iQ0HuewUKVefD57PE,7586
sqlalchemy/sql/functions.py,sha256=kMMYplvuIHFAPwxBI03SizwaLcYEHzysecWk-R1V-JM,63762
sqlalchemy/sql/lambdas.py,sha256=DP0Qz7Ypo8QhzMwygGHYgRhwJMx-rNezO1euouH3iYU,49292
sqlalchemy/sql/naming.py,sha256=ZHs1qSV3ou8TYmZ92uvU3sfdklUQlIz4uhe330n05SU,6858
sqlalchemy/sql/operators.py,sha256=himArRqBzrljob3Zfhi_ZS-Jleg1u6YFp0g3d7Co6IM,76106
sqlalchemy/sql/roles.py,sha256=pOsVn_OZD7mF2gJByHf24Rjopt0_Hu3dUCEOK5t4KS8,7662
sqlalchemy/sql/schema.py,sha256=iFleWHkxi-3mKGiK_N1TzUqxnNwOpypB4bWDuAVQe8c,229717
sqlalchemy/sql/selectable.py,sha256=cgyV0AsPy4CXAFdhMiTCkbgaHiFilW9sclzxlHJKH3o,236460
sqlalchemy/sql/sqltypes.py,sha256=5_N9MhprQFWYc3yjcXgFC_DmvkQU-Jz-Ok9nIMYp2Q4,127469
sqlalchemy/sql/traversals.py,sha256=3ScTC1fh1-y8Y478h_2Azmd2xdQdWPWkDve4YgrwMf8,33664
sqlalchemy/sql/type_api.py,sha256=SN16_oNZG6G65cvG6ABPcptz_YV5vfB2fknwJZxrkOs,84464
sqlalchemy/sql/util.py,sha256=qGHQF-tPCj-m1FBerzT7weCanGcXU7dK5m-W7NHio-4,48077
sqlalchemy/sql/visitors.py,sha256=71wdVvhhZL4nJvVwFAs6ssaW-qZgNRSmKjpAcOzF_TA,36317
sqlalchemy/testing/__init__.py,sha256=zgitAYzsCWT_U48ZiifXHHLJFo8nZBYmI-5TueA4_lE,3160
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=IgQG7l94WaiRP8nTbilJh1ZHZl125g7GPq-S5kmQZN0,16817
sqlalchemy/testing/asyncio.py,sha256=kM8uuOqDBagZF0r9xvGmsiirUVLUQ_KBzjUFU67W-b8,3830
sqlalchemy/testing/config.py,sha256=AqyH1qub_gDqX0BvlL-JBQe7N-t2wo8655FtwblUNOY,12090
sqlalchemy/testing/engines.py,sha256=HFJceEBD3Q_TTFQMTtIV5wGWO_a7oUgoKtUF_z636SM,13481
sqlalchemy/testing/entities.py,sha256=IphFegPKbff3Un47jY6bi7_MQXy6qkx_50jX2tHZJR4,3354
sqlalchemy/testing/exclusions.py,sha256=T8B01hmm8WVs-EKcUOQRzabahPqblWJfOidi6bHJ6GA,12460
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=gdxiwNFIzDlNGSOdvM3gbwDceVCC9t8oM5kKbwyhGBk,11973
sqlalchemy/testing/fixtures/orm.py,sha256=8EFbnaBbXX_Bf4FcCzBUaAHgyVpsLGBHX16SGLqE3Fg,6095
sqlalchemy/testing/fixtures/sql.py,sha256=KZMjco9_3dsuspmkew5Ejp88Wlr9PsSBB1qeJGFxQAk,15900
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=BgNzWNEmgpK4CwhyblQQKnH-7FDKVi_Uul5vw8fFjBU,21578
sqlalchemy/testing/plugin/pytestplugin.py,sha256=6jkQHH2VQMD75k2As9CuWXmEy9jrscoFRhCNg6-PaTw,27656
sqlalchemy/testing/profiling.py,sha256=PbuPhRFbauFilUONeY3tV_Y_5lBkD7iCa8VVyH2Sk9Y,10148
sqlalchemy/testing/provision.py,sha256=3qFor_sN1FFlS7odUGkKqLUxGmQZC9XM67I9vQ_zeXo,14626
sqlalchemy/testing/requirements.py,sha256=Z__o-1Rj9B7dI8E_l3qsKTvsg0rK198vB0A1p7A5dcM,52832
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=9azifj6-OCD7s8h_tAO1uPw100ibQv8YoKc_VA3hn3c,18824
sqlalchemy/testing/suite/test_reflection.py,sha256=7sML8-owubSQeEM7Ve6LbnB8uIVlNV00WWepKwII2a8,109648
sqlalchemy/testing/suite/test_results.py,sha256=X720GafdA4p75SOGS93j-dXkt6QDEnnJbU2bh18VCcg,16914
sqlalchemy/testing/suite/test_rowcount.py,sha256=3KDTlRgjpQ1OVfp__1cv8Hvq4CsDKzmrhJQ_WIJWoJg,7900
sqlalchemy/testing/suite/test_select.py,sha256=ulRZQJlzkwwcewEyisuBEXVWFR0Wshz9MEDxYYiYLwQ,61732
sqlalchemy/testing/suite/test_sequence.py,sha256=66bCoy4xo99GBSaX6Hxb88foANAykLGRz1YEKbvpfuA,9923
sqlalchemy/testing/suite/test_types.py,sha256=K4MGHvnTtgqeksoQOBCZRVQYC7HoYO6Z6rVt5vj2t9o,67805
sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=c3_eIxLyORuSOhNDP0jWKxPyUf3SwMFpdalxtquwqlM,6141
sqlalchemy/testing/suite/test_update_delete.py,sha256=yTiM2unnfOK9rK8ZkqeTTU_MkT-RsKFLmdYliniZfAY,3994
sqlalchemy/testing/util.py,sha256=qldXKw8gRJ4I2x3uXsBssYMqwatmcMFMTOveRQCmfDU,14469
sqlalchemy/testing/warnings.py,sha256=fJ-QJUY2zY2PPxZJKv9medW-BKKbCNbA4Ns_V3YwFXM,1546
sqlalchemy/types.py,sha256=cQFM-hFRmaf1GErun1qqgEs6QxufvzMuwKqj9tuMPpE,3168
sqlalchemy/util/__init__.py,sha256=5D5Mquvx3SOmud0QErKzzGvBTkqMdhrrd_sXijOILeo,8312
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=aZoSAVOXnHBoYEsxDOi0O9odg9wqLbGb7PGjaWQKiyY,20078
sqlalchemy/util/_concurrency_py3k.py,sha256=zb0Bow2Y_QjTdaACEviBEEaFvqDuVvpJfmwCjaw8xNE,9170
sqlalchemy/util/_has_cy.py,sha256=wCQmeSjT3jaH_oxfCEtGk-1g0gbSpt5MCK5UcWdMWqk,1247
sqlalchemy/util/_py_collections.py,sha256=U6L5AoyLdgSv7cdqB4xxQbw1rpeJjyOZVXffgxgga8I,16714
sqlalchemy/util/compat.py,sha256=cnucBQOKspo58vjRpQXUBrHGguHOSFvftpD-I8vfUy0,8760
sqlalchemy/util/concurrency.py,sha256=9lT_cMoO1fZNdY8QTUZ22oeSf-L5I-79Ke7chcBNPA0,3304
sqlalchemy/util/deprecations.py,sha256=YBwvvYhSB8LhasIZRKvg_-WNoVhPUcaYI1ZrnjDn868,11971
sqlalchemy/util/langhelpers.py,sha256=uIK3szZuq9aMnO-vEpSlNekNWv4I-E391e56bkTnUm0,65090
sqlalchemy/util/preloaded.py,sha256=az7NmLJLsqs0mtM9uBkIu10-841RYDq8wOyqJ7xXvqE,5904
sqlalchemy/util/queue.py,sha256=CaeSEaYZ57YwtmLdNdOIjT5PK_LCuwMFiO0mpp39ybM,10185
sqlalchemy/util/tool_support.py,sha256=9braZyidaiNrZVsWtGmkSmus50-byhuYrlAqvhjcmnA,6135
sqlalchemy/util/topological.py,sha256=N3M3Le7KzGHCmqPGg0ZBqixTDGwmFLhOZvBtc4rHL_g,3458
sqlalchemy/util/typing.py,sha256=lFcGo1dJbZIZ9drAnvef-PzP0cX4LMxMSwgk3lJBb0g,18182

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: setuptools (75.1.0)
Root-Is-Purelib: false
Tag: cp312-cp312-manylinux_2_17_x86_64
Tag: cp312-cp312-manylinux2014_x86_64

@ -0,0 +1,16 @@
# -*- test-case-name: automat -*-
"""
State-machines.
"""
from ._typed import TypeMachineBuilder, pep614, AlreadyBuiltError, TypeMachine
from ._core import NoTransition
from ._methodical import MethodicalMachine
__all__ = [
"TypeMachineBuilder",
"TypeMachine",
"NoTransition",
"AlreadyBuiltError",
"pep614",
"MethodicalMachine",
]

@ -0,0 +1,203 @@
# -*- test-case-name: automat._test.test_core -*-
"""
A core state-machine abstraction.
Perhaps something that could be replaced with or integrated into machinist.
"""
from __future__ import annotations
import sys
from itertools import chain
from typing import Callable, Generic, Optional, Sequence, TypeVar, Hashable
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
_NO_STATE = "<no state>"
State = TypeVar("State", bound=Hashable)
Input = TypeVar("Input", bound=Hashable)
Output = TypeVar("Output", bound=Hashable)
class NoTransition(Exception, Generic[State, Input]):
"""
A finite state machine in C{state} has no transition for C{symbol}.
@ivar state: See C{state} init parameter.
@ivar symbol: See C{symbol} init parameter.
"""
def __init__(self, state: State, symbol: Input):
"""
Construct a L{NoTransition}.
@param state: the finite state machine's state at the time of the
illegal transition.
@param symbol: the input symbol for which no transition exists.
"""
self.state = state
self.symbol = symbol
super(Exception, self).__init__(
"no transition for {} in {}".format(symbol, state)
)
class Automaton(Generic[State, Input, Output]):
"""
A declaration of a finite state machine.
Note that this is not the machine itself; it is immutable.
"""
def __init__(self, initial: State | None = None) -> None:
"""
Initialize the set of transitions and the initial state.
"""
if initial is None:
initial = _NO_STATE # type:ignore[assignment]
assert initial is not None
self._initialState: State = initial
self._transitions: set[tuple[State, Input, State, Sequence[Output]]] = set()
self._unhandledTransition: Optional[tuple[State, Sequence[Output]]] = None
@property
def initialState(self) -> State:
"""
Return this automaton's initial state.
"""
return self._initialState
@initialState.setter
def initialState(self, state: State) -> None:
"""
Set this automaton's initial state. Raises a ValueError if
this automaton already has an initial state.
"""
if self._initialState is not _NO_STATE:
raise ValueError(
"initial state already set to {}".format(self._initialState)
)
self._initialState = state
def addTransition(
self,
inState: State,
inputSymbol: Input,
outState: State,
outputSymbols: tuple[Output, ...],
):
"""
Add the given transition to the outputSymbol. Raise ValueError if
there is already a transition with the same inState and inputSymbol.
"""
# keeping self._transitions in a flat list makes addTransition
# O(n^2), but state machines don't tend to have hundreds of
# transitions.
for anInState, anInputSymbol, anOutState, _ in self._transitions:
if anInState == inState and anInputSymbol == inputSymbol:
raise ValueError(
"already have transition from {} to {} via {}".format(
inState, anOutState, inputSymbol
)
)
self._transitions.add((inState, inputSymbol, outState, tuple(outputSymbols)))
def unhandledTransition(
self, outState: State, outputSymbols: Sequence[Output]
) -> None:
"""
All unhandled transitions will be handled by transitioning to the given
error state and error-handling output symbols.
"""
self._unhandledTransition = (outState, tuple(outputSymbols))
def allTransitions(self) -> frozenset[tuple[State, Input, State, Sequence[Output]]]:
"""
All transitions.
"""
return frozenset(self._transitions)
def inputAlphabet(self) -> set[Input]:
"""
The full set of symbols acceptable to this automaton.
"""
return {
inputSymbol
for (inState, inputSymbol, outState, outputSymbol) in self._transitions
}
def outputAlphabet(self) -> set[Output]:
"""
The full set of symbols which can be produced by this automaton.
"""
return set(
chain.from_iterable(
outputSymbols
for (inState, inputSymbol, outState, outputSymbols) in self._transitions
)
)
def states(self) -> frozenset[State]:
"""
All valid states; "Q" in the mathematical description of a state
machine.
"""
return frozenset(
chain.from_iterable(
(inState, outState)
for (inState, inputSymbol, outState, outputSymbol) in self._transitions
)
)
def outputForInput(
self, inState: State, inputSymbol: Input
) -> tuple[State, Sequence[Output]]:
"""
A 2-tuple of (outState, outputSymbols) for inputSymbol.
"""
for anInState, anInputSymbol, outState, outputSymbols in self._transitions:
if (inState, inputSymbol) == (anInState, anInputSymbol):
return (outState, list(outputSymbols))
if self._unhandledTransition is None:
raise NoTransition(state=inState, symbol=inputSymbol)
return self._unhandledTransition
OutputTracer = Callable[[Output], None]
Tracer: TypeAlias = "Callable[[State, Input, State], OutputTracer[Output] | None]"
class Transitioner(Generic[State, Input, Output]):
"""
The combination of a current state and an L{Automaton}.
"""
def __init__(self, automaton: Automaton[State, Input, Output], initialState: State):
self._automaton: Automaton[State, Input, Output] = automaton
self._state: State = initialState
self._tracer: Tracer[State, Input, Output] | None = None
def setTrace(self, tracer: Tracer[State, Input, Output] | None) -> None:
self._tracer = tracer
def transition(
self, inputSymbol: Input
) -> tuple[Sequence[Output], OutputTracer[Output] | None]:
"""
Transition between states, returning any outputs.
"""
outState, outputSymbols = self._automaton.outputForInput(
self._state, inputSymbol
)
outTracer = None
if self._tracer:
outTracer = self._tracer(self._state, inputSymbol, outState)
self._state = outState
return (outputSymbols, outTracer)

@ -0,0 +1,168 @@
from __future__ import annotations
import collections
import inspect
from typing import Any, Iterator
from twisted.python.modules import PythonAttribute, PythonModule, getModule
from automat import MethodicalMachine
from ._typed import TypeMachine, InputProtocol, Core
def isOriginalLocation(attr: PythonAttribute | PythonModule) -> bool:
"""
Attempt to discover if this appearance of a PythonAttribute
representing a class refers to the module where that class was
defined.
"""
sourceModule = inspect.getmodule(attr.load())
if sourceModule is None:
return False
currentModule = attr
while not isinstance(currentModule, PythonModule):
currentModule = currentModule.onObject
return currentModule.name == sourceModule.__name__
def findMachinesViaWrapper(
within: PythonModule | PythonAttribute,
) -> Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]]:
"""
Recursively yield L{MethodicalMachine}s and their FQPNs within a
L{PythonModule} or a L{twisted.python.modules.PythonAttribute}
wrapper object.
Note that L{PythonModule}s may refer to packages, as well.
The discovery heuristic considers L{MethodicalMachine} instances
that are module-level attributes or class-level attributes
accessible from module scope. Machines inside nested classes will
be discovered, but those returned from functions or methods will not be.
@type within: L{PythonModule} or L{twisted.python.modules.PythonAttribute}
@param within: Where to start the search.
@return: a generator which yields FQPN, L{MethodicalMachine} pairs.
"""
queue = collections.deque([within])
visited: set[
PythonModule
| PythonAttribute
| MethodicalMachine
| TypeMachine[InputProtocol, Core]
| type[Any]
] = set()
while queue:
attr = queue.pop()
value = attr.load()
if (
isinstance(value, MethodicalMachine) or isinstance(value, TypeMachine)
) and value not in visited:
visited.add(value)
yield attr.name, value
elif (
inspect.isclass(value) and isOriginalLocation(attr) and value not in visited
):
visited.add(value)
queue.extendleft(attr.iterAttributes())
elif isinstance(attr, PythonModule) and value not in visited:
visited.add(value)
queue.extendleft(attr.iterAttributes())
queue.extendleft(attr.iterModules())
class InvalidFQPN(Exception):
"""
The given FQPN was not a dot-separated list of Python objects.
"""
class NoModule(InvalidFQPN):
"""
A prefix of the FQPN was not an importable module or package.
"""
class NoObject(InvalidFQPN):
"""
A suffix of the FQPN was not an accessible object
"""
def wrapFQPN(fqpn: str) -> PythonModule | PythonAttribute:
"""
Given an FQPN, retrieve the object via the global Python module
namespace and wrap it with a L{PythonModule} or a
L{twisted.python.modules.PythonAttribute}.
"""
# largely cribbed from t.p.reflect.namedAny
if not fqpn:
raise InvalidFQPN("FQPN was empty")
components = collections.deque(fqpn.split("."))
if "" in components:
raise InvalidFQPN(
"name must be a string giving a '.'-separated list of Python "
"identifiers, not %r" % (fqpn,)
)
component = components.popleft()
try:
module = getModule(component)
except KeyError:
raise NoModule(component)
# find the bottom-most module
while components:
component = components.popleft()
try:
module = module[component]
except KeyError:
components.appendleft(component)
break
else:
module.load()
else:
return module
# find the bottom-most attribute
attribute = module
for component in components:
try:
attribute = next(
child
for child in attribute.iterAttributes()
if child.name.rsplit(".", 1)[-1] == component
)
except StopIteration:
raise NoObject("{}.{}".format(attribute.name, component))
return attribute
def findMachines(
fqpn: str,
) -> Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]]:
"""
Recursively yield L{MethodicalMachine}s and their FQPNs in and under the a
Python object specified by an FQPN.
The discovery heuristic considers L{MethodicalMachine} instances that are
module-level attributes or class-level attributes accessible from module
scope. Machines inside nested classes will be discovered, but those
returned from functions or methods will not be.
@param fqpn: a fully-qualified Python identifier (i.e. the dotted
identifier of an object defined at module or class scope, including the
package and modele names); where to start the search.
@return: a generator which yields (C{FQPN}, L{MethodicalMachine}) pairs.
"""
return findMachinesViaWrapper(wrapFQPN(fqpn))

@ -0,0 +1,57 @@
"""
Python introspection helpers.
"""
from types import CodeType as code, FunctionType as function
def copycode(template, changes):
if hasattr(code, "replace"):
return template.replace(**{"co_" + k: v for k, v in changes.items()})
names = [
"argcount",
"nlocals",
"stacksize",
"flags",
"code",
"consts",
"names",
"varnames",
"filename",
"name",
"firstlineno",
"lnotab",
"freevars",
"cellvars",
]
if hasattr(code, "co_kwonlyargcount"):
names.insert(1, "kwonlyargcount")
if hasattr(code, "co_posonlyargcount"):
# PEP 570 added "positional only arguments"
names.insert(1, "posonlyargcount")
values = [changes.get(name, getattr(template, "co_" + name)) for name in names]
return code(*values)
def copyfunction(template, funcchanges, codechanges):
names = [
"globals",
"name",
"defaults",
"closure",
]
values = [
funcchanges.get(name, getattr(template, "__" + name + "__")) for name in names
]
return function(copycode(template.__code__, codechanges), *values)
def preserveName(f):
"""
Preserve the name of the given function on the decorated function.
"""
def decorator(decorated):
return copyfunction(decorated, dict(name=f.__name__), dict(name=f.__name__))
return decorator

@ -0,0 +1,545 @@
# -*- test-case-name: automat._test.test_methodical -*-
from __future__ import annotations
import collections
import sys
from dataclasses import dataclass, field
from functools import wraps
from inspect import getfullargspec as getArgsSpec
from itertools import count
from typing import Any, Callable, Hashable, Iterable, TypeVar
if sys.version_info < (3, 10):
from typing_extensions import TypeAlias
else:
from typing import TypeAlias
from ._core import Automaton, OutputTracer, Tracer, Transitioner
from ._introspection import preserveName
ArgSpec = collections.namedtuple(
"ArgSpec",
[
"args",
"varargs",
"varkw",
"defaults",
"kwonlyargs",
"kwonlydefaults",
"annotations",
],
)
def _getArgSpec(func):
"""
Normalize inspect.ArgSpec across python versions
and convert mutable attributes to immutable types.
:param Callable func: A function.
:return: The function's ArgSpec.
:rtype: ArgSpec
"""
spec = getArgsSpec(func)
return ArgSpec(
args=tuple(spec.args),
varargs=spec.varargs,
varkw=spec.varkw,
defaults=spec.defaults if spec.defaults else (),
kwonlyargs=tuple(spec.kwonlyargs),
kwonlydefaults=(
tuple(spec.kwonlydefaults.items()) if spec.kwonlydefaults else ()
),
annotations=tuple(spec.annotations.items()),
)
def _getArgNames(spec):
"""
Get the name of all arguments defined in a function signature.
The name of * and ** arguments is normalized to "*args" and "**kwargs".
:param ArgSpec spec: A function to interrogate for a signature.
:return: The set of all argument names in `func`s signature.
:rtype: Set[str]
"""
return set(
spec.args
+ spec.kwonlyargs
+ (("*args",) if spec.varargs else ())
+ (("**kwargs",) if spec.varkw else ())
+ spec.annotations
)
def _keywords_only(f):
"""
Decorate a function so all its arguments must be passed by keyword.
A useful utility for decorators that take arguments so that they don't
accidentally get passed the thing they're decorating as their first
argument.
Only works for methods right now.
"""
@wraps(f)
def g(self, **kw):
return f(self, **kw)
return g
@dataclass(frozen=True)
class MethodicalState(object):
"""
A state for a L{MethodicalMachine}.
"""
machine: MethodicalMachine = field(repr=False)
method: Callable[..., Any] = field()
serialized: bool = field(repr=False)
def upon(
self,
input: MethodicalInput,
enter: MethodicalState | None = None,
outputs: Iterable[MethodicalOutput] | None = None,
collector: Callable[[Iterable[T]], object] = list,
) -> None:
"""
Declare a state transition within the L{MethodicalMachine} associated
with this L{MethodicalState}: upon the receipt of the `input`, enter
the `state`, emitting each output in `outputs`.
@param input: The input triggering a state transition.
@param enter: The resulting state.
@param outputs: The outputs to be triggered as a result of the declared
state transition.
@param collector: The function to be used when collecting output return
values.
@raises TypeError: if any of the `outputs` signatures do not match the
`inputs` signature.
@raises ValueError: if the state transition from `self` via `input` has
already been defined.
"""
if enter is None:
enter = self
if outputs is None:
outputs = []
inputArgs = _getArgNames(input.argSpec)
for output in outputs:
outputArgs = _getArgNames(output.argSpec)
if not outputArgs.issubset(inputArgs):
raise TypeError(
"method {input} signature {inputSignature} "
"does not match output {output} "
"signature {outputSignature}".format(
input=input.method.__name__,
output=output.method.__name__,
inputSignature=getArgsSpec(input.method),
outputSignature=getArgsSpec(output.method),
)
)
self.machine._oneTransition(self, input, enter, outputs, collector)
def _name(self) -> str:
return self.method.__name__
def _transitionerFromInstance(
oself: object,
symbol: str,
automaton: Automaton[MethodicalState, MethodicalInput, MethodicalOutput],
) -> Transitioner[MethodicalState, MethodicalInput, MethodicalOutput]:
"""
Get a L{Transitioner}
"""
transitioner = getattr(oself, symbol, None)
if transitioner is None:
transitioner = Transitioner(
automaton,
automaton.initialState,
)
setattr(oself, symbol, transitioner)
return transitioner
def _empty():
pass
def _docstring():
"""docstring"""
def assertNoCode(f: Callable[..., Any]) -> None:
# The function body must be empty, i.e. "pass" or "return None", which
# both yield the same bytecode: LOAD_CONST (None), RETURN_VALUE. We also
# accept functions with only a docstring, which yields slightly different
# bytecode, because the "None" is put in a different constant slot.
# Unfortunately, this does not catch function bodies that return a
# constant value, e.g. "return 1", because their code is identical to a
# "return None". They differ in the contents of their constant table, but
# checking that would require us to parse the bytecode, find the index
# being returned, then making sure the table has a None at that index.
if f.__code__.co_code not in (_empty.__code__.co_code, _docstring.__code__.co_code):
raise ValueError("function body must be empty")
def _filterArgs(args, kwargs, inputSpec, outputSpec):
"""
Filter out arguments that were passed to input that output won't accept.
:param tuple args: The *args that input received.
:param dict kwargs: The **kwargs that input received.
:param ArgSpec inputSpec: The input's arg spec.
:param ArgSpec outputSpec: The output's arg spec.
:return: The args and kwargs that output will accept.
:rtype: Tuple[tuple, dict]
"""
named_args = tuple(zip(inputSpec.args[1:], args))
if outputSpec.varargs:
# Only return all args if the output accepts *args.
return_args = args
else:
# Filter out arguments that don't appear
# in the output's method signature.
return_args = [v for n, v in named_args if n in outputSpec.args]
# Get any of input's default arguments that were not passed.
passed_arg_names = tuple(kwargs)
for name, value in named_args:
passed_arg_names += (name, value)
defaults = zip(inputSpec.args[::-1], inputSpec.defaults[::-1])
full_kwargs = {n: v for n, v in defaults if n not in passed_arg_names}
full_kwargs.update(kwargs)
if outputSpec.varkw:
# Only pass all kwargs if the output method accepts **kwargs.
return_kwargs = full_kwargs
else:
# Filter out names that the output method does not accept.
all_accepted_names = outputSpec.args[1:] + outputSpec.kwonlyargs
return_kwargs = {
n: v for n, v in full_kwargs.items() if n in all_accepted_names
}
return return_args, return_kwargs
T = TypeVar("T")
R = TypeVar("R")
@dataclass(eq=False)
class MethodicalInput(object):
"""
An input for a L{MethodicalMachine}.
"""
automaton: Automaton[MethodicalState, MethodicalInput, MethodicalOutput] = field(
repr=False
)
method: Callable[..., Any] = field()
symbol: str = field(repr=False)
collectors: dict[MethodicalState, Callable[[Iterable[T]], R]] = field(
default_factory=dict, repr=False
)
argSpec: ArgSpec = field(init=False, repr=False)
def __post_init__(self) -> None:
self.argSpec = _getArgSpec(self.method)
assertNoCode(self.method)
def __get__(self, oself: object, type: None = None) -> object:
"""
Return a function that takes no arguments and returns values returned
by output functions produced by the given L{MethodicalInput} in
C{oself}'s current state.
"""
transitioner = _transitionerFromInstance(oself, self.symbol, self.automaton)
@preserveName(self.method)
@wraps(self.method)
def doInput(*args: object, **kwargs: object) -> object:
self.method(oself, *args, **kwargs)
previousState = transitioner._state
(outputs, outTracer) = transitioner.transition(self)
collector = self.collectors[previousState]
values = []
for output in outputs:
if outTracer is not None:
outTracer(output)
a, k = _filterArgs(args, kwargs, self.argSpec, output.argSpec)
value = output(oself, *a, **k)
values.append(value)
return collector(values)
return doInput
def _name(self) -> str:
return self.method.__name__
@dataclass(frozen=True)
class MethodicalOutput(object):
"""
An output for a L{MethodicalMachine}.
"""
machine: MethodicalMachine = field(repr=False)
method: Callable[..., Any]
argSpec: ArgSpec = field(init=False, repr=False, compare=False)
def __post_init__(self) -> None:
self.__dict__["argSpec"] = _getArgSpec(self.method)
def __get__(self, oself, type=None):
"""
Outputs are private, so raise an exception when we attempt to get one.
"""
raise AttributeError(
"{cls}.{method} is a state-machine output method; "
"to produce this output, call an input method instead.".format(
cls=type.__name__, method=self.method.__name__
)
)
def __call__(self, oself, *args, **kwargs):
"""
Call the underlying method.
"""
return self.method(oself, *args, **kwargs)
def _name(self) -> str:
return self.method.__name__
StringOutputTracer = Callable[[str], None]
StringTracer: TypeAlias = "Callable[[str, str, str], StringOutputTracer | None]"
def wrapTracer(
wrapped: StringTracer | None,
) -> Tracer[MethodicalState, MethodicalInput, MethodicalOutput] | None:
if wrapped is None:
return None
def tracer(
state: MethodicalState,
input: MethodicalInput,
output: MethodicalState,
) -> OutputTracer[MethodicalOutput] | None:
result = wrapped(state._name(), input._name(), output._name())
if result is not None:
return lambda out: result(out._name())
return None
return tracer
@dataclass(eq=False)
class MethodicalTracer(object):
automaton: Automaton[MethodicalState, MethodicalInput, MethodicalOutput] = field(
repr=False
)
symbol: str = field(repr=False)
def __get__(
self, oself: object, type: object = None
) -> Callable[[StringTracer], None]:
transitioner = _transitionerFromInstance(oself, self.symbol, self.automaton)
def setTrace(tracer: StringTracer | None) -> None:
transitioner.setTrace(wrapTracer(tracer))
return setTrace
counter = count()
def gensym():
"""
Create a unique Python identifier.
"""
return "_symbol_" + str(next(counter))
class MethodicalMachine(object):
"""
A L{MethodicalMachine} is an interface to an L{Automaton} that uses methods
on a class.
"""
def __init__(self):
self._automaton = Automaton()
self._reducers = {}
self._symbol = gensym()
def __get__(self, oself, type=None):
"""
L{MethodicalMachine} is an implementation detail for setting up
class-level state; applications should never need to access it on an
instance.
"""
if oself is not None:
raise AttributeError("MethodicalMachine is an implementation detail.")
return self
@_keywords_only
def state(
self, initial: bool = False, terminal: bool = False, serialized: Hashable = None
):
"""
Declare a state, possibly an initial state or a terminal state.
This is a decorator for methods, but it will modify the method so as
not to be callable any more.
@param initial: is this state the initial state? Only one state on
this L{automat.MethodicalMachine} may be an initial state; more
than one is an error.
@param terminal: Is this state a terminal state? i.e. a state that the
machine can end up in? (This is purely informational at this
point.)
@param serialized: a serializable value to be used to represent this
state to external systems. This value should be hashable; L{str}
is a good type to use.
"""
def decorator(stateMethod):
state = MethodicalState(
machine=self, method=stateMethod, serialized=serialized
)
if initial:
self._automaton.initialState = state
return state
return decorator
@_keywords_only
def input(self):
"""
Declare an input.
This is a decorator for methods.
"""
def decorator(inputMethod):
return MethodicalInput(
automaton=self._automaton, method=inputMethod, symbol=self._symbol
)
return decorator
@_keywords_only
def output(self):
"""
Declare an output.
This is a decorator for methods.
This method will be called when the state machine transitions to this
state as specified in the decorated `output` method.
"""
def decorator(outputMethod):
return MethodicalOutput(machine=self, method=outputMethod)
return decorator
def _oneTransition(self, startState, inputToken, endState, outputTokens, collector):
"""
See L{MethodicalState.upon}.
"""
# FIXME: tests for all of this (some of it is wrong)
# if not isinstance(startState, MethodicalState):
# raise NotImplementedError("start state {} isn't a state"
# .format(startState))
# if not isinstance(inputToken, MethodicalInput):
# raise NotImplementedError("start state {} isn't an input"
# .format(inputToken))
# if not isinstance(endState, MethodicalState):
# raise NotImplementedError("end state {} isn't a state"
# .format(startState))
# for output in outputTokens:
# if not isinstance(endState, MethodicalState):
# raise NotImplementedError("output state {} isn't a state"
# .format(endState))
self._automaton.addTransition(
startState, inputToken, endState, tuple(outputTokens)
)
inputToken.collectors[startState] = collector
@_keywords_only
def serializer(self):
""" """
def decorator(decoratee):
@wraps(decoratee)
def serialize(oself):
transitioner = _transitionerFromInstance(
oself, self._symbol, self._automaton
)
return decoratee(oself, transitioner._state.serialized)
return serialize
return decorator
@_keywords_only
def unserializer(self):
""" """
def decorator(decoratee):
@wraps(decoratee)
def unserialize(oself, *args, **kwargs):
state = decoratee(oself, *args, **kwargs)
mapping = {}
for eachState in self._automaton.states():
mapping[eachState.serialized] = eachState
transitioner = _transitionerFromInstance(
oself, self._symbol, self._automaton
)
transitioner._state = mapping[state]
return None # it's on purpose
return unserialize
return decorator
@property
def _setTrace(self) -> MethodicalTracer:
return MethodicalTracer(self._automaton, self._symbol)
def asDigraph(self):
"""
Generate a L{graphviz.Digraph} that represents this machine's
states and transitions.
@return: L{graphviz.Digraph} object; for more information, please
see the documentation for
U{graphviz<https://graphviz.readthedocs.io/>}
"""
from ._visualize import makeDigraph
return makeDigraph(
self._automaton,
stateAsString=lambda state: state.method.__name__,
inputAsString=lambda input: input.method.__name__,
outputAsString=lambda output: output.method.__name__,
)

@ -0,0 +1,62 @@
"""
Workaround for U{the lack of TypeForm
<https://github.com/python/mypy/issues/9773>}.
"""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING, Callable, Protocol, TypeVar
from inspect import signature, Signature
T = TypeVar("T")
ProtocolAtRuntime = Callable[[], T]
def runtime_name(x: ProtocolAtRuntime[T]) -> str:
return x.__name__
from inspect import getmembers, isfunction
emptyProtocolMethods: frozenset[str]
if not TYPE_CHECKING:
emptyProtocolMethods = frozenset(
name
for name, each in getmembers(type("Example", tuple([Protocol]), {}), isfunction)
)
def actuallyDefinedProtocolMethods(protocol: object) -> frozenset[str]:
"""
Attempt to ignore implementation details, and get all the methods that the
protocol actually defines.
that includes locally defined methods and also those defined in inherited
superclasses.
"""
return (
frozenset(name for name, each in getmembers(protocol, isfunction))
- emptyProtocolMethods
)
def _fixAnnotation(method: Callable[..., object], it: object, ann: str) -> None:
annotation = getattr(it, ann)
if isinstance(annotation, str):
setattr(it, ann, eval(annotation, method.__globals__))
def _liveSignature(method: Callable[..., object]) -> Signature:
"""
Get a signature with evaluated annotations.
"""
# TODO: could this be replaced with get_type_hints?
result = signature(method)
for param in result.parameters.values():
_fixAnnotation(method, param, "_annotation")
_fixAnnotation(method, result, "_return_annotation")
return result

@ -0,0 +1,97 @@
from unittest import TestCase
from .._core import Automaton, NoTransition, Transitioner
class CoreTests(TestCase):
"""
Tests for Automat's (currently private, implementation detail) core.
"""
def test_NoTransition(self):
"""
A L{NoTransition} exception describes the state and input symbol
that caused it.
"""
# NoTransition requires two arguments
with self.assertRaises(TypeError):
NoTransition()
state = "current-state"
symbol = "transitionless-symbol"
noTransitionException = NoTransition(state=state, symbol=symbol)
self.assertIs(noTransitionException.symbol, symbol)
self.assertIn(state, str(noTransitionException))
self.assertIn(symbol, str(noTransitionException))
def test_unhandledTransition(self) -> None:
"""
Automaton.unhandledTransition sets the outputs and end-state to be used
for all unhandled transitions.
"""
a: Automaton[str, str, str] = Automaton("start")
a.addTransition("oops-state", "check", "start", tuple(["checked"]))
a.unhandledTransition("oops-state", ["oops-out"])
t = Transitioner(a, "start")
self.assertEqual(t.transition("check"), (tuple(["oops-out"]), None))
self.assertEqual(t.transition("check"), (["checked"], None))
self.assertEqual(t.transition("check"), (tuple(["oops-out"]), None))
def test_noOutputForInput(self):
"""
L{Automaton.outputForInput} raises L{NoTransition} if no
transition for that input is defined.
"""
a = Automaton()
self.assertRaises(NoTransition, a.outputForInput, "no-state", "no-symbol")
def test_oneTransition(self):
"""
L{Automaton.addTransition} adds its input symbol to
L{Automaton.inputAlphabet}, all its outputs to
L{Automaton.outputAlphabet}, and causes L{Automaton.outputForInput} to
start returning the new state and output symbols.
"""
a = Automaton()
a.addTransition("beginning", "begin", "ending", ["end"])
self.assertEqual(a.inputAlphabet(), {"begin"})
self.assertEqual(a.outputAlphabet(), {"end"})
self.assertEqual(a.outputForInput("beginning", "begin"), ("ending", ["end"]))
self.assertEqual(a.states(), {"beginning", "ending"})
def test_oneTransition_nonIterableOutputs(self):
"""
L{Automaton.addTransition} raises a TypeError when given outputs
that aren't iterable and doesn't add any transitions.
"""
a = Automaton()
nonIterableOutputs = 1
self.assertRaises(
TypeError,
a.addTransition,
"fromState",
"viaSymbol",
"toState",
nonIterableOutputs,
)
self.assertFalse(a.inputAlphabet())
self.assertFalse(a.outputAlphabet())
self.assertFalse(a.states())
self.assertFalse(a.allTransitions())
def test_initialState(self):
"""
L{Automaton.initialState} is a descriptor that sets the initial
state if it's not yet set, and raises L{ValueError} if it is.
"""
a = Automaton()
a.initialState = "a state"
self.assertEqual(a.initialState, "a state")
with self.assertRaises(ValueError):
a.initialState = "another state"
# FIXME: addTransition for transition that's been added before

@ -0,0 +1,638 @@
import operator
import os
import shutil
import sys
import textwrap
import tempfile
from unittest import skipIf, TestCase
def isTwistedInstalled():
try:
__import__("twisted")
except ImportError:
return False
else:
return True
class _WritesPythonModules(TestCase):
"""
A helper that enables generating Python module test fixtures.
"""
def setUp(self):
super(_WritesPythonModules, self).setUp()
from twisted.python.modules import getModule, PythonPath
from twisted.python.filepath import FilePath
self.getModule = getModule
self.PythonPath = PythonPath
self.FilePath = FilePath
self.originalSysModules = set(sys.modules.keys())
self.savedSysPath = sys.path[:]
self.pathDir = tempfile.mkdtemp()
self.makeImportable(self.pathDir)
def tearDown(self):
super(_WritesPythonModules, self).tearDown()
sys.path[:] = self.savedSysPath
modulesToDelete = sys.modules.keys() - self.originalSysModules
for module in modulesToDelete:
del sys.modules[module]
shutil.rmtree(self.pathDir)
def makeImportable(self, path):
sys.path.append(path)
def writeSourceInto(self, source, path, moduleName):
directory = self.FilePath(path)
module = directory.child(moduleName)
# FilePath always opens a file in binary mode - but that will
# break on Python 3
with open(module.path, "w") as f:
f.write(textwrap.dedent(source))
return self.PythonPath([directory.path])
def makeModule(self, source, path, moduleName):
pythonModuleName, _ = os.path.splitext(moduleName)
return self.writeSourceInto(source, path, moduleName)[pythonModuleName]
def attributesAsDict(self, hasIterAttributes):
return {attr.name: attr for attr in hasIterAttributes.iterAttributes()}
def loadModuleAsDict(self, module):
module.load()
return self.attributesAsDict(module)
def makeModuleAsDict(self, source, path, name):
return self.loadModuleAsDict(self.makeModule(source, path, name))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class OriginalLocationTests(_WritesPythonModules):
"""
Tests that L{isOriginalLocation} detects when a
L{PythonAttribute}'s FQPN refers to an object inside the module
where it was defined.
For example: A L{twisted.python.modules.PythonAttribute} with a
name of 'foo.bar' that refers to a 'bar' object defined in module
'baz' does *not* refer to bar's original location, while a
L{PythonAttribute} with a name of 'baz.bar' does.
"""
def setUp(self):
super(OriginalLocationTests, self).setUp()
from .._discover import isOriginalLocation
self.isOriginalLocation = isOriginalLocation
def test_failsWithNoModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to an
object whose source module cannot be determined.
"""
source = """\
class Fake(object):
pass
hasEmptyModule = Fake()
hasEmptyModule.__module__ = None
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, "empty_module_attr.py")
self.assertFalse(
self.isOriginalLocation(moduleDict["empty_module_attr.hasEmptyModule"])
)
def test_failsWithDifferentModule(self):
"""
L{isOriginalLocation} returns False when the attribute refers to
an object outside of the module where that object was defined.
"""
originalSource = """\
class ImportThisClass(object):
pass
importThisObject = ImportThisClass()
importThisNestingObject = ImportThisClass()
importThisNestingObject.nestedObject = ImportThisClass()
"""
importingSource = """\
from original import (ImportThisClass,
importThisObject,
importThisNestingObject)
"""
self.makeModule(originalSource, self.pathDir, "original.py")
importingDict = self.makeModuleAsDict(
importingSource, self.pathDir, "importing.py"
)
self.assertFalse(
self.isOriginalLocation(importingDict["importing.ImportThisClass"])
)
self.assertFalse(
self.isOriginalLocation(importingDict["importing.importThisObject"])
)
nestingObject = importingDict["importing.importThisNestingObject"]
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict[
"importing.importThisNestingObject.nestedObject"
]
self.assertFalse(self.isOriginalLocation(nestedObject))
def test_succeedsWithSameModule(self):
"""
L{isOriginalLocation} returns True when the attribute refers to an
object inside the module where that object was defined.
"""
mSource = textwrap.dedent(
"""
class ThisClassWasDefinedHere(object):
pass
anObject = ThisClassWasDefinedHere()
aNestingObject = ThisClassWasDefinedHere()
aNestingObject.nestedObject = ThisClassWasDefinedHere()
"""
)
mDict = self.makeModuleAsDict(mSource, self.pathDir, "m.py")
self.assertTrue(self.isOriginalLocation(mDict["m.ThisClassWasDefinedHere"]))
self.assertTrue(self.isOriginalLocation(mDict["m.aNestingObject"]))
nestingObject = mDict["m.aNestingObject"]
nestingObjectDict = self.attributesAsDict(nestingObject)
nestedObject = nestingObjectDict["m.aNestingObject.nestedObject"]
self.assertTrue(self.isOriginalLocation(nestedObject))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesViaWrapperTests(_WritesPythonModules):
"""
L{findMachinesViaWrapper} recursively yields FQPN,
L{MethodicalMachine} pairs in and under a given
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute}.
"""
def setUp(self):
super(FindMachinesViaWrapperTests, self).setUp()
from .._discover import findMachinesViaWrapper
self.findMachinesViaWrapper = findMachinesViaWrapper
def test_yieldsMachine(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
directly to a L{MethodicalMachine}, L{findMachinesViaWrapper}
yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, "root.py")
rootMachine = moduleDict["root.rootMachine"]
self.assertIn(
("root.rootMachine", rootMachine.load()),
list(self.findMachinesViaWrapper(rootMachine)),
)
def test_yieldsTypeMachine(self) -> None:
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
directly to a L{TypeMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import TypeMachineBuilder
from typing import Protocol, Callable
class P(Protocol):
def method(self) -> None: ...
class C:...
def buildBuilder() -> Callable[[C], P]:
builder = TypeMachineBuilder(P, C)
return builder.build()
rootMachine = buildBuilder()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, "root.py")
rootMachine = moduleDict["root.rootMachine"]
self.assertIn(
("root.rootMachine", rootMachine.load()),
list(self.findMachinesViaWrapper(rootMachine)),
)
def test_yieldsMachineInClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class that contains a L{MethodicalMachine} as a class
variable, L{findMachinesViaWrapper} yields that machine and
its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, "clsmod.py")
PythonClass = moduleDict["clsmod.PythonClass"]
self.assertIn(
("clsmod.PythonClass._classMachine", PythonClass.load()._classMachine),
list(self.findMachinesViaWrapper(PythonClass)),
)
def test_yieldsMachineInNestedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a nested class that contains a L{MethodicalMachine} as a
class variable, L{findMachinesViaWrapper} yields that machine
and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
moduleDict = self.makeModuleAsDict(source, self.pathDir, "nestedcls.py")
PythonClass = moduleDict["nestedcls.PythonClass"]
self.assertIn(
(
"nestedcls.PythonClass.NestedClass._classMachine",
PythonClass.load().NestedClass._classMachine,
),
list(self.findMachinesViaWrapper(PythonClass)),
)
def test_yieldsMachineInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
a module that contains a L{MethodicalMachine},
L{findMachinesViaWrapper} yields that machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
rootMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, "root.py")
rootMachine = self.loadModuleAsDict(module)["root.rootMachine"].load()
self.assertIn(
("root.rootMachine", rootMachine), list(self.findMachinesViaWrapper(module))
)
def test_yieldsMachineInClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, "clsmod.py")
PythonClass = self.loadModuleAsDict(module)["clsmod.PythonClass"].load()
self.assertIn(
("clsmod.PythonClass._classMachine", PythonClass._classMachine),
list(self.findMachinesViaWrapper(module)),
)
def test_yieldsMachineInNestedClassInModule(self):
"""
When given a L{twisted.python.modules.PythonModule} that refers to
the original module of a nested class containing a
L{MethodicalMachine}, L{findMachinesViaWrapper} yields that
machine and its FQPN.
"""
source = """\
from automat import MethodicalMachine
class PythonClass(object):
class NestedClass(object):
_classMachine = MethodicalMachine()
"""
module = self.makeModule(source, self.pathDir, "nestedcls.py")
PythonClass = self.loadModuleAsDict(module)["nestedcls.PythonClass"].load()
self.assertIn(
(
"nestedcls.PythonClass.NestedClass._classMachine",
PythonClass.NestedClass._classMachine,
),
list(self.findMachinesViaWrapper(module)),
)
def test_ignoresImportedClass(self):
"""
When given a L{twisted.python.modules.PythonAttribute} that refers
to a class imported from another module, any
L{MethodicalMachine}s on that class are ignored.
This behavior ensures that a machine is only discovered on a
class when visiting the module where that class was defined.
"""
originalSource = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
"""
importingSource = """
from original import PythonClass
"""
self.makeModule(originalSource, self.pathDir, "original.py")
importingModule = self.makeModule(importingSource, self.pathDir, "importing.py")
self.assertFalse(list(self.findMachinesViaWrapper(importingModule)))
def test_descendsIntoPackages(self):
"""
L{findMachinesViaWrapper} descends into packages to discover
machines.
"""
pythonPath = self.PythonPath([self.pathDir])
package = self.FilePath(self.pathDir).child("test_package")
package.makedirs()
package.child("__init__.py").touch()
source = """
from automat import MethodicalMachine
class PythonClass(object):
_classMachine = MethodicalMachine()
rootMachine = MethodicalMachine()
"""
self.makeModule(source, package.path, "module.py")
test_package = pythonPath["test_package"]
machines = sorted(
self.findMachinesViaWrapper(test_package), key=operator.itemgetter(0)
)
moduleDict = self.loadModuleAsDict(test_package["module"])
rootMachine = moduleDict["test_package.module.rootMachine"].load()
PythonClass = moduleDict["test_package.module.PythonClass"].load()
expectedMachines = sorted(
[
("test_package.module.rootMachine", rootMachine),
(
"test_package.module.PythonClass._classMachine",
PythonClass._classMachine,
),
],
key=operator.itemgetter(0),
)
self.assertEqual(expectedMachines, machines)
def test_infiniteLoop(self):
"""
L{findMachinesViaWrapper} ignores infinite loops.
Note this test can't fail - it can only run forever!
"""
source = """
class InfiniteLoop(object):
pass
InfiniteLoop.loop = InfiniteLoop
"""
module = self.makeModule(source, self.pathDir, "loop.py")
self.assertFalse(list(self.findMachinesViaWrapper(module)))
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class WrapFQPNTests(TestCase):
"""
Tests that ensure L{wrapFQPN} loads the
L{twisted.python.modules.PythonModule} or
L{twisted.python.modules.PythonAttribute} for a given FQPN.
"""
def setUp(self):
from twisted.python.modules import PythonModule, PythonAttribute
from .._discover import wrapFQPN, InvalidFQPN, NoModule, NoObject
self.PythonModule = PythonModule
self.PythonAttribute = PythonAttribute
self.wrapFQPN = wrapFQPN
self.InvalidFQPN = InvalidFQPN
self.NoModule = NoModule
self.NoObject = NoObject
def assertModuleWrapperRefersTo(self, moduleWrapper, module):
"""
Assert that a L{twisted.python.modules.PythonModule} refers to a
particular Python module.
"""
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertEqual(moduleWrapper.name, module.__name__)
self.assertIs(moduleWrapper.load(), module)
def assertAttributeWrapperRefersTo(self, attributeWrapper, fqpn, obj):
"""
Assert that a L{twisted.python.modules.PythonAttribute} refers to a
particular Python object.
"""
self.assertIsInstance(attributeWrapper, self.PythonAttribute)
self.assertEqual(attributeWrapper.name, fqpn)
self.assertIs(attributeWrapper.load(), obj)
def test_failsWithEmptyFQPN(self):
"""
L{wrapFQPN} raises L{InvalidFQPN} when given an empty string.
"""
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN("")
def test_failsWithBadDotting(self):
""" "
L{wrapFQPN} raises L{InvalidFQPN} when given a badly-dotted
FQPN. (e.g., x..y).
"""
for bad in (".fails", "fails.", "this..fails"):
with self.assertRaises(self.InvalidFQPN):
self.wrapFQPN(bad)
def test_singleModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single module a dotless FQPN describes.
"""
import os
moduleWrapper = self.wrapFQPN("os")
self.assertIsInstance(moduleWrapper, self.PythonModule)
self.assertIs(moduleWrapper.load(), os)
def test_failsWithMissingSingleModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoModule} when given a dotless FQPN that does
not refer to a module or package.
"""
with self.assertRaises(self.NoModule):
self.wrapFQPN("this is not an acceptable name!")
def test_singlePackage(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the single package a dotless FQPN describes.
"""
import xml
self.assertModuleWrapperRefersTo(self.wrapFQPN("xml"), xml)
def test_multiplePackages(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest package described by dotted FQPN.
"""
import xml.etree
self.assertModuleWrapperRefersTo(self.wrapFQPN("xml.etree"), xml.etree)
def test_multiplePackagesFinalModule(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonModule}
referring to the deepest module described by dotted FQPN.
"""
import xml.etree.ElementTree
self.assertModuleWrapperRefersTo(
self.wrapFQPN("xml.etree.ElementTree"), xml.etree.ElementTree
)
def test_singleModuleObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object an FQPN names, traversing one module.
"""
import os
self.assertAttributeWrapperRefersTo(
self.wrapFQPN("os.path"), "os.path", os.path
)
def test_multiplePackagesObject(self):
"""
L{wrapFQPN} returns a L{twisted.python.modules.PythonAttribute}
referring to the deepest object described by an FQPN,
descending through several packages.
"""
import xml.etree.ElementTree
import automat
for fqpn, obj in [
("xml.etree.ElementTree.fromstring", xml.etree.ElementTree.fromstring),
("automat.MethodicalMachine.__doc__", automat.MethodicalMachine.__doc__),
]:
self.assertAttributeWrapperRefersTo(self.wrapFQPN(fqpn), fqpn, obj)
def test_failsWithMultiplePackagesMissingModuleOrPackage(self):
"""
L{wrapFQPN} raises L{NoObject} when given an FQPN that contains a
missing attribute, module, or package.
"""
for bad in ("xml.etree.nope!", "xml.etree.nope!.but.the.rest.is.believable"):
with self.assertRaises(self.NoObject):
self.wrapFQPN(bad)
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class FindMachinesIntegrationTests(_WritesPythonModules):
"""
Integration tests to check that L{findMachines} yields all
machines discoverable at or below an FQPN.
"""
SOURCE = """
from automat import MethodicalMachine
class PythonClass(object):
_machine = MethodicalMachine()
ignored = "i am ignored"
rootLevel = MethodicalMachine()
ignored = "i am ignored"
"""
def setUp(self):
super(FindMachinesIntegrationTests, self).setUp()
from .._discover import findMachines
self.findMachines = findMachines
packageDir = self.FilePath(self.pathDir).child("test_package")
packageDir.makedirs()
self.pythonPath = self.PythonPath([self.pathDir])
self.writeSourceInto(self.SOURCE, packageDir.path, "__init__.py")
subPackageDir = packageDir.child("subpackage")
subPackageDir.makedirs()
subPackageDir.child("__init__.py").touch()
self.makeModule(self.SOURCE, subPackageDir.path, "module.py")
self.packageDict = self.loadModuleAsDict(self.pythonPath["test_package"])
self.moduleDict = self.loadModuleAsDict(
self.pythonPath["test_package"]["subpackage"]["module"]
)
def test_discoverAll(self):
"""
Given a top-level package FQPN, L{findMachines} discovers all
L{MethodicalMachine} instances in and below it.
"""
machines = sorted(self.findMachines("test_package"), key=operator.itemgetter(0))
tpRootLevel = self.packageDict["test_package.rootLevel"].load()
tpPythonClass = self.packageDict["test_package.PythonClass"].load()
mRLAttr = self.moduleDict["test_package.subpackage.module.rootLevel"]
mRootLevel = mRLAttr.load()
mPCAttr = self.moduleDict["test_package.subpackage.module.PythonClass"]
mPythonClass = mPCAttr.load()
expectedMachines = sorted(
[
("test_package.rootLevel", tpRootLevel),
("test_package.PythonClass._machine", tpPythonClass._machine),
("test_package.subpackage.module.rootLevel", mRootLevel),
(
"test_package.subpackage.module.PythonClass._machine",
mPythonClass._machine,
),
],
key=operator.itemgetter(0),
)
self.assertEqual(expectedMachines, machines)

@ -0,0 +1,717 @@
"""
Tests for the public interface of Automat.
"""
from functools import reduce
from unittest import TestCase
from automat._methodical import ArgSpec, _getArgNames, _getArgSpec, _filterArgs
from .. import MethodicalMachine, NoTransition
from .. import _methodical
class MethodicalTests(TestCase):
"""
Tests for L{MethodicalMachine}.
"""
def test_oneTransition(self):
"""
L{MethodicalMachine} provides a way for you to declare a state machine
with inputs, outputs, and states as methods. When you have declared an
input, an output, and a state, calling the input method in that state
will produce the specified output.
"""
class Machination(object):
machine = MethodicalMachine()
@machine.input()
def anInput(self):
"an input"
@machine.output()
def anOutput(self):
"an output"
return "an-output-value"
@machine.output()
def anotherOutput(self):
"another output"
return "another-output-value"
@machine.state(initial=True)
def anState(self):
"a state"
@machine.state()
def anotherState(self):
"another state"
anState.upon(anInput, enter=anotherState, outputs=[anOutput])
anotherState.upon(anInput, enter=anotherState, outputs=[anotherOutput])
m = Machination()
self.assertEqual(m.anInput(), ["an-output-value"])
self.assertEqual(m.anInput(), ["another-output-value"])
def test_machineItselfIsPrivate(self):
"""
L{MethodicalMachine} is an implementation detail. If you attempt to
access it on an instance of your class, you will get an exception.
However, since tools may need to access it for the purposes of, for
example, visualization, you may access it on the class itself.
"""
expectedMachine = MethodicalMachine()
class Machination(object):
machine = expectedMachine
machination = Machination()
with self.assertRaises(AttributeError) as cm:
machination.machine
self.assertIn(
"MethodicalMachine is an implementation detail", str(cm.exception)
)
self.assertIs(Machination.machine, expectedMachine)
def test_outputsArePrivate(self):
"""
One of the benefits of using a state machine is that your output method
implementations don't need to take invalid state transitions into
account - the methods simply won't be called. This property would be
broken if client code called output methods directly, so output methods
are not directly visible under their names.
"""
class Machination(object):
machine = MethodicalMachine()
counter = 0
@machine.input()
def anInput(self):
"an input"
@machine.output()
def anOutput(self):
self.counter += 1
@machine.state(initial=True)
def state(self):
"a machine state"
state.upon(anInput, enter=state, outputs=[anOutput])
mach1 = Machination()
mach1.anInput()
self.assertEqual(mach1.counter, 1)
mach2 = Machination()
with self.assertRaises(AttributeError) as cm:
mach2.anOutput
self.assertEqual(mach2.counter, 0)
self.assertIn(
"Machination.anOutput is a state-machine output method; to "
"produce this output, call an input method instead.",
str(cm.exception),
)
def test_multipleMachines(self):
"""
Two machines may co-exist happily on the same instance; they don't
interfere with each other.
"""
class MultiMach(object):
a = MethodicalMachine()
b = MethodicalMachine()
@a.input()
def inputA(self):
"input A"
@b.input()
def inputB(self):
"input B"
@a.state(initial=True)
def initialA(self):
"initial A"
@b.state(initial=True)
def initialB(self):
"initial B"
@a.output()
def outputA(self):
return "A"
@b.output()
def outputB(self):
return "B"
initialA.upon(inputA, initialA, [outputA])
initialB.upon(inputB, initialB, [outputB])
mm = MultiMach()
self.assertEqual(mm.inputA(), ["A"])
self.assertEqual(mm.inputB(), ["B"])
def test_collectOutputs(self):
"""
Outputs can be combined with the "collector" argument to "upon".
"""
import operator
class Machine(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
@m.output()
def outputA(self):
return "A"
@m.output()
def outputB(self):
return "B"
@m.state(initial=True)
def state(self):
"a state"
state.upon(
input,
state,
[outputA, outputB],
collector=lambda x: reduce(operator.add, x),
)
m = Machine()
self.assertEqual(m.input(), "AB")
def test_methodName(self):
"""
Input methods preserve their declared names.
"""
class Mech(object):
m = MethodicalMachine()
@m.input()
def declaredInputName(self):
"an input"
@m.state(initial=True)
def aState(self):
"state"
m = Mech()
with self.assertRaises(TypeError) as cm:
m.declaredInputName("too", "many", "arguments")
self.assertIn("declaredInputName", str(cm.exception))
def test_inputWithArguments(self):
"""
If an input takes an argument, it will pass that along to its output.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def input(self, x, y=1):
"an input"
@m.state(initial=True)
def state(self):
"a state"
@m.output()
def output(self, x, y=1):
self._x = x
return x + y
state.upon(input, state, [output])
m = Mechanism()
self.assertEqual(m.input(3), [4])
self.assertEqual(m._x, 3)
def test_outputWithSubsetOfArguments(self):
"""
Inputs pass arguments that output will accept.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def input(self, x, y=1):
"an input"
@m.state(initial=True)
def state(self):
"a state"
@m.output()
def outputX(self, x):
self._x = x
return x
@m.output()
def outputY(self, y):
self._y = y
return y
@m.output()
def outputNoArgs(self):
return None
state.upon(input, state, [outputX, outputY, outputNoArgs])
m = Mechanism()
# Pass x as positional argument.
self.assertEqual(m.input(3), [3, 1, None])
self.assertEqual(m._x, 3)
self.assertEqual(m._y, 1)
# Pass x as key word argument.
self.assertEqual(m.input(x=4), [4, 1, None])
self.assertEqual(m._x, 4)
self.assertEqual(m._y, 1)
# Pass y as positional argument.
self.assertEqual(m.input(6, 3), [6, 3, None])
self.assertEqual(m._x, 6)
self.assertEqual(m._y, 3)
# Pass y as key word argument.
self.assertEqual(m.input(5, y=2), [5, 2, None])
self.assertEqual(m._x, 5)
self.assertEqual(m._y, 2)
def test_inputFunctionsMustBeEmpty(self):
"""
The wrapped input function must have an empty body.
"""
# input functions are executed to assert that the signature matches,
# but their body must be empty
_methodical._empty() # chase coverage
_methodical._docstring()
class Mechanism(object):
m = MethodicalMachine()
with self.assertRaises(ValueError) as cm:
@m.input()
def input(self):
"an input"
list() # pragma: no cover
self.assertEqual(str(cm.exception), "function body must be empty")
# all three of these cases should be valid. Functions/methods with
# docstrings produce slightly different bytecode than ones without.
class MechanismWithDocstring(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstring().input()
class MechanismWithPass(object):
m = MethodicalMachine()
@m.input()
def input(self):
pass
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithPass().input()
class MechanismWithDocstringAndPass(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
pass
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstringAndPass().input()
class MechanismReturnsNone(object):
m = MethodicalMachine()
@m.input()
def input(self):
return None
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismReturnsNone().input()
class MechanismWithDocstringAndReturnsNone(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
return None
@m.state(initial=True)
def start(self):
"starting state"
start.upon(input, enter=start, outputs=[])
MechanismWithDocstringAndReturnsNone().input()
def test_inputOutputMismatch(self):
"""
All the argument lists of the outputs for a given input must match; if
one does not the call to C{upon} will raise a C{TypeError}.
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def nameOfInput(self, a):
"an input"
@m.output()
def outputThatMatches(self, a):
"an output that matches"
@m.output()
def outputThatDoesntMatch(self, b):
"an output that doesn't match"
@m.state()
def state(self):
"a state"
with self.assertRaises(TypeError) as cm:
state.upon(
nameOfInput, state, [outputThatMatches, outputThatDoesntMatch]
)
self.assertIn("nameOfInput", str(cm.exception))
self.assertIn("outputThatDoesntMatch", str(cm.exception))
def test_stateLoop(self):
"""
It is possible to write a self-loop by omitting "enter"
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def input(self):
"an input"
@m.input()
def say_hi(self):
"an input"
@m.output()
def _start_say_hi(self):
return "hi"
@m.state(initial=True)
def start(self):
"a state"
def said_hi(self):
"a state with no inputs"
start.upon(input, outputs=[])
start.upon(say_hi, outputs=[_start_say_hi])
a_mechanism = Mechanism()
[a_greeting] = a_mechanism.say_hi()
self.assertEqual(a_greeting, "hi")
def test_defaultOutputs(self):
"""
It is possible to write a transition with no outputs
"""
class Mechanism(object):
m = MethodicalMachine()
@m.input()
def finish(self):
"final transition"
@m.state(initial=True)
def start(self):
"a start state"
@m.state()
def finished(self):
"a final state"
start.upon(finish, enter=finished)
Mechanism().finish()
def test_getArgNames(self):
"""
Type annotations should be included in the set of
"""
spec = ArgSpec(
args=("a", "b"),
varargs=None,
varkw=None,
defaults=None,
kwonlyargs=(),
kwonlydefaults=None,
annotations=(("a", int), ("b", str)),
)
self.assertEqual(
_getArgNames(spec),
{"a", "b", ("a", int), ("b", str)},
)
def test_filterArgs(self):
"""
filterArgs() should not filter the `args` parameter
if outputSpec accepts `*args`.
"""
inputSpec = _getArgSpec(lambda *args, **kwargs: None)
outputSpec = _getArgSpec(lambda *args, **kwargs: None)
argsIn = ()
argsOut, _ = _filterArgs(argsIn, {}, inputSpec, outputSpec)
self.assertIs(argsIn, argsOut)
def test_multipleInitialStatesFailure(self):
"""
A L{MethodicalMachine} can only have one initial state.
"""
class WillFail(object):
m = MethodicalMachine()
@m.state(initial=True)
def firstInitialState(self):
"The first initial state -- this is OK."
with self.assertRaises(ValueError):
@m.state(initial=True)
def secondInitialState(self):
"The second initial state -- results in a ValueError."
def test_multipleTransitionsFailure(self):
"""
A L{MethodicalMachine} can only have one transition per start/event
pair.
"""
class WillFail(object):
m = MethodicalMachine()
@m.state(initial=True)
def start(self):
"We start here."
@m.state()
def end(self):
"Rainbows end."
@m.input()
def event(self):
"An event."
start.upon(event, enter=end, outputs=[])
with self.assertRaises(ValueError):
start.upon(event, enter=end, outputs=[])
def test_badTransitionForCurrentState(self):
"""
Calling any input method that lacks a transition for the machine's
current state raises an informative L{NoTransition}.
"""
class OnlyOnePath(object):
m = MethodicalMachine()
@m.state(initial=True)
def start(self):
"Start state."
@m.state()
def end(self):
"End state."
@m.input()
def advance(self):
"Move from start to end."
@m.input()
def deadEnd(self):
"A transition from nowhere to nowhere."
start.upon(advance, end, [])
machine = OnlyOnePath()
with self.assertRaises(NoTransition) as cm:
machine.deadEnd()
self.assertIn("deadEnd", str(cm.exception))
self.assertIn("start", str(cm.exception))
machine.advance()
with self.assertRaises(NoTransition) as cm:
machine.deadEnd()
self.assertIn("deadEnd", str(cm.exception))
self.assertIn("end", str(cm.exception))
def test_saveState(self):
"""
L{MethodicalMachine.serializer} is a decorator that modifies its
decoratee's signature to take a "state" object as its first argument,
which is the "serialized" argument to the L{MethodicalMachine.state}
decorator.
"""
class Mechanism(object):
m = MethodicalMachine()
def __init__(self):
self.value = 1
@m.state(serialized="first-state", initial=True)
def first(self):
"First state."
@m.state(serialized="second-state")
def second(self):
"Second state."
@m.serializer()
def save(self, state):
return {
"machine-state": state,
"some-value": self.value,
}
self.assertEqual(
Mechanism().save(),
{
"machine-state": "first-state",
"some-value": 1,
},
)
def test_restoreState(self):
"""
L{MethodicalMachine.unserializer} decorates a function that becomes a
machine-state unserializer; its return value is mapped to the
C{serialized} parameter to C{state}, and the L{MethodicalMachine}
associated with that instance's state is updated to that state.
"""
class Mechanism(object):
m = MethodicalMachine()
def __init__(self):
self.value = 1
self.ranOutput = False
@m.state(serialized="first-state", initial=True)
def first(self):
"First state."
@m.state(serialized="second-state")
def second(self):
"Second state."
@m.input()
def input(self):
"an input"
@m.output()
def output(self):
self.value = 2
self.ranOutput = True
return 1
@m.output()
def output2(self):
return 2
first.upon(input, second, [output], collector=lambda x: list(x)[0])
second.upon(input, second, [output2], collector=lambda x: list(x)[0])
@m.serializer()
def save(self, state):
return {
"machine-state": state,
"some-value": self.value,
}
@m.unserializer()
def _restore(self, blob):
self.value = blob["some-value"]
return blob["machine-state"]
@classmethod
def fromBlob(cls, blob):
self = cls()
self._restore(blob)
return self
m1 = Mechanism()
m1.input()
blob = m1.save()
m2 = Mechanism.fromBlob(blob)
self.assertEqual(m2.ranOutput, False)
self.assertEqual(m2.input(), 2)
self.assertEqual(
m2.save(),
{
"machine-state": "second-state",
"some-value": 2,
},
)
# FIXME: error for wrong types on any call to _oneTransition
# FIXME: better public API for .upon; maybe a context manager?
# FIXME: when transitions are defined, validate that we can always get to
# terminal? do we care about this?
# FIXME: implementation (and use-case/example) for passing args from in to out
# FIXME: possibly these need some kind of support from core
# FIXME: wildcard state (in all states, when input X, emit Y and go to Z)
# FIXME: wildcard input (in state X, when any input, emit Y and go to Z)
# FIXME: combined wildcards (in any state for any input, emit Y go to Z)

@ -0,0 +1,142 @@
from unittest import TestCase
from .._methodical import MethodicalMachine
class SampleObject(object):
mm = MethodicalMachine()
@mm.state(initial=True)
def begin(self):
"initial state"
@mm.state()
def middle(self):
"middle state"
@mm.state()
def end(self):
"end state"
@mm.input()
def go1(self):
"sample input"
@mm.input()
def go2(self):
"sample input"
@mm.input()
def back(self):
"sample input"
@mm.output()
def out(self):
"sample output"
setTrace = mm._setTrace
begin.upon(go1, middle, [out])
middle.upon(go2, end, [out])
end.upon(back, middle, [])
middle.upon(back, begin, [])
class TraceTests(TestCase):
def test_only_inputs(self):
traces = []
def tracer(old_state, input, new_state):
traces.append((old_state, input, new_state))
return None # "I only care about inputs, not outputs"
s = SampleObject()
s.setTrace(tracer)
s.go1()
self.assertEqual(
traces,
[
("begin", "go1", "middle"),
],
)
s.go2()
self.assertEqual(
traces,
[
("begin", "go1", "middle"),
("middle", "go2", "end"),
],
)
s.setTrace(None)
s.back()
self.assertEqual(
traces,
[
("begin", "go1", "middle"),
("middle", "go2", "end"),
],
)
s.go2()
self.assertEqual(
traces,
[
("begin", "go1", "middle"),
("middle", "go2", "end"),
],
)
def test_inputs_and_outputs(self):
traces = []
def tracer(old_state, input, new_state):
traces.append((old_state, input, new_state, None))
def trace_outputs(output):
traces.append((old_state, input, new_state, output))
return trace_outputs # "I care about outputs too"
s = SampleObject()
s.setTrace(tracer)
s.go1()
self.assertEqual(
traces,
[
("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
],
)
s.go2()
self.assertEqual(
traces,
[
("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
],
)
s.setTrace(None)
s.back()
self.assertEqual(
traces,
[
("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
],
)
s.go2()
self.assertEqual(
traces,
[
("begin", "go1", "middle", None),
("begin", "go1", "middle", "out"),
("middle", "go2", "end", None),
("middle", "go2", "end", "out"),
],
)

@ -0,0 +1,534 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Generic, List, Protocol, TypeVar
from unittest import TestCase, skipIf
from .. import AlreadyBuiltError, NoTransition, TypeMachineBuilder, pep614
try:
from zope.interface import Interface, implementer # type:ignore[import-untyped]
except ImportError:
hasInterface = False
else:
hasInterface = True
class ISomething(Interface):
def something() -> int: ... # type:ignore[misc,empty-body]
T = TypeVar("T")
class ProtocolForTesting(Protocol):
def change(self) -> None:
"Switch to the other state."
def value(self) -> int:
"Give a value specific to the given state."
class ArgTaker(Protocol):
def takeSomeArgs(self, arg1: int = 0, arg2: str = "") -> None: ...
def value(self) -> int: ...
class NoOpCore:
"Just an object, you know?"
@dataclass
class Gen(Generic[T]):
t: T
def buildTestBuilder() -> tuple[
TypeMachineBuilder[ProtocolForTesting, NoOpCore],
Callable[[NoOpCore], ProtocolForTesting],
]:
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
first = builder.state("first")
second = builder.state("second")
first.upon(ProtocolForTesting.change).to(second).returns(None)
second.upon(ProtocolForTesting.change).to(first).returns(None)
@pep614(first.upon(ProtocolForTesting.value).loop())
def firstValue(machine: ProtocolForTesting, core: NoOpCore) -> int:
return 3
@pep614(second.upon(ProtocolForTesting.value).loop())
def secondValue(machine: ProtocolForTesting, core: NoOpCore) -> int:
return 4
return builder, builder.build()
builder, machineFactory = buildTestBuilder()
def needsSomething(proto: ProtocolForTesting, core: NoOpCore, value: str) -> int:
"we need data to build this state"
return 3 # pragma: no cover
def needsNothing(proto: ArgTaker, core: NoOpCore) -> str:
return "state-specific data" # pragma: no cover
class SimpleProtocol(Protocol):
def method(self) -> None:
"A method"
class Counter(Protocol):
def start(self) -> None:
"enter the counting state"
def increment(self) -> None:
"increment the counter"
def stop(self) -> int:
"stop"
@dataclass
class Count:
value: int = 0
class TypeMachineTests(TestCase):
def test_oneTransition(self) -> None:
machine = machineFactory(NoOpCore())
self.assertEqual(machine.value(), 3)
machine.change()
self.assertEqual(machine.value(), 4)
self.assertEqual(machine.value(), 4)
machine.change()
self.assertEqual(machine.value(), 3)
def test_stateSpecificData(self) -> None:
builder = TypeMachineBuilder(Counter, NoOpCore)
initial = builder.state("initial")
counting = builder.state("counting", lambda machine, core: Count())
initial.upon(Counter.start).to(counting).returns(None)
@pep614(counting.upon(Counter.increment).loop())
def incf(counter: Counter, core: NoOpCore, count: Count) -> None:
count.value += 1
@pep614(counting.upon(Counter.stop).to(initial))
def finish(counter: Counter, core: NoOpCore, count: Count) -> int:
return count.value
machineFactory = builder.build()
machine = machineFactory(NoOpCore())
machine.start()
machine.increment()
machine.increment()
self.assertEqual(machine.stop(), 2)
machine.start()
machine.increment()
self.assertEqual(machine.stop(), 1)
def test_stateSpecificDataWithoutData(self) -> None:
"""
To facilitate common implementations of transition behavior methods,
sometimes you want to implement a transition within a data state
without taking a data parameter. To do this, pass the 'nodata=True'
parameter to 'upon'.
"""
builder = TypeMachineBuilder(Counter, NoOpCore)
initial = builder.state("initial")
counting = builder.state("counting", lambda machine, core: Count())
startCalls = []
@pep614(initial.upon(Counter.start).to(counting))
@pep614(counting.upon(Counter.start, nodata=True).loop())
def start(counter: Counter, core: NoOpCore) -> None:
startCalls.append("started!")
@pep614(counting.upon(Counter.increment).loop())
def incf(counter: Counter, core: NoOpCore, count: Count) -> None:
count.value += 1
@pep614(counting.upon(Counter.stop).to(initial))
def finish(counter: Counter, core: NoOpCore, count: Count) -> int:
return count.value
machineFactory = builder.build()
machine = machineFactory(NoOpCore())
machine.start()
self.assertEqual(len(startCalls), 1)
machine.start()
self.assertEqual(len(startCalls), 2)
machine.increment()
self.assertEqual(machine.stop(), 1)
def test_incompleteTransitionDefinition(self) -> None:
builder = TypeMachineBuilder(SimpleProtocol, NoOpCore)
sample = builder.state("sample")
sample.upon(SimpleProtocol.method).loop() # oops, no '.returns(None)'
with self.assertRaises(ValueError) as raised:
builder.build()
self.assertIn(
"incomplete transition from sample to sample upon SimpleProtocol.method",
str(raised.exception),
)
def test_dataToData(self) -> None:
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
@dataclass
class Data1:
value: int
@dataclass
class Data2:
stuff: List[str]
initial = builder.state("initial")
counting = builder.state("counting", lambda proto, core: Data1(1))
appending = builder.state("appending", lambda proto, core: Data2([]))
initial.upon(ProtocolForTesting.change).to(counting).returns(None)
@pep614(counting.upon(ProtocolForTesting.value).loop())
def countup(p: ProtocolForTesting, c: NoOpCore, d: Data1) -> int:
d.value *= 2
return d.value
counting.upon(ProtocolForTesting.change).to(appending).returns(None)
@pep614(appending.upon(ProtocolForTesting.value).loop())
def appendup(p: ProtocolForTesting, c: NoOpCore, d: Data2) -> int:
d.stuff.extend("abc")
return len(d.stuff)
machineFactory = builder.build()
machine = machineFactory(NoOpCore())
machine.change()
self.assertEqual(machine.value(), 2)
self.assertEqual(machine.value(), 4)
machine.change()
self.assertEqual(machine.value(), 3)
self.assertEqual(machine.value(), 6)
def test_dataFactoryArgs(self) -> None:
"""
Any data factory that takes arguments will constrain the allowed
signature of all protocol methods that transition into that state.
"""
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
initial = builder.state("initial")
data = builder.state("data", needsSomething)
data2 = builder.state("data2", needsSomething)
# toState = initial.to(data)
# 'assertions' in the form of expected type errors:
# (no data -> data)
uponNoData = initial.upon(ProtocolForTesting.change)
uponNoData.to(data) # type:ignore[arg-type]
# (data -> data)
uponData = data.upon(ProtocolForTesting.change)
uponData.to(data2) # type:ignore[arg-type]
def test_dataFactoryNoArgs(self) -> None:
"""
Inverse of C{test_dataFactoryArgs} where the data factory specifically
does I{not} take arguments, but the input specified does.
"""
builder = TypeMachineBuilder(ArgTaker, NoOpCore)
initial = builder.state("initial")
data = builder.state("data", needsNothing)
(
initial.upon(ArgTaker.takeSomeArgs)
.to(data) # type:ignore[arg-type]
.returns(None)
)
def test_invalidTransition(self) -> None:
"""
Invalid transitions raise a NoTransition exception.
"""
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
builder.state("initial")
factory = builder.build()
machine = factory(NoOpCore())
with self.assertRaises(NoTransition):
machine.change()
def test_reentrancy(self) -> None:
"""
During the execution of a transition behavior implementation function,
you may invoke other methods on your state machine. However, the
execution of the behavior of those methods will be deferred until the
current behavior method is done executing. In order to implement that
deferral, we restrict the set of methods that can be invoked to those
that return None.
@note: it may be possible to implement deferral via Awaitables or
Deferreds later, but we are starting simple.
"""
class SomeMethods(Protocol):
def start(self) -> None:
"Start the machine."
def later(self) -> None:
"Do some deferrable work."
builder = TypeMachineBuilder(SomeMethods, NoOpCore)
initial = builder.state("initial")
second = builder.state("second")
order = []
@pep614(initial.upon(SomeMethods.start).to(second))
def startup(methods: SomeMethods, core: NoOpCore) -> None:
order.append("startup")
methods.later()
order.append("startup done")
@pep614(second.upon(SomeMethods.later).loop())
def later(methods: SomeMethods, core: NoOpCore) -> None:
order.append("later")
machineFactory = builder.build()
machine = machineFactory(NoOpCore())
machine.start()
self.assertEqual(order, ["startup", "startup done", "later"])
def test_reentrancyNotNoneError(self) -> None:
class SomeMethods(Protocol):
def start(self) -> None:
"Start the machine."
def later(self) -> int:
"Do some deferrable work."
builder = TypeMachineBuilder(SomeMethods, NoOpCore)
initial = builder.state("initial")
second = builder.state("second")
order = []
@pep614(initial.upon(SomeMethods.start).to(second))
def startup(methods: SomeMethods, core: NoOpCore) -> None:
order.append("startup")
methods.later()
order.append("startup done") # pragma: no cover
@pep614(second.upon(SomeMethods.later).loop())
def later(methods: SomeMethods, core: NoOpCore) -> int:
order.append("later")
return 3
machineFactory = builder.build()
machine = machineFactory(NoOpCore())
with self.assertRaises(RuntimeError):
machine.start()
self.assertEqual(order, ["startup"])
# We do actually do the state transition, which happens *before* the
# output is generated; TODO: maybe we should have exception handling
# that transitions into an error state that requires explicit recovery?
self.assertEqual(machine.later(), 3)
self.assertEqual(order, ["startup", "later"])
def test_buildLock(self) -> None:
"""
``.build()`` locks the builder so it can no longer be modified.
"""
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
state = builder.state("test-state")
state2 = builder.state("state2")
state3 = builder.state("state3")
upon = state.upon(ProtocolForTesting.change)
to = upon.to(state2)
to2 = upon.to(state3)
to.returns(None)
with self.assertRaises(ValueError) as ve:
to2.returns(None)
with self.assertRaises(AlreadyBuiltError):
to.returns(None)
builder.build()
with self.assertRaises(AlreadyBuiltError):
builder.state("hello")
with self.assertRaises(AlreadyBuiltError):
builder.build()
def test_methodMembership(self) -> None:
"""
Input methods must be members of their protocol.
"""
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
state = builder.state("test-state")
def stateful(proto: ProtocolForTesting, core: NoOpCore) -> int:
return 4 # pragma: no cover
state2 = builder.state("state2", stateful)
def change(self: ProtocolForTesting) -> None: ...
def rogue(self: ProtocolForTesting) -> int:
return 3 # pragma: no cover
with self.assertRaises(ValueError):
state.upon(change)
with self.assertRaises(ValueError) as ve:
state2.upon(change)
with self.assertRaises(ValueError):
state.upon(rogue)
def test_startInAlternateState(self) -> None:
"""
The state machine can be started in an alternate state.
"""
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
one = builder.state("one")
two = builder.state("two")
@dataclass
class Three:
proto: ProtocolForTesting
core: NoOpCore
value: int = 0
three = builder.state("three", Three)
one.upon(ProtocolForTesting.change).to(two).returns(None)
one.upon(ProtocolForTesting.value).loop().returns(1)
two.upon(ProtocolForTesting.change).to(three).returns(None)
two.upon(ProtocolForTesting.value).loop().returns(2)
@pep614(three.upon(ProtocolForTesting.value).loop())
def threevalue(proto: ProtocolForTesting, core: NoOpCore, three: Three) -> int:
return 3 + three.value
onetwothree = builder.build()
# confirm positive behavior first, particularly the value of the three
# state's change
normal = onetwothree(NoOpCore())
self.assertEqual(normal.value(), 1)
normal.change()
self.assertEqual(normal.value(), 2)
normal.change()
self.assertEqual(normal.value(), 3)
# now try deserializing it in each state
self.assertEqual(onetwothree(NoOpCore()).value(), 1)
self.assertEqual(onetwothree(NoOpCore(), two).value(), 2)
self.assertEqual(
onetwothree(
NoOpCore(), three, lambda proto, core: Three(proto, core, 4)
).value(),
7,
)
def test_genericData(self) -> None:
"""
Test to cover get_origin in generic assertion.
"""
builder = TypeMachineBuilder(ArgTaker, NoOpCore)
one = builder.state("one")
def dat(
proto: ArgTaker, core: NoOpCore, arg1: int = 0, arg2: str = ""
) -> Gen[int]:
return Gen(arg1)
two = builder.state("two", dat)
one.upon(ArgTaker.takeSomeArgs).to(two).returns(None)
@pep614(two.upon(ArgTaker.value).loop())
def val(proto: ArgTaker, core: NoOpCore, data: Gen[int]) -> int:
return data.t
b = builder.build()
m = b(NoOpCore())
m.takeSomeArgs(3)
self.assertEqual(m.value(), 3)
@skipIf(not hasInterface, "zope.interface not installed")
def test_interfaceData(self) -> None:
"""
Test to cover providedBy assertion.
"""
builder = TypeMachineBuilder(ArgTaker, NoOpCore)
one = builder.state("one")
@implementer(ISomething)
@dataclass
class Something:
val: int
def something(self) -> int:
return self.val
def dat(
proto: ArgTaker, core: NoOpCore, arg1: int = 0, arg2: str = ""
) -> ISomething:
return Something(arg1) # type:ignore[return-value]
two = builder.state("two", dat)
one.upon(ArgTaker.takeSomeArgs).to(two).returns(None)
@pep614(two.upon(ArgTaker.value).loop())
def val(proto: ArgTaker, core: NoOpCore, data: ISomething) -> int:
return data.something() # type:ignore[misc]
b = builder.build()
m = b(NoOpCore())
m.takeSomeArgs(3)
self.assertEqual(m.value(), 3)
def test_noMethodsInAltStateDataFactory(self) -> None:
"""
When the state machine is received by a data factory during
construction, it is in an invalid state. It may be invoked after
construction is complete.
"""
builder = TypeMachineBuilder(ProtocolForTesting, NoOpCore)
@dataclass
class Data:
value: int
proto: ProtocolForTesting
start = builder.state("start")
data = builder.state("data", lambda proto, core: Data(3, proto))
@pep614(data.upon(ProtocolForTesting.value).loop())
def getval(proto: ProtocolForTesting, core: NoOpCore, data: Data) -> int:
return data.value
@pep614(start.upon(ProtocolForTesting.value).loop())
def minusone(proto: ProtocolForTesting, core: NoOpCore) -> int:
return -1
factory = builder.build()
self.assertEqual(factory(NoOpCore()).value(), -1)
def touchproto(proto: ProtocolForTesting, core: NoOpCore) -> Data:
return Data(proto.value(), proto)
catchdata = []
def notouchproto(proto: ProtocolForTesting, core: NoOpCore) -> Data:
catchdata.append(new := Data(4, proto))
return new
with self.assertRaises(NoTransition):
factory(NoOpCore(), data, touchproto)
machine = factory(NoOpCore(), data, notouchproto)
self.assertIs(machine, catchdata[0].proto)
self.assertEqual(machine.value(), 4)

@ -0,0 +1,478 @@
from __future__ import annotations
import functools
import os
import subprocess
from dataclasses import dataclass
from typing import Protocol
from unittest import TestCase, skipIf
from automat import TypeMachineBuilder, pep614
from .._methodical import MethodicalMachine
from .._typed import TypeMachine
from .test_discover import isTwistedInstalled
def isGraphvizModuleInstalled():
"""
Is the graphviz Python module installed?
"""
try:
__import__("graphviz")
except ImportError:
return False
else:
return True
def isGraphvizInstalled():
"""
Are the graphviz tools installed?
"""
r, w = os.pipe()
os.close(w)
try:
return not subprocess.call("dot", stdin=r, shell=True)
finally:
os.close(r)
def sampleMachine():
"""
Create a sample L{MethodicalMachine} with some sample states.
"""
mm = MethodicalMachine()
class SampleObject(object):
@mm.state(initial=True)
def begin(self):
"initial state"
@mm.state()
def end(self):
"end state"
@mm.input()
def go(self):
"sample input"
@mm.output()
def out(self):
"sample output"
begin.upon(go, end, [out])
so = SampleObject()
so.go()
return mm
class Sample(Protocol):
def go(self) -> None: ...
class Core: ...
def sampleTypeMachine() -> TypeMachine[Sample, Core]:
"""
Create a sample L{TypeMachine} with some sample states.
"""
builder = TypeMachineBuilder(Sample, Core)
begin = builder.state("begin")
def buildit(proto: Sample, core: Core) -> int:
return 3 # pragma: no cover
data = builder.state("data", buildit)
end = builder.state("end")
begin.upon(Sample.go).to(data).returns(None)
data.upon(Sample.go).to(end).returns(None)
@pep614(end.upon(Sample.go).to(begin))
def out(sample: Sample, core: Core) -> None: ...
return builder.build()
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class ElementMakerTests(TestCase):
"""
L{elementMaker} generates HTML representing the specified element.
"""
def setUp(self):
from .._visualize import elementMaker
self.elementMaker = elementMaker
def test_sortsAttrs(self):
"""
L{elementMaker} orders HTML attributes lexicographically.
"""
expected = r'<div a="1" b="2" c="3"></div>'
self.assertEqual(expected, self.elementMaker("div", b="2", a="1", c="3"))
def test_quotesAttrs(self):
"""
L{elementMaker} quotes HTML attributes according to DOT's quoting rule.
See U{http://www.graphviz.org/doc/info/lang.html}, footnote 1.
"""
expected = r'<div a="1" b="a \" quote" c="a string"></div>'
self.assertEqual(
expected, self.elementMaker("div", b='a " quote', a=1, c="a string")
)
def test_noAttrs(self):
"""
L{elementMaker} should render an element with no attributes.
"""
expected = r"<div ></div>"
self.assertEqual(expected, self.elementMaker("div"))
@dataclass
class HTMLElement(object):
"""Holds an HTML element, as created by elementMaker."""
name: str
children: list[HTMLElement]
attributes: dict[str, str]
def findElements(element, predicate):
"""
Recursively collect all elements in an L{HTMLElement} tree that
match the optional predicate.
"""
if predicate(element):
return [element]
elif isLeaf(element):
return []
return [
result
for child in element.children
for result in findElements(child, predicate)
]
def isLeaf(element):
"""
This HTML element is actually leaf node.
"""
return not isinstance(element, HTMLElement)
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class TableMakerTests(TestCase):
"""
Tests that ensure L{tableMaker} generates HTML tables usable as
labels in DOT graphs.
For more information, read the "HTML-Like Labels" section of
U{http://www.graphviz.org/doc/info/shapes.html}.
"""
def fakeElementMaker(self, name, *children, **attributes):
return HTMLElement(name=name, children=children, attributes=attributes)
def setUp(self):
from .._visualize import tableMaker
self.inputLabel = "input label"
self.port = "the port"
self.tableMaker = functools.partial(tableMaker, _E=self.fakeElementMaker)
def test_inputLabelRow(self):
"""
The table returned by L{tableMaker} always contains the input
symbol label in its first row, and that row contains one cell
with a port attribute set to the provided port.
"""
def hasPort(element):
return not isLeaf(element) and element.attributes.get("port") == self.port
for outputLabels in ([], ["an output label"]):
table = self.tableMaker(self.inputLabel, outputLabels, port=self.port)
self.assertGreater(len(table.children), 0)
inputLabelRow = table.children[0]
portCandidates = findElements(table, hasPort)
self.assertEqual(len(portCandidates), 1)
self.assertEqual(portCandidates[0].name, "td")
self.assertEqual(findElements(inputLabelRow, isLeaf), [self.inputLabel])
def test_noOutputLabels(self):
"""
L{tableMaker} does not add a colspan attribute to the input
label's cell or a second row if there no output labels.
"""
table = self.tableMaker("input label", (), port=self.port)
self.assertEqual(len(table.children), 1)
(inputLabelRow,) = table.children
self.assertNotIn("colspan", inputLabelRow.attributes)
def test_withOutputLabels(self):
"""
L{tableMaker} adds a colspan attribute to the input label's cell
equal to the number of output labels and a second row that
contains the output labels.
"""
table = self.tableMaker(
self.inputLabel, ("output label 1", "output label 2"), port=self.port
)
self.assertEqual(len(table.children), 2)
inputRow, outputRow = table.children
def hasCorrectColspan(element):
return (
not isLeaf(element)
and element.name == "td"
and element.attributes.get("colspan") == "2"
)
self.assertEqual(len(findElements(inputRow, hasCorrectColspan)), 1)
self.assertEqual(
findElements(outputRow, isLeaf), ["output label 1", "output label 2"]
)
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class IntegrationTests(TestCase):
"""
Tests which make sure Graphviz can understand the output produced by
Automat.
"""
def test_validGraphviz(self) -> None:
"""
C{graphviz} emits valid graphviz data.
"""
digraph = sampleMachine().asDigraph()
text = "".join(digraph).encode("utf-8")
p = subprocess.Popen("dot", stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out, err = p.communicate(text)
self.assertEqual(p.returncode, 0)
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class SpotChecks(TestCase):
"""
Tests to make sure that the output contains salient features of the machine
being generated.
"""
def test_containsMachineFeatures(self):
"""
The output of L{graphviz.Digraph} should contain the names of the
states, inputs, outputs in the state machine.
"""
gvout = "".join(sampleMachine().asDigraph())
self.assertIn("begin", gvout)
self.assertIn("end", gvout)
self.assertIn("go", gvout)
self.assertIn("out", gvout)
def test_containsTypeMachineFeatures(self):
"""
The output of L{graphviz.Digraph} should contain the names of the states,
inputs, outputs in the state machine.
"""
gvout = "".join(sampleTypeMachine().asDigraph())
self.assertIn("begin", gvout)
self.assertIn("end", gvout)
self.assertIn("go", gvout)
self.assertIn("data:buildit", gvout)
self.assertIn("out", gvout)
class RecordsDigraphActions(object):
"""
Records calls made to L{FakeDigraph}.
"""
def __init__(self):
self.reset()
def reset(self):
self.renderCalls = []
self.saveCalls = []
class FakeDigraph(object):
"""
A fake L{graphviz.Digraph}. Instantiate it with a
L{RecordsDigraphActions}.
"""
def __init__(self, recorder):
self._recorder = recorder
def render(self, **kwargs):
self._recorder.renderCalls.append(kwargs)
def save(self, **kwargs):
self._recorder.saveCalls.append(kwargs)
class FakeMethodicalMachine(object):
"""
A fake L{MethodicalMachine}. Instantiate it with a L{FakeDigraph}
"""
def __init__(self, digraph):
self._digraph = digraph
def asDigraph(self):
return self._digraph
@skipIf(not isGraphvizModuleInstalled(), "Graphviz module is not installed.")
@skipIf(not isGraphvizInstalled(), "Graphviz tools are not installed.")
@skipIf(not isTwistedInstalled(), "Twisted is not installed.")
class VisualizeToolTests(TestCase):
def setUp(self):
self.digraphRecorder = RecordsDigraphActions()
self.fakeDigraph = FakeDigraph(self.digraphRecorder)
self.fakeProgname = "tool-test"
self.fakeSysPath = ["ignored"]
self.collectedOutput = []
self.fakeFQPN = "fake.fqpn"
def collectPrints(self, *args):
self.collectedOutput.append(" ".join(args))
def fakeFindMachines(self, fqpn):
yield fqpn, FakeMethodicalMachine(self.fakeDigraph)
def tool(
self, progname=None, argv=None, syspath=None, findMachines=None, print=None
):
from .._visualize import tool
return tool(
_progname=progname or self.fakeProgname,
_argv=argv or [self.fakeFQPN],
_syspath=syspath or self.fakeSysPath,
_findMachines=findMachines or self.fakeFindMachines,
_print=print or self.collectPrints,
)
def test_checksCurrentDirectory(self):
"""
L{tool} adds '' to sys.path to ensure
L{automat._discover.findMachines} searches the current
directory.
"""
self.tool(argv=[self.fakeFQPN])
self.assertEqual(self.fakeSysPath[0], "")
def test_quietHidesOutput(self):
"""
Passing -q/--quiet hides all output.
"""
self.tool(argv=[self.fakeFQPN, "--quiet"])
self.assertFalse(self.collectedOutput)
self.tool(argv=[self.fakeFQPN, "-q"])
self.assertFalse(self.collectedOutput)
def test_onlySaveDot(self):
"""
Passing an empty string for --image-directory/-i disables
rendering images.
"""
for arg in ("--image-directory", "-i"):
self.digraphRecorder.reset()
self.collectedOutput = []
self.tool(argv=[self.fakeFQPN, arg, ""])
self.assertFalse(any("image" in line for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
(call,) = self.digraphRecorder.saveCalls
self.assertEqual("{}.dot".format(self.fakeFQPN), call["filename"])
self.assertFalse(self.digraphRecorder.renderCalls)
def test_saveOnlyImage(self):
"""
Passing an empty string for --dot-directory/-d disables saving dot
files.
"""
for arg in ("--dot-directory", "-d"):
self.digraphRecorder.reset()
self.collectedOutput = []
self.tool(argv=[self.fakeFQPN, arg, ""])
self.assertFalse(any("dot" in line for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
(call,) = self.digraphRecorder.renderCalls
self.assertEqual("{}.dot".format(self.fakeFQPN), call["filename"])
self.assertTrue(call["cleanup"])
self.assertFalse(self.digraphRecorder.saveCalls)
def test_saveDotAndImagesInDifferentDirectories(self):
"""
Passing different directories to --image-directory and --dot-directory
writes images and dot files to those directories.
"""
imageDirectory = "image"
dotDirectory = "dot"
self.tool(
argv=[
self.fakeFQPN,
"--image-directory",
imageDirectory,
"--dot-directory",
dotDirectory,
]
)
self.assertTrue(any("image" in line for line in self.collectedOutput))
self.assertTrue(any("dot" in line for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
(renderCall,) = self.digraphRecorder.renderCalls
self.assertEqual(renderCall["directory"], imageDirectory)
self.assertTrue(renderCall["cleanup"])
self.assertEqual(len(self.digraphRecorder.saveCalls), 1)
(saveCall,) = self.digraphRecorder.saveCalls
self.assertEqual(saveCall["directory"], dotDirectory)
def test_saveDotAndImagesInSameDirectory(self):
"""
Passing the same directory to --image-directory and --dot-directory
writes images and dot files to that one directory.
"""
directory = "imagesAndDot"
self.tool(
argv=[
self.fakeFQPN,
"--image-directory",
directory,
"--dot-directory",
directory,
]
)
self.assertTrue(any("image and dot" in line for line in self.collectedOutput))
self.assertEqual(len(self.digraphRecorder.renderCalls), 1)
(renderCall,) = self.digraphRecorder.renderCalls
self.assertEqual(renderCall["directory"], directory)
self.assertFalse(renderCall["cleanup"])
self.assertFalse(len(self.digraphRecorder.saveCalls))

@ -0,0 +1,736 @@
# -*- test-case-name: automat._test.test_type_based -*-
from __future__ import annotations
import sys
from dataclasses import dataclass, field
from typing import (
TYPE_CHECKING,
get_origin,
Any,
Callable,
Generic,
Iterable,
Literal,
Protocol,
TypeVar,
overload,
)
if TYPE_CHECKING:
from graphviz import Digraph
try:
from zope.interface.interface import InterfaceClass # type:ignore[import-untyped]
except ImportError:
hasInterface = False
else:
hasInterface = True
if sys.version_info < (3, 10):
from typing_extensions import Concatenate, ParamSpec, TypeAlias
else:
from typing import Concatenate, ParamSpec, TypeAlias
from ._core import Automaton, Transitioner
from ._runtimeproto import (
ProtocolAtRuntime,
_liveSignature,
actuallyDefinedProtocolMethods,
runtime_name,
)
class AlreadyBuiltError(Exception):
"""
The L{TypeMachine} is already built, and thus can no longer be
modified.
"""
InputProtocol = TypeVar("InputProtocol")
Core = TypeVar("Core")
Data = TypeVar("Data")
P = ParamSpec("P")
P1 = ParamSpec("P1")
R = TypeVar("R")
OtherData = TypeVar("OtherData")
Decorator = Callable[[Callable[P, R]], Callable[P, R]]
FactoryParams = ParamSpec("FactoryParams")
OtherFactoryParams = ParamSpec("OtherFactoryParams")
def pep614(t: R) -> R:
"""
This is a workaround for Python 3.8, which has U{some restrictions on its
grammar for decorators <https://peps.python.org/pep-0614/>}, and makes
C{@state.to(other).upon(Protocol.input)} invalid syntax; for code that
needs to run on these older Python versions, you can do
C{@pep614(state.to(other).upon(Protocol.input))} instead.
"""
return t
@dataclass()
class TransitionRegistrar(Generic[P, P1, R]):
"""
This is a record of a transition that need finalizing; it is the result of
calling L{TypeMachineBuilder.state} and then ``.upon(input).to(state)`` on
the result of that.
It can be used as a decorator, like::
registrar = state.upon(Proto.input).to(state2)
@registrar
def inputImplementation(proto: Proto, core: Core) -> Result: ...
Or, it can be used used to implement a constant return value with
L{TransitionRegistrar.returns}, like::
registrar = state.upon(Proto.input).to(state2)
registrar.returns(value)
Type parameter P: the precise signature of the decorated implementation
callable.
Type parameter P1: the precise signature of the input method from the
outward-facing state-machine protocol.
Type parameter R: the return type of both the protocol method and the input
method.
"""
_signature: Callable[P1, R]
_old: AnyState
_new: AnyState
_nodata: bool = False
_callback: Callable[P, R] | None = None
def __post_init__(self) -> None:
self._old.builder._registrars.append(self)
def __call__(self, impl: Callable[P, R]) -> Callable[P, R]:
"""
Finalize it with C{__call__} to indicate that there is an
implementation to the transition, which can be treated as an output.
"""
if self._callback is not None:
raise AlreadyBuiltError(
f"already registered transition from {self._old.name!r} to {self._new.name!r}"
)
self._callback = impl
builder = self._old.builder
assert builder is self._new.builder, "states must be from the same builder"
builder._automaton.addTransition(
self._old,
self._signature.__name__,
self._new,
tuple(self._new._produceOutputs(impl, self._old, self._nodata)),
)
return impl
def returns(self, result: R) -> None:
"""
Finalize it with C{.returns(constant)} to indicate that there is no
method body, and the given result can just be yielded each time after
the state transition. The only output generated in this case would be
the data-construction factory for the target state.
"""
def constant(*args: object, **kwargs: object) -> R:
return result
constant.__name__ = f"returns({result})"
self(constant)
def _checkComplete(self) -> None:
"""
Raise an exception if the user forgot to decorate a method
implementation or supply a return value for this transition.
"""
# TODO: point at the line where `.to`/`.loop`/`.upon` are called so the
# user can more immediately see the incomplete transition
if not self._callback:
raise ValueError(
f"incomplete transition from {self._old.name} to "
f"{self._new.name} upon {self._signature.__qualname__}: "
"remember to use the transition as a decorator or call "
"`.returns` on it."
)
@dataclass
class UponFromNo(Generic[InputProtocol, Core, P, R]):
"""
Type parameter P: the signature of the input method.
"""
old: TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Any, ...]
input: Callable[Concatenate[InputProtocol, P], R]
@overload
def to(
self, state: TypedState[InputProtocol, Core]
) -> TransitionRegistrar[Concatenate[InputProtocol, Core, P], P, R]: ...
@overload
def to(
self,
state: TypedDataState[InputProtocol, Core, OtherData, P],
) -> TransitionRegistrar[
Concatenate[InputProtocol, Core, P],
Concatenate[InputProtocol, P],
R,
]: ...
def to(
self,
state: (
TypedState[InputProtocol, Core]
| TypedDataState[InputProtocol, Core, Any, P]
),
) -> (
TransitionRegistrar[Concatenate[InputProtocol, Core, P], P, R]
| TransitionRegistrar[
Concatenate[InputProtocol, Core, P],
Concatenate[InputProtocol, P],
R,
]
):
"""
Declare a state transition to a new state.
"""
return TransitionRegistrar(self.input, self.old, state, True)
def loop(self) -> TransitionRegistrar[
Concatenate[InputProtocol, Core, P],
Concatenate[InputProtocol, P],
R,
]:
"""
Register a transition back to the same state.
"""
return TransitionRegistrar(self.input, self.old, self.old, True)
@dataclass
class UponFromData(Generic[InputProtocol, Core, P, R, Data]):
"""
Type parameter P: the signature of the input method.
"""
old: TypedDataState[InputProtocol, Core, Data, ...]
input: Callable[Concatenate[InputProtocol, P], R]
@overload
def to(
self, state: TypedState[InputProtocol, Core]
) -> TransitionRegistrar[
Concatenate[InputProtocol, Core, Data, P], Concatenate[InputProtocol, P], R
]: ...
@overload
def to(
self,
state: TypedDataState[InputProtocol, Core, OtherData, P],
) -> TransitionRegistrar[
Concatenate[InputProtocol, Core, Data, P],
Concatenate[InputProtocol, P],
R,
]: ...
def to(
self,
state: (
TypedState[InputProtocol, Core]
| TypedDataState[InputProtocol, Core, Any, P]
),
) -> (
TransitionRegistrar[Concatenate[InputProtocol, Core, P], P, R]
| TransitionRegistrar[
Concatenate[InputProtocol, Core, Data, P],
Concatenate[InputProtocol, P],
R,
]
):
"""
Declare a state transition to a new state.
"""
return TransitionRegistrar(self.input, self.old, state)
def loop(self) -> TransitionRegistrar[
Concatenate[InputProtocol, Core, Data, P],
Concatenate[InputProtocol, P],
R,
]:
"""
Register a transition back to the same state.
"""
return TransitionRegistrar(self.input, self.old, self.old)
@dataclass(frozen=True)
class TypedState(Generic[InputProtocol, Core]):
"""
The result of L{.state() <automat.TypeMachineBuilder.state>}.
"""
name: str
builder: TypeMachineBuilder[InputProtocol, Core] = field(repr=False)
def upon(
self, input: Callable[Concatenate[InputProtocol, P], R]
) -> UponFromNo[InputProtocol, Core, P, R]:
".upon()"
self.builder._checkMembership(input)
return UponFromNo(self, input)
def _produceOutputs(
self,
impl: Callable[..., object],
old: (
TypedDataState[InputProtocol, Core, OtherData, OtherFactoryParams]
| TypedState[InputProtocol, Core]
),
nodata: bool = False,
) -> Iterable[SomeOutput]:
yield MethodOutput._fromImpl(impl, isinstance(old, TypedDataState))
@dataclass(frozen=True)
class TypedDataState(Generic[InputProtocol, Core, Data, FactoryParams]):
name: str
builder: TypeMachineBuilder[InputProtocol, Core] = field(repr=False)
factory: Callable[Concatenate[InputProtocol, Core, FactoryParams], Data]
@overload
def upon(
self, input: Callable[Concatenate[InputProtocol, P], R]
) -> UponFromData[InputProtocol, Core, P, R, Data]: ...
@overload
def upon(
self, input: Callable[Concatenate[InputProtocol, P], R], nodata: Literal[False]
) -> UponFromData[InputProtocol, Core, P, R, Data]: ...
@overload
def upon(
self, input: Callable[Concatenate[InputProtocol, P], R], nodata: Literal[True]
) -> UponFromNo[InputProtocol, Core, P, R]: ...
def upon(
self,
input: Callable[Concatenate[InputProtocol, P], R],
nodata: bool = False,
) -> (
UponFromData[InputProtocol, Core, P, R, Data]
| UponFromNo[InputProtocol, Core, P, R]
):
self.builder._checkMembership(input)
if nodata:
return UponFromNo(self, input)
else:
return UponFromData(self, input)
def _produceOutputs(
self,
impl: Callable[..., object],
old: (
TypedDataState[InputProtocol, Core, OtherData, OtherFactoryParams]
| TypedState[InputProtocol, Core]
),
nodata: bool,
) -> Iterable[SomeOutput]:
if self is not old:
yield DataOutput(self.factory)
yield MethodOutput._fromImpl(
impl, isinstance(old, TypedDataState) and not nodata
)
AnyState: TypeAlias = "TypedState[Any, Any] | TypedDataState[Any, Any, Any, Any]"
@dataclass
class TypedInput:
name: str
class SomeOutput(Protocol):
"""
A state machine output.
"""
@property
def name(self) -> str:
"read-only name property"
def __call__(*args: Any, **kwargs: Any) -> Any: ...
def __hash__(self) -> int:
"must be hashable"
@dataclass
class InputImplementer(Generic[InputProtocol, Core]):
"""
An L{InputImplementer} implements an input protocol in terms of a
state machine.
When the factory returned from L{TypeMachine}
"""
__automat_core__: Core
__automat_transitioner__: Transitioner[
TypedState[InputProtocol, Core]
| TypedDataState[InputProtocol, Core, object, ...],
str,
SomeOutput,
]
__automat_data__: object | None = None
__automat_postponed__: list[Callable[[], None]] | None = None
def implementMethod(
method: Callable[..., object],
) -> Callable[..., object]:
"""
Construct a function for populating in the synthetic provider of the Input
Protocol to a L{TypeMachineBuilder}. It should have a signature matching that
of the C{method} parameter, a function from that protocol.
"""
methodInput = method.__name__
# side-effects can be re-ordered until later. If you need to compute a
# value in your method, then obviously it can't be invoked reentrantly.
returnAnnotation = _liveSignature(method).return_annotation
returnsNone = returnAnnotation is None
def implementation(
self: InputImplementer[InputProtocol, Core], *args: object, **kwargs: object
) -> object:
transitioner = self.__automat_transitioner__
dataAtStart = self.__automat_data__
if self.__automat_postponed__ is not None:
if not returnsNone:
raise RuntimeError(
f"attempting to reentrantly run {method.__qualname__} "
f"but it wants to return {returnAnnotation!r} not None"
)
def rerunme() -> None:
implementation(self, *args, **kwargs)
self.__automat_postponed__.append(rerunme)
return None
postponed = self.__automat_postponed__ = []
try:
[outputs, tracer] = transitioner.transition(methodInput)
result: Any = None
for output in outputs:
# here's the idea: there will be a state-setup output and a
# state-teardown output. state-setup outputs are added to the
# *beginning* of any entry into a state, so that by the time you
# are running the *implementation* of a method that has entered
# that state, the protocol is in a self-consistent state and can
# run reentrant outputs. not clear that state-teardown outputs are
# necessary
result = output(self, dataAtStart, *args, **kwargs)
finally:
self.__automat_postponed__ = None
while postponed:
postponed.pop(0)()
return result
implementation.__qualname__ = implementation.__name__ = (
f"<implementation for {method}>"
)
return implementation
@dataclass(frozen=True)
class MethodOutput(Generic[Core]):
"""
This is the thing that goes into the automaton's outputs list, and thus
(per the implementation of L{implementMethod}) takes the 'self' of the
InputImplementer instance (i.e. the synthetic protocol implementation) and the
previous result computed by the former output, which will be None
initially.
"""
method: Callable[..., Any]
requiresData: bool
_assertion: Callable[[object], None]
@classmethod
def _fromImpl(
cls: type[MethodOutput[Core]], method: Callable[..., Any], requiresData: bool
) -> MethodOutput[Core]:
parameter = None
annotation: type[object] = object
def assertion(data: object) -> None:
"""
No assertion about the data.
"""
# Do our best to compute the declared signature, so that we caan verify
# it's the right type. We can't always do that.
try:
sig = _liveSignature(method)
except NameError:
...
# An inner function may refer to type aliases that only appear as
# local variables, and those are just lost here; give up.
else:
if requiresData:
# 0: self, 1: self.__automat_core__, 2: self.__automat_data__
declaredParams = list(sig.parameters.values())
if len(declaredParams) >= 3:
parameter = declaredParams[2]
annotation = parameter.annotation
origin = get_origin(annotation)
if origin is not None:
annotation = origin
if hasInterface and isinstance(annotation, InterfaceClass):
def assertion(data: object) -> None:
assert annotation.providedBy(data), (
f"expected {parameter} to provide {annotation} "
f"but got {type(data)} instead"
)
else:
def assertion(data: object) -> None:
assert isinstance(data, annotation), (
f"expected {parameter} to be {annotation} "
f"but got {type(data)} instead"
)
return cls(method, requiresData, assertion)
@property
def name(self) -> str:
return f"{self.method.__name__}"
def __call__(
self,
machine: InputImplementer[InputProtocol, Core],
dataAtStart: Data,
/,
*args: object,
**kwargs: object,
) -> object:
extraArgs = [machine, machine.__automat_core__]
if self.requiresData:
self._assertion(dataAtStart)
extraArgs += [dataAtStart]
# if anything is invoked reentrantly here, then we can't possibly have
# set __automat_data__ and the data argument to the reentrant method
# will be wrong. we *need* to split out the construction / state-enter
# hook, because it needs to run separately.
return self.method(*extraArgs, *args, **kwargs)
@dataclass(frozen=True)
class DataOutput(Generic[Data]):
"""
Construct an output for the given data objects.
"""
dataFactory: Callable[..., Data]
@property
def name(self) -> str:
return f"data:{self.dataFactory.__name__}"
def __call__(
realself,
self: InputImplementer[InputProtocol, Core],
dataAtStart: object,
*args: object,
**kwargs: object,
) -> Data:
newData = realself.dataFactory(self, self.__automat_core__, *args, **kwargs)
self.__automat_data__ = newData
return newData
INVALID_WHILE_DESERIALIZING: TypedState[Any, Any] = TypedState(
"automat:invalid-while-deserializing",
None, # type:ignore[arg-type]
)
@dataclass(frozen=True)
class TypeMachine(Generic[InputProtocol, Core]):
"""
A L{TypeMachine} is a factory for instances of C{InputProtocol}.
"""
__automat_type__: type[InputImplementer[InputProtocol, Core]]
__automat_automaton__: Automaton[
TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Any, ...],
str,
SomeOutput,
]
@overload
def __call__(self, core: Core) -> InputProtocol: ...
@overload
def __call__(
self, core: Core, state: TypedState[InputProtocol, Core]
) -> InputProtocol: ...
@overload
def __call__(
self,
core: Core,
state: TypedDataState[InputProtocol, Core, OtherData, ...],
dataFactory: Callable[[InputProtocol, Core], OtherData],
) -> InputProtocol: ...
def __call__(
self,
core: Core,
state: (
TypedState[InputProtocol, Core]
| TypedDataState[InputProtocol, Core, OtherData, ...]
| None
) = None,
dataFactory: Callable[[InputProtocol, Core], OtherData] | None = None,
) -> InputProtocol:
"""
Construct an instance of C{InputProtocol} from an instance of the
C{Core} protocol.
"""
if state is None:
state = initial = self.__automat_automaton__.initialState
elif isinstance(state, TypedDataState):
assert dataFactory is not None, "data state requires a data factory"
# Ensure that the machine is in a state with *no* transitions while
# we are doing the initial construction of its state-specific data.
initial = INVALID_WHILE_DESERIALIZING
else:
initial = state
internals: InputImplementer[InputProtocol, Core] = self.__automat_type__(
core, txnr := Transitioner(self.__automat_automaton__, initial)
)
result: InputProtocol = internals # type:ignore[assignment]
if dataFactory is not None:
internals.__automat_data__ = dataFactory(result, core)
txnr._state = state
return result
def asDigraph(self) -> Digraph:
from ._visualize import makeDigraph
return makeDigraph(
self.__automat_automaton__,
stateAsString=lambda state: state.name,
inputAsString=lambda input: input,
outputAsString=lambda output: output.name,
)
@dataclass(eq=False)
class TypeMachineBuilder(Generic[InputProtocol, Core]):
"""
The main entry-point into Automat, used to construct a factory for
instances of C{InputProtocol} that take an instance of C{Core}.
Describe the machine with L{TypeMachineBuilder.state} L{.upon
<automat._typed.TypedState.upon>} L{.to
<automat._typed.UponFromNo.to>}, then build it with
L{TypeMachineBuilder.build}, like so::
from typing import Protocol
class Inputs(Protocol):
def method(self) -> None: ...
class Core: ...
from automat import TypeMachineBuilder
builder = TypeMachineBuilder(Inputs, Core)
state = builder.state("state")
state.upon(Inputs.method).loop().returns(None)
Machine = builder.build()
machine = Machine(Core())
machine.method()
"""
# Public constructor parameters.
inputProtocol: ProtocolAtRuntime[InputProtocol]
coreType: type[Core]
# Internal state, not in the constructor.
_automaton: Automaton[
TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Any, ...],
str,
SomeOutput,
] = field(default_factory=Automaton, repr=False, init=False)
_initial: bool = field(default=True, init=False)
_registrars: list[TransitionRegistrar[..., ..., Any]] = field(
default_factory=list, init=False
)
_built: bool = field(default=False, init=False)
@overload
def state(self, name: str) -> TypedState[InputProtocol, Core]: ...
@overload
def state(
self,
name: str,
dataFactory: Callable[Concatenate[InputProtocol, Core, P], Data],
) -> TypedDataState[InputProtocol, Core, Data, P]: ...
def state(
self,
name: str,
dataFactory: Callable[Concatenate[InputProtocol, Core, P], Data] | None = None,
) -> TypedState[InputProtocol, Core] | TypedDataState[InputProtocol, Core, Data, P]:
"""
Construct a state.
"""
if self._built:
raise AlreadyBuiltError(
"Cannot add states to an already-built state machine."
)
if dataFactory is None:
state = TypedState(name, self)
if self._initial:
self._initial = False
self._automaton.initialState = state
return state
else:
assert not self._initial, "initial state cannot require state-specific data"
return TypedDataState(name, self, dataFactory)
def build(self) -> TypeMachine[InputProtocol, Core]:
"""
Create a L{TypeMachine}, and prevent further modification to the state
machine being built.
"""
# incompleteness check
if self._built:
raise AlreadyBuiltError("Cannot build a state machine twice.")
self._built = True
for registrar in self._registrars:
registrar._checkComplete()
# We were only hanging on to these for error-checking purposes, so we
# can drop them now.
del self._registrars[:]
runtimeType: type[InputImplementer[InputProtocol, Core]] = type(
f"Typed<{runtime_name(self.inputProtocol)}>",
tuple([InputImplementer]),
{
method_name: implementMethod(getattr(self.inputProtocol, method_name))
for method_name in actuallyDefinedProtocolMethods(self.inputProtocol)
},
)
return TypeMachine(runtimeType, self._automaton)
def _checkMembership(self, input: Callable[..., object]) -> None:
"""
Ensure that ``input`` is a valid member function of the input protocol,
not just a function that happens to take the right first argument.
"""
if (checked := getattr(self.inputProtocol, input.__name__, None)) is not input:
raise ValueError(
f"{input.__qualname__} is not a member of {self.inputProtocol.__module__}.{self.inputProtocol.__name__}"
)

@ -0,0 +1,230 @@
from __future__ import annotations
import argparse
import sys
from functools import wraps
from typing import Callable, Iterator
import graphviz
from ._core import Automaton, Input, Output, State
from ._discover import findMachines
from ._methodical import MethodicalMachine
from ._typed import TypeMachine, InputProtocol, Core
def _gvquote(s: str) -> str:
return '"{}"'.format(s.replace('"', r"\""))
def _gvhtml(s: str) -> str:
return "<{}>".format(s)
def elementMaker(name: str, *children: str, **attrs: str) -> str:
"""
Construct a string from the HTML element description.
"""
formattedAttrs = " ".join(
"{}={}".format(key, _gvquote(str(value)))
for key, value in sorted(attrs.items())
)
formattedChildren = "".join(children)
return "<{name} {attrs}>{children}</{name}>".format(
name=name, attrs=formattedAttrs, children=formattedChildren
)
def tableMaker(
inputLabel: str,
outputLabels: list[str],
port: str,
_E: Callable[..., str] = elementMaker,
) -> str:
"""
Construct an HTML table to label a state transition.
"""
colspan = {}
if outputLabels:
colspan["colspan"] = str(len(outputLabels))
inputLabelCell = _E(
"td",
_E("font", inputLabel, face="menlo-italic"),
color="purple",
port=port,
**colspan,
)
pointSize = {"point-size": "9"}
outputLabelCells = [
_E("td", _E("font", outputLabel, **pointSize), color="pink")
for outputLabel in outputLabels
]
rows = [_E("tr", inputLabelCell)]
if outputLabels:
rows.append(_E("tr", *outputLabelCells))
return _E("table", *rows)
def escapify(x: Callable[[State], str]) -> Callable[[State], str]:
@wraps(x)
def impl(t: State) -> str:
return x(t).replace("<", "&lt;").replace(">", "&gt;")
return impl
def makeDigraph(
automaton: Automaton[State, Input, Output],
inputAsString: Callable[[Input], str] = repr,
outputAsString: Callable[[Output], str] = repr,
stateAsString: Callable[[State], str] = repr,
) -> graphviz.Digraph:
"""
Produce a L{graphviz.Digraph} object from an automaton.
"""
inputAsString = escapify(inputAsString)
outputAsString = escapify(outputAsString)
stateAsString = escapify(stateAsString)
digraph = graphviz.Digraph(
graph_attr={"pack": "true", "dpi": "100"},
node_attr={"fontname": "Menlo"},
edge_attr={"fontname": "Menlo"},
)
for state in automaton.states():
if state is automaton.initialState:
stateShape = "bold"
fontName = "Menlo-Bold"
else:
stateShape = ""
fontName = "Menlo"
digraph.node(
stateAsString(state),
fontame=fontName,
shape="ellipse",
style=stateShape,
color="blue",
)
for n, eachTransition in enumerate(automaton.allTransitions()):
inState, inputSymbol, outState, outputSymbols = eachTransition
thisTransition = "t{}".format(n)
inputLabel = inputAsString(inputSymbol)
port = "tableport"
table = tableMaker(
inputLabel,
[outputAsString(outputSymbol) for outputSymbol in outputSymbols],
port=port,
)
digraph.node(thisTransition, label=_gvhtml(table), margin="0.2", shape="none")
digraph.edge(
stateAsString(inState),
"{}:{}:w".format(thisTransition, port),
arrowhead="none",
)
digraph.edge("{}:{}:e".format(thisTransition, port), stateAsString(outState))
return digraph
def tool(
_progname: str = sys.argv[0],
_argv: list[str] = sys.argv[1:],
_syspath: list[str] = sys.path,
_findMachines: Callable[
[str],
Iterator[tuple[str, MethodicalMachine | TypeMachine[InputProtocol, Core]]],
] = findMachines,
_print: Callable[..., None] = print,
) -> None:
"""
Entry point for command line utility.
"""
DESCRIPTION = """
Visualize automat.MethodicalMachines as graphviz graphs.
"""
EPILOG = """
You must have the graphviz tool suite installed. Please visit
http://www.graphviz.org for more information.
"""
if _syspath[0]:
_syspath.insert(0, "")
argumentParser = argparse.ArgumentParser(
prog=_progname, description=DESCRIPTION, epilog=EPILOG
)
argumentParser.add_argument(
"fqpn",
help="A Fully Qualified Path name" " representing where to find machines.",
)
argumentParser.add_argument(
"--quiet", "-q", help="suppress output", default=False, action="store_true"
)
argumentParser.add_argument(
"--dot-directory",
"-d",
help="Where to write out .dot files.",
default=".automat_visualize",
)
argumentParser.add_argument(
"--image-directory",
"-i",
help="Where to write out image files.",
default=".automat_visualize",
)
argumentParser.add_argument(
"--image-type",
"-t",
help="The image format.",
choices=graphviz.FORMATS,
default="png",
)
argumentParser.add_argument(
"--view",
"-v",
help="View rendered graphs with" " default image viewer",
default=False,
action="store_true",
)
args = argumentParser.parse_args(_argv)
explicitlySaveDot = args.dot_directory and (
not args.image_directory or args.image_directory != args.dot_directory
)
if args.quiet:
def _print(*args):
pass
for fqpn, machine in _findMachines(args.fqpn):
_print(fqpn, "...discovered")
digraph = machine.asDigraph()
if explicitlySaveDot:
digraph.save(filename="{}.dot".format(fqpn), directory=args.dot_directory)
_print(fqpn, "...wrote dot into", args.dot_directory)
if args.image_directory:
deleteDot = not args.dot_directory or explicitlySaveDot
digraph.format = args.image_type
digraph.render(
filename="{}.dot".format(fqpn),
directory=args.image_directory,
view=args.view,
cleanup=deleteDot,
)
if deleteDot:
msg = "...wrote image into"
else:
msg = "...wrote image and dot into"
_print(fqpn, msg, args.image_directory)

@ -0,0 +1,20 @@
Copyright 2010 Jason Kirtland
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,60 @@
Metadata-Version: 2.3
Name: blinker
Version: 1.9.0
Summary: Fast, simple object-to-object and broadcast signaling
Author: Jason Kirtland
Maintainer-email: Pallets Ecosystem <contact@palletsprojects.com>
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Typing :: Typed
Project-URL: Chat, https://discord.gg/pallets
Project-URL: Documentation, https://blinker.readthedocs.io
Project-URL: Source, https://github.com/pallets-eco/blinker/
# Blinker
Blinker provides a fast dispatching system that allows any number of
interested parties to subscribe to events, or "signals".
## Pallets Community Ecosystem
> [!IMPORTANT]\
> This project is part of the Pallets Community Ecosystem. Pallets is the open
> source organization that maintains Flask; Pallets-Eco enables community
> maintenance of related projects. If you are interested in helping maintain
> this project, please reach out on [the Pallets Discord server][discord].
>
> [discord]: https://discord.gg/pallets
## Example
Signal receivers can subscribe to specific senders or receive signals
sent by any sender.
```pycon
>>> from blinker import signal
>>> started = signal('round-started')
>>> def each(round):
... print(f"Round {round}")
...
>>> started.connect(each)
>>> def round_two(round):
... print("This is round two.")
...
>>> started.connect(round_two, sender=2)
>>> for round in range(1, 4):
... started.send(round)
...
Round 1!
Round 2!
This is round two.
Round 3!
```

@ -0,0 +1,12 @@
blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054
blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633
blinker-1.9.0.dist-info/RECORD,,
blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317
blinker/__pycache__/__init__.cpython-312.pyc,,
blinker/__pycache__/_utilities.cpython-312.pyc,,
blinker/__pycache__/base.cpython-312.pyc,,
blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675
blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132
blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: flit 3.10.1
Root-Is-Purelib: true
Tag: py3-none-any

@ -0,0 +1,17 @@
from __future__ import annotations
from .base import ANY
from .base import default_namespace
from .base import NamedSignal
from .base import Namespace
from .base import Signal
from .base import signal
__all__ = [
"ANY",
"default_namespace",
"NamedSignal",
"Namespace",
"Signal",
"signal",
]

@ -0,0 +1,64 @@
from __future__ import annotations
import collections.abc as c
import inspect
import typing as t
from weakref import ref
from weakref import WeakMethod
T = t.TypeVar("T")
class Symbol:
"""A constant symbol, nicer than ``object()``. Repeated calls return the
same instance.
>>> Symbol('foo') is Symbol('foo')
True
>>> Symbol('foo')
foo
"""
symbols: t.ClassVar[dict[str, Symbol]] = {}
def __new__(cls, name: str) -> Symbol:
if name in cls.symbols:
return cls.symbols[name]
obj = super().__new__(cls)
cls.symbols[name] = obj
return obj
def __init__(self, name: str) -> None:
self.name = name
def __repr__(self) -> str:
return self.name
def __getnewargs__(self) -> tuple[t.Any, ...]:
return (self.name,)
def make_id(obj: object) -> c.Hashable:
"""Get a stable identifier for a receiver or sender, to be used as a dict
key or in a set.
"""
if inspect.ismethod(obj):
# The id of a bound method is not stable, but the id of the unbound
# function and instance are.
return id(obj.__func__), id(obj.__self__)
if isinstance(obj, (str, int)):
# Instances with the same value always compare equal and have the same
# hash, even if the id may change.
return obj
# Assume other types are not hashable but will always be the same instance.
return id(obj)
def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]:
if inspect.ismethod(obj):
return WeakMethod(obj, callback) # type: ignore[arg-type, return-value]
return ref(obj, callback)

@ -0,0 +1,512 @@
from __future__ import annotations
import collections.abc as c
import sys
import typing as t
import weakref
from collections import defaultdict
from contextlib import contextmanager
from functools import cached_property
from inspect import iscoroutinefunction
from ._utilities import make_id
from ._utilities import make_ref
from ._utilities import Symbol
F = t.TypeVar("F", bound=c.Callable[..., t.Any])
ANY = Symbol("ANY")
"""Symbol for "any sender"."""
ANY_ID = 0
class Signal:
"""A notification emitter.
:param doc: The docstring for the signal.
"""
ANY = ANY
"""An alias for the :data:`~blinker.ANY` sender symbol."""
set_class: type[set[t.Any]] = set
"""The set class to use for tracking connected receivers and senders.
Python's ``set`` is unordered. If receivers must be dispatched in the order
they were connected, an ordered set implementation can be used.
.. versionadded:: 1.7
"""
@cached_property
def receiver_connected(self) -> Signal:
"""Emitted at the end of each :meth:`connect` call.
The signal sender is the signal instance, and the :meth:`connect`
arguments are passed through: ``receiver``, ``sender``, and ``weak``.
.. versionadded:: 1.2
"""
return Signal(doc="Emitted after a receiver connects.")
@cached_property
def receiver_disconnected(self) -> Signal:
"""Emitted at the end of each :meth:`disconnect` call.
The sender is the signal instance, and the :meth:`disconnect` arguments
are passed through: ``receiver`` and ``sender``.
This signal is emitted **only** when :meth:`disconnect` is called
explicitly. This signal cannot be emitted by an automatic disconnect
when a weakly referenced receiver or sender goes out of scope, as the
instance is no longer be available to be used as the sender for this
signal.
An alternative approach is available by subscribing to
:attr:`receiver_connected` and setting up a custom weakref cleanup
callback on weak receivers and senders.
.. versionadded:: 1.2
"""
return Signal(doc="Emitted after a receiver disconnects.")
def __init__(self, doc: str | None = None) -> None:
if doc:
self.__doc__ = doc
self.receivers: dict[
t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any]
] = {}
"""The map of connected receivers. Useful to quickly check if any
receivers are connected to the signal: ``if s.receivers:``. The
structure and data is not part of the public API, but checking its
boolean value is.
"""
self.is_muted: bool = False
self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {}
def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F:
"""Connect ``receiver`` to be called when the signal is sent by
``sender``.
:param receiver: The callable to call when :meth:`send` is called with
the given ``sender``, passing ``sender`` as a positional argument
along with any extra keyword arguments.
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
called when :meth:`send` is called with this sender. If ``ANY``, the
receiver will be called for any sender. A receiver may be connected
to multiple senders by calling :meth:`connect` multiple times.
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
be automatically disconnected when it is garbage collected. When
connecting a receiver defined within a function, set to ``False``,
otherwise it will be disconnected when the function scope ends.
"""
receiver_id = make_id(receiver)
sender_id = ANY_ID if sender is ANY else make_id(sender)
if weak:
self.receivers[receiver_id] = make_ref(
receiver, self._make_cleanup_receiver(receiver_id)
)
else:
self.receivers[receiver_id] = receiver
self._by_sender[sender_id].add(receiver_id)
self._by_receiver[receiver_id].add(sender_id)
if sender is not ANY and sender_id not in self._weak_senders:
# store a cleanup for weakref-able senders
try:
self._weak_senders[sender_id] = make_ref(
sender, self._make_cleanup_sender(sender_id)
)
except TypeError:
pass
if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:
try:
self.receiver_connected.send(
self, receiver=receiver, sender=sender, weak=weak
)
except TypeError:
# TODO no explanation or test for this
self.disconnect(receiver, sender)
raise
return receiver
def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]:
"""Connect the decorated function to be called when the signal is sent
by ``sender``.
The decorated function will be called when :meth:`send` is called with
the given ``sender``, passing ``sender`` as a positional argument along
with any extra keyword arguments.
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
called when :meth:`send` is called with this sender. If ``ANY``, the
receiver will be called for any sender. A receiver may be connected
to multiple senders by calling :meth:`connect` multiple times.
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
be automatically disconnected when it is garbage collected. When
connecting a receiver defined within a function, set to ``False``,
otherwise it will be disconnected when the function scope ends.=
.. versionadded:: 1.1
"""
def decorator(fn: F) -> F:
self.connect(fn, sender, weak)
return fn
return decorator
@contextmanager
def connected_to(
self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY
) -> c.Generator[None, None, None]:
"""A context manager that temporarily connects ``receiver`` to the
signal while a ``with`` block executes. When the block exits, the
receiver is disconnected. Useful for tests.
:param receiver: The callable to call when :meth:`send` is called with
the given ``sender``, passing ``sender`` as a positional argument
along with any extra keyword arguments.
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
called when :meth:`send` is called with this sender. If ``ANY``, the
receiver will be called for any sender.
.. versionadded:: 1.1
"""
self.connect(receiver, sender=sender, weak=False)
try:
yield None
finally:
self.disconnect(receiver)
@contextmanager
def muted(self) -> c.Generator[None, None, None]:
"""A context manager that temporarily disables the signal. No receivers
will be called if the signal is sent, until the ``with`` block exits.
Useful for tests.
"""
self.is_muted = True
try:
yield None
finally:
self.is_muted = False
def send(
self,
sender: t.Any | None = None,
/,
*,
_async_wrapper: c.Callable[
[c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any]
]
| None = None,
**kwargs: t.Any,
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
"""Call all receivers that are connected to the given ``sender``
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
argument along with any extra keyword arguments. Return a list of
``(receiver, return value)`` tuples.
The order receivers are called is undefined, but can be influenced by
setting :attr:`set_class`.
If a receiver raises an exception, that exception will propagate up.
This makes debugging straightforward, with an assumption that correctly
implemented receivers will not raise.
:param sender: Call receivers connected to this sender, in addition to
those connected to :data:`ANY`.
:param _async_wrapper: Will be called on any receivers that are async
coroutines to turn them into sync callables. For example, could run
the receiver with an event loop.
:param kwargs: Extra keyword arguments to pass to each receiver.
.. versionchanged:: 1.7
Added the ``_async_wrapper`` argument.
"""
if self.is_muted:
return []
results = []
for receiver in self.receivers_for(sender):
if iscoroutinefunction(receiver):
if _async_wrapper is None:
raise RuntimeError("Cannot send to a coroutine function.")
result = _async_wrapper(receiver)(sender, **kwargs)
else:
result = receiver(sender, **kwargs)
results.append((receiver, result))
return results
async def send_async(
self,
sender: t.Any | None = None,
/,
*,
_sync_wrapper: c.Callable[
[c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]
]
| None = None,
**kwargs: t.Any,
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
"""Await all receivers that are connected to the given ``sender``
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
argument along with any extra keyword arguments. Return a list of
``(receiver, return value)`` tuples.
The order receivers are called is undefined, but can be influenced by
setting :attr:`set_class`.
If a receiver raises an exception, that exception will propagate up.
This makes debugging straightforward, with an assumption that correctly
implemented receivers will not raise.
:param sender: Call receivers connected to this sender, in addition to
those connected to :data:`ANY`.
:param _sync_wrapper: Will be called on any receivers that are sync
callables to turn them into async coroutines. For example,
could call the receiver in a thread.
:param kwargs: Extra keyword arguments to pass to each receiver.
.. versionadded:: 1.7
"""
if self.is_muted:
return []
results = []
for receiver in self.receivers_for(sender):
if not iscoroutinefunction(receiver):
if _sync_wrapper is None:
raise RuntimeError("Cannot send to a non-coroutine function.")
result = await _sync_wrapper(receiver)(sender, **kwargs)
else:
result = await receiver(sender, **kwargs)
results.append((receiver, result))
return results
def has_receivers_for(self, sender: t.Any) -> bool:
"""Check if there is at least one receiver that will be called with the
given ``sender``. A receiver connected to :data:`ANY` will always be
called, regardless of sender. Does not check if weakly referenced
receivers are still live. See :meth:`receivers_for` for a stronger
search.
:param sender: Check for receivers connected to this sender, in addition
to those connected to :data:`ANY`.
"""
if not self.receivers:
return False
if self._by_sender[ANY_ID]:
return True
if sender is ANY:
return False
return make_id(sender) in self._by_sender
def receivers_for(
self, sender: t.Any
) -> c.Generator[c.Callable[..., t.Any], None, None]:
"""Yield each receiver to be called for ``sender``, in addition to those
to be called for :data:`ANY`. Weakly referenced receivers that are not
live will be disconnected and skipped.
:param sender: Yield receivers connected to this sender, in addition
to those connected to :data:`ANY`.
"""
# TODO: test receivers_for(ANY)
if not self.receivers:
return
sender_id = make_id(sender)
if sender_id in self._by_sender:
ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]
else:
ids = self._by_sender[ANY_ID].copy()
for receiver_id in ids:
receiver = self.receivers.get(receiver_id)
if receiver is None:
continue
if isinstance(receiver, weakref.ref):
strong = receiver()
if strong is None:
self._disconnect(receiver_id, ANY_ID)
continue
yield strong
else:
yield receiver
def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None:
"""Disconnect ``receiver`` from being called when the signal is sent by
``sender``.
:param receiver: A connected receiver callable.
:param sender: Disconnect from only this sender. By default, disconnect
from all senders.
"""
sender_id: c.Hashable
if sender is ANY:
sender_id = ANY_ID
else:
sender_id = make_id(sender)
receiver_id = make_id(receiver)
self._disconnect(receiver_id, sender_id)
if (
"receiver_disconnected" in self.__dict__
and self.receiver_disconnected.receivers
):
self.receiver_disconnected.send(self, receiver=receiver, sender=sender)
def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None:
if sender_id == ANY_ID:
if self._by_receiver.pop(receiver_id, None) is not None:
for bucket in self._by_sender.values():
bucket.discard(receiver_id)
self.receivers.pop(receiver_id, None)
else:
self._by_sender[sender_id].discard(receiver_id)
self._by_receiver[receiver_id].discard(sender_id)
def _make_cleanup_receiver(
self, receiver_id: c.Hashable
) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:
"""Create a callback function to disconnect a weakly referenced
receiver when it is garbage collected.
"""
def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:
# If the interpreter is shutting down, disconnecting can result in a
# weird ignored exception. Don't call it in that case.
if not sys.is_finalizing():
self._disconnect(receiver_id, ANY_ID)
return cleanup
def _make_cleanup_sender(
self, sender_id: c.Hashable
) -> c.Callable[[weakref.ref[t.Any]], None]:
"""Create a callback function to disconnect all receivers for a weakly
referenced sender when it is garbage collected.
"""
assert sender_id != ANY_ID
def cleanup(ref: weakref.ref[t.Any]) -> None:
self._weak_senders.pop(sender_id, None)
for receiver_id in self._by_sender.pop(sender_id, ()):
self._by_receiver[receiver_id].discard(sender_id)
return cleanup
def _cleanup_bookkeeping(self) -> None:
"""Prune unused sender/receiver bookkeeping. Not threadsafe.
Connecting & disconnecting leaves behind a small amount of bookkeeping
data. Typical workloads using Blinker, for example in most web apps,
Flask, CLI scripts, etc., are not adversely affected by this
bookkeeping.
With a long-running process performing dynamic signal routing with high
volume, e.g. connecting to function closures, senders are all unique
object instances. Doing all of this over and over may cause memory usage
to grow due to extraneous bookkeeping. (An empty ``set`` for each stale
sender/receiver pair.)
This method will prune that bookkeeping away, with the caveat that such
pruning is not threadsafe. The risk is that cleanup of a fully
disconnected receiver/sender pair occurs while another thread is
connecting that same pair. If you are in the highly dynamic, unique
receiver/sender situation that has lead you to this method, that failure
mode is perhaps not a big deal for you.
"""
for mapping in (self._by_sender, self._by_receiver):
for ident, bucket in list(mapping.items()):
if not bucket:
mapping.pop(ident, None)
def _clear_state(self) -> None:
"""Disconnect all receivers and senders. Useful for tests."""
self._weak_senders.clear()
self.receivers.clear()
self._by_sender.clear()
self._by_receiver.clear()
class NamedSignal(Signal):
"""A named generic notification emitter. The name is not used by the signal
itself, but matches the key in the :class:`Namespace` that it belongs to.
:param name: The name of the signal within the namespace.
:param doc: The docstring for the signal.
"""
def __init__(self, name: str, doc: str | None = None) -> None:
super().__init__(doc)
#: The name of this signal.
self.name: str = name
def __repr__(self) -> str:
base = super().__repr__()
return f"{base[:-1]}; {self.name!r}>" # noqa: E702
class Namespace(dict[str, NamedSignal]):
"""A dict mapping names to signals."""
def signal(self, name: str, doc: str | None = None) -> NamedSignal:
"""Return the :class:`NamedSignal` for the given ``name``, creating it
if required. Repeated calls with the same name return the same signal.
:param name: The name of the signal.
:param doc: The docstring of the signal.
"""
if name not in self:
self[name] = NamedSignal(name, doc)
return self[name]
class _PNamespaceSignal(t.Protocol):
def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...
default_namespace: Namespace = Namespace()
"""A default :class:`Namespace` for creating named signals. :func:`signal`
creates a :class:`NamedSignal` in this namespace.
"""
signal: _PNamespaceSignal = default_namespace.signal
"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given
``name``, creating it if required. Repeated calls with the same name return the
same signal.
"""

@ -0,0 +1,28 @@
Copyright 2018 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,64 @@
Metadata-Version: 2.1
Name: cachelib
Version: 0.13.0
Summary: A collection of cache libraries in the same API interface.
Home-page: https://github.com/pallets-eco/cachelib/
Maintainer: Pallets
Maintainer-email: contact@palletsprojects.com
License: BSD-3-Clause
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Documentation, https://cachelib.readthedocs.io/
Project-URL: Changes, https://cachelib.readthedocs.io/changes/
Project-URL: Source Code, https://github.com/pallets-eco/cachelib/
Project-URL: Issue Tracker, https://github.com/pallets-eco/cachelib/issues/
Project-URL: Twitter, https://twitter.com/PalletsTeam
Project-URL: Chat, https://discord.gg/pallets
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-File: LICENSE.rst
CacheLib
========
A collection of cache libraries in the same API interface. Extracted
from Werkzeug.
Installing
----------
Install and update using `pip`_:
.. code-block:: text
$ pip install -U cachelib
.. _pip: https://pip.pypa.io/en/stable/getting-started/
Donate
------
The Pallets organization develops and supports Flask and the libraries
it uses. In order to grow the community of contributors and users, and
allow the maintainers to devote more time to the projects, `please
donate today`_.
.. _please donate today: https://palletsprojects.com/donate
Links
-----
- Documentation: https://cachelib.readthedocs.io/
- Changes: https://cachelib.readthedocs.io/changes/
- PyPI Releases: https://pypi.org/project/cachelib/
- Source Code: https://github.com/pallets/cachelib/
- Issue Tracker: https://github.com/pallets/cachelib/issues/
- Twitter: https://twitter.com/PalletsTeam
- Chat: https://discord.gg/pallets

@ -0,0 +1,27 @@
cachelib-0.13.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
cachelib-0.13.0.dist-info/LICENSE.rst,sha256=zUGBIIEtwmJiga4CfoG2SCKdFmtaynRyzs1RADjTbn0,1475
cachelib-0.13.0.dist-info/METADATA,sha256=lqDsbA03sUETX-dGoDR1uDqqXtyu1m0sS0wBjCDEeXE,1960
cachelib-0.13.0.dist-info/RECORD,,
cachelib-0.13.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
cachelib-0.13.0.dist-info/top_level.txt,sha256=AYC4q8wgGd_hR_F2YcDkmtQm41gv9-5AThKuQtNPEXk,9
cachelib/__init__.py,sha256=xTX_F13-FA58DjBGip4XZeCSlEo6B4c4VTRi49MBywY,575
cachelib/__pycache__/__init__.cpython-312.pyc,,
cachelib/__pycache__/base.cpython-312.pyc,,
cachelib/__pycache__/dynamodb.cpython-312.pyc,,
cachelib/__pycache__/file.cpython-312.pyc,,
cachelib/__pycache__/memcached.cpython-312.pyc,,
cachelib/__pycache__/mongodb.cpython-312.pyc,,
cachelib/__pycache__/redis.cpython-312.pyc,,
cachelib/__pycache__/serializers.cpython-312.pyc,,
cachelib/__pycache__/simple.cpython-312.pyc,,
cachelib/__pycache__/uwsgi.cpython-312.pyc,,
cachelib/base.py,sha256=3_B-cB1VEh_x-VzH9g3qvzdqCxDX2ywDzQ7a_aYFJlE,6731
cachelib/dynamodb.py,sha256=fSmp8G7V0yBcRC2scdIhz8d0D2-9OMZEwQ9AcBONyC8,8512
cachelib/file.py,sha256=xu6m7nzTMcca_wYS0oM4iOxLarQHZFrhUnQnFYansy4,12270
cachelib/memcached.py,sha256=KyUN4wblVPf2XNLYk15kwN9QTfkFK6jrpVGrj4NAoFA,7160
cachelib/mongodb.py,sha256=b9l8fTKMFm8hAXFn748GKertHUASSVDBgPgrtGaZ6cA,6901
cachelib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
cachelib/redis.py,sha256=hSKV9fVD7gzk1X_B3Ac9XJYN3G3F6eYBoreqDo9pces,6295
cachelib/serializers.py,sha256=MXk1moN6ljOPUFQ0E0D129mZlrDDwuQ5DvInNkitvoI,3343
cachelib/simple.py,sha256=8UPp95_oc3bLeW_gzFUzdppIKV8hV_o_yQYoKb8gVMk,3422
cachelib/uwsgi.py,sha256=4DX3C9QGvB6mVcg1d7qpLIEkI6bccuq-8M6I_YbPicY,2563

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.42.0)
Root-Is-Purelib: true
Tag: py3-none-any

@ -0,0 +1,22 @@
from cachelib.base import BaseCache
from cachelib.base import NullCache
from cachelib.dynamodb import DynamoDbCache
from cachelib.file import FileSystemCache
from cachelib.memcached import MemcachedCache
from cachelib.mongodb import MongoDbCache
from cachelib.redis import RedisCache
from cachelib.simple import SimpleCache
from cachelib.uwsgi import UWSGICache
__all__ = [
"BaseCache",
"NullCache",
"SimpleCache",
"FileSystemCache",
"MemcachedCache",
"RedisCache",
"UWSGICache",
"DynamoDbCache",
"MongoDbCache",
]
__version__ = "0.13.0"

Some files were not shown because too many files have changed in this diff Show More