├── .bumpversion.cfg ├── .dockerignore ├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README ├── README.rst ├── docker ├── entrypoint-test.sh ├── entrypoint.sh └── pre_commit_init.sh ├── examples ├── hp6632b_serial.py ├── multimeter_tcp.py ├── prologix_usb.py └── tdklambda_tcp.py ├── poetry.lock ├── pyproject.toml ├── src └── scpi │ ├── __init__.py │ ├── devices │ ├── TDKLambdaZPlus.py │ ├── __init__.py │ ├── generic.py │ └── hp6632b.py │ ├── errors │ └── __init__.py │ ├── py.typed │ ├── scpi.py │ ├── transports │ ├── __init__.py │ ├── baseclass.py │ ├── gpib │ │ ├── __init__.py │ │ ├── base.py │ │ └── prologix.py │ ├── rs232.py │ └── tcp.py │ └── wrapper.py ├── tests ├── __init__.py ├── conftest.py └── test_scpi.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.5.1 3 | commit = False 4 | tag = False 5 | 6 | [bumpversion:file:pyproject.toml] 7 | search = version = "{current_version}" 8 | replace = version = "{new_version}" 9 | 10 | [bumpversion:file:src/scpi/__init__.py] 11 | search = __version__ = "{current_version}" 12 | replace = __version__ = "{new_version}" 13 | 14 | [bumpversion:file:tests/test_scpi.py] 15 | search = __version__ == "{current_version}" 16 | replace = __version__ == "{new_version}" 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # General good ideas to ignore 2 | .idea 3 | .git 4 | .cache 5 | .venv 6 | 7 | # docker build files (so just changing them does not invalidate *all* caches) 8 | .dockerignore 9 | Dockerfile 10 | 11 | # Byte-compiled / optimized / DLL files 12 | **/__pycache__/ 13 | **/*.py[cod] 14 | **/*$py.class 15 | 16 | # Distribution / packaging 17 | **/.Python 18 | **/build/ 19 | **/develop-eggs/ 20 | **/dist/ 21 | **/downloads/ 22 | **/eggs/ 23 | **/.eggs/ 24 | **/lib/ 25 | **/lib64/ 26 | **/parts/ 27 | **/sdist/ 28 | **/var/ 29 | **/wheels/ 30 | **/pip-wheel-metadata/ 31 | **/share/python-wheels/ 32 | **/*.egg-info/ 33 | **/.installed.cfg 34 | **/*.egg 35 | 36 | # Unit test / coverage reports 37 | **/htmlcov/ 38 | **/.tox/ 39 | **/.nox/ 40 | **/.coverage 41 | **/.coverage.* 42 | **/.cache 43 | **/nosetests.xml 44 | **/coverage.xml 45 | **/*.cover 46 | **/*.py,cover 47 | **/.hypothesis/ 48 | **/.pytest_cache/ 49 | 50 | # mypy 51 | **/.mypy_cache/ 52 | **/.dmypy.json 53 | **/dmypy.json 54 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE settings 2 | .idea 3 | 4 | # ci artefacts 5 | pytest*.xml 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | default_language_version: 4 | python: python3 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: no-commit-to-branch 10 | - id: check-executables-have-shebangs 11 | - id: check-ast 12 | - id: check-toml 13 | - id: trailing-whitespace 14 | - id: end-of-file-fixer 15 | - id: check-yaml 16 | - id: check-added-large-files 17 | - id: check-case-conflict 18 | - id: check-json 19 | - id: check-merge-conflict 20 | - id: check-symlinks 21 | - id: pretty-format-json 22 | args: 23 | - --autofix 24 | - repo: https://github.com/psf/black 25 | rev: 24.1.1 26 | hooks: 27 | - id: black 28 | language_version: python3 29 | - repo: https://github.com/pre-commit/mirrors-mypy 30 | rev: v1.8.0 31 | hooks: 32 | - id: mypy 33 | language: system 34 | args: ["--strict"] 35 | - repo: https://github.com/pycqa/pylint 36 | rev: v3.0.3 37 | hooks: 38 | - id: pylint 39 | language: system 40 | - repo: https://github.com/Lucas-C/pre-commit-hooks 41 | rev: v1.5.4 42 | hooks: 43 | - id: forbid-crlf 44 | - id: remove-crlf 45 | - id: forbid-tabs 46 | - id: remove-tabs 47 | - repo: https://github.com/PyCQA/bandit 48 | rev: 1.7.7 49 | hooks: 50 | - id: bandit 51 | args: ["--skip=B101"] 52 | - repo: https://github.com/Lucas-C/pre-commit-hooks-markup 53 | rev: v1.0.1 54 | hooks: 55 | - id: rst-linter 56 | - repo: https://github.com/Yelp/detect-secrets 57 | rev: v1.4.0 58 | hooks: 59 | - id: detect-secrets 60 | language: system 61 | exclude: "poetry.lock" 62 | # args: ['--baseline', '.secrets.baseline'] 63 | - repo: https://github.com/shellcheck-py/shellcheck-py 64 | rev: v0.9.0.6 65 | hooks: 66 | - id: shellcheck 67 | args: ["--external-sources"] 68 | - repo: https://github.com/python-poetry/poetry 69 | rev: '1.7.1' 70 | hooks: 71 | - id: poetry-check 72 | - id: poetry-lock 73 | args: ["--no-update"] 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.1.7-experimental 2 | ############################################# 3 | # Tox testsuite for multiple python version # 4 | ############################################# 5 | FROM advian/tox-base:alpine as tox 6 | ARG PYTHON_VERSIONS="3.8.7 3.9.1 3.7.9 3.6.12" 7 | ARG POETRY_VERSION="1.1.4" 8 | RUN for pyver in $PYTHON_VERSIONS; do pyenv install -s $pyver; done \ 9 | && pyenv global $PYTHON_VERSIONS \ 10 | && poetry self update $POETRY_VERSION || pip install -U poetry==$POETRY_VERSION \ 11 | && python -m pip install -U tox \ 12 | && apk add --no-cache \ 13 | git \ 14 | && poetry install \ 15 | && docker/pre_commit_init.sh \ 16 | && true 17 | 18 | ###################### 19 | # Base builder image # 20 | ###################### 21 | FROM python:3.7-alpine as builder_base 22 | 23 | ENV \ 24 | # locale 25 | LC_ALL=C.UTF-8 \ 26 | # python: 27 | PYTHONFAULTHANDLER=1 \ 28 | PYTHONUNBUFFERED=1 \ 29 | PYTHONHASHSEED=random \ 30 | # pip: 31 | PIP_NO_CACHE_DIR=off \ 32 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 33 | PIP_DEFAULT_TIMEOUT=100 \ 34 | # poetry: 35 | POETRY_VERSION=1.1.4 36 | 37 | 38 | RUN apk add --no-cache \ 39 | curl \ 40 | git \ 41 | bash \ 42 | build-base \ 43 | libffi-dev \ 44 | linux-headers \ 45 | openssl \ 46 | openssl-dev \ 47 | zeromq \ 48 | tini \ 49 | openssh-client \ 50 | cargo \ 51 | # githublab ssh 52 | && mkdir -p -m 0700 ~/.ssh && ssh-keyscan gitlab.com github.com | sort > ~/.ssh/known_hosts \ 53 | # Install poetry package manager their way (pypi package sometimes has issues) 54 | && curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 \ 55 | && echo 'source $HOME/.poetry/env' >>/root/.profile \ 56 | && source $HOME/.poetry/env \ 57 | # We're in a container, do not create useless virtualenvs 58 | && poetry config virtualenvs.create false \ 59 | && true 60 | 61 | SHELL ["/bin/bash", "-lc"] 62 | 63 | 64 | # Copy only requirements, to cache them in docker layer: 65 | WORKDIR /pysetup 66 | COPY ./poetry.lock ./pyproject.toml /pysetup/ 67 | # Install basic requirements (utilizing an internal docker wheelhouse if available) 68 | RUN --mount=type=ssh pip3 install wheel \ 69 | && poetry export -f requirements.txt --without-hashes -o /tmp/requirements.txt \ 70 | && pip3 wheel --wheel-dir=/tmp/wheelhouse --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 -r /tmp/requirements.txt \ 71 | && pip3 install --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 --find-links=/tmp/wheelhouse/ /tmp/wheelhouse/*.whl \ 72 | && true 73 | 74 | 75 | #################################### 76 | # Base stage for production builds # 77 | #################################### 78 | FROM builder_base as production_build 79 | # Copy entrypoint script 80 | COPY ./docker/entrypoint.sh /docker-entrypoint.sh 81 | # Only files needed by production setup 82 | COPY ./poetry.lock ./pyproject.toml ./README.rst ./src /app/ 83 | WORKDIR /app 84 | # Build the wheel package with poetry and add it to the wheelhouse 85 | RUN --mount=type=ssh poetry build -f wheel --no-interaction --no-ansi \ 86 | && cp dist/*.whl /tmp/wheelhouse \ 87 | && chmod a+x /docker-entrypoint.sh \ 88 | && true 89 | 90 | 91 | ######################### 92 | # Main production build # 93 | ######################### 94 | FROM python:3.7-alpine as production 95 | COPY --from=production_build /tmp/wheelhouse /tmp/wheelhouse 96 | COPY --from=production_build /docker-entrypoint.sh /docker-entrypoint.sh 97 | WORKDIR /app 98 | # Install system level deps for running the package (not devel versions for building wheels) 99 | # and install the wheels we built in the previous step. generate default config 100 | RUN --mount=type=ssh apk add --no-cache \ 101 | bash \ 102 | tini \ 103 | && chmod a+x /docker-entrypoint.sh \ 104 | && pip3 install --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 --find-links=/tmp/wheelhouse/ /tmp/wheelhouse/scpi-*.whl \ 105 | && rm -rf /tmp/wheelhouse/ \ 106 | # Do whatever else you need to 107 | && true 108 | ENTRYPOINT ["/sbin/tini", "--", "/docker-entrypoint.sh"] 109 | 110 | 111 | ##################################### 112 | # Base stage for development builds # 113 | ##################################### 114 | FROM builder_base as devel_build 115 | # Install deps 116 | WORKDIR /pysetup 117 | RUN --mount=type=ssh poetry install --no-interaction --no-ansi \ 118 | && true 119 | 120 | 121 | #0############ 122 | # Run tests # 123 | ############# 124 | FROM devel_build as test 125 | COPY . /app 126 | WORKDIR /app 127 | ENTRYPOINT ["/sbin/tini", "--", "docker/entrypoint-test.sh"] 128 | # Re run install to get the service itself installed 129 | RUN --mount=type=ssh poetry install --no-interaction --no-ansi \ 130 | && docker/pre_commit_init.sh \ 131 | && true 132 | 133 | 134 | ########### 135 | # Hacking # 136 | ########### 137 | FROM devel_build as devel_shell 138 | # Copy everything to the image 139 | COPY . /app 140 | WORKDIR /app 141 | RUN apk add --no-cache zsh \ 142 | && sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ 143 | && echo "source /root/.profile" >>/root/.zshrc \ 144 | && pip3 install git-up \ 145 | && true 146 | ENTRYPOINT ["/bin/zsh", "-l"] 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Pure-python SCPI interface 2 | Copyright (C) 2014 Eero af Heurlin 3 | 4 | This library is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU Lesser General Public 6 | License as published by the Free Software Foundation; either 7 | version 2.1 of the License, or (at your option) any later version. 8 | 9 | This library is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | Lesser General Public License for more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public 15 | License along with this library; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | scpi 3 | ==== 4 | 5 | New asyncio_ version. Only for Python 3.6 and above 6 | 7 | Since all the other wrappers either require VISA binary or are not generic (and do not implement the device I need) 8 | 9 | Basic idea here is to make transport-independent command sender/parser and a device baseclass that implements the common SCPI commands 10 | 11 | A device specific implementation can then add the device-specific commands. 12 | 13 | Pro tip for thos wishing to work on the code https://python-poetry.org/ 14 | 15 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 16 | 17 | 18 | ## Usage 19 | 20 | Install the package to your virtualenv with poetry or from pip 21 | 22 | - Instatiate a transport (for GPIB you will need `GPIBDeviceTransport` to be able to use the device helper class) 23 | - Instatiate `SCPIProtocol` with the transport (optional, see below) 24 | - Instantiate `SCPIDevice` with the protocol (or as a shorthand: with the transport directly) 25 | - Use the asyncio eventloop to run the device methods (all of which are coroutines) 26 | 27 | Or if you're just playing around in the REPL use `AIOWrapper` to hide the eventloop handling 28 | for traditional non-concurrent approach. 29 | 30 | See the examples directory for more. 31 | 32 | TODO 33 | ---- 34 | 35 | Check Carrier-Detect for RS232 transport 36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 37 | 38 | in the RS232 transport check getCD to make sure the device is present before doing anything. 39 | CTS can also be checked even if hw flow control is not in use. 40 | 41 | Basically wait for it for X seconds and abort if not found 42 | -------------------------------------------------------------------------------- /docker/entrypoint-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -l 2 | set -e 3 | if [ "$#" -eq 0 ]; then 4 | # Kill cache, pytest complains about it if running local and docker tests in mapped volume 5 | find tests -type d -name '__pycache__' -print0 | xargs -0 rm -rf {} 6 | # Make sure the service itself is installed 7 | poetry install 8 | # Make sure pre-commit checks were not missed because reasons 9 | pre-commit run --all-files 10 | # Then run the tests 11 | pytest --junitxml=pytest.xml tests/ 12 | mypy --strict src tests 13 | bandit -r src 14 | else 15 | exec "$@" 16 | fi 17 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -l 2 | set -e 3 | if [ "$#" -eq 0 ]; then 4 | # TODO: Put your actual program start here 5 | exec true 6 | else 7 | exec "$@" 8 | fi 9 | -------------------------------------------------------------------------------- /docker/pre_commit_init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -l 2 | if [ ! -d .git ] 3 | then 4 | git init 5 | git checkout -b precommit_init 6 | git add . 7 | fi 8 | set -e 9 | poetry run pre-commit install 10 | poetry run pre-commit run --all-files 11 | -------------------------------------------------------------------------------- /examples/hp6632b_serial.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example/test script for using the HP 6632B power supply via serial interface""" 3 | import atexit 4 | import os 5 | import sys 6 | 7 | from scpi.devices import hp6632b 8 | from scpi.wrapper import AIOWrapper 9 | 10 | # pylint: disable=R0801 11 | 12 | if __name__ == "__main__": 13 | if len(sys.argv) < 2: 14 | print(f"run with python -i {__file__} /dev/ttyUSB0") 15 | sys.exit(1) 16 | # Then put to interactive mode 17 | os.environ["PYTHONINSPECT"] = "1" 18 | aiodev = hp6632b.rs232(sys.argv[1], rtscts=True, baudrate=9600) 19 | dev = AIOWrapper(aiodev) 20 | 21 | atexit.register(dev.quit) 22 | 23 | print(dev.identify()) 24 | -------------------------------------------------------------------------------- /examples/multimeter_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example/test script for using the Generic multimeter via TCP""" 3 | import atexit 4 | import os 5 | import sys 6 | 7 | from scpi.transports import TCPTransport 8 | from scpi.devices.generic import MultiMeter 9 | from scpi.wrapper import AIOWrapper 10 | 11 | # pylint: disable=R0801 12 | 13 | if __name__ == "__main__": 14 | if len(sys.argv) < 3: 15 | print(f"run with python -i {__file__} IP PORT") 16 | sys.exit(1) 17 | # Then put to interactive mode 18 | os.environ["PYTHONINSPECT"] = "1" 19 | xport = TCPTransport(ipaddr=sys.argv[1], port=int(sys.argv[2])) 20 | aiodev = MultiMeter(xport) 21 | dev = AIOWrapper(aiodev) 22 | 23 | atexit.register(dev.quit) 24 | 25 | print(dev.identify()) 26 | -------------------------------------------------------------------------------- /examples/prologix_usb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example/test script for use the with Prologix USB GPIB interface""" 3 | import atexit 4 | import os 5 | import sys 6 | 7 | from scpi import SCPIDevice 8 | from scpi.transports.gpib import prologix 9 | from scpi.wrapper import AIOWrapper 10 | from scpi.devices.hp6632b import HP6632B 11 | from scpi.devices.generic import MultiMeter 12 | 13 | # pylint: disable=R0801 14 | 15 | if __name__ == "__main__": 16 | if len(sys.argv) < 2: 17 | print(f"run with python -i {__file__} /dev/ttyUSB0") 18 | sys.exit(1) 19 | # Then put to interactive mode 20 | os.environ["PYTHONINSPECT"] = "1" 21 | # Get the low-level GPIB transport 22 | aiogpib = prologix.get(sys.argv[1]) 23 | # And the mapper that handlers asyncio transparently 24 | gpib = AIOWrapper(aiogpib) 25 | atexit.register(gpib.quit) 26 | 27 | print("*** Scanning bus for devices ***") 28 | devlist = gpib.scan_devices() 29 | devdict = {} 30 | for addr, idstr in devlist: 31 | man, model, _, _ = idstr.split(",") 32 | # Get device specific transport instance 33 | dtransport = aiogpib.get_device_transport(addr) 34 | # Get the device class with the transport 35 | aiodev = SCPIDevice(dtransport) 36 | if man == "HEWLETT-PACKARD": 37 | if model == "6632B": 38 | aiodev = HP6632B(dtransport) 39 | if model == "34401A": 40 | aiodev = MultiMeter(dtransport) 41 | # And get the mapper that handles asyncio transparently 42 | devdict[addr] = AIOWrapper(aiodev) 43 | print("Added {:s} as devdict[{:d}]".format(idstr, addr)) 44 | -------------------------------------------------------------------------------- /examples/tdklambda_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Created on febrary 21 2020. 4 | 5 | @author: qmor 6 | """ 7 | 8 | import atexit 9 | import os 10 | import time 11 | 12 | from scpi.devices import TDKLambdaZPlus 13 | from scpi.wrapper import AIOWrapper 14 | 15 | if __name__ == "__main__": 16 | # Then put to interactive mode 17 | os.environ["PYTHONINSPECT"] = "1" 18 | aiodev = TDKLambdaZPlus.tcp("192.168.3.34", 8003) 19 | dev = AIOWrapper(aiodev) 20 | atexit.register(dev.quit) 21 | dev.identify() 22 | while True: 23 | dev.query_voltage() 24 | dev.query_current() 25 | dev.query_output() 26 | time.sleep(1) 27 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "astroid" 5 | version = "3.3.8" 6 | description = "An abstract syntax tree for Python with inference support." 7 | optional = false 8 | python-versions = ">=3.9.0" 9 | files = [ 10 | {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, 11 | {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 16 | 17 | [[package]] 18 | name = "bandit" 19 | version = "1.8.0" 20 | description = "Security oriented static analyser for python code." 21 | optional = false 22 | python-versions = ">=3.9" 23 | files = [ 24 | {file = "bandit-1.8.0-py3-none-any.whl", hash = "sha256:b1a61d829c0968aed625381e426aa378904b996529d048f8d908fa28f6b13e38"}, 25 | {file = "bandit-1.8.0.tar.gz", hash = "sha256:b5bfe55a095abd9fe20099178a7c6c060f844bfd4fe4c76d28e35e4c52b9d31e"}, 26 | ] 27 | 28 | [package.dependencies] 29 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 30 | PyYAML = ">=5.3.1" 31 | rich = "*" 32 | stevedore = ">=1.20.0" 33 | 34 | [package.extras] 35 | baseline = ["GitPython (>=3.1.30)"] 36 | sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] 37 | test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] 38 | toml = ["tomli (>=1.1.0)"] 39 | yaml = ["PyYAML"] 40 | 41 | [[package]] 42 | name = "black" 43 | version = "24.10.0" 44 | description = "The uncompromising code formatter." 45 | optional = false 46 | python-versions = ">=3.9" 47 | files = [ 48 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 49 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 50 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 51 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 52 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 53 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 54 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 55 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 56 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 57 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 58 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 59 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 60 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 61 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 62 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 63 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 64 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 65 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 66 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 67 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 68 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 69 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 70 | ] 71 | 72 | [package.dependencies] 73 | click = ">=8.0.0" 74 | mypy-extensions = ">=0.4.3" 75 | packaging = ">=22.0" 76 | pathspec = ">=0.9.0" 77 | platformdirs = ">=2" 78 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 79 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 80 | 81 | [package.extras] 82 | colorama = ["colorama (>=0.4.3)"] 83 | d = ["aiohttp (>=3.10)"] 84 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 85 | uvloop = ["uvloop (>=0.15.2)"] 86 | 87 | [[package]] 88 | name = "bump2version" 89 | version = "1.0.1" 90 | description = "Version-bump your software with a single command!" 91 | optional = false 92 | python-versions = ">=3.5" 93 | files = [ 94 | {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, 95 | {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, 96 | ] 97 | 98 | [[package]] 99 | name = "certifi" 100 | version = "2024.12.14" 101 | description = "Python package for providing Mozilla's CA Bundle." 102 | optional = false 103 | python-versions = ">=3.6" 104 | files = [ 105 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 106 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 107 | ] 108 | 109 | [[package]] 110 | name = "cfgv" 111 | version = "3.4.0" 112 | description = "Validate configuration and produce human readable error messages." 113 | optional = false 114 | python-versions = ">=3.8" 115 | files = [ 116 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 117 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 118 | ] 119 | 120 | [[package]] 121 | name = "charset-normalizer" 122 | version = "3.4.1" 123 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 124 | optional = false 125 | python-versions = ">=3.7" 126 | files = [ 127 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 128 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 129 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 130 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 131 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 132 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 133 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 134 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 135 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 136 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 137 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 138 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 139 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 140 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 141 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 142 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 143 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 144 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 145 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 146 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 147 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 148 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 149 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 150 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 151 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 152 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 153 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 154 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 155 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 156 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 157 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 158 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 159 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 160 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 161 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 162 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 163 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 164 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 165 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 166 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 167 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 168 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 169 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 170 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 171 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 172 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 173 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 174 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 175 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 176 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 177 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 178 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 179 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 180 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 181 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 182 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 183 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 184 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 185 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 186 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 187 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 188 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 189 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 190 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 191 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 192 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 193 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 194 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 195 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 196 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 197 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 198 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 199 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 200 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 201 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 202 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 203 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 204 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 205 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 206 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 207 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 208 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 209 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 210 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 211 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 212 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 213 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 214 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 215 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 216 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 217 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 218 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 219 | ] 220 | 221 | [[package]] 222 | name = "click" 223 | version = "8.1.8" 224 | description = "Composable command line interface toolkit" 225 | optional = false 226 | python-versions = ">=3.7" 227 | files = [ 228 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 229 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 230 | ] 231 | 232 | [package.dependencies] 233 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 234 | 235 | [[package]] 236 | name = "colorama" 237 | version = "0.4.6" 238 | description = "Cross-platform colored terminal text." 239 | optional = false 240 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 241 | files = [ 242 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 243 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 244 | ] 245 | 246 | [[package]] 247 | name = "coverage" 248 | version = "7.6.10" 249 | description = "Code coverage measurement for Python" 250 | optional = false 251 | python-versions = ">=3.9" 252 | files = [ 253 | {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, 254 | {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, 255 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, 256 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, 257 | {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, 258 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, 259 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, 260 | {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, 261 | {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, 262 | {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, 263 | {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, 264 | {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, 265 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, 266 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, 267 | {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, 268 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, 269 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, 270 | {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, 271 | {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, 272 | {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, 273 | {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, 274 | {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, 275 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, 276 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, 277 | {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, 278 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, 279 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, 280 | {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, 281 | {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, 282 | {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, 283 | {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, 284 | {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, 285 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, 286 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, 287 | {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, 288 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, 289 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, 290 | {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, 291 | {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, 292 | {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, 293 | {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, 294 | {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, 295 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, 296 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, 297 | {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, 298 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, 299 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, 300 | {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, 301 | {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, 302 | {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, 303 | {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, 304 | {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, 305 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, 306 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, 307 | {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, 308 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, 309 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, 310 | {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, 311 | {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, 312 | {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, 313 | {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, 314 | {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, 315 | ] 316 | 317 | [package.dependencies] 318 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 319 | 320 | [package.extras] 321 | toml = ["tomli"] 322 | 323 | [[package]] 324 | name = "detect-secrets" 325 | version = "1.5.0" 326 | description = "Tool for detecting secrets in the codebase" 327 | optional = false 328 | python-versions = "*" 329 | files = [ 330 | {file = "detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060"}, 331 | {file = "detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a"}, 332 | ] 333 | 334 | [package.dependencies] 335 | pyyaml = "*" 336 | requests = "*" 337 | 338 | [package.extras] 339 | gibberish = ["gibberish-detector"] 340 | word-list = ["pyahocorasick"] 341 | 342 | [[package]] 343 | name = "dill" 344 | version = "0.3.9" 345 | description = "serialize all of Python" 346 | optional = false 347 | python-versions = ">=3.8" 348 | files = [ 349 | {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, 350 | {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, 351 | ] 352 | 353 | [package.extras] 354 | graph = ["objgraph (>=1.7.2)"] 355 | profile = ["gprof2dot (>=2022.7.29)"] 356 | 357 | [[package]] 358 | name = "distlib" 359 | version = "0.3.9" 360 | description = "Distribution utilities" 361 | optional = false 362 | python-versions = "*" 363 | files = [ 364 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 365 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 366 | ] 367 | 368 | [[package]] 369 | name = "exceptiongroup" 370 | version = "1.2.2" 371 | description = "Backport of PEP 654 (exception groups)" 372 | optional = false 373 | python-versions = ">=3.7" 374 | files = [ 375 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 376 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 377 | ] 378 | 379 | [package.extras] 380 | test = ["pytest (>=6)"] 381 | 382 | [[package]] 383 | name = "filelock" 384 | version = "3.16.1" 385 | description = "A platform independent file lock." 386 | optional = false 387 | python-versions = ">=3.8" 388 | files = [ 389 | {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, 390 | {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, 391 | ] 392 | 393 | [package.extras] 394 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 395 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] 396 | typing = ["typing-extensions (>=4.12.2)"] 397 | 398 | [[package]] 399 | name = "identify" 400 | version = "2.6.5" 401 | description = "File identification library for Python" 402 | optional = false 403 | python-versions = ">=3.9" 404 | files = [ 405 | {file = "identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566"}, 406 | {file = "identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc"}, 407 | ] 408 | 409 | [package.extras] 410 | license = ["ukkonen"] 411 | 412 | [[package]] 413 | name = "idna" 414 | version = "3.10" 415 | description = "Internationalized Domain Names in Applications (IDNA)" 416 | optional = false 417 | python-versions = ">=3.6" 418 | files = [ 419 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 420 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 421 | ] 422 | 423 | [package.extras] 424 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 425 | 426 | [[package]] 427 | name = "iniconfig" 428 | version = "2.0.0" 429 | description = "brain-dead simple config-ini parsing" 430 | optional = false 431 | python-versions = ">=3.7" 432 | files = [ 433 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 434 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 435 | ] 436 | 437 | [[package]] 438 | name = "isort" 439 | version = "5.13.2" 440 | description = "A Python utility / library to sort Python imports." 441 | optional = false 442 | python-versions = ">=3.8.0" 443 | files = [ 444 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 445 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 446 | ] 447 | 448 | [package.extras] 449 | colors = ["colorama (>=0.4.6)"] 450 | 451 | [[package]] 452 | name = "markdown-it-py" 453 | version = "3.0.0" 454 | description = "Python port of markdown-it. Markdown parsing, done right!" 455 | optional = false 456 | python-versions = ">=3.8" 457 | files = [ 458 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 459 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 460 | ] 461 | 462 | [package.dependencies] 463 | mdurl = ">=0.1,<1.0" 464 | 465 | [package.extras] 466 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 467 | code-style = ["pre-commit (>=3.0,<4.0)"] 468 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 469 | linkify = ["linkify-it-py (>=1,<3)"] 470 | plugins = ["mdit-py-plugins"] 471 | profiling = ["gprof2dot"] 472 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 473 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 474 | 475 | [[package]] 476 | name = "mccabe" 477 | version = "0.7.0" 478 | description = "McCabe checker, plugin for flake8" 479 | optional = false 480 | python-versions = ">=3.6" 481 | files = [ 482 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 483 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 484 | ] 485 | 486 | [[package]] 487 | name = "mdurl" 488 | version = "0.1.2" 489 | description = "Markdown URL utilities" 490 | optional = false 491 | python-versions = ">=3.7" 492 | files = [ 493 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 494 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 495 | ] 496 | 497 | [[package]] 498 | name = "mypy" 499 | version = "1.14.1" 500 | description = "Optional static typing for Python" 501 | optional = false 502 | python-versions = ">=3.8" 503 | files = [ 504 | {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, 505 | {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, 506 | {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, 507 | {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, 508 | {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, 509 | {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, 510 | {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, 511 | {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, 512 | {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, 513 | {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, 514 | {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, 515 | {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, 516 | {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, 517 | {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, 518 | {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, 519 | {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, 520 | {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, 521 | {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, 522 | {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, 523 | {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, 524 | {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, 525 | {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, 526 | {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, 527 | {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, 528 | {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, 529 | {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, 530 | {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, 531 | {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, 532 | {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, 533 | {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, 534 | {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, 535 | {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, 536 | {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, 537 | {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, 538 | {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, 539 | {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, 540 | {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, 541 | {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, 542 | ] 543 | 544 | [package.dependencies] 545 | mypy_extensions = ">=1.0.0" 546 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 547 | typing_extensions = ">=4.6.0" 548 | 549 | [package.extras] 550 | dmypy = ["psutil (>=4.0)"] 551 | faster-cache = ["orjson"] 552 | install-types = ["pip"] 553 | mypyc = ["setuptools (>=50)"] 554 | reports = ["lxml"] 555 | 556 | [[package]] 557 | name = "mypy-extensions" 558 | version = "1.0.0" 559 | description = "Type system extensions for programs checked with the mypy type checker." 560 | optional = false 561 | python-versions = ">=3.5" 562 | files = [ 563 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 564 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 565 | ] 566 | 567 | [[package]] 568 | name = "nodeenv" 569 | version = "1.9.1" 570 | description = "Node.js virtual environment builder" 571 | optional = false 572 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 573 | files = [ 574 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 575 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 576 | ] 577 | 578 | [[package]] 579 | name = "packaging" 580 | version = "24.2" 581 | description = "Core utilities for Python packages" 582 | optional = false 583 | python-versions = ">=3.8" 584 | files = [ 585 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 586 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 587 | ] 588 | 589 | [[package]] 590 | name = "pathspec" 591 | version = "0.12.1" 592 | description = "Utility library for gitignore style pattern matching of file paths." 593 | optional = false 594 | python-versions = ">=3.8" 595 | files = [ 596 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 597 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 598 | ] 599 | 600 | [[package]] 601 | name = "pbr" 602 | version = "6.1.0" 603 | description = "Python Build Reasonableness" 604 | optional = false 605 | python-versions = ">=2.6" 606 | files = [ 607 | {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, 608 | {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, 609 | ] 610 | 611 | [[package]] 612 | name = "platformdirs" 613 | version = "4.3.6" 614 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 615 | optional = false 616 | python-versions = ">=3.8" 617 | files = [ 618 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 619 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 620 | ] 621 | 622 | [package.extras] 623 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 624 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 625 | type = ["mypy (>=1.11.2)"] 626 | 627 | [[package]] 628 | name = "pluggy" 629 | version = "1.5.0" 630 | description = "plugin and hook calling mechanisms for python" 631 | optional = false 632 | python-versions = ">=3.8" 633 | files = [ 634 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 635 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 636 | ] 637 | 638 | [package.extras] 639 | dev = ["pre-commit", "tox"] 640 | testing = ["pytest", "pytest-benchmark"] 641 | 642 | [[package]] 643 | name = "pre-commit" 644 | version = "3.8.0" 645 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 646 | optional = false 647 | python-versions = ">=3.9" 648 | files = [ 649 | {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, 650 | {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, 651 | ] 652 | 653 | [package.dependencies] 654 | cfgv = ">=2.0.0" 655 | identify = ">=1.0.0" 656 | nodeenv = ">=0.11.1" 657 | pyyaml = ">=5.1" 658 | virtualenv = ">=20.10.0" 659 | 660 | [[package]] 661 | name = "pygments" 662 | version = "2.19.1" 663 | description = "Pygments is a syntax highlighting package written in Python." 664 | optional = false 665 | python-versions = ">=3.8" 666 | files = [ 667 | {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, 668 | {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, 669 | ] 670 | 671 | [package.extras] 672 | windows-terminal = ["colorama (>=0.4.6)"] 673 | 674 | [[package]] 675 | name = "pylint" 676 | version = "3.3.3" 677 | description = "python code static checker" 678 | optional = false 679 | python-versions = ">=3.9.0" 680 | files = [ 681 | {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, 682 | {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, 683 | ] 684 | 685 | [package.dependencies] 686 | astroid = ">=3.3.8,<=3.4.0-dev0" 687 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 688 | dill = [ 689 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 690 | {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, 691 | {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, 692 | ] 693 | isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" 694 | mccabe = ">=0.6,<0.8" 695 | platformdirs = ">=2.2.0" 696 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 697 | tomlkit = ">=0.10.1" 698 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 699 | 700 | [package.extras] 701 | spelling = ["pyenchant (>=3.2,<4.0)"] 702 | testutils = ["gitpython (>3)"] 703 | 704 | [[package]] 705 | name = "pyserial" 706 | version = "3.5" 707 | description = "Python Serial Port Extension" 708 | optional = false 709 | python-versions = "*" 710 | files = [ 711 | {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, 712 | {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, 713 | ] 714 | 715 | [package.extras] 716 | cp2110 = ["hidapi"] 717 | 718 | [[package]] 719 | name = "pytest" 720 | version = "8.3.4" 721 | description = "pytest: simple powerful testing with Python" 722 | optional = false 723 | python-versions = ">=3.8" 724 | files = [ 725 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 726 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 727 | ] 728 | 729 | [package.dependencies] 730 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 731 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 732 | iniconfig = "*" 733 | packaging = "*" 734 | pluggy = ">=1.5,<2" 735 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 736 | 737 | [package.extras] 738 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 739 | 740 | [[package]] 741 | name = "pytest-asyncio" 742 | version = "0.25.1" 743 | description = "Pytest support for asyncio" 744 | optional = false 745 | python-versions = ">=3.9" 746 | files = [ 747 | {file = "pytest_asyncio-0.25.1-py3-none-any.whl", hash = "sha256:c84878849ec63ff2ca509423616e071ef9cd8cc93c053aa33b5b8fb70a990671"}, 748 | {file = "pytest_asyncio-0.25.1.tar.gz", hash = "sha256:79be8a72384b0c917677e00daa711e07db15259f4d23203c59012bcd989d4aee"}, 749 | ] 750 | 751 | [package.dependencies] 752 | pytest = ">=8.2,<9" 753 | 754 | [package.extras] 755 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] 756 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 757 | 758 | [[package]] 759 | name = "pytest-cov" 760 | version = "4.1.0" 761 | description = "Pytest plugin for measuring coverage." 762 | optional = false 763 | python-versions = ">=3.7" 764 | files = [ 765 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 766 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 767 | ] 768 | 769 | [package.dependencies] 770 | coverage = {version = ">=5.2.1", extras = ["toml"]} 771 | pytest = ">=4.6" 772 | 773 | [package.extras] 774 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 775 | 776 | [[package]] 777 | name = "pyyaml" 778 | version = "6.0.2" 779 | description = "YAML parser and emitter for Python" 780 | optional = false 781 | python-versions = ">=3.8" 782 | files = [ 783 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 784 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 785 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 786 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 787 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 788 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 789 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 790 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 791 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 792 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 793 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 794 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 795 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 796 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 797 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 798 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 799 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 800 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 801 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 802 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 803 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 804 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 805 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 806 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 807 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 808 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 809 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 810 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 811 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 812 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 813 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 814 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 815 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 816 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 817 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 818 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 819 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 820 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 821 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 822 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 823 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 824 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 825 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 826 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 827 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 828 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 829 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 830 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 831 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 832 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 833 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 834 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 835 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 836 | ] 837 | 838 | [[package]] 839 | name = "requests" 840 | version = "2.32.3" 841 | description = "Python HTTP for Humans." 842 | optional = false 843 | python-versions = ">=3.8" 844 | files = [ 845 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 846 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 847 | ] 848 | 849 | [package.dependencies] 850 | certifi = ">=2017.4.17" 851 | charset-normalizer = ">=2,<4" 852 | idna = ">=2.5,<4" 853 | urllib3 = ">=1.21.1,<3" 854 | 855 | [package.extras] 856 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 857 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 858 | 859 | [[package]] 860 | name = "rich" 861 | version = "13.9.4" 862 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 863 | optional = false 864 | python-versions = ">=3.8.0" 865 | files = [ 866 | {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, 867 | {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, 868 | ] 869 | 870 | [package.dependencies] 871 | markdown-it-py = ">=2.2.0" 872 | pygments = ">=2.13.0,<3.0.0" 873 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} 874 | 875 | [package.extras] 876 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 877 | 878 | [[package]] 879 | name = "stevedore" 880 | version = "5.4.0" 881 | description = "Manage dynamic plugins for Python applications" 882 | optional = false 883 | python-versions = ">=3.9" 884 | files = [ 885 | {file = "stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857"}, 886 | {file = "stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d"}, 887 | ] 888 | 889 | [package.dependencies] 890 | pbr = ">=2.0.0" 891 | 892 | [[package]] 893 | name = "tomli" 894 | version = "2.2.1" 895 | description = "A lil' TOML parser" 896 | optional = false 897 | python-versions = ">=3.8" 898 | files = [ 899 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 900 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 901 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 902 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 903 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 904 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 905 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 906 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 907 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 908 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 909 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 910 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 911 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 912 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 913 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 914 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 915 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 916 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 917 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 918 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 919 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 920 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 921 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 922 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 923 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 924 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 925 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 926 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 927 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 928 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 929 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 930 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 931 | ] 932 | 933 | [[package]] 934 | name = "tomlkit" 935 | version = "0.13.2" 936 | description = "Style preserving TOML library" 937 | optional = false 938 | python-versions = ">=3.8" 939 | files = [ 940 | {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, 941 | {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, 942 | ] 943 | 944 | [[package]] 945 | name = "typing-extensions" 946 | version = "4.12.2" 947 | description = "Backported and Experimental Type Hints for Python 3.8+" 948 | optional = false 949 | python-versions = ">=3.8" 950 | files = [ 951 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 952 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 953 | ] 954 | 955 | [[package]] 956 | name = "urllib3" 957 | version = "2.3.0" 958 | description = "HTTP library with thread-safe connection pooling, file post, and more." 959 | optional = false 960 | python-versions = ">=3.9" 961 | files = [ 962 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 963 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 964 | ] 965 | 966 | [package.extras] 967 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 968 | h2 = ["h2 (>=4,<5)"] 969 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 970 | zstd = ["zstandard (>=0.18.0)"] 971 | 972 | [[package]] 973 | name = "virtualenv" 974 | version = "20.28.1" 975 | description = "Virtual Python Environment builder" 976 | optional = false 977 | python-versions = ">=3.8" 978 | files = [ 979 | {file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, 980 | {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, 981 | ] 982 | 983 | [package.dependencies] 984 | distlib = ">=0.3.7,<1" 985 | filelock = ">=3.12.2,<4" 986 | platformdirs = ">=3.9.1,<5" 987 | 988 | [package.extras] 989 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 990 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 991 | 992 | [metadata] 993 | lock-version = "2.0" 994 | python-versions = "^3.9" 995 | content-hash = "f806d11c0b3682a78f69850e0e12c0f72bcff57ffd9a4135927170e7dd409ef1" 996 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "scpi" 3 | version = "2.5.1" 4 | description = "Basic idea here is to make transport-independent command sender/parser and a device baseclass that implements the common SCPI commands" 5 | authors = ["Eero af Heurlin "] 6 | homepage = "https://github.com/rambo/python-scpi/" 7 | repository = "https://github.com/rambo/python-scpi/" 8 | license = "LGPL" 9 | readme = "README.rst" 10 | 11 | [tool.black] 12 | line-length = 120 13 | target-version = ['py37'] 14 | exclude = ''' 15 | ( 16 | /( 17 | \.eggs # exclude a few common directories in the 18 | | \.git # root of the project 19 | | \.hg 20 | | \.mypy_cache 21 | | \.tox 22 | | \.venv 23 | | _build 24 | | buck-out 25 | | build 26 | | dist 27 | )/ 28 | | __pycache__ 29 | ) 30 | ''' 31 | 32 | [tool.pytest.ini_options] 33 | junit_family="xunit2" 34 | addopts="--cov=scpi --cov-branch" 35 | 36 | [tool.pylint.format] 37 | max-line-length = 120 38 | 39 | [tool.pylint.messages_control] 40 | disable=["fixme", "W1202", "C0209"] 41 | 42 | [tool.coverage.run] 43 | omit = ["tests/*"] 44 | branch = true 45 | 46 | [tool.poetry.dependencies] 47 | python = "^3.9" 48 | pyserial = "^3.4" 49 | 50 | [tool.poetry.dev-dependencies] 51 | pytest = "^8.0" 52 | coverage = "^7.4" 53 | pytest-cov = "^4.1" 54 | pylint = "^3.0" 55 | black = "^24.1" 56 | bandit = "^1.7" 57 | mypy = "^1.8" 58 | pre-commit = "^3.6" 59 | pytest-asyncio = ">=0.21,<1.0" # caret behaviour on 0.x is to lock to 0.x.* 60 | bump2version = "^1.0" 61 | detect-secrets = "^1.4" 62 | 63 | [build-system] 64 | requires = ["poetry-core>=1.0.0"] 65 | build-backend = "poetry.core.masonry.api" 66 | -------------------------------------------------------------------------------- /src/scpi/__init__.py: -------------------------------------------------------------------------------- 1 | """SCPI module, the scpi class implements the base command set, devices may extend it. 2 | transports are separate from devices (so you can use for example hp6632b with either serial port or GPIB)""" 3 | 4 | __version__ = "2.5.1" # NOTE Use `bump2version --config-file patch` to bump versions correctly 5 | from .scpi import SCPIProtocol, SCPIDevice 6 | from .errors import CommandError 7 | 8 | __all__ = ["SCPIProtocol", "CommandError", "SCPIDevice"] 9 | -------------------------------------------------------------------------------- /src/scpi/devices/TDKLambdaZPlus.py: -------------------------------------------------------------------------------- 1 | """TDK Lambda power supplies""" 2 | 3 | # pylint: disable=C0103 4 | from typing import Optional, Union, Any 5 | from dataclasses import dataclass, field 6 | import decimal 7 | import logging 8 | 9 | import serial as pyserial # type: ignore 10 | 11 | from ..scpi import SCPIDevice, SCPIProtocol 12 | from ..transports.rs232 import RS232Transport 13 | from ..transports.tcp import get as get_tcp 14 | from .generic import PowerSupply 15 | 16 | StrIntCombo = Union[str, int] 17 | LOGGER = logging.getLogger(__name__) 18 | 19 | 20 | class TDKSCPI(SCPIDevice): 21 | """Baseclass for TDK SCPI devices""" 22 | 23 | async def set_power_on_status_clear(self, setting: StrIntCombo) -> None: 24 | """ 25 | Set the Power-On Status Clear setting. 26 | * ON/1/True - This choice enables the power-on clearing of the listed registers 27 | * OFF/0/false - This choice disable the clearing of the listed registers and they retain 28 | their status when a power-on condition occurs 29 | """ 30 | setting = str(setting).upper() 31 | if setting in ("1", "ON", "TRUE"): 32 | setting = "1" 33 | elif setting in ("0", "OFF", "FALSE"): 34 | setting = "0" 35 | else: 36 | raise ValueError 37 | await super().set_power_on_status_clear(setting) 38 | 39 | async def restore_state(self, state: int) -> None: 40 | """ 41 | Restores the power supply to a state previously stored in memory by *SAV command. 42 | """ 43 | state = int(state) 44 | if state not in (1, 2, 3, 4): 45 | raise ValueError("invalid state") 46 | 47 | await super().restore_state(state) 48 | 49 | async def save_state(self, state: int) -> None: 50 | """ 51 | The SAV command saves all applied configuration settings. 52 | """ 53 | state = int(state) 54 | if state not in (1, 2, 3, 4): 55 | raise ValueError("Invalid state") 56 | 57 | await super().save_state(state) 58 | 59 | async def power_on_state(self, setting: StrIntCombo) -> None: 60 | """ 61 | Set the power-on behavior of the system 62 | * 1 - AUTO - The power supply output will return to its previous value 63 | when the latching fault condition is removed or to the 64 | stored value after AC recycle. 65 | * 0 - SAFE - The power supply output will remain Off after the fault 66 | condition is removed or after AC recycle. 67 | """ 68 | setting = str(setting).upper() 69 | if setting in ("1", "ON", "TRUE"): 70 | setting = "1" 71 | elif setting in ("0", "OFF", "FALSE"): 72 | setting = "0" 73 | else: 74 | raise ValueError 75 | await super().power_on_state(setting) 76 | 77 | 78 | @dataclass 79 | class TDKLambdaZplus(PowerSupply, TDKSCPI): 80 | """TDK Lambda Z+ power supply""" 81 | 82 | voltage_limit: float = field(default=20.0) 83 | current_limit: float = field(default=10.0) 84 | 85 | async def measure_current(self, extra_params: str = "") -> decimal.Decimal: 86 | """ 87 | Returns the actual output current in amps. 88 | 89 | extra_params: String to append to the command. The only valid command 90 | for this device is ":DC" 91 | """ 92 | 93 | resp = await self.ask(f"MEAS:CURR{extra_params}?") 94 | return decimal.Decimal(resp) 95 | 96 | async def measure_voltage(self, extra_params: str = "") -> decimal.Decimal: 97 | """ 98 | Returns the actual output voltage in volts. 99 | 100 | extra_params: String to append to the command. The only valid command 101 | for this device is ":DC" 102 | """ 103 | 104 | resp = await self.ask(f"MEAS:VOLT{extra_params}?") 105 | return decimal.Decimal(resp) 106 | 107 | async def measure_power(self, extra_params: str = "") -> decimal.Decimal: 108 | """ 109 | Returns the actual output power in watts. 110 | 111 | extra_params: String to append to the command. The only valid command 112 | for this device is ":DC" 113 | """ 114 | 115 | resp = await self.ask(f"MEAS:POW{extra_params}?") 116 | return decimal.Decimal(resp) 117 | 118 | async def select_active_instrument(self, select_id: int) -> None: 119 | """ 120 | Select the power supply for communication. 121 | 122 | id: the ID of the power supply to select. int from 1-31 123 | """ 124 | 125 | _id = int(select_id) 126 | 127 | if _id < 1 or _id > 31: 128 | raise ValueError("id %d is outside of the valid id range" % _id) 129 | 130 | await self.command(f"INSTrument:NSELect {_id:d}") 131 | 132 | async def query_active_instrument(self) -> int: 133 | """ 134 | Returns the ID of the active instrument. 135 | """ 136 | 137 | resp = await self.ask("INSTrument:NSELect?") 138 | return int(resp) 139 | 140 | async def couple_mode(self, couple: str = "NONE") -> None: 141 | """ 142 | Couple for all Z+ power supplies. 143 | """ 144 | 145 | couple = couple.upper() 146 | 147 | if couple in ("NONE", "ALL"): 148 | await self.command("INSTrument:COUPle %s" % couple) 149 | else: 150 | raise ValueError("Argument '%s' not valid for INST:COUP" % couple) 151 | 152 | async def set_voltage_protection(self, volts: Any) -> None: 153 | """ 154 | Set over-voltage protection level. 155 | """ 156 | 157 | _volts = str(volts).upper() 158 | # FIXME: shouldn't we pass _volts here ?? Also what are valid types/values ?? 159 | await self.command("VOLTage:PROTection:LEVel") 160 | 161 | async def query_voltage_protection(self, mode: Optional[str] = None) -> decimal.Decimal: 162 | """ 163 | Query the voltage protection level. Depending on mode, returns the current level, the 164 | minimum level, or the maximum level. 165 | 166 | mode: Which value to return. 167 | - None (default): returns the current voltage protection level 168 | - "MAX": returns the maximum possible voltage protection level 169 | - "MIN": returns the minimum possible voltage protection level, approx. 105% the 170 | current voltage setting 171 | """ 172 | 173 | if mode is None: 174 | resp = await self.ask("VOLTage:PROTection:LEVel?") 175 | else: 176 | resp = await self.ask(f"VOLTage:PROTection:LEVel? {mode}") 177 | return decimal.Decimal(resp) 178 | 179 | async def flash_display(self, setting: StrIntCombo) -> None: 180 | """ 181 | Make the front panel voltage and Current displays flash. 182 | """ 183 | 184 | setting = str(setting).upper() 185 | if setting in ("1", "ON", "TRUE"): 186 | setting = "1" 187 | elif setting in ("0", "OFF", "FALSE"): 188 | setting = "0" 189 | else: 190 | raise ValueError 191 | await self.command(f"DISPlay:FLASh {setting}") 192 | 193 | async def global_enable(self, setting: StrIntCombo) -> None: 194 | """ 195 | Set enable status of all units. 196 | """ 197 | 198 | setting = str(setting).upper() 199 | if setting in ("1", "ON", "TRUE"): 200 | setting = "1" 201 | elif setting in ("0", "OFF", "FALSE"): 202 | setting = "0" 203 | else: 204 | raise ValueError 205 | await self.command(f"GLOBal:OUTPut:STATe {setting}") 206 | 207 | async def global_set_voltage(self, volts: float) -> None: 208 | """ 209 | Set enable status of all units. 210 | """ 211 | 212 | _volts = str(volts) 213 | # FIXME: probably just using volts:f would work fine 214 | await self.command(f"GLOBal:VOLTage:AMPLitude {_volts}") 215 | 216 | async def global_reset(self) -> None: 217 | """ 218 | Reset all units. 219 | """ 220 | 221 | await self.command("GLOBal:*RST") 222 | 223 | async def set_voltage(self, millivolts: float, extra_params: str = "") -> None: 224 | """ 225 | Sets the desired output voltage (but does not auto-enable outputs) in 226 | millivolts, pass extra_params string to append to the command (like ":PROT") 227 | 228 | Limited to five percent greater than the voltage limit of the unit. 229 | """ 230 | 231 | if millivolts / 1000.0 > 1.05 * self.voltage_limit or millivolts < 0: 232 | raise ValueError 233 | 234 | await super().set_voltage(millivolts, extra_params=extra_params) 235 | 236 | async def set_current(self, milliamps: float, extra_params: str = "") -> None: 237 | """ 238 | Sets the desired output current (but does not auto-enable outputs) in 239 | milliamps, pass extra_params string to append to the command (like ":TRIG") 240 | 241 | Limited to five percent greater than the current limit of the unit. 242 | """ 243 | 244 | if milliamps / 1000.0 > 1.05 * self.current_limit or milliamps < 0: 245 | raise ValueError 246 | 247 | await super().set_current(milliamps, extra_params=extra_params) 248 | 249 | 250 | def tcp(ipaddr: str, port: int) -> TDKLambdaZplus: 251 | """Quick helper to connect via TCP""" 252 | 253 | transport = get_tcp(ipaddr, port) 254 | protocol = SCPIProtocol(transport) 255 | dev = TDKLambdaZplus(protocol) 256 | return dev 257 | 258 | 259 | def serial(serial_url: str, baudrate: int = 9600) -> TDKLambdaZplus: 260 | """Quick helper to connect via serial""" 261 | port = pyserial.serial_for_url( 262 | serial_url, 263 | baudrate=baudrate, 264 | bytesize=8, 265 | parity=pyserial.PARITY_NONE, 266 | stopbits=1, 267 | xonxoff=False, 268 | rtscts=False, 269 | dsrdtr=False, 270 | timeout=10, 271 | ) 272 | transport = RS232Transport(serialdevice=port) 273 | protocol = SCPIProtocol(transport) 274 | dev = TDKLambdaZplus(protocol) 275 | return dev 276 | -------------------------------------------------------------------------------- /src/scpi/devices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rambo/python-scpi/df6126d606d964e8af06aa3753a033ccfe2de258/src/scpi/devices/__init__.py -------------------------------------------------------------------------------- /src/scpi/devices/generic.py: -------------------------------------------------------------------------------- 1 | """Mixins for different device functionalities""" 2 | 3 | import decimal 4 | 5 | from ..scpi import SCPIDevice 6 | 7 | 8 | class MultiMeter(SCPIDevice): 9 | """Multimeter related features""" 10 | 11 | async def measure_voltage(self, extra_params: str = "") -> decimal.Decimal: 12 | """Returns the measured (scalar) actual output voltage (in volts), 13 | pass extra_params string to append to the command (like ":ACDC")""" 14 | resp = await self.ask(f"MEAS:SCAL:VOLT{extra_params}?") 15 | return decimal.Decimal(resp) 16 | 17 | async def measure_current(self, extra_params: str = "") -> decimal.Decimal: 18 | """Returns the measured (scalar) actual output current (in amps), 19 | pass extra_params string to append to the command (like ":ACDC")""" 20 | resp = await self.ask(f"MEAS:SCAL:CURR{extra_params}?") 21 | return decimal.Decimal(resp) 22 | 23 | async def set_measure_current_max(self, amps: float) -> None: 24 | """Sets the upper bound (in amps) of current to measure, 25 | on some devices low-current accuracy can be increased by keeping this low""" 26 | await self.command(f"SENS:CURR:RANG {amps:f}") 27 | 28 | async def query_measure_current_max(self) -> decimal.Decimal: 29 | """Returns the upper bound (in amps) of current to measure, 30 | this is not neccessarily same number as set with set_measure_current_max""" 31 | resp = await self.ask("SENS:CURR:RANG?") 32 | return decimal.Decimal(resp) 33 | 34 | 35 | class PowerSupply(SCPIDevice): 36 | """Power supply related features""" 37 | 38 | async def set_voltage(self, millivolts: float, extra_params: str = "") -> None: 39 | """Sets the desired output voltage (but does not auto-enable outputs) in millivolts, 40 | pass extra_params string to append to the command (like ":PROT")""" 41 | await self.command(f"SOUR:VOLT{extra_params} {millivolts:f} MV") 42 | 43 | async def query_voltage(self, extra_params: str = "") -> decimal.Decimal: 44 | """Returns the set output voltage (in volts), 45 | pass extra_params string to append to the command (like ":PROT")""" 46 | resp = await self.ask(f"SOUR:VOLT{extra_params}?") 47 | return decimal.Decimal(resp) 48 | 49 | async def set_current(self, milliamps: float, extra_params: str = "") -> None: 50 | """Sets the desired output current (but does not auto-enable outputs) in milliamps, 51 | pass extra_params string to append to the command (like ":TRIG")""" 52 | await self.command(f"SOUR:CURR{extra_params} {milliamps:f} MA") 53 | 54 | async def query_current(self, extra_params: str = "") -> decimal.Decimal: 55 | """Returns the set output current (in amps), 56 | pass extra_params string to append to the command (like ":TRIG")""" 57 | resp = await self.ask(f"SOUR:CURR{extra_params}?") 58 | return decimal.Decimal(resp) 59 | 60 | async def set_output(self, state: bool) -> None: 61 | """Enables/disables output""" 62 | await self.command(f"OUTP:STAT {state:d}") 63 | 64 | async def query_output(self) -> bool: 65 | """Returns the output state""" 66 | resp = await self.ask("OUTP:STAT?") 67 | return bool(int(resp)) 68 | -------------------------------------------------------------------------------- /src/scpi/devices/hp6632b.py: -------------------------------------------------------------------------------- 1 | """HP/Agilent 3362B specific device implementation and helpers""" 2 | 3 | from typing import Any 4 | import logging 5 | import decimal 6 | 7 | from ..scpi import SCPIDevice, SCPIProtocol 8 | from ..transports.rs232 import RS232Transport 9 | from ..transports.rs232 import get as get_rs232 10 | from .generic import MultiMeter, PowerSupply 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class HP6632B(PowerSupply, MultiMeter, SCPIDevice): 16 | """Adds the HP/Agilent 3362B specific SCPI commands as methods""" 17 | 18 | async def set_low_current_mode(self, state: bool) -> None: 19 | """The low-current mode is enabled by setting the range to (max) 20mA, anything over that is high-current mode. 20 | This model has max 5A output""" 21 | if state: 22 | return await self.set_measure_current_max(0.020) 23 | return await self.set_measure_current_max(5.0) 24 | 25 | async def query_low_current_mode(self) -> bool: 26 | """Returns boolean indicating whether we are in low or high current mode""" 27 | max_current = await self.query_measure_current_max() 28 | if max_current <= 0.020: 29 | return True 30 | return False 31 | 32 | async def measure_current_autorange(self, extra_params: str = "") -> decimal.Decimal: 33 | """Measures the current, then make sure we are running on the correct measurement range 34 | and if not switch range and measure again""" 35 | ret = await self.measure_current(extra_params) 36 | if abs(ret) < 0.020: 37 | # We need to be in low-current mode 38 | if not await self.query_low_current_mode(): 39 | # Enter low current mode and measure again 40 | await self.set_low_current_mode(True) 41 | return await self.measure_current(extra_params) 42 | return ret 43 | # We need to be in high-current mode 44 | if await self.query_low_current_mode(): 45 | # Switch mode and measure again 46 | await self.set_low_current_mode(False) 47 | return await self.measure_current(extra_params) 48 | return ret 49 | 50 | def ensure_transport_is_rs232(self) -> None: 51 | """Ensures transport is RS232, raises error if not""" 52 | if not isinstance(self.protocol.transport, RS232Transport): 53 | raise RuntimeError("Only usable with RS232 transports") 54 | 55 | async def set_remote_mode(self, state: bool = True) -> None: 56 | """RS232 only, prevent accidental button mashing on the fron panel, this switches between SYSTem:REMote 57 | and SYSTem:LOCal according to state, this overrides previous value set with set_rwlock""" 58 | self.ensure_transport_is_rs232() 59 | if state: 60 | return await self.command("SYST:REM") 61 | return await self.command("SYST:LOC") 62 | 63 | async def set_rwlock(self, state: bool = True) -> None: 64 | """RS232 only, prevent *any* button mashing on the fron panel, this switches between SYSTem:RWLock 65 | and SYSTem:LOCal according to state, this overrides previous value set with set_remote_mode""" 66 | self.ensure_transport_is_rs232() 67 | if state: 68 | return await self.command("SYST:RWL") 69 | return await self.command("SYST:LOC") 70 | 71 | async def display_on(self, state: bool = True) -> None: 72 | """Sets display on/off""" 73 | if state: 74 | return await self.command("DISP ON") 75 | return await self.command("DISP OFF") 76 | 77 | async def set_display_mode(self, mode: str) -> None: 78 | """Set the display mode, valied values are NORM and TEXT""" 79 | mode = mode.upper() 80 | if mode not in ("NORM", "TEXT"): 81 | raise ValueError("Invalid mode %s, valid ones are NORM and TEXT" % mode) 82 | return await self.command(f"DISP:MODE {mode}") 83 | 84 | async def set_display_text(self, text: str) -> None: 85 | """Used to display text on the display, max 14 characters, 86 | NOTE: does *not* set display mode, you need to do it yourself""" 87 | if len(text) > 14: 88 | raise ValueError("Max text length is 14 characters") 89 | if '"' in text and "'" in text: 90 | raise ValueError("Text may only contain either single or double quotes, not both") 91 | if '"' in text: 92 | return await self.command(f"DISP:TEXT '{text}'") 93 | return await self.command(f"""DISP:TEXT "{text}" """.strip()) 94 | 95 | 96 | def rs232(serial_url: str, **kwargs: Any) -> HP6632B: 97 | """Quick helper to connect via RS232 port""" 98 | transport = get_rs232(serial_url, **kwargs) 99 | protocol = SCPIProtocol(transport) 100 | dev = HP6632B(protocol) 101 | return dev 102 | -------------------------------------------------------------------------------- /src/scpi/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """SCPI module specific errors""" 2 | 3 | from typing import Any 4 | 5 | 6 | class CommandError(RuntimeError): 7 | """Error executing SCPI command""" 8 | 9 | def __init__(self, command: str, code: int, message: str) -> None: 10 | """initialize the error""" 11 | self.command = command 12 | self.code = code 13 | self.message = message 14 | super().__init__() 15 | 16 | def __str__(self) -> str: 17 | """format as string""" 18 | return f"'{self.command}' returned error {self.code:d}: {self.message}" 19 | -------------------------------------------------------------------------------- /src/scpi/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rambo/python-scpi/df6126d606d964e8af06aa3753a033ccfe2de258/src/scpi/py.typed -------------------------------------------------------------------------------- /src/scpi/scpi.py: -------------------------------------------------------------------------------- 1 | """Generic SCPI commands, allow sending and reading of raw data, helpers to parse information""" 2 | 3 | from typing import Any, Tuple, Sequence, Union, Optional, cast 4 | import asyncio 5 | import re 6 | import logging 7 | from dataclasses import dataclass, field 8 | 9 | from .errors import CommandError 10 | from .transports.baseclass import AbstractTransport, BaseTransport 11 | from .transports.gpib import GPIBDeviceTransport, GPIBTransport 12 | 13 | 14 | COMMAND_DEFAULT_TIMEOUT = 1.0 15 | ERROR_RE = re.compile(r'([+-]?\d+),"(.*?)"') 16 | LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | # FIXME rename to mixin and use as such 20 | class BitEnum: # pylint: disable=R0903 21 | """Baseclass for bit definitions of various status registers""" 22 | 23 | @classmethod 24 | def test_bit(cls, statusvalue: int, bitname: str) -> bool: 25 | """Test if the given status value has the given bit set""" 26 | bitval = cast(int, getattr(cls, bitname)) 27 | return bool(bitval & statusvalue) 28 | 29 | 30 | # FIXME use enum.IntEnum as baseclass (maybe, we can't do the docstrings the names if we do...) 31 | class ESRBit(BitEnum): 32 | """Define meanings of the Event Status Register (ESR) bits""" 33 | 34 | @property 35 | def power_on(self) -> int: 36 | """Power-on. The power has cycled""" 37 | return 128 38 | 39 | @property 40 | def user_request(self) -> int: 41 | """User request. The instrument operator has issued a request, 42 | for instance turning a knob on the front panel.""" 43 | return 64 44 | 45 | @property 46 | def command_error(self) -> int: 47 | """Command Error. A command error has occurred.""" 48 | return 32 49 | 50 | @property 51 | def exec_error(self) -> int: 52 | """Execution error. The instrument was not able to execute a command for 53 | some reason. The reason can be that the supplied data is out of range but 54 | can also be an external event like a safety switch/knob or some hardware / 55 | software error.""" 56 | return 16 57 | 58 | @property 59 | def device_error(self) -> int: 60 | """Device Specific Error.""" 61 | return 8 62 | 63 | @property 64 | def query_error(self) -> int: 65 | """Query Error. Error occurred during query processing.""" 66 | return 4 67 | 68 | @property 69 | def control_request(self) -> int: 70 | """Request Control. The instrument is requesting to become active controller.""" 71 | return 2 72 | 73 | @property 74 | def operation_complete(self) -> int: 75 | """Operation Complete. The instrument has completed all operations. 76 | This bit is used for synchronisation purposes.""" 77 | return 1 78 | 79 | 80 | class STBBit(BitEnum): 81 | """Define meanings of the STatus Byte register (STB) bits""" 82 | 83 | @property 84 | def rqs_mss(self) -> int: 85 | """RQS, ReQuested Service. This bit is set when the instrument has requested 86 | service by means of the SeRvice Request (SRQ). When the controller reacts 87 | by performing a serial poll, the STatus Byte register (STB) is transmitted with 88 | this bit set. Afand cleared afterwards. It is only set again when a new event 89 | occurs that requires service. 90 | 91 | MSS, Master Summary Status. This bit is a summary of the STB and the 92 | SRE register bits 1..5 and 7. Thus it is not cleared when a serial poll occurs. 93 | It is cleared when the event which caused the setting of MSS is cleared or 94 | when the corresponding bits in the SRE register are cleared.""" 95 | return 64 96 | 97 | @property 98 | def rqs(self) -> int: 99 | """alias for rqs_mss""" 100 | return self.rqs_mss 101 | 102 | @property 103 | def mss(self) -> int: 104 | """alias for rqs_mss""" 105 | return self.rqs_mss 106 | 107 | @property 108 | def esb(self) -> int: 109 | """ESB, Event Summary Bit. This is a summary bit of the standard status 110 | registers ESR and ESE""" 111 | return 32 112 | 113 | @property 114 | def event_summary(self) -> int: 115 | """Alias for esb""" 116 | return self.esb 117 | 118 | @property 119 | def mav(self) -> int: 120 | """MAV, Message AVailable. This bit is set when there is data in the output 121 | queue waiting to be read.""" 122 | return 16 123 | 124 | @property 125 | def message_available(self) -> int: 126 | """Alias for mav""" 127 | return self.mav 128 | 129 | @property 130 | def eav(self) -> int: 131 | """EAV, Error AVailable. This bit is set when there is data in the output 132 | queue waiting to be read.""" 133 | return 4 134 | 135 | @property 136 | def error_available(self) -> int: 137 | """Alias for eav""" 138 | return self.eav 139 | 140 | 141 | @dataclass 142 | class SCPIProtocol: 143 | """Implements the SCPI protocol talks over the given transport""" 144 | 145 | transport: Union[BaseTransport, GPIBDeviceTransport, GPIBTransport] = field() 146 | lock: asyncio.Lock = field(default_factory=asyncio.Lock) 147 | _checking_error: bool = field(default=False) 148 | 149 | async def quit(self) -> None: 150 | """Shuts down any background threads that might be active""" 151 | await self.transport.quit() 152 | 153 | async def abort_command(self) -> None: 154 | """Shortcut to the transports abort_command call""" 155 | await self.transport.abort_command() 156 | 157 | async def get_error(self) -> Tuple[int, str]: 158 | """Asks for the error code and string""" 159 | if self._checking_error: 160 | raise RuntimeError("Recursion on get_error detected") 161 | try: 162 | self._checking_error = True 163 | response = await self.ask("SYST:ERR?", auto_check_error=False) 164 | match = ERROR_RE.search(response) 165 | if not match: 166 | # PONDER: Make our own exceptions ?? 167 | raise ValueError("Response '{:s}' does not have correct error format".format(response)) 168 | code = int(match.group(1)) 169 | errstr = match.group(2) 170 | return (code, errstr) 171 | finally: 172 | self._checking_error = False 173 | 174 | async def check_error(self, prev_command: str = "") -> None: 175 | """Check for error and raise exception if present""" 176 | code, errstr = await self.get_error() 177 | if code != 0: 178 | raise CommandError(prev_command, code, errstr) 179 | 180 | async def command( 181 | self, 182 | command: str, 183 | cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, 184 | abort_on_timeout: bool = True, 185 | *, 186 | auto_check_error: bool = True, 187 | ) -> None: 188 | """Sends a command, does not wait for response""" 189 | try: 190 | 191 | async def _command(command: str) -> None: 192 | """Wrap the actual work""" 193 | nonlocal self 194 | async with self.lock: 195 | await self.transport.send_command(command) 196 | 197 | await asyncio.wait_for(_command(command), timeout=cmd_timeout) 198 | except asyncio.TimeoutError as err: 199 | # check for the actual error if available 200 | if auto_check_error: 201 | await self.check_error(command) 202 | if abort_on_timeout: 203 | await self.abort_command() 204 | # re-raise the timeout if no other error found 205 | raise err 206 | except asyncio.CancelledError: 207 | LOGGER.info("Cancelled") 208 | # other errors are allowed to bubble-up as-is 209 | 210 | async def safe_command(self, command: str, *args: Any, **kwargs: Any) -> None: 211 | """See "command", this just auto-checks for errors each time""" 212 | await self.command(command, *args, **kwargs) 213 | await self.check_error(command) 214 | 215 | async def ask( 216 | self, 217 | command: str, 218 | cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, 219 | abort_on_timeout: bool = True, 220 | *, 221 | auto_check_error: bool = True, 222 | ) -> str: 223 | """Send a command and waits for response, returns the response""" 224 | try: 225 | 226 | async def _ask(command: str) -> str: 227 | """Wrap the actual work""" 228 | nonlocal self 229 | async with self.lock: 230 | await self.transport.send_command(command) 231 | return await self.transport.get_response() 232 | 233 | return await asyncio.wait_for(_ask(command), timeout=cmd_timeout) 234 | except asyncio.TimeoutError as err: 235 | # check for the actual error if available 236 | if auto_check_error: 237 | await self.check_error(command) 238 | if abort_on_timeout: 239 | await self.abort_command() 240 | # re-raise the timeout if no other error found 241 | raise err 242 | except asyncio.CancelledError: 243 | LOGGER.info("Cancelled") 244 | # gotta return something or raise an error 245 | raise 246 | # other errors are allowed to bubble-up as-is 247 | 248 | async def safe_ask(self, command: str, *args: Any, **kwargs: Any) -> str: 249 | """See "ask", this just autp-checks for errors each time""" 250 | response = await self.ask(command, *args, **kwargs) 251 | await self.check_error(command) 252 | return response 253 | 254 | 255 | @dataclass 256 | class SCPIDevice: # pylint: disable=R0904 257 | """Implements nicer wrapper methods for the raw commands from the generic SCPI command set 258 | 259 | See also devices.mixins for mixin classes with more features""" 260 | 261 | instancefrom: Union[BaseTransport, "SCPIDevice", SCPIProtocol, GPIBDeviceTransport, GPIBTransport] 262 | use_safe_variants: bool = field(default=True) 263 | protocol: SCPIProtocol = field(init=False) 264 | transport: AbstractTransport = field(init=False) 265 | _can_poll: bool = field(default=False) 266 | 267 | def __post_init__(self) -> None: 268 | """Set protocol and transport based on what we're instancing from""" 269 | protocol: Optional[SCPIProtocol] = None 270 | if isinstance(self.instancefrom, SCPIProtocol): 271 | protocol = self.instancefrom 272 | if isinstance(self.instancefrom, (BaseTransport, GPIBDeviceTransport, GPIBTransport)): 273 | protocol = SCPIProtocol(self.instancefrom) 274 | if isinstance(self.instancefrom, SCPIDevice): 275 | protocol = self.instancefrom.protocol 276 | if not protocol: 277 | raise RuntimeError("Could not resolve protocol/transport") 278 | self.protocol = protocol 279 | self.transport = self.protocol.transport 280 | # Check if transport poll method exists 281 | # TODO: the transport class should have a marker property for this we should use 282 | try: 283 | _ = self.transport.poll # type: ignore 284 | self._can_poll = True 285 | except AttributeError: 286 | pass 287 | 288 | async def command( 289 | self, command: str, cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, abort_on_timeout: bool = True 290 | ) -> None: 291 | """Wrap the protocol command (using safe version if requested)""" 292 | if self.use_safe_variants: 293 | return await self.protocol.safe_command(command, cmd_timeout, abort_on_timeout) 294 | return await self.protocol.command(command, cmd_timeout, abort_on_timeout) 295 | 296 | async def ask( 297 | self, command: str, cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, abort_on_timeout: bool = True 298 | ) -> str: 299 | """Wrap the protocol ask (using safe version if requested)""" 300 | if self.use_safe_variants: 301 | return await self.protocol.safe_ask(command, cmd_timeout, abort_on_timeout) 302 | return await self.protocol.ask(command, cmd_timeout, abort_on_timeout) 303 | 304 | async def quit(self) -> None: 305 | """Shuts down any background threads that might be active""" 306 | await self.protocol.quit() 307 | 308 | async def abort(self) -> None: 309 | """Tells the protocol layer to issue "Device clear" to abort the command currently hanging""" 310 | await self.protocol.abort_command() 311 | 312 | async def get_error(self) -> Tuple[int, str]: 313 | """Shorthand for procotols method of the same name""" 314 | return await self.protocol.get_error() 315 | 316 | async def reset(self) -> None: 317 | """Resets the device to known state (with *RST) and clears the error log""" 318 | return await self.protocol.command("*RST;*CLS") 319 | 320 | async def wait_for_complete(self, wait_timeout: float) -> bool: 321 | """Wait for all queued operations to complete (up-to defined timeout)""" 322 | resp = await self.ask("*WAI;*OPC?", cmd_timeout=wait_timeout) 323 | return bool(int(resp)) 324 | 325 | async def identify(self) -> Sequence[str]: 326 | """Returns the identification data, standard order is: 327 | Manufacturer, Model no, Serial no (or 0), Firmware version""" 328 | resp = await self.ask("*IDN?") 329 | return resp.split(",") 330 | 331 | async def query_esr(self) -> int: 332 | """Queries the event status register (ESR) NOTE: The register is cleared when read! 333 | returns int instead of Decimal like the other number queries since we need to be able 334 | to do bitwise comparisons""" 335 | resp = await self.ask("*ESR?") 336 | return int(resp) 337 | 338 | async def query_ese(self) -> int: 339 | """Queries the event status enable (ESE). 340 | returns int instead of Decimal like the other number queries since we need to be able 341 | to do bitwise comparisons""" 342 | resp = await self.ask("*ESE?") 343 | return int(resp) 344 | 345 | async def set_ese(self, state: int) -> None: 346 | """Sets ESE to given value. 347 | Construct the value with bitwise OR operations using ESRBit properties, for example to enable OPC and exec_error 348 | error bits in the status flag use: set_ese(ESRBit.operation_complete | ESRBit.exec_error)""" 349 | await self.command(f"*ESE {state:d}") 350 | 351 | async def query_sre(self) -> int: 352 | """Queries the service request enable (SRE). 353 | returns int instead of Decimal like the other number queries since we need to be able 354 | to do bitwise comparisons""" 355 | resp = await self.ask("*SRE?") 356 | return int(resp) 357 | 358 | async def set_sre(self, state: int) -> None: 359 | """Sets SRE to given value. 360 | Construct the value with bitwise OR operations using STBBit properties, for example to enable SRQ generation 361 | on any error or message use: set_sre(STBBit.mav | STBBit.eav)""" 362 | await self.command(f"*SRE {state:d}") 363 | 364 | async def query_stb(self) -> int: 365 | """Queries the status byte (STB). 366 | returns int instead of Decimal like the other number queries since we need to be able 367 | to do bitwise comparisons 368 | 369 | If transport implements "serial poll", will use that instead of SCPI query to get the value""" 370 | if self._can_poll: 371 | resp = await self.transport.poll() # type: ignore 372 | else: 373 | resp = await self.ask("*STB?") 374 | return int(resp) 375 | 376 | async def trigger(self) -> None: 377 | """Send the TRiGger command via SCPI. 378 | NOTE: For GPIB devices the Group Execute Trigger is way better, use it when possible 379 | however we do not do it transparently here since it triggers all devices on the bus""" 380 | await self.command("*TRG") 381 | 382 | async def clear_status(self) -> None: 383 | """ 384 | Sends a clear status command. 385 | """ 386 | await self.command("*CLS") 387 | 388 | async def operation_complete(self) -> None: 389 | """ 390 | Sends an Operation Complete command. 391 | """ 392 | await self.command("*OPC") 393 | 394 | async def query_options(self) -> str: 395 | """ 396 | Queries the model's options. 397 | """ 398 | return await self.ask("*OPT?") 399 | 400 | async def set_power_on_status_clear(self, setting: str) -> None: 401 | """ 402 | Set the Power-On Status Clear setting. 403 | """ 404 | await self.command(f"*PSC {setting}") 405 | 406 | async def save_state(self, state: int) -> None: 407 | """ 408 | The SAV command saves all applied configuration settings. 409 | """ 410 | state = int(state) 411 | await self.command("*SAV {state:d}") 412 | 413 | async def restore_state(self, state: int) -> None: 414 | """ 415 | Restores the power supply to a state previously stored in memory by *SAV command. 416 | """ 417 | state = int(state) 418 | await self.command(f"*RCL {state:d}") 419 | 420 | async def power_on_state(self, setting: str) -> None: 421 | """ 422 | Set the power-on behavior of the system 423 | """ 424 | setting = str(setting).upper() 425 | await self.command(f"*OUTP:PON {setting}") 426 | -------------------------------------------------------------------------------- /src/scpi/transports/__init__.py: -------------------------------------------------------------------------------- 1 | """Transport layers for the SCPI module""" 2 | 3 | from .rs232 import RS232Transport 4 | from .tcp import TCPTransport 5 | 6 | __all__ = ["RS232Transport", "TCPTransport"] 7 | -------------------------------------------------------------------------------- /src/scpi/transports/baseclass.py: -------------------------------------------------------------------------------- 1 | """Baseclass for all the transports, if common methods are needed they will be defined here 2 | 3 | All transports must define certain basic methods (check all the raise NotImplementedError) 4 | """ 5 | 6 | from typing import Optional, Callable 7 | import asyncio 8 | import logging 9 | import threading 10 | from abc import ABC, abstractmethod 11 | from dataclasses import dataclass, field 12 | 13 | 14 | LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | class AbstractTransport(ABC): # pylint: disable=R0903 18 | """So that for example GPIBDeviceTransport can be identified as transport without inheriting 19 | the low-level transport methods""" 20 | 21 | 22 | @dataclass 23 | class BaseTransport(AbstractTransport, ABC): 24 | """Baseclass for SCPI tranport layers, abstracts away details, must be subclasses to implement""" 25 | 26 | message_callback: Optional[Callable[[str], None]] = field(default=None) 27 | unsolicited_message_callback: Optional[Callable[[str], None]] = field(default=None) 28 | lock: asyncio.Lock = field(default_factory=asyncio.Lock) 29 | aioevent: asyncio.Event = field(default_factory=asyncio.Event) 30 | blevent: threading.Event = field(default_factory=threading.Event) 31 | 32 | @abstractmethod 33 | async def quit(self) -> None: 34 | """Must shutdown all background threads (if any)""" 35 | raise NotImplementedError() 36 | 37 | @abstractmethod 38 | async def send_command(self, command: str) -> None: 39 | """Sends a complete command to the device, line termination, write timeouts etc are handled by the transport 40 | note: the transport probably should handle locking transparently using 41 | 'with (await self.lock):' as context manager""" 42 | raise NotImplementedError() 43 | 44 | @abstractmethod 45 | async def get_response(self) -> str: 46 | """Tells the device send a response, reads and returns it""" 47 | raise NotImplementedError() 48 | 49 | def message_received(self, message: str) -> None: 50 | """Passes the message to the callback expecting it, or to the unsolicited callback""" 51 | if self.message_callback is not None: 52 | self.message_callback(message) 53 | self.message_callback = None 54 | return 55 | # Fall-through for unsolicited messages 56 | if self.unsolicited_message_callback is not None: 57 | self.unsolicited_message_callback(message) 58 | return 59 | LOGGER.info("Got unsolicited message but have no callback to send it to") 60 | 61 | @abstractmethod 62 | async def abort_command(self) -> None: 63 | """Send the "device clear" command to abort a running command 64 | note: the transport probably should handle locking transparently using 65 | 'async with self.lock:' as context manager""" 66 | raise NotImplementedError() 67 | -------------------------------------------------------------------------------- /src/scpi/transports/gpib/__init__.py: -------------------------------------------------------------------------------- 1 | """GPIB related transports""" 2 | 3 | from .base import GPIBDeviceTransport, GPIBTransport 4 | 5 | __all__ = ["GPIBDeviceTransport", "GPIBTransport"] 6 | -------------------------------------------------------------------------------- /src/scpi/transports/gpib/base.py: -------------------------------------------------------------------------------- 1 | """GPIB Related baseclasses""" 2 | 3 | from __future__ import annotations 4 | from typing import Optional, Tuple, Sequence, Union 5 | from abc import ABC, abstractmethod 6 | from dataclasses import dataclass, field 7 | import logging 8 | 9 | from ..baseclass import AbstractTransport, BaseTransport 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | AddressTuple = Tuple[int, Optional[int]] 14 | 15 | 16 | @dataclass 17 | class GPIBDeviceTransport(AbstractTransport): 18 | """Device specific transport, handles addressing transparently""" 19 | 20 | lltransport: GPIBTransport = field() 21 | address: Union[AddressTuple, int] = field() 22 | 23 | def __post_init__(self) -> None: 24 | """Make sure address is always tuple""" 25 | if isinstance(self.address, int): 26 | self.address = (self.address, None) 27 | 28 | async def _set_ll_address(self) -> None: 29 | """Set the lowlevel transport address""" 30 | assert not isinstance(self.address, int) 31 | await self.lltransport.set_address(self.address[0], self.address[1]) 32 | 33 | async def send_command(self, command: str) -> None: 34 | """Transparently set address when talking, see low-level transport method docs for more info""" 35 | await self._set_ll_address() 36 | await self.lltransport.send_command(command) 37 | 38 | async def get_response(self) -> str: 39 | """Transparently set address when talking, see low-level transport method docs for more info""" 40 | await self._set_ll_address() 41 | return await self.lltransport.get_response() 42 | 43 | async def abort_command(self) -> None: 44 | """Transparently set address when talking, see low-level transport method docs for more info""" 45 | await self._set_ll_address() 46 | return await self.lltransport.abort_command() 47 | 48 | async def send_scd(self) -> None: 49 | """Sends the Selected Device Clear (SDC) message to the device""" 50 | await self._set_ll_address() 51 | await self.lltransport.send_scd() 52 | 53 | async def send_llo(self) -> None: 54 | """Send LLO (disable front panel) to the device""" 55 | await self._set_ll_address() 56 | await self.lltransport.send_llo() 57 | 58 | async def send_loc(self) -> None: 59 | """Send LOC (enable front panel) to the device""" 60 | await self._set_ll_address() 61 | await self.lltransport.send_loc() 62 | 63 | async def quit(self) -> None: 64 | """Nop for devices""" 65 | LOGGER.debug("quit should not be called on device level but we must have the method for type compatibility") 66 | 67 | 68 | class GPIBTransport(BaseTransport, ABC): 69 | """Baseclass for GPIB transports""" 70 | 71 | @abstractmethod 72 | async def set_address(self, primary: int, secondary: Optional[int] = None) -> None: 73 | """Set the address we want to talk to""" 74 | raise NotImplementedError() 75 | 76 | @abstractmethod 77 | async def query_address(self) -> AddressTuple: 78 | """Query the address we are talking to, returns tuple with primary and secondary parts 79 | secondary is None if not set""" 80 | raise NotImplementedError() 81 | 82 | @abstractmethod 83 | async def scan_devices(self) -> Sequence[Tuple[int, str]]: 84 | """Scan for devices in the bus. 85 | Returns list of addresses and identifiers for found primary addresses (0-30)""" 86 | raise NotImplementedError() 87 | 88 | @abstractmethod 89 | async def send_scd(self) -> None: 90 | """Sends the Selected Device Clear (SDC) message to the currently specified GPIB address""" 91 | raise NotImplementedError() 92 | 93 | @abstractmethod 94 | async def send_ifc(self) -> None: 95 | """Asserts GPIB IFC signal""" 96 | raise NotImplementedError() 97 | 98 | @abstractmethod 99 | async def send_llo(self) -> None: 100 | """Send LLO (disable front panel) to currently specified address""" 101 | raise NotImplementedError() 102 | 103 | @abstractmethod 104 | async def send_loc(self) -> None: 105 | """Send LOC (enable front panel) to currently specified address""" 106 | raise NotImplementedError() 107 | 108 | @abstractmethod 109 | async def send_group_trig(self) -> None: 110 | """Send Group Execute Trigger to the bus""" 111 | raise NotImplementedError() 112 | 113 | @abstractmethod 114 | async def get_srq(self) -> int: 115 | """Get SRQ assertion status""" 116 | raise NotImplementedError() 117 | 118 | @abstractmethod 119 | async def poll(self) -> int: 120 | """Do serial poll on the selected device""" 121 | raise NotImplementedError() 122 | 123 | def get_device_transport(self, address: int, secondary: Optional[int] = None) -> GPIBDeviceTransport: 124 | """Gets a device-specific transport instance for given address""" 125 | return GPIBDeviceTransport(self, (address, secondary)) 126 | -------------------------------------------------------------------------------- /src/scpi/transports/gpib/prologix.py: -------------------------------------------------------------------------------- 1 | """"Driver" for http://prologix.biz/gpib-usb-controller.html GPIB controller""" 2 | 3 | from typing import Optional, Sequence, List, Any, Tuple, cast 4 | import asyncio 5 | import logging 6 | from dataclasses import dataclass 7 | 8 | import serial # type: ignore 9 | import serial.threaded # type: ignore 10 | 11 | from ..rs232 import RS232SerialProtocol, RS232Transport 12 | from .base import GPIBTransport, AddressTuple 13 | 14 | SCAN_DEVICE_TIMEOUT = 0.5 15 | READ_TIMEOUT = 1.0 16 | LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | class PrologixRS232SerialProtocol(RS232SerialProtocol): 20 | """Basically the same deal as with the stock RS232 PySerial "protocol" but different EOL""" 21 | 22 | TERMINATOR = b"\n" 23 | 24 | 25 | @dataclass 26 | class PrologixGPIBTransport(GPIBTransport, RS232Transport): 27 | """Transport "driver" for the Prologix USB-GPIB controller (v6 protocol)""" 28 | 29 | def __post_init__(self) -> None: 30 | """Init the serial and controller""" 31 | self._serialhandler = serial.threaded.ReaderThread(self.serialdevice, PrologixRS232SerialProtocol) 32 | self._serialhandler.start() 33 | self._serialhandler.protocol.handle_line = self.message_received 34 | self.initialize_controller() 35 | 36 | def initialize_controller(self) -> None: 37 | """Initializes the controller to known state""" 38 | if not self._serialhandler: 39 | raise RuntimeError("Serialhandler isn't") 40 | # Set to controller mode 41 | self._serialhandler.protocol.write_line("++mode 1") 42 | # Disable automatic read after write 43 | self._serialhandler.protocol.write_line("++auto 0") 44 | # Auto-assert End Of Instruction after commands 45 | self._serialhandler.protocol.write_line("++eoi 1") 46 | # Append CRLF to device commands (EOI above *should* be enough but this is probably more compatible) 47 | self._serialhandler.protocol.write_line("++eos 0") 48 | # We do not have parsing support for the EOT character so disable it 49 | self._serialhandler.protocol.write_line("++eot_enable 0") 50 | # Set inter-character timeout for read commands 51 | self._serialhandler.protocol.write_line("++read_tmo_ms 500") 52 | # Assert IFC, make us Controller In Charge 53 | self._serialhandler.protocol.write_line("++ifc") 54 | 55 | async def send_command(self, command: str) -> None: 56 | """Wrapper for write_line on the protocol with some sanity checks""" 57 | if not self._serialhandler or not self._serialhandler.is_alive(): 58 | raise RuntimeError("Serial handler not ready") 59 | async with self.lock: 60 | self._serialhandler.protocol.write_line(command) 61 | 62 | async def get_response(self) -> str: 63 | """Get device response""" 64 | return await self.send_and_read("++read eoi") 65 | 66 | async def send_and_read(self, send: str) -> str: 67 | """Send a line, read the response. NOTE: This is for talking with the controller, device responses 68 | need to use get_response as usual""" 69 | if not self._serialhandler: 70 | raise RuntimeError("Serialhandler isn't") 71 | 72 | async def _send_and_read(send: str) -> str: 73 | """Wrap the actual work""" 74 | nonlocal self 75 | if not self._serialhandler: 76 | raise RuntimeError("Serialhandler isn't") 77 | 78 | async with self.lock: 79 | response: Optional[str] = None 80 | 81 | # pylint: disable=R0801 82 | def set_response(message: str) -> None: 83 | """Callback for setting the response""" 84 | nonlocal response, self 85 | response = message 86 | self.blevent.set() 87 | 88 | self.blevent.clear() 89 | self.message_callback = set_response 90 | self._serialhandler.protocol.write_line(send) 91 | await asyncio.get_event_loop().run_in_executor(None, self.blevent.wait) 92 | self.message_callback = None 93 | return cast(str, response) 94 | 95 | return await asyncio.wait_for(_send_and_read(send), timeout=READ_TIMEOUT) 96 | 97 | async def set_address(self, primary: int, secondary: Optional[int] = None) -> None: 98 | """Set the address we want to talk to""" 99 | if secondary is None: 100 | await self.send_command(f"++addr {primary:d}") 101 | else: 102 | await self.send_command(f"++addr {primary:d} {secondary:d}") 103 | 104 | while True: 105 | await asyncio.sleep(0.001) 106 | resp = await self.query_address() 107 | if resp == (primary, secondary): 108 | break 109 | 110 | async def query_address(self) -> AddressTuple: 111 | """Query the address we are talking to, returns tuple with primary and secondary parts 112 | secondary is None if not set""" 113 | resp = await self.send_and_read("++addr") 114 | parts = resp.split(" ") 115 | primary = int(parts[0]) 116 | secondary: Optional[int] = None 117 | if len(parts) > 1: 118 | secondary = int(parts[1]) 119 | return (primary, secondary) 120 | 121 | async def send_scd(self) -> None: 122 | """Sends the Selected Device Clear (SDC) message to the currently specified GPIB address""" 123 | await self.send_command("++clr") 124 | 125 | async def send_ifc(self) -> None: 126 | """Asserts GPIB IFC signal""" 127 | await self.send_command("++ifc") 128 | 129 | async def send_llo(self) -> None: 130 | """Send LLO (disable front panel) to currently specified address""" 131 | await self.send_command("++llo") 132 | 133 | async def send_loc(self) -> None: 134 | """Send LOC (enable front panel) to currently specified address""" 135 | await self.send_command("++loc") 136 | 137 | async def get_srq(self) -> int: 138 | """Get SRQ assertion status""" 139 | resp = await self.send_and_read("++srq") 140 | return int(resp) 141 | 142 | async def poll(self) -> int: 143 | """Do serial poll on the selected device""" 144 | resp = await self.send_and_read("++spoll") 145 | return int(resp) 146 | 147 | async def send_group_trig(self, addresses: Optional[Sequence[int]] = None) -> None: # pylint: disable=W0221 148 | """Send trigger to listed addresses 149 | 150 | For some reason Prologix does not trigger the whole bus but only listed devices (if none listed then 151 | the currently selected device is used)""" 152 | if addresses is None: 153 | return await self.send_command("++trg") 154 | await self.send_command("++trg " + " ".join((str(x) for x in addresses))) 155 | 156 | async def scan_devices(self) -> Sequence[Tuple[int, str]]: 157 | """Scan for devices in the bus. 158 | Returns list of addresses and identifiers for found primary addresses (0-30)""" 159 | if not self._serialhandler: 160 | raise RuntimeError("Serialhandler isn't") 161 | found_addresses: List[int] = [] 162 | # We do not lock on this level since the commands we use need to manipulate the lock 163 | prev_addr = await self.query_address() 164 | prev_read_tmo_ms = int(await self.send_and_read("++read_tmo_ms")) 165 | new_read_tmo_ms = int((SCAN_DEVICE_TIMEOUT / 2) * 1000) 166 | self._serialhandler.protocol.write_line(f"++read_tmo_ms {new_read_tmo_ms:d}") 167 | for addr in range(0, 31): # 0-30 inclusive 168 | 169 | async def _scan_addr(addr: int) -> None: 170 | """Sacn single address""" 171 | nonlocal found_addresses, self 172 | await self.set_address(addr) 173 | await self.poll() 174 | found_addresses.append(addr) 175 | 176 | try: 177 | await asyncio.wait_for(_scan_addr(addr), timeout=SCAN_DEVICE_TIMEOUT) 178 | except (asyncio.TimeoutError, asyncio.CancelledError): 179 | pass 180 | self._serialhandler.protocol.write_line(f"++read_tmo_ms {prev_read_tmo_ms:d}") 181 | # Wait a moment for things to settle 182 | await asyncio.sleep(float(prev_read_tmo_ms) / 1000) 183 | # Get ids for the devices we found 184 | ret = [] 185 | for addr in found_addresses: 186 | await self.set_address(addr) 187 | await self.send_command("*IDN?") 188 | idstr = await self.get_response() 189 | ret.append((addr, idstr)) 190 | await self.set_address(*prev_addr) 191 | return ret 192 | 193 | async def abort_command(self) -> None: 194 | """Not implemented for prologix""" 195 | LOGGER.debug("not implemented on PrologixGPIBTransport") 196 | 197 | 198 | def get(serial_url: str, **serial_kwargs: Any) -> PrologixGPIBTransport: 199 | """Shorthand for creating the port from url and initializing the transport""" 200 | port = serial.serial_for_url(serial_url, **serial_kwargs) 201 | return PrologixGPIBTransport(serialdevice=port) 202 | -------------------------------------------------------------------------------- /src/scpi/transports/rs232.py: -------------------------------------------------------------------------------- 1 | """Serial port transport layer""" 2 | 3 | from __future__ import annotations 4 | from typing import Optional, Any, Dict, cast 5 | import asyncio 6 | import logging 7 | from dataclasses import field, dataclass 8 | 9 | import serial # type: ignore 10 | import serial.threaded # type: ignore 11 | 12 | from .baseclass import BaseTransport 13 | 14 | LOGGER = logging.getLogger(__name__) 15 | WRITE_TIMEOUT = 1.0 16 | 17 | 18 | class RS232SerialProtocol(serial.threaded.LineReader): # type: ignore 19 | """PySerial "protocol" class for handling stuff""" 20 | 21 | ENCODING = "ascii" 22 | 23 | def connection_made(self, transport: RS232Transport) -> None: 24 | """Overridden to make sure we have write_timeout set""" 25 | super().connection_made(transport) 26 | # Make sure we have a write timeout of expected size 27 | self.transport.write_timeout = WRITE_TIMEOUT 28 | 29 | def handle_line(self, line: str) -> None: 30 | raise RuntimeError("This should have been overloaded by RS232Transport") 31 | 32 | 33 | @dataclass 34 | class RS232Transport(BaseTransport): 35 | """Uses PySerials ReaderThread in the background to save us some pain""" 36 | 37 | serialdevice: Optional[serial.SerialBase] = field(default=None) 38 | _serialhandler: Optional[serial.threaded.ReaderThread] = field(default=None, repr=False) 39 | 40 | def __post_init__(self) -> None: 41 | """Initialize the transport""" 42 | if not self.serialdevice: 43 | raise ValueError("serialdevice must be given") 44 | self._serialhandler = serial.threaded.ReaderThread(self.serialdevice, RS232SerialProtocol) 45 | self._serialhandler.start() 46 | self._serialhandler.protocol.handle_line = self.message_received 47 | 48 | async def send_command(self, command: str) -> None: 49 | """Wrapper for write_line on the protocol with some sanity checks""" 50 | if not self._serialhandler or not self._serialhandler.is_alive(): 51 | raise RuntimeError("Serial handler not ready") 52 | async with self.lock: 53 | self._serialhandler.protocol.write_line(command) 54 | 55 | async def get_response(self) -> str: 56 | """Serial devices send responses without needing to be told to, just reads it""" 57 | # TODO: we probably have a race-condition possibility here, maybe always put all received 58 | # messages to a stack and return popleft ?? 59 | async with self.lock: 60 | response: Optional[str] = None 61 | 62 | # pylint: disable=R0801 63 | def set_response(message: str) -> None: 64 | """Callback for setting the response""" 65 | nonlocal response, self 66 | response = message 67 | self.blevent.set() 68 | 69 | self.blevent.clear() 70 | self.message_callback = set_response 71 | await asyncio.get_event_loop().run_in_executor(None, self.blevent.wait) 72 | self.message_callback = None 73 | return cast(str, response) 74 | 75 | async def abort_command(self) -> None: 76 | """Uses the break-command to issue "Device clear", from the SCPI documentation (for HP6632B): 77 | The status registers, the error queue, and all configuration states are left unchanged when a device 78 | clear message is received. Device clear performs the following actions: 79 | - The input and output buffers of the dc source are cleared. 80 | - The dc source is prepared to accept a new command string.""" 81 | if not self._serialhandler: 82 | raise RuntimeError("No serialhandler") 83 | if not self._serialhandler.serial: 84 | raise RuntimeError("No serialhandler.serial") 85 | async with self.lock: 86 | self._serialhandler.serial.send_break() 87 | 88 | async def quit(self) -> None: 89 | """Closes the port and background threads""" 90 | if not self._serialhandler: 91 | raise RuntimeError("No serialhandler") 92 | if not self._serialhandler.serial: 93 | raise RuntimeError("No serialhandler.serial") 94 | self._serialhandler.close() 95 | 96 | 97 | def get(serial_url: str, **serial_kwargs: Dict[str, Any]) -> RS232Transport: 98 | """Shorthand for creating the port from url and initializing the transport""" 99 | port = serial.serial_for_url(serial_url, **serial_kwargs) 100 | return RS232Transport(serialdevice=port) 101 | -------------------------------------------------------------------------------- /src/scpi/transports/tcp.py: -------------------------------------------------------------------------------- 1 | """TCP based transport""" 2 | 3 | from typing import Optional 4 | import asyncio 5 | from dataclasses import dataclass, field 6 | import logging 7 | 8 | 9 | from .baseclass import BaseTransport 10 | 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | @dataclass 16 | class TCPTransport(BaseTransport): 17 | """TCP based transport""" 18 | 19 | ipaddr: Optional[str] = field(default=None) 20 | port: Optional[int] = field(default=None) 21 | reader: Optional[asyncio.StreamReader] = field(default=None) 22 | writer: Optional[asyncio.StreamWriter] = field(default=None) 23 | 24 | async def open_connection(self, ipaddr: str, port: int) -> None: 25 | """Open a connection (also update the IP/port)""" 26 | self.reader, self.writer = await asyncio.open_connection(ipaddr, port) 27 | self.ipaddr = ipaddr 28 | self.port = port 29 | 30 | def __post_init__(self) -> None: 31 | """Call open_connection in an eventloop""" 32 | if self.ipaddr is None or self.port is None: 33 | raise ValueError("ipaddr and port must be given") 34 | 35 | async def send_command(self, command: str) -> None: 36 | """Write command to the stream""" 37 | if not self.writer: 38 | assert self.ipaddr 39 | assert self.port 40 | await self.open_connection(self.ipaddr, self.port) 41 | if not self.writer: 42 | raise RuntimeError("Writer not set") 43 | async with self.lock: 44 | LOGGER.debug("sending command: {}".format(command)) 45 | self.writer.write((command + "\r\n").encode()) 46 | await asyncio.sleep(0.05) 47 | await self.writer.drain() 48 | 49 | async def get_response(self) -> str: 50 | """Get response from the stream""" 51 | if not self.reader: 52 | assert self.ipaddr 53 | assert self.port 54 | await self.open_connection(self.ipaddr, self.port) 55 | if not self.reader: 56 | raise RuntimeError("Reader not set") 57 | async with self.lock: 58 | data = await self.reader.readline() 59 | res = data.decode() 60 | LOGGER.debug("Got response: {}".format(res.strip())) 61 | return res 62 | 63 | async def quit(self) -> None: 64 | """Closes the connection and background threads""" 65 | if not self.writer: 66 | raise RuntimeError("Writer not set") 67 | self.writer.close() 68 | await self.writer.wait_closed() 69 | 70 | async def abort_command(self) -> None: 71 | """This does not apply on TCP transport""" 72 | LOGGER.debug("TCP transport does not know what to do here") 73 | 74 | 75 | def get(ipaddr: str, port: int) -> TCPTransport: 76 | """Shorthand for creating the port from ip and port and initializing the transport""" 77 | return TCPTransport(ipaddr=ipaddr, port=port) 78 | -------------------------------------------------------------------------------- /src/scpi/wrapper.py: -------------------------------------------------------------------------------- 1 | """Helper class to allow using of device in traditional blocking style without having to deal with the ioloop""" 2 | 3 | from typing import Any 4 | import asyncio 5 | import functools 6 | import inspect 7 | import logging 8 | 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | class AIOWrapper: # pylint: disable=R0903 14 | """Wraps all coroutine methods into asyncio run_until_complete calls""" 15 | 16 | def __init__(self, to_be_wrapped: Any) -> None: 17 | """Init wrapper for device""" 18 | self._device = to_be_wrapped 19 | self._loop = asyncio.get_event_loop() 20 | for attr in functools.WRAPPER_ASSIGNMENTS: 21 | try: 22 | setattr(self, attr, getattr(self._device, attr)) 23 | except AttributeError: 24 | try: 25 | setattr(self.__class__, attr, getattr(self._device.__class__, attr)) 26 | except AttributeError: 27 | LOGGER.debug("Could not copy {}".format(attr)) 28 | 29 | def __getattr__(self, item: str) -> Any: 30 | """Get a memeber, if it's a coroutine autowrap it to eventloop run""" 31 | orig = getattr(self._device, item) 32 | if inspect.iscoroutinefunction(orig): 33 | 34 | @functools.wraps(orig) 35 | def wrapped(*args: Any, **kwargs: Any) -> Any: 36 | """Gets the waitable and tells the event loop to run it""" 37 | nonlocal self 38 | waitable = orig(*args, **kwargs) 39 | return self._loop.run_until_complete(waitable) 40 | 41 | return wrapped 42 | return orig 43 | 44 | def __dir__(self) -> Any: 45 | """Proxy the dir on the device""" 46 | return dir(self._device) 47 | 48 | def quit(self) -> None: 49 | """Calls the device.quit via loop and closes the loop""" 50 | self._loop.run_until_complete(self._device.quit()) 51 | self._loop.close() 52 | 53 | 54 | class DeviceWrapper(AIOWrapper): # pylint: disable=R0903 55 | """Legacy name for the AsyncIO wrapper class for backwards compatibility""" 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for datastreamservicelib""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """pytest automagics""" 2 | -------------------------------------------------------------------------------- /tests/test_scpi.py: -------------------------------------------------------------------------------- 1 | """Package level tests""" 2 | 3 | from scpi import __version__ 4 | 5 | 6 | def test_version() -> None: 7 | """Make sure version matches expected""" 8 | assert __version__ == "2.5.1" 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = true 3 | ;pylint has issue with Optional and Union type hints on 3.9, re-enable when fixed 4 | ;envlist = py39,py38,py37 5 | envlist = py38,py37 6 | 7 | [testenv] 8 | whitelist_externals = poetry 9 | passenv = SSH_AUTH_SOCK SKIP 10 | commands = 11 | poetry install -v 12 | poetry run docker/pre_commit_init.sh # this also checks all files 13 | poetry run pytest --junitxml=pytest-{envname}.xml tests/ 14 | poetry run mypy --strict src tests 15 | poetry run bandit -r src 16 | --------------------------------------------------------------------------------