pax_global_header00006660000000000000000000000064140412460120014504gustar00rootroot0000000000000052 comment=34fd43aa481248f60ae0579a65f8bc8d3cfa1c53 python-scpi-2.4.0/000077500000000000000000000000001404124601200137645ustar00rootroot00000000000000python-scpi-2.4.0/.bumpversion.cfg000066400000000000000000000006471404124601200171030ustar00rootroot00000000000000[bumpversion] current_version = 2.4.0 commit = False tag = False [bumpversion:file:pyproject.toml] search = version = "{current_version}" replace = version = "{new_version}" [bumpversion:file:src/scpi/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [bumpversion:file:tests/test_scpi.py] search = __version__ == "{current_version}" replace = __version__ == "{new_version}" python-scpi-2.4.0/.dockerignore000066400000000000000000000013461404124601200164440ustar00rootroot00000000000000# General good ideas to ignore .idea .git .cache .venv # docker build files (so just changing them does not invalidate *all* caches) .dockerignore Dockerfile # Byte-compiled / optimized / DLL files **/__pycache__/ **/*.py[cod] **/*$py.class # Distribution / packaging **/.Python **/build/ **/develop-eggs/ **/dist/ **/downloads/ **/eggs/ **/.eggs/ **/lib/ **/lib64/ **/parts/ **/sdist/ **/var/ **/wheels/ **/pip-wheel-metadata/ **/share/python-wheels/ **/*.egg-info/ **/.installed.cfg **/*.egg # Unit test / coverage reports **/htmlcov/ **/.tox/ **/.nox/ **/.coverage **/.coverage.* **/.cache **/nosetests.xml **/coverage.xml **/*.cover **/*.py,cover **/.hypothesis/ **/.pytest_cache/ # mypy **/.mypy_cache/ **/.dmypy.json **/dmypy.json python-scpi-2.4.0/.flake8000066400000000000000000000000371404124601200151370ustar00rootroot00000000000000[flake8] max-line-length = 120 python-scpi-2.4.0/.gitignore000066400000000000000000000034711404124601200157610ustar00rootroot00000000000000# IDE settings .idea # ci artefacts pytest*.xml # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ python-scpi-2.4.0/.pre-commit-config.yaml000066400000000000000000000032421404124601200202460ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks default_language_version: python: python3.7 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: no-commit-to-branch - id: check-executables-have-shebangs - id: check-ast - id: check-toml - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - id: check-case-conflict - id: check-json - id: check-merge-conflict - id: check-symlinks - id: pretty-format-json args: - --autofix - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black language_version: python3.7 - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.800 hooks: - id: mypy language: system args: [--strict] #, --ignore-missing-imports] - repo: https://github.com/pre-commit/mirrors-pylint rev: v2.4.4 hooks: - id: pylint language: system - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.1.7 hooks: - id: forbid-crlf - id: remove-crlf - id: forbid-tabs - id: remove-tabs - repo: https://github.com/PyCQA/bandit rev: 1.6.2 hooks: - id: bandit args: ["--skip=B101"] - repo: git://github.com/Lucas-C/pre-commit-hooks-markup rev: v1.0.0 hooks: - id: rst-linter - repo: https://github.com/Yelp/detect-secrets rev: v1.0.3 hooks: - id: detect-secrets language: system exclude: "poetry.lock" # args: ['--baseline', '.secrets.baseline'] python-scpi-2.4.0/Dockerfile000066400000000000000000000112551404124601200157620ustar00rootroot00000000000000# syntax=docker/dockerfile:1.1.7-experimental ############################################# # Tox testsuite for multiple python version # ############################################# FROM advian/tox-base:alpine as tox ARG PYTHON_VERSIONS="3.8.7 3.9.1 3.7.9 3.6.12" ARG POETRY_VERSION="1.1.4" RUN for pyver in $PYTHON_VERSIONS; do pyenv install -s $pyver; done \ && pyenv global $PYTHON_VERSIONS \ && poetry self update $POETRY_VERSION || pip install -U poetry==$POETRY_VERSION \ && python -m pip install -U tox \ && apk add --no-cache \ git \ && poetry install \ && docker/pre_commit_init.sh \ && true ###################### # Base builder image # ###################### FROM python:3.7-alpine as builder_base ENV \ # locale LC_ALL=C.UTF-8 \ # python: PYTHONFAULTHANDLER=1 \ PYTHONUNBUFFERED=1 \ PYTHONHASHSEED=random \ # pip: PIP_NO_CACHE_DIR=off \ PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DEFAULT_TIMEOUT=100 \ # poetry: POETRY_VERSION=1.1.4 RUN apk add --no-cache \ curl \ git \ bash \ build-base \ libffi-dev \ linux-headers \ openssl \ openssl-dev \ zeromq \ tini \ openssh-client \ cargo \ # githublab ssh && mkdir -p -m 0700 ~/.ssh && ssh-keyscan gitlab.com github.com | sort > ~/.ssh/known_hosts \ # Install poetry package manager their way (pypi package sometimes has issues) && curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 \ && echo 'source $HOME/.poetry/env' >>/root/.profile \ && source $HOME/.poetry/env \ # We're in a container, do not create useless virtualenvs && poetry config virtualenvs.create false \ && true SHELL ["/bin/bash", "-lc"] # Copy only requirements, to cache them in docker layer: WORKDIR /pysetup COPY ./poetry.lock ./pyproject.toml /pysetup/ # Install basic requirements (utilizing an internal docker wheelhouse if available) RUN --mount=type=ssh pip3 install wheel \ && poetry export -f requirements.txt --without-hashes -o /tmp/requirements.txt \ && pip3 wheel --wheel-dir=/tmp/wheelhouse --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 -r /tmp/requirements.txt \ && pip3 install --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 --find-links=/tmp/wheelhouse/ /tmp/wheelhouse/*.whl \ && true #################################### # Base stage for production builds # #################################### FROM builder_base as production_build # Copy entrypoint script COPY ./docker/entrypoint.sh /docker-entrypoint.sh # Only files needed by production setup COPY ./poetry.lock ./pyproject.toml ./README.rst ./src /app/ WORKDIR /app # Build the wheel package with poetry and add it to the wheelhouse RUN --mount=type=ssh poetry build -f wheel --no-interaction --no-ansi \ && cp dist/*.whl /tmp/wheelhouse \ && chmod a+x /docker-entrypoint.sh \ && true ######################### # Main production build # ######################### FROM python:3.7-alpine as production COPY --from=production_build /tmp/wheelhouse /tmp/wheelhouse COPY --from=production_build /docker-entrypoint.sh /docker-entrypoint.sh WORKDIR /app # Install system level deps for running the package (not devel versions for building wheels) # and install the wheels we built in the previous step. generate default config RUN --mount=type=ssh apk add --no-cache \ bash \ tini \ && chmod a+x /docker-entrypoint.sh \ && pip3 install --trusted-host 172.17.0.1 --find-links=http://172.17.0.1:3141 --find-links=/tmp/wheelhouse/ /tmp/wheelhouse/scpi-*.whl \ && rm -rf /tmp/wheelhouse/ \ # Do whatever else you need to && true ENTRYPOINT ["/sbin/tini", "--", "/docker-entrypoint.sh"] ##################################### # Base stage for development builds # ##################################### FROM builder_base as devel_build # Install deps WORKDIR /pysetup RUN --mount=type=ssh poetry install --no-interaction --no-ansi \ && true #0############ # Run tests # ############# FROM devel_build as test COPY . /app WORKDIR /app ENTRYPOINT ["/sbin/tini", "--", "docker/entrypoint-test.sh"] # Re run install to get the service itself installed RUN --mount=type=ssh poetry install --no-interaction --no-ansi \ && docker/pre_commit_init.sh \ && true ########### # Hacking # ########### FROM devel_build as devel_shell # Copy everything to the image COPY . /app WORKDIR /app RUN apk add --no-cache zsh \ && sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ && echo "source /root/.profile" >>/root/.zshrc \ && pip3 install git-up \ && true ENTRYPOINT ["/bin/zsh", "-l"] python-scpi-2.4.0/LICENSE000066400000000000000000000013771404124601200150010ustar00rootroot00000000000000Pure-python SCPI interface Copyright (C) 2014 Eero af Heurlin This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA python-scpi-2.4.0/README000077700000000000000000000000001404124601200163262README.rstustar00rootroot00000000000000python-scpi-2.4.0/README.rst000066400000000000000000000026651404124601200154640ustar00rootroot00000000000000==== scpi ==== New asyncio_ version. Only for Python 3.6 and above Since all the other wrappers either require VISA binary or are not generic (and do not implement the device I need) Basic idea here is to make transport-independent command sender/parser and a device baseclass that implements the common SCPI commands A device specific implementation can then add the device-specific commands. Pro tip for thos wishing to work on the code https://python-poetry.org/ .. _asyncio: https://docs.python.org/3/library/asyncio.html ## Usage Install the package to your virtualenv with poetry or from pip - Instatiate a transport (for GPIB you will need `GPIBDeviceTransport` to be able to use the device helper class) - Instatiate `SCPIProtocol` with the transport (optional, see below) - Instantiate `SCPIDevice` with the protocol (or as a shorthand: with the transport directly) - Use the asyncio eventloop to run the device methods (all of which are coroutines) Or if you're just playing around in the REPL use `AIOWrapper` to hide the eventloop handling for traditional non-concurrent approach. See the examples directory for more. TODO ---- Check Carrier-Detect for RS232 transport ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ in the RS232 transport check getCD to make sure the device is present before doing anything. CTS can also be checked even if hw flow control is not in use. Basically wait for it for X seconds and abort if not found python-scpi-2.4.0/docker/000077500000000000000000000000001404124601200152335ustar00rootroot00000000000000python-scpi-2.4.0/docker/entrypoint-test.sh000077500000000000000000000007511404124601200207650ustar00rootroot00000000000000#!/bin/bash -l set -e if [ "$#" -eq 0 ]; then # Kill cache, pytest complains about it if running local and docker tests in mapped volume find tests -type d -name '__pycache__' -print0 | xargs -0 rm -rf {} # Make sure the service itself is installed poetry install # Make sure pre-commit checks were not missed because reasons pre-commit run --all-files # Then run the tests pytest --junitxml=pytest.xml tests/ mypy --strict src tests bandit -r src else exec "$@" fi python-scpi-2.4.0/docker/entrypoint.sh000077500000000000000000000001731404124601200200060ustar00rootroot00000000000000#!/bin/bash -l set -e if [ "$#" -eq 0 ]; then # TODO: Put your actual program start here exec true else exec "$@" fi python-scpi-2.4.0/docker/pre_commit_init.sh000077500000000000000000000002531404124601200207530ustar00rootroot00000000000000#!/bin/bash -l if [ ! -d .git ] then git init git checkout -b precommit_init git add . fi set -e poetry run pre-commit install poetry run pre-commit run --all-files python-scpi-2.4.0/examples/000077500000000000000000000000001404124601200156025ustar00rootroot00000000000000python-scpi-2.4.0/examples/hp6632b_serial.py000077500000000000000000000010761404124601200206140ustar00rootroot00000000000000#!/usr/bin/env python3 """Example/test script for using the HP 6632B power supply via serial interface""" import atexit import os import sys from scpi.devices import hp6632b from scpi.wrapper import AIOWrapper if __name__ == "__main__": if len(sys.argv) < 2: print(f"run with python -i {__file__} /dev/ttyUSB0") sys.exit(1) # Then put to interactive mode os.environ["PYTHONINSPECT"] = "1" aiodev = hp6632b.rs232(sys.argv[1], rtscts=True, baudrate=9600) dev = AIOWrapper(aiodev) atexit.register(dev.quit) print(dev.identify()) python-scpi-2.4.0/examples/prologix_usb.py000077500000000000000000000026711404124601200207010ustar00rootroot00000000000000#!/usr/bin/env python3 """Example/test script for use the with Prologix USB GPIB interface""" import atexit import os import sys from scpi import SCPIDevice from scpi.transports.gpib import prologix from scpi.wrapper import AIOWrapper from scpi.devices.hp6632b import HP6632B from scpi.devices.generic import MultiMeter if __name__ == "__main__": if len(sys.argv) < 2: print(f"run with python -i {__file__} /dev/ttyUSB0") sys.exit(1) # Then put to interactive mode os.environ["PYTHONINSPECT"] = "1" # Get the low-level GPIB transport aiogpib = prologix.get(sys.argv[1]) # And the mapper that handlers asyncio transparently gpib = AIOWrapper(aiogpib) atexit.register(gpib.quit) print("*** Scanning bus for devices ***") devlist = gpib.scan_devices() devdict = {} for addr, idstr in devlist: man, model, _, _ = idstr.split(",") # Get device specific transport instance dtransport = aiogpib.get_device_transport(addr) # Get the device class with the transport aiodev = SCPIDevice(dtransport) if man == "HEWLETT-PACKARD": if model == "6632B": aiodev = HP6632B(dtransport) if model == "34401A": aiodev = MultiMeter(dtransport) # And get the mapper that handles asyncio transparently devdict[addr] = AIOWrapper(aiodev) print("Added {:s} as devdict[{:d}]".format(idstr, addr)) python-scpi-2.4.0/examples/tdklambda_tcp.py000066400000000000000000000010071404124601200207430ustar00rootroot00000000000000""" Created on febrary 21 2020. @author: qmor """ import atexit import os import time from scpi.devices import TDKLambdaZPlus from scpi.wrapper import AIOWrapper if __name__ == "__main__": # Then put to interactive mode os.environ["PYTHONINSPECT"] = "1" aiodev = TDKLambdaZPlus.tcp("192.168.3.34", 8003) dev = AIOWrapper(aiodev) atexit.register(dev.quit) dev.identify() while True: dev.query_voltage() dev.query_current() dev.query_output() time.sleep(1) python-scpi-2.4.0/poetry.lock000066400000000000000000001553601404124601200161720ustar00rootroot00000000000000[[package]] name = "appdirs" version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = "*" [[package]] name = "aspy.yaml" version = "1.3.0" description = "A few extensions to pyyaml." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyyaml = "*" [[package]] name = "astroid" version = "2.5.1" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} wrapt = ">=1.11,<1.13" [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" version = "20.3.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] docs = ["furo", "sphinx", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] [[package]] name = "bandit" version = "1.7.0" description = "Security oriented static analyser for python code." category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=1.0.1" PyYAML = ">=5.3.1" six = ">=1.10.0" stevedore = ">=1.20.0" [[package]] name = "black" version = "20.8b1" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] appdirs = "*" click = ">=7.1.2" dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" pathspec = ">=0.6,<1" regex = ">=2020.1.8" toml = ">=0.10.1" typed-ast = ">=1.4.0" typing-extensions = ">=3.7.4" [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "bump2version" version = "1.0.1" description = "Version-bump your software with a single command!" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "certifi" version = "2020.12.5" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = "*" [[package]] name = "cfgv" version = "3.0.0" description = "Validate configuration and produce human readable error messages." category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "chardet" version = "4.0.0" description = "Universal encoding detector for Python 2 and 3" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "click" version = "7.1.2" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" version = "5.5" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.dependencies] toml = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["toml"] [[package]] name = "dataclasses" version = "0.8" description = "A backport of the dataclasses module for Python 3.6" category = "dev" optional = false python-versions = ">=3.6, <3.7" [[package]] name = "detect-secrets" version = "1.0.3" description = "Tool for detecting secrets in the codebase" category = "dev" optional = false python-versions = "*" [package.dependencies] pyyaml = "*" requests = "*" [package.extras] word_list = ["pyahocorasick"] [[package]] name = "distlib" version = "0.3.1" description = "Distribution utilities" category = "dev" optional = false python-versions = "*" [[package]] name = "filelock" version = "3.0.12" description = "A platform independent file lock." category = "dev" optional = false python-versions = "*" [[package]] name = "gitdb" version = "4.0.7" description = "Git Object Database" category = "dev" optional = false python-versions = ">=3.4" [package.dependencies] smmap = ">=3.0.1,<5" [[package]] name = "gitpython" version = "3.1.14" description = "Python Git Library" category = "dev" optional = false python-versions = ">=3.4" [package.dependencies] gitdb = ">=4.0.1,<5" [[package]] name = "identify" version = "1.6.2" description = "File identification library for Python" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.extras] license = ["editdistance"] [[package]] name = "idna" version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" version = "3.8.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "importlib-resources" version = "5.1.2" description = "Read resources from Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"] [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = "*" [[package]] name = "isort" version = "5.8.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false python-versions = ">=3.6,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] [[package]] name = "lazy-object-proxy" version = "1.6.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = "*" [[package]] name = "mypy" version = "0.800" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" typed-ast = ">=1.4.0,<1.5.0" typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "dev" optional = false python-versions = "*" [[package]] name = "nodeenv" version = "1.5.0" description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = "*" [[package]] name = "packaging" version = "20.9" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" [[package]] name = "pathspec" version = "0.8.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pbr" version = "5.5.1" description = "Python Build Reasonableness" category = "dev" optional = false python-versions = ">=2.6" [[package]] name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] [[package]] name = "pre-commit" version = "1.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] "aspy.yaml" = "*" cfgv = ">=2.0.0" identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} importlib-resources = {version = "*", markers = "python_version < \"3.7\""} nodeenv = ">=0.11.1" pyyaml = "*" six = "*" toml = "*" virtualenv = ">=15.2" [[package]] name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pylint" version = "2.7.2" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] astroid = ">=2.5.1,<2.6" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" toml = ">=0.7.1" [package.extras] docs = ["sphinx (==3.5.1)", "python-docs-theme (==2020.12)"] [[package]] name = "pyparsing" version = "2.4.7" description = "Python parsing module" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pyserial" version = "3.5" description = "Python Serial Port Extension" category = "main" optional = false python-versions = "*" [package.extras] cp2110 = ["hidapi"] [[package]] name = "pytest" version = "6.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<1.0.0a1" py = ">=1.8.2" toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.14.0" description = "Pytest support for asyncio." category = "dev" optional = false python-versions = ">= 3.5" [package.dependencies] pytest = ">=5.4.0" [package.extras] testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "2.11.1" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] coverage = ">=5.2.1" pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] [[package]] name = "pyyaml" version = "5.4.1" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" version = "2021.3.17" description = "Alternative regular expression module, to replace re." category = "dev" optional = false python-versions = "*" [[package]] name = "requests" version = "2.25.1" description = "Python HTTP for Humans." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" chardet = ">=3.0.2,<5" idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.27" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] name = "six" version = "1.15.0" description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "smmap" version = "4.0.0" description = "A pure Python implementation of a sliding window memory map manager" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "stevedore" version = "3.3.0" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typed-ast" version = "1.4.2" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = "*" [[package]] name = "typing-extensions" version = "3.7.4.3" description = "Backported and Experimental Type Hints for Python 3.5+" category = "dev" optional = false python-versions = "*" [[package]] name = "urllib3" version = "1.26.4" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "virtualenv" version = "20.4.3" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.dependencies] appdirs = ">=1.4.3,<2" distlib = ">=0.3.1,<1" filelock = ">=3.0.0,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] [[package]] name = "wrapt" version = "1.12.1" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false python-versions = "*" [[package]] name = "zipp" version = "3.4.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "^3.6" content-hash = "999d8f0f93a81965b901feacb700aecc3ad5e377b3211a239205ff11ebba97fe" [metadata.files] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] "aspy.yaml" = [ {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"}, {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"}, ] astroid = [ {file = "astroid-2.5.1-py3-none-any.whl", hash = "sha256:21d735aab248253531bb0f1e1e6d068f0ee23533e18ae8a6171ff892b98297cf"}, {file = "astroid-2.5.1.tar.gz", hash = "sha256:cfc35498ee64017be059ceffab0a25bedf7548ab76f2bea691c5565896e7128d"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, ] bandit = [ {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, ] black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] bump2version = [ {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] cfgv = [ {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] detect-secrets = [ {file = "detect_secrets-1.0.3-py2.py3-none-any.whl", hash = "sha256:715d2351d1f0f18cf4e7861e74b4135af909652c07a79501abac86400c9d2164"}, {file = "detect_secrets-1.0.3.tar.gz", hash = "sha256:e2ea3f6b687a4d6ed4321f93ea7314ceceb383c643c85864ee38c0591ec42112"}, ] distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] gitdb = [ {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, ] gitpython = [ {file = "GitPython-3.1.14-py3-none-any.whl", hash = "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b"}, {file = "GitPython-3.1.14.tar.gz", hash = "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"}, ] identify = [ {file = "identify-1.6.2-py2.py3-none-any.whl", hash = "sha256:8f9879b5b7cca553878d31548a419ec2f227d3328da92fe8202bc5e546d5cbc3"}, {file = "identify-1.6.2.tar.gz", hash = "sha256:1c2014f6985ed02e62b2e6955578acf069cb2c54859e17853be474bfe7e13bed"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] importlib-metadata = [ {file = "importlib_metadata-3.8.0-py3-none-any.whl", hash = "sha256:ce8ae4d781be528001aad74ecfec7512641e36f1272effeea55164e47ae26841"}, {file = "importlib_metadata-3.8.0.tar.gz", hash = "sha256:9dd89454add8894cc93619426949a58bdad9c1ef42822e46b884e7fc2d2a901e"}, ] importlib-resources = [ {file = "importlib_resources-5.1.2-py3-none-any.whl", hash = "sha256:ebab3efe74d83b04d6bf5cd9a17f0c5c93e60fb60f30c90f56265fce4682a469"}, {file = "importlib_resources-5.1.2.tar.gz", hash = "sha256:642586fc4740bd1cad7690f836b3321309402b20b332529f25617ff18e8e1370"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] mypy = [ {file = "mypy-0.800-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:e1c84c65ff6d69fb42958ece5b1255394714e0aac4df5ffe151bc4fe19c7600a"}, {file = "mypy-0.800-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:947126195bfe4709c360e89b40114c6746ae248f04d379dca6f6ab677aa07641"}, {file = "mypy-0.800-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:b95068a3ce3b50332c40e31a955653be245666a4bc7819d3c8898aa9fb9ea496"}, {file = "mypy-0.800-cp35-cp35m-win_amd64.whl", hash = "sha256:ca7ad5aed210841f1e77f5f2f7d725b62c78fa77519312042c719ed2ab937876"}, {file = "mypy-0.800-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e32b7b282c4ed4e378bba8b8dfa08e1cfa6f6574067ef22f86bee5b1039de0c9"}, {file = "mypy-0.800-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e497a544391f733eca922fdcb326d19e894789cd4ff61d48b4b195776476c5cf"}, {file = "mypy-0.800-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:5615785d3e2f4f03ab7697983d82c4b98af5c321614f51b8f1034eb9ebe48363"}, {file = "mypy-0.800-cp36-cp36m-win_amd64.whl", hash = "sha256:2b216eacca0ec0ee124af9429bfd858d5619a0725ee5f88057e6e076f9eb1a7b"}, {file = "mypy-0.800-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e3b8432f8df19e3c11235c4563a7250666dc9aa7cdda58d21b4177b20256ca9f"}, {file = "mypy-0.800-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d16c54b0dffb861dc6318a8730952265876d90c5101085a4bc56913e8521ba19"}, {file = "mypy-0.800-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d2fc8beb99cd88f2d7e20d69131353053fbecea17904ee6f0348759302c52fa"}, {file = "mypy-0.800-cp37-cp37m-win_amd64.whl", hash = "sha256:aa9d4901f3ee1a986a3a79fe079ffbf7f999478c281376f48faa31daaa814e86"}, {file = "mypy-0.800-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:319ee5c248a7c3f94477f92a729b7ab06bf8a6d04447ef3aa8c9ba2aa47c6dcf"}, {file = "mypy-0.800-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:74f5aa50d0866bc6fb8e213441c41e466c86678c800700b87b012ed11c0a13e0"}, {file = "mypy-0.800-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a301da58d566aca05f8f449403c710c50a9860782148332322decf73a603280b"}, {file = "mypy-0.800-cp38-cp38-win_amd64.whl", hash = "sha256:b9150db14a48a8fa114189bfe49baccdff89da8c6639c2717750c7ae62316738"}, {file = "mypy-0.800-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5fdf935a46aa20aa937f2478480ebf4be9186e98e49cc3843af9a5795a49a25"}, {file = "mypy-0.800-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6f8425fecd2ba6007e526209bb985ce7f49ed0d2ac1cc1a44f243380a06a84fb"}, {file = "mypy-0.800-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5ff616787122774f510caeb7b980542a7cc2222be3f00837a304ea85cd56e488"}, {file = "mypy-0.800-cp39-cp39-win_amd64.whl", hash = "sha256:90b6f46dc2181d74f80617deca611925d7e63007cf416397358aa42efb593e07"}, {file = "mypy-0.800-py3-none-any.whl", hash = "sha256:3e0c159a7853e3521e3f582adb1f3eac66d0b0639d434278e2867af3a8c62653"}, {file = "mypy-0.800.tar.gz", hash = "sha256:e0202e37756ed09daf4b0ba64ad2c245d357659e014c3f51d8cd0681ba66940a"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] nodeenv = [ {file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"}, {file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"}, ] packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] pbr = [ {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"}, {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] pre-commit = [ {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"}, {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pylint = [ {file = "pylint-2.7.2-py3-none-any.whl", hash = "sha256:d09b0b07ba06bcdff463958f53f23df25e740ecd81895f7d2699ec04bbd8dc3b"}, {file = "pylint-2.7.2.tar.gz", hash = "sha256:0e21d3b80b96740909d77206d741aa3ce0b06b41be375d92e1f3244a274c1f8a"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pyserial = [ {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, ] pytest = [ {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.14.0.tar.gz", hash = "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"}, {file = "pytest_asyncio-0.14.0-py3-none-any.whl", hash = "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d"}, ] pytest-cov = [ {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] regex = [ {file = "regex-2021.3.17-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b97ec5d299c10d96617cc851b2e0f81ba5d9d6248413cd374ef7f3a8871ee4a6"}, {file = "regex-2021.3.17-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cb4ee827857a5ad9b8ae34d3c8cc51151cb4a3fe082c12ec20ec73e63cc7c6f0"}, {file = "regex-2021.3.17-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:633497504e2a485a70a3268d4fc403fe3063a50a50eed1039083e9471ad0101c"}, {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a59a2ee329b3de764b21495d78c92ab00b4ea79acef0f7ae8c1067f773570afa"}, {file = "regex-2021.3.17-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f85d6f41e34f6a2d1607e312820971872944f1661a73d33e1e82d35ea3305e14"}, {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4651f839dbde0816798e698626af6a2469eee6d9964824bb5386091255a1694f"}, {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:39c44532d0e4f1639a89e52355b949573e1e2c5116106a395642cbbae0ff9bcd"}, {file = "regex-2021.3.17-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3d9a7e215e02bd7646a91fb8bcba30bc55fd42a719d6b35cf80e5bae31d9134e"}, {file = "regex-2021.3.17-cp36-cp36m-win32.whl", hash = "sha256:159fac1a4731409c830d32913f13f68346d6b8e39650ed5d704a9ce2f9ef9cb3"}, {file = "regex-2021.3.17-cp36-cp36m-win_amd64.whl", hash = "sha256:13f50969028e81765ed2a1c5fcfdc246c245cf8d47986d5172e82ab1a0c42ee5"}, {file = "regex-2021.3.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9d8d286c53fe0cbc6d20bf3d583cabcd1499d89034524e3b94c93a5ab85ca90"}, {file = "regex-2021.3.17-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:201e2619a77b21a7780580ab7b5ce43835e242d3e20fef50f66a8df0542e437f"}, {file = "regex-2021.3.17-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d47d359545b0ccad29d572ecd52c9da945de7cd6cf9c0cfcb0269f76d3555689"}, {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ea2f41445852c660ba7c3ebf7d70b3779b20d9ca8ba54485a17740db49f46932"}, {file = "regex-2021.3.17-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:486a5f8e11e1f5bbfcad87f7c7745eb14796642323e7e1829a331f87a713daaa"}, {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:18e25e0afe1cf0f62781a150c1454b2113785401ba285c745acf10c8ca8917df"}, {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a2ee026f4156789df8644d23ef423e6194fad0bc53575534101bb1de5d67e8ce"}, {file = "regex-2021.3.17-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4c0788010a93ace8a174d73e7c6c9d3e6e3b7ad99a453c8ee8c975ddd9965643"}, {file = "regex-2021.3.17-cp37-cp37m-win32.whl", hash = "sha256:575a832e09d237ae5fedb825a7a5bc6a116090dd57d6417d4f3b75121c73e3be"}, {file = "regex-2021.3.17-cp37-cp37m-win_amd64.whl", hash = "sha256:8e65e3e4c6feadf6770e2ad89ad3deb524bcb03d8dc679f381d0568c024e0deb"}, {file = "regex-2021.3.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0df9a0ad2aad49ea3c7f65edd2ffb3d5c59589b85992a6006354f6fb109bb18"}, {file = "regex-2021.3.17-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b98bc9db003f1079caf07b610377ed1ac2e2c11acc2bea4892e28cc5b509d8d5"}, {file = "regex-2021.3.17-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:808404898e9a765e4058bf3d7607d0629000e0a14a6782ccbb089296b76fa8fe"}, {file = "regex-2021.3.17-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:5770a51180d85ea468234bc7987f5597803a4c3d7463e7323322fe4a1b181578"}, {file = "regex-2021.3.17-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:976a54d44fd043d958a69b18705a910a8376196c6b6ee5f2596ffc11bff4420d"}, {file = "regex-2021.3.17-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:63f3ca8451e5ff7133ffbec9eda641aeab2001be1a01878990f6c87e3c44b9d5"}, {file = "regex-2021.3.17-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bcd945175c29a672f13fce13a11893556cd440e37c1b643d6eeab1988c8b209c"}, {file = "regex-2021.3.17-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:3d9356add82cff75413bec360c1eca3e58db4a9f5dafa1f19650958a81e3249d"}, {file = "regex-2021.3.17-cp38-cp38-win32.whl", hash = "sha256:f5d0c921c99297354cecc5a416ee4280bd3f20fd81b9fb671ca6be71499c3fdf"}, {file = "regex-2021.3.17-cp38-cp38-win_amd64.whl", hash = "sha256:14de88eda0976020528efc92d0a1f8830e2fb0de2ae6005a6fc4e062553031fa"}, {file = "regex-2021.3.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c2e364491406b7888c2ad4428245fc56c327e34a5dfe58fd40df272b3c3dab3"}, {file = "regex-2021.3.17-cp39-cp39-manylinux1_i686.whl", hash = "sha256:8bd4f91f3fb1c9b1380d6894bd5b4a519409135bec14c0c80151e58394a4e88a"}, {file = "regex-2021.3.17-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:882f53afe31ef0425b405a3f601c0009b44206ea7f55ee1c606aad3cc213a52c"}, {file = "regex-2021.3.17-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:07ef35301b4484bce843831e7039a84e19d8d33b3f8b2f9aab86c376813d0139"}, {file = "regex-2021.3.17-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:360a01b5fa2ad35b3113ae0c07fb544ad180603fa3b1f074f52d98c1096fa15e"}, {file = "regex-2021.3.17-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:709f65bb2fa9825f09892617d01246002097f8f9b6dde8d1bb4083cf554701ba"}, {file = "regex-2021.3.17-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:c66221e947d7207457f8b6f42b12f613b09efa9669f65a587a2a71f6a0e4d106"}, {file = "regex-2021.3.17-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c782da0e45aff131f0bed6e66fbcfa589ff2862fc719b83a88640daa01a5aff7"}, {file = "regex-2021.3.17-cp39-cp39-win32.whl", hash = "sha256:dc9963aacb7da5177e40874585d7407c0f93fb9d7518ec58b86e562f633f36cd"}, {file = "regex-2021.3.17-cp39-cp39-win_amd64.whl", hash = "sha256:a0d04128e005142260de3733591ddf476e4902c0c23c1af237d9acf3c96e1b38"}, {file = "regex-2021.3.17.tar.gz", hash = "sha256:4b8a1fb724904139149a43e172850f35aa6ea97fb0545244dc0b805e0154ed68"}, ] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] smmap = [ {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, ] stevedore = [ {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typed-ast = [ {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, ] typing-extensions = [ {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] urllib3 = [ {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, ] virtualenv = [ {file = "virtualenv-20.4.3-py2.py3-none-any.whl", hash = "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"}, {file = "virtualenv-20.4.3.tar.gz", hash = "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107"}, ] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] zipp = [ {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, ] python-scpi-2.4.0/pyproject.toml000066400000000000000000000025601404124601200167030ustar00rootroot00000000000000[tool.poetry] name = "scpi" version = "2.4.0" description = "Basic idea here is to make transport-independent command sender/parser and a device baseclass that implements the common SCPI commands" authors = ["Eero af Heurlin "] homepage = "https://github.com/rambo/python-scpi/" repository = "https://github.com/rambo/python-scpi/" license = "LGPL" readme = "README.rst" [tool.black] line-length = 120 target-version = ['py37'] exclude = ''' ( /( \.eggs # exclude a few common directories in the | \.git # root of the project | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ | __pycache__ ) ''' [tool.pytest.ini_options] junit_family="xunit2" addopts="--cov=scpi --cov-branch" [tool.pylint.format] max-line-length = 120 [tool.pylint.messages_control] disable=["fixme", "W1202", "C0330"] [tool.coverage.run] omit = ["tests/*"] branch = true [tool.poetry.dependencies] python = "^3.6" pyserial = "^3.4" [tool.poetry.dev-dependencies] pytest = "^6.2" coverage = {version = "^5.4", extras = ["toml"]} pytest-cov = "^2.11" pylint = "^2.5" black = "=20.8b1" bandit = "^1.6" mypy = "^0.800" pre-commit = "^1.20" pytest-asyncio = "^0.14" bump2version = "^1.0" detect-secrets = "^1.0" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" python-scpi-2.4.0/src/000077500000000000000000000000001404124601200145535ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/000077500000000000000000000000001404124601200155115ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/__init__.py000066400000000000000000000006521404124601200176250ustar00rootroot00000000000000"""SCPI module, the scpi class implements the base command set, devices may extend it. transports are separate from devices (so you can use for example hp6632b with either serial port or GPIB)""" __version__ = "2.4.0" # NOTE Use `bump2version --config-file patch` to bump versions correctly from .scpi import SCPIProtocol, SCPIDevice from .errors import CommandError __all__ = ["SCPIProtocol", "CommandError", "SCPIDevice"] python-scpi-2.4.0/src/scpi/devices/000077500000000000000000000000001404124601200171335ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/devices/TDKLambdaZPlus.py000066400000000000000000000215171404124601200222340ustar00rootroot00000000000000"""TDK Lambda power supplies""" # pylint: disable=C0103 from typing import Optional, Union, Any from dataclasses import dataclass, field import decimal import logging import serial as pyserial # type: ignore from ..scpi import SCPIDevice, SCPIProtocol from ..transports.rs232 import RS232Transport from ..transports.tcp import get as get_tcp from .generic import PowerSupply StrIntCombo = Union[str, int] LOGGER = logging.getLogger(__name__) class TDKSCPI(SCPIDevice): """Baseclass for TDK SCPI devices""" async def set_power_on_status_clear(self, setting: StrIntCombo) -> None: """ Set the Power-On Status Clear setting. * ON/1/True - This choice enables the power-on clearing of the listed registers * OFF/0/false - This choice disable the clearing of the listed registers and they retain their status when a power-on condition occurs """ setting = str(setting).upper() if setting in ("1", "ON", "TRUE"): setting = "1" elif setting in ("0", "OFF", "FALSE"): setting = "0" else: raise ValueError await super().set_power_on_status_clear(setting) async def restore_state(self, state: int) -> None: """ Restores the power supply to a state previously stored in memory by *SAV command. """ state = int(state) if state not in (1, 2, 3, 4): raise ValueError("invalid state") await super().restore_state(state) async def save_state(self, state: int) -> None: """ The SAV command saves all applied configuration settings. """ state = int(state) if state not in (1, 2, 3, 4): raise ValueError("Invalid state") await super().save_state(state) async def power_on_state(self, setting: StrIntCombo) -> None: """ Set the power-on behavior of the system * 1 - AUTO - The power supply output will return to its previous value when the latching fault condition is removed or to the stored value after AC recycle. * 0 - SAFE - The power supply output will remain Off after the fault condition is removed or after AC recycle. """ setting = str(setting).upper() if setting in ("1", "ON", "TRUE"): setting = "1" elif setting in ("0", "OFF", "FALSE"): setting = "0" else: raise ValueError await super().power_on_state(setting) @dataclass class TDKLambdaZplus(PowerSupply, TDKSCPI): """TDK Lambda Z+ power supply""" voltage_limit: float = field(default=20.0) current_limit: float = field(default=10.0) async def measure_current(self, extra_params: str = "") -> decimal.Decimal: """ Returns the actual output current in amps. extra_params: String to append to the command. The only valid command for this device is ":DC" """ resp = await self.ask(f"MEAS:CURR{extra_params}?") return decimal.Decimal(resp) async def measure_voltage(self, extra_params: str = "") -> decimal.Decimal: """ Returns the actual output voltage in volts. extra_params: String to append to the command. The only valid command for this device is ":DC" """ resp = await self.ask(f"MEAS:VOLT{extra_params}?") return decimal.Decimal(resp) async def measure_power(self, extra_params: str = "") -> decimal.Decimal: """ Returns the actual output power in watts. extra_params: String to append to the command. The only valid command for this device is ":DC" """ resp = await self.ask(f"MEAS:POW{extra_params}?") return decimal.Decimal(resp) async def select_active_instrument(self, select_id: int) -> None: """ Select the power supply for communication. id: the ID of the power supply to select. int from 1-31 """ _id = int(select_id) if _id < 1 or _id > 31: raise ValueError("id %d is outside of the valid id range" % _id) await self.command(f"INSTrument:NSELect {_id:d}") async def query_active_instrument(self) -> int: """ Returns the ID of the active instrument. """ resp = await self.ask("INSTrument:NSELect?") return int(resp) async def couple_mode(self, couple: str = "NONE") -> None: """ Couple for all Z+ power supplies. """ couple = couple.upper() if couple in ("NONE", "ALL"): await self.command("INSTrument:COUPle %s" % couple) else: raise ValueError("Argument '%s' not valid for INST:COUP" % couple) async def set_voltage_protection(self, volts: Any) -> None: """ Set over-voltage protection level. """ _volts = str(volts).upper() # FIXME: shouldn't we pass _volts here ?? Also what are valid types/values ?? await self.command("VOLTage:PROTection:LEVel") async def query_voltage_protection(self, mode: Optional[str] = None) -> decimal.Decimal: """ Query the voltage protection level. Depending on mode, returns the current level, the minimum level, or the maximum level. mode: Which value to return. - None (default): returns the current voltage protection level - "MAX": returns the maximum possible voltage protection level - "MIN": returns the minimum possible voltage protection level, approx. 105% the current voltage setting """ if mode is None: resp = await self.ask("VOLTage:PROTection:LEVel?") else: resp = await self.ask(f"VOLTage:PROTection:LEVel? {mode}") return decimal.Decimal(resp) async def flash_display(self, setting: StrIntCombo) -> None: """ Make the front panel voltage and Current displays flash. """ setting = str(setting).upper() if setting in ("1", "ON", "TRUE"): setting = "1" elif setting in ("0", "OFF", "FALSE"): setting = "0" else: raise ValueError await self.command(f"DISPlay:FLASh {setting}") async def global_enable(self, setting: StrIntCombo) -> None: """ Set enable status of all units. """ setting = str(setting).upper() if setting in ("1", "ON", "TRUE"): setting = "1" elif setting in ("0", "OFF", "FALSE"): setting = "0" else: raise ValueError await self.command(f"GLOBal:OUTPut:STATe {setting}") async def global_set_voltage(self, volts: float) -> None: """ Set enable status of all units. """ _volts = str(volts) # FIXME: probably just using volts:f would work fine await self.command(f"GLOBal:VOLTage:AMPLitude {_volts}") async def global_reset(self) -> None: """ Reset all units. """ await self.command("GLOBal:*RST") async def set_voltage(self, millivolts: float, extra_params: str = "") -> None: """ Sets the desired output voltage (but does not auto-enable outputs) in millivolts, pass extra_params string to append to the command (like ":PROT") Limited to five percent greater than the voltage limit of the unit. """ if millivolts / 1000.0 > 1.05 * self.voltage_limit or millivolts < 0: raise ValueError await super().set_voltage(millivolts, extra_params=extra_params) async def set_current(self, milliamps: float, extra_params: str = "") -> None: """ Sets the desired output current (but does not auto-enable outputs) in milliamps, pass extra_params string to append to the command (like ":TRIG") Limited to five percent greater than the current limit of the unit. """ if milliamps / 1000.0 > 1.05 * self.current_limit or milliamps < 0: raise ValueError await super().set_current(milliamps, extra_params=extra_params) def tcp(ipaddr: str, port: int) -> TDKLambdaZplus: """Quick helper to connect via TCP""" transport = get_tcp(ipaddr, port) protocol = SCPIProtocol(transport) dev = TDKLambdaZplus(protocol) return dev def serial(serial_url: str, baudrate: int = 9600) -> TDKLambdaZplus: """ Quick helper to connect via serial """ port = pyserial.serial_for_url( serial_url, baudrate=baudrate, bytesize=8, parity=pyserial.PARITY_NONE, stopbits=1, xonxoff=False, rtscts=False, dsrdtr=False, timeout=10, ) transport = RS232Transport(serialdevice=port) protocol = SCPIProtocol(transport) dev = TDKLambdaZplus(protocol) return dev python-scpi-2.4.0/src/scpi/devices/__init__.py000066400000000000000000000000001404124601200212320ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/devices/generic.py000066400000000000000000000057411404124601200211300ustar00rootroot00000000000000"""Mixins for different device functionalities""" import decimal from ..scpi import SCPIDevice class MultiMeter(SCPIDevice): """Multimeter related features""" async def measure_voltage(self, extra_params: str = "") -> decimal.Decimal: """Returns the measured (scalar) actual output voltage (in volts), pass extra_params string to append to the command (like ":ACDC")""" resp = await self.ask(f"MEAS:SCAL:VOLT{extra_params}?") return decimal.Decimal(resp) async def measure_current(self, extra_params: str = "") -> decimal.Decimal: """Returns the measured (scalar) actual output current (in amps), pass extra_params string to append to the command (like ":ACDC")""" resp = await self.ask(f"MEAS:SCAL:CURR{extra_params}?") return decimal.Decimal(resp) async def set_measure_current_max(self, amps: float) -> None: """Sets the upper bound (in amps) of current to measure, on some devices low-current accuracy can be increased by keeping this low""" await self.command(f"SENS:CURR:RANG {amps:f}") async def query_measure_current_max(self) -> decimal.Decimal: """Returns the upper bound (in amps) of current to measure, this is not neccessarily same number as set with set_measure_current_max""" resp = await self.ask("SENS:CURR:RANG?") return decimal.Decimal(resp) class PowerSupply(SCPIDevice): """Power supply related features""" async def set_voltage(self, millivolts: float, extra_params: str = "") -> None: """Sets the desired output voltage (but does not auto-enable outputs) in millivolts, pass extra_params string to append to the command (like ":PROT")""" await self.command(f"SOUR:VOLT{extra_params} {millivolts:f} MV") async def query_voltage(self, extra_params: str = "") -> decimal.Decimal: """Returns the set output voltage (in volts), pass extra_params string to append to the command (like ":PROT")""" resp = await self.ask(f"SOUR:VOLT{extra_params}?") return decimal.Decimal(resp) async def set_current(self, milliamps: float, extra_params: str = "") -> None: """Sets the desired output current (but does not auto-enable outputs) in milliamps, pass extra_params string to append to the command (like ":TRIG")""" await self.command(f"SOUR:CURR{extra_params} {milliamps:f} MA") async def query_current(self, extra_params: str = "") -> decimal.Decimal: """Returns the set output current (in amps), pass extra_params string to append to the command (like ":TRIG")""" resp = await self.ask(f"SOUR:CURR{extra_params}?") return decimal.Decimal(resp) async def set_output(self, state: bool) -> None: """Enables/disables output""" await self.command(f"OUTP:STAT {state:d}") async def query_output(self) -> bool: """Returns the output state""" resp = await self.ask("OUTP:STAT?") return bool(int(resp)) python-scpi-2.4.0/src/scpi/devices/hp6632b.py000066400000000000000000000105521404124601200206020ustar00rootroot00000000000000"""HP/Agilent 3362B specific device implementation and helpers""" from typing import Any import logging import decimal from ..scpi import SCPIDevice, SCPIProtocol from ..transports.rs232 import RS232Transport from ..transports.rs232 import get as get_rs232 from .generic import MultiMeter, PowerSupply LOGGER = logging.getLogger(__name__) class HP6632B(PowerSupply, MultiMeter, SCPIDevice): """Adds the HP/Agilent 3362B specific SCPI commands as methods""" async def set_low_current_mode(self, state: bool) -> None: """The low-current mode is enabled by setting the range to (max) 20mA, anything over that is high-current mode. This model has max 5A output""" if state: return await self.set_measure_current_max(0.020) return await self.set_measure_current_max(5.0) async def query_low_current_mode(self) -> bool: """Returns boolean indicating whether we are in low or high current mode""" max_current = await self.query_measure_current_max() if max_current <= 0.020: return True return False async def measure_current_autorange(self, extra_params: str = "") -> decimal.Decimal: """Measures the current, then make sure we are running on the correct measurement range and if not switch range and measure again""" ret = await self.measure_current(extra_params) if abs(ret) < 0.020: # We need to be in low-current mode if not await self.query_low_current_mode(): # Enter low current mode and measure again await self.set_low_current_mode(True) return await self.measure_current(extra_params) return ret # We need to be in high-current mode if await self.query_low_current_mode(): # Switch mode and measure again await self.set_low_current_mode(False) return await self.measure_current(extra_params) return ret def ensure_transport_is_rs232(self) -> None: """Ensures transport is RS232, raises error if not""" if not isinstance(self.protocol.transport, RS232Transport): raise RuntimeError("Only usable with RS232 transports") async def set_remote_mode(self, state: bool = True) -> None: """RS232 only, prevent accidental button mashing on the fron panel, this switches between SYSTem:REMote and SYSTem:LOCal according to state, this overrides previous value set with set_rwlock""" self.ensure_transport_is_rs232() if state: return await self.command("SYST:REM") return await self.command("SYST:LOC") async def set_rwlock(self, state: bool = True) -> None: """RS232 only, prevent *any* button mashing on the fron panel, this switches between SYSTem:RWLock and SYSTem:LOCal according to state, this overrides previous value set with set_remote_mode""" self.ensure_transport_is_rs232() if state: return await self.command("SYST:RWL") return await self.command("SYST:LOC") async def display_on(self, state: bool = True) -> None: """Sets display on/off""" if state: return await self.command("DISP ON") return await self.command("DISP OFF") async def set_display_mode(self, mode: str) -> None: """Set the display mode, valied values are NORM and TEXT""" mode = mode.upper() if mode not in ("NORM", "TEXT"): raise ValueError("Invalid mode %s, valid ones are NORM and TEXT" % mode) return await self.command(f"DISP:MODE {mode}") async def set_display_text(self, text: str) -> None: """Used to display text on the display, max 14 characters, NOTE: does *not* set display mode, you need to do it yourself""" if len(text) > 14: raise ValueError("Max text length is 14 characters") if '"' in text and "'" in text: raise ValueError("Text may only contain either single or double quotes, not both") if '"' in text: return await self.command(f"DISP:TEXT '{text}'") return await self.command(f"""DISP:TEXT "{text}" """.strip()) def rs232(serial_url: str, **kwargs: Any) -> HP6632B: """Quick helper to connect via RS232 port""" transport = get_rs232(serial_url, **kwargs) protocol = SCPIProtocol(transport) dev = HP6632B(protocol) return dev python-scpi-2.4.0/src/scpi/errors/000077500000000000000000000000001404124601200170255ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/errors/__init__.py000066400000000000000000000007571404124601200211470ustar00rootroot00000000000000"""SCPI module specific errors""" from typing import Any class CommandError(RuntimeError): """Error executing SCPI command""" def __init__(self, command: str, code: int, message: str) -> None: """initialize the error""" self.command = command self.code = code self.message = message super().__init__() def __str__(self) -> str: """format as string""" return f"'{self.command}' returned error {self.code:d}: {self.message}" python-scpi-2.4.0/src/scpi/py.typed000066400000000000000000000000001404124601200171760ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/scpi.py000066400000000000000000000367771404124601200170450ustar00rootroot00000000000000"""Generic SCPI commands, allow sending and reading of raw data, helpers to parse information""" from typing import Any, Tuple, Sequence, Union, Optional, cast import asyncio import re import logging from dataclasses import dataclass, field from .errors import CommandError from .transports.baseclass import AbstractTransport, BaseTransport from .transports.gpib import GPIBDeviceTransport, GPIBTransport COMMAND_DEFAULT_TIMEOUT = 1.0 ERROR_RE = re.compile(r'([+-]?\d+),"(.*?)"') LOGGER = logging.getLogger(__name__) # FIXME rename to mixin and use as such class BitEnum: # pylint: disable=R0903 """Baseclass for bit definitions of various status registers""" @classmethod def test_bit(cls, statusvalue: int, bitname: str) -> bool: """Test if the given status value has the given bit set""" bitval = cast(int, getattr(cls, bitname)) return bool(bitval & statusvalue) # FIXME use enum.IntEnum as baseclass (maybe, we can't do the docstrings the names if we do...) class ESRBit(BitEnum): """Define meanings of the Event Status Register (ESR) bits""" @property def power_on(self) -> int: """Power-on. The power has cycled""" return 128 @property def user_request(self) -> int: """User request. The instrument operator has issued a request, for instance turning a knob on the front panel.""" return 64 @property def command_error(self) -> int: """Command Error. A command error has occurred.""" return 32 @property def exec_error(self) -> int: """Execution error. The instrument was not able to execute a command for some reason. The reason can be that the supplied data is out of range but can also be an external event like a safety switch/knob or some hardware / software error.""" return 16 @property def device_error(self) -> int: """Device Specific Error.""" return 8 @property def query_error(self) -> int: """Query Error. Error occurred during query processing.""" return 4 @property def control_request(self) -> int: """Request Control. The instrument is requesting to become active controller.""" return 2 @property def operation_complete(self) -> int: """Operation Complete. The instrument has completed all operations. This bit is used for synchronisation purposes.""" return 1 class STBBit(BitEnum): """Define meanings of the STatus Byte register (STB) bits""" @property def rqs_mss(self) -> int: """RQS, ReQuested Service. This bit is set when the instrument has requested service by means of the SeRvice Request (SRQ). When the controller reacts by performing a serial poll, the STatus Byte register (STB) is transmitted with this bit set. Afand cleared afterwards. It is only set again when a new event occurs that requires service. MSS, Master Summary Status. This bit is a summary of the STB and the SRE register bits 1..5 and 7. Thus it is not cleared when a serial poll occurs. It is cleared when the event which caused the setting of MSS is cleared or when the corresponding bits in the SRE register are cleared.""" return 64 @property def rqs(self) -> int: """alias for rqs_mss""" return self.rqs_mss @property def mss(self) -> int: """alias for rqs_mss""" return self.rqs_mss @property def esb(self) -> int: """ESB, Event Summary Bit. This is a summary bit of the standard status registers ESR and ESE""" return 32 @property def event_summary(self) -> int: """Alias for esb""" return self.esb @property def mav(self) -> int: """MAV, Message AVailable. This bit is set when there is data in the output queue waiting to be read.""" return 16 @property def message_available(self) -> int: """Alias for mav""" return self.mav @property def eav(self) -> int: """EAV, Error AVailable. This bit is set when there is data in the output queue waiting to be read.""" return 4 @property def error_available(self) -> int: """Alias for eav""" return self.eav @dataclass class SCPIProtocol: """Implements the SCPI protocol talks over the given transport""" transport: Union[BaseTransport, GPIBDeviceTransport, GPIBTransport] = field() lock: asyncio.Lock = field(default_factory=asyncio.Lock) _checking_error: bool = field(default=False) async def quit(self) -> None: """Shuts down any background threads that might be active""" await self.transport.quit() async def abort_command(self) -> None: """Shortcut to the transports abort_command call""" await self.transport.abort_command() async def get_error(self) -> Tuple[int, str]: """Asks for the error code and string""" if self._checking_error: raise RuntimeError("Recursion on get_error detected") try: self._checking_error = True response = await self.ask("SYST:ERR?", auto_check_error=False) match = ERROR_RE.search(response) if not match: # PONDER: Make our own exceptions ?? raise ValueError("Response '{:s}' does not have correct error format".format(response)) code = int(match.group(1)) errstr = match.group(2) return (code, errstr) finally: self._checking_error = False async def check_error(self, prev_command: str = "") -> None: """Check for error and raise exception if present""" code, errstr = await self.get_error() if code != 0: raise CommandError(prev_command, code, errstr) async def command( self, command: str, cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, abort_on_timeout: bool = True, *, auto_check_error: bool = True, ) -> None: """Sends a command, does not wait for response""" try: async def _command(command: str) -> None: """Wrap the actual work""" nonlocal self async with self.lock: await self.transport.send_command(command) await asyncio.wait_for(_command(command), timeout=cmd_timeout) except asyncio.TimeoutError as err: # check for the actual error if available if auto_check_error: await self.check_error(command) if abort_on_timeout: await self.abort_command() # re-raise the timeout if no other error found raise err except asyncio.CancelledError: LOGGER.info("Cancelled") # other errors are allowed to bubble-up as-is async def safe_command(self, command: str, *args: Any, **kwargs: Any) -> None: """See "command", this just auto-checks for errors each time""" await self.command(command, *args, **kwargs) await self.check_error(command) async def ask( self, command: str, cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, abort_on_timeout: bool = True, *, auto_check_error: bool = True, ) -> str: """Send a command and waits for response, returns the response""" try: async def _ask(command: str) -> str: """Wrap the actual work""" nonlocal self async with self.lock: await self.transport.send_command(command) return await self.transport.get_response() return await asyncio.wait_for(_ask(command), timeout=cmd_timeout) except asyncio.TimeoutError as err: # check for the actual error if available if auto_check_error: await self.check_error(command) if abort_on_timeout: self.abort_command() # re-raise the timeout if no other error found raise err except asyncio.CancelledError: LOGGER.info("Cancelled") # gotta return something or raise an error raise # other errors are allowed to bubble-up as-is async def safe_ask(self, command: str, *args: Any, **kwargs: Any) -> str: """See "ask", this just autp-checks for errors each time""" response = await self.ask(command, *args, **kwargs) await self.check_error(command) return response @dataclass class SCPIDevice: # pylint: disable=R0904 """Implements nicer wrapper methods for the raw commands from the generic SCPI command set See also devices.mixins for mixin classes with more features""" instancefrom: Union[BaseTransport, "SCPIDevice", SCPIProtocol, GPIBDeviceTransport, GPIBTransport] use_safe_variants: bool = field(default=True) protocol: SCPIProtocol = field(init=False) transport: AbstractTransport = field(init=False) _can_poll: bool = field(default=False) def __post_init__(self) -> None: """Set protocol and transport based on what we're instancing from""" protocol: Optional[SCPIProtocol] = None if isinstance(self.instancefrom, SCPIProtocol): protocol = self.instancefrom if isinstance(self.instancefrom, (BaseTransport, GPIBDeviceTransport, GPIBTransport)): protocol = SCPIProtocol(self.instancefrom) if isinstance(self.instancefrom, SCPIDevice): protocol = self.instancefrom.protocol if not protocol: raise RuntimeError("Could not resolve protocol/transport") self.protocol = protocol self.transport = self.protocol.transport # Check if transport poll method exists # TODO: the transport class should have a marker property for this we should use try: _ = self.transport.poll # type: ignore self._can_poll = True except AttributeError: pass async def command( self, command: str, cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, abort_on_timeout: bool = True ) -> None: """Wrap the protocol command (using safe version if requested)""" if self.use_safe_variants: return await self.protocol.safe_command(command, cmd_timeout, abort_on_timeout) return await self.protocol.command(command, cmd_timeout, abort_on_timeout) async def ask( self, command: str, cmd_timeout: float = COMMAND_DEFAULT_TIMEOUT, abort_on_timeout: bool = True ) -> str: """Wrap the protocol ask (using safe version if requested)""" if self.use_safe_variants: return await self.protocol.safe_ask(command, cmd_timeout, abort_on_timeout) return await self.protocol.ask(command, cmd_timeout, abort_on_timeout) async def quit(self) -> None: """Shuts down any background threads that might be active""" await self.protocol.quit() async def abort(self) -> None: """Tells the protocol layer to issue "Device clear" to abort the command currently hanging""" await self.protocol.abort_command() async def get_error(self) -> Tuple[int, str]: """Shorthand for procotols method of the same name""" return await self.protocol.get_error() async def reset(self) -> None: """Resets the device to known state (with *RST) and clears the error log""" return await self.protocol.command("*RST;*CLS") async def wait_for_complete(self, wait_timeout: float) -> bool: """Wait for all queued operations to complete (up-to defined timeout)""" resp = await self.ask("*WAI;*OPC?", cmd_timeout=wait_timeout) return bool(int(resp)) async def identify(self) -> Sequence[str]: """Returns the identification data, standard order is: Manufacturer, Model no, Serial no (or 0), Firmware version""" resp = await self.ask("*IDN?") return resp.split(",") async def query_esr(self) -> int: """Queries the event status register (ESR) NOTE: The register is cleared when read! returns int instead of Decimal like the other number queries since we need to be able to do bitwise comparisons""" resp = await self.ask("*ESR?") return int(resp) async def query_ese(self) -> int: """Queries the event status enable (ESE). returns int instead of Decimal like the other number queries since we need to be able to do bitwise comparisons""" resp = await self.ask("*ESE?") return int(resp) async def set_ese(self, state: int) -> None: """Sets ESE to given value. Construct the value with bitwise OR operations using ESRBit properties, for example to enable OPC and exec_error error bits in the status flag use: set_ese(ESRBit.operation_complete | ESRBit.exec_error)""" await self.command(f"*ESE {state:d}") async def query_sre(self) -> int: """Queries the service request enable (SRE). returns int instead of Decimal like the other number queries since we need to be able to do bitwise comparisons""" resp = await self.ask("*SRE?") return int(resp) async def set_sre(self, state: int) -> None: """Sets SRE to given value. Construct the value with bitwise OR operations using STBBit properties, for example to enable SRQ generation on any error or message use: set_sre(STBBit.mav | STBBit.eav)""" await self.command(f"*SRE {state:d}") async def query_stb(self) -> int: """Queries the status byte (STB). returns int instead of Decimal like the other number queries since we need to be able to do bitwise comparisons If transport implements "serial poll", will use that instead of SCPI query to get the value""" if self._can_poll: resp = await self.transport.poll() # type: ignore else: resp = await self.ask("*STB?") return int(resp) async def trigger(self) -> None: """Send the TRiGger command via SCPI. NOTE: For GPIB devices the Group Execute Trigger is way better, use it when possible however we do not do it transparently here since it triggers all devices on the bus""" await self.command("*TRG") async def clear_status(self) -> None: """ Sends a clear status command. """ await self.command("*CLS") async def operation_complete(self) -> None: """ Sends an Operation Complete command. """ await self.command("*OPC") async def query_options(self) -> str: """ Queries the model's options. """ return await self.ask("*OPT?") async def set_power_on_status_clear(self, setting: str) -> None: """ Set the Power-On Status Clear setting. """ await self.command(f"*PSC {setting}") async def save_state(self, state: int) -> None: """ The SAV command saves all applied configuration settings. """ state = int(state) await self.command("*SAV {state:d}") async def restore_state(self, state: int) -> None: """ Restores the power supply to a state previously stored in memory by *SAV command. """ state = int(state) await self.command(f"*RCL {state:d}") async def power_on_state(self, setting: str) -> None: """ Set the power-on behavior of the system """ setting = str(setting).upper() await self.command(f"*OUTP:PON {setting}") python-scpi-2.4.0/src/scpi/transports/000077500000000000000000000000001404124601200177305ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/transports/__init__.py000066400000000000000000000001531404124601200220400ustar00rootroot00000000000000"""Transport layers for the SCPI module""" from .rs232 import RS232Transport from .tcp import TCPTransport python-scpi-2.4.0/src/scpi/transports/baseclass.py000066400000000000000000000052371404124601200222510ustar00rootroot00000000000000"""Baseclass for all the transports, if common methods are needed they will be defined here All transports must define certain basic methods (check all the raise NotImplementedError) """ from typing import Optional, Callable import asyncio import logging import threading from abc import ABC, abstractmethod from dataclasses import dataclass, field LOGGER = logging.getLogger(__name__) class AbstractTransport(ABC): # pylint: disable=R0903 """So that for example GPIBDeviceTransport can be identified as transport without inheriting the low-level transport methods""" @dataclass # type: ignore # there's something weird with dataclasses and ABCs class BaseTransport(AbstractTransport, ABC): """Baseclass for SCPI tranport layers, abstracts away details, must be subclasses to implement""" message_callback: Optional[Callable[[str], None]] = field(default=None) unsolicited_message_callback: Optional[Callable[[str], None]] = field(default=None) lock: asyncio.Lock = field(default_factory=asyncio.Lock) aioevent: asyncio.Event = field(default_factory=asyncio.Event) blevent: threading.Event = field(default_factory=threading.Event) @abstractmethod async def quit(self) -> None: """Must shutdown all background threads (if any)""" raise NotImplementedError() @abstractmethod async def send_command(self, command: str) -> None: """Sends a complete command to the device, line termination, write timeouts etc are handled by the transport note: the transport probably should handle locking transparently using 'with (await self.lock):' as context manager""" raise NotImplementedError() @abstractmethod async def get_response(self) -> str: """Tells the device send a response, reads and returns it""" raise NotImplementedError() def message_received(self, message: str) -> None: """Passes the message to the callback expecting it, or to the unsolicited callback""" if self.message_callback is not None: self.message_callback(message) self.message_callback = None return # Fall-through for unsolicited messages if self.unsolicited_message_callback is not None: self.unsolicited_message_callback(message) return LOGGER.info("Got unsolicited message but have no callback to send it to") @abstractmethod async def abort_command(self) -> None: """Send the "device clear" command to abort a running command note: the transport probably should handle locking transparently using 'async with self.lock:' as context manager""" raise NotImplementedError() python-scpi-2.4.0/src/scpi/transports/gpib/000077500000000000000000000000001404124601200206515ustar00rootroot00000000000000python-scpi-2.4.0/src/scpi/transports/gpib/__init__.py000066400000000000000000000002071404124601200227610ustar00rootroot00000000000000"""GPIB related transports""" from .base import GPIBDeviceTransport, GPIBTransport __all__ = ["GPIBDeviceTransport", "GPIBTransport"] python-scpi-2.4.0/src/scpi/transports/gpib/base.py000066400000000000000000000106561404124601200221450ustar00rootroot00000000000000"""GPIB Related baseclasses""" from __future__ import annotations from typing import Optional, Tuple, Sequence, Union from abc import ABC, abstractmethod from dataclasses import dataclass, field import logging from ..baseclass import AbstractTransport, BaseTransport LOGGER = logging.getLogger(__name__) AddressTuple = Tuple[int, Optional[int]] @dataclass class GPIBDeviceTransport(AbstractTransport): """Device specific transport, handles addressing transparently""" lltransport: GPIBTransport = field() address: Union[AddressTuple, int] = field() def __post_init__(self) -> None: """Make sure address is always tuple""" if isinstance(self.address, int): self.address = (self.address, None) async def _set_ll_address(self) -> None: """Set the lowlevel transport address""" assert not isinstance(self.address, int) await self.lltransport.set_address(self.address[0], self.address[1]) async def send_command(self, command: str) -> None: """Transparently set address when talking, see low-level transport method docs for more info""" await self._set_ll_address() await self.lltransport.send_command(command) async def get_response(self) -> str: """Transparently set address when talking, see low-level transport method docs for more info""" await self._set_ll_address() return await self.lltransport.get_response() async def abort_command(self) -> None: """Transparently set address when talking, see low-level transport method docs for more info""" await self._set_ll_address() return await self.lltransport.abort_command() async def send_scd(self) -> None: """Sends the Selected Device Clear (SDC) message to the device""" await self._set_ll_address() await self.lltransport.send_scd() async def send_llo(self) -> None: """Send LLO (disable front panel) to the device""" await self._set_ll_address() await self.lltransport.send_llo() async def send_loc(self) -> None: """Send LOC (enable front panel) to the device""" await self._set_ll_address() await self.lltransport.send_loc() async def quit(self) -> None: """Nop for devices""" LOGGER.debug("quit should not be called on device level but we must have the method for type compatibility") class GPIBTransport(BaseTransport, ABC): """Baseclass for GPIB transports""" @abstractmethod async def set_address(self, primary: int, secondary: Optional[int] = None) -> None: """Set the address we want to talk to""" raise NotImplementedError() @abstractmethod async def query_address(self) -> AddressTuple: """Query the address we are talking to, returns tuple with primary and secondary parts secondary is None if not set""" raise NotImplementedError() @abstractmethod async def scan_devices(self) -> Sequence[Tuple[int, str]]: """Scan for devices in the bus. Returns list of addresses and identifiers for found primary addresses (0-30)""" raise NotImplementedError() @abstractmethod async def send_scd(self) -> None: """Sends the Selected Device Clear (SDC) message to the currently specified GPIB address""" raise NotImplementedError() @abstractmethod async def send_ifc(self) -> None: """Asserts GPIB IFC signal""" raise NotImplementedError() @abstractmethod async def send_llo(self) -> None: """Send LLO (disable front panel) to currently specified address""" raise NotImplementedError() @abstractmethod async def send_loc(self) -> None: """Send LOC (enable front panel) to currently specified address""" raise NotImplementedError() @abstractmethod async def send_group_trig(self) -> None: """Send Group Execute Trigger to the bus""" raise NotImplementedError() @abstractmethod async def get_srq(self) -> int: """Get SRQ assertion status""" raise NotImplementedError() @abstractmethod async def poll(self) -> int: """Do serial poll on the selected device""" raise NotImplementedError() def get_device_transport(self, address: int, secondary: Optional[int] = None) -> GPIBDeviceTransport: """Gets a device-specific transport instance for given address""" return GPIBDeviceTransport(self, (address, secondary)) python-scpi-2.4.0/src/scpi/transports/gpib/prologix.py000066400000000000000000000203001404124601200230610ustar00rootroot00000000000000""""Driver" for http://prologix.biz/gpib-usb-controller.html GPIB controller""" from typing import Optional, Sequence, List, Any, Tuple, cast import asyncio import logging from dataclasses import dataclass import serial # type: ignore import serial.threaded # type: ignore from ..rs232 import RS232SerialProtocol, RS232Transport from .base import GPIBTransport, AddressTuple SCAN_DEVICE_TIMEOUT = 0.5 READ_TIMEOUT = 1.0 LOGGER = logging.getLogger(__name__) class PrologixRS232SerialProtocol(RS232SerialProtocol): """Basically the same deal as with the stock RS232 PySerial "protocol" but different EOL""" TERMINATOR = b"\n" @dataclass class PrologixGPIBTransport(GPIBTransport, RS232Transport): """Transport "driver" for the Prologix USB-GPIB controller (v6 protocol)""" def __post_init__(self) -> None: """Init the serial and controller""" self._serialhandler = serial.threaded.ReaderThread(self.serialdevice, PrologixRS232SerialProtocol) self._serialhandler.start() self._serialhandler.protocol.handle_line = self.message_received self.initialize_controller() def initialize_controller(self) -> None: """Initializes the controller to known state""" if not self._serialhandler: raise RuntimeError("Serialhandler isn't") # Set to controller mode self._serialhandler.protocol.write_line("++mode 1") # Disable automatic read after write self._serialhandler.protocol.write_line("++auto 0") # Auto-assert End Of Instruction after commands self._serialhandler.protocol.write_line("++eoi 1") # Append CRLF to device commands (EOI above *should* be enough but this is probably more compatible) self._serialhandler.protocol.write_line("++eos 0") # We do not have parsing support for the EOT character so disable it self._serialhandler.protocol.write_line("++eot_enable 0") # Set inter-character timeout for read commands self._serialhandler.protocol.write_line("++read_tmo_ms 500") # Assert IFC, make us Controller In Charge self._serialhandler.protocol.write_line("++ifc") async def send_command(self, command: str) -> None: """Wrapper for write_line on the protocol with some sanity checks""" if not self._serialhandler or not self._serialhandler.is_alive(): raise RuntimeError("Serial handler not ready") async with self.lock: self._serialhandler.protocol.write_line(command) async def get_response(self) -> str: """Get device response""" return await self.send_and_read("++read eoi") async def send_and_read(self, send: str) -> str: """Send a line, read the response. NOTE: This is for talking with the controller, device responses need to use get_response as usual""" if not self._serialhandler: raise RuntimeError("Serialhandler isn't") async def _send_and_read(send: str) -> str: """Wrap the actual work""" nonlocal self if not self._serialhandler: raise RuntimeError("Serialhandler isn't") async with self.lock: response: Optional[str] = None def set_response(message: str) -> None: """Callback for setting the response""" nonlocal response, self response = message self.blevent.set() self.blevent.clear() self.message_callback = set_response self._serialhandler.protocol.write_line(send) await asyncio.get_event_loop().run_in_executor(None, self.blevent.wait) self.message_callback = None return cast(str, response) return await asyncio.wait_for(_send_and_read(send), timeout=READ_TIMEOUT) async def set_address(self, primary: int, secondary: Optional[int] = None) -> None: """Set the address we want to talk to""" if secondary is None: await self.send_command(f"++addr {primary:d}") else: await self.send_command(f"++addr {primary:d} {secondary:d}") while True: await asyncio.sleep(0.001) resp = await self.query_address() if resp == (primary, secondary): break async def query_address(self) -> AddressTuple: """Query the address we are talking to, returns tuple with primary and secondary parts secondary is None if not set""" resp = await self.send_and_read("++addr") parts = resp.split(" ") primary = int(parts[0]) secondary: Optional[int] = None if len(parts) > 1: secondary = int(parts[1]) return (primary, secondary) async def send_scd(self) -> None: """Sends the Selected Device Clear (SDC) message to the currently specified GPIB address""" await self.send_command("++clr") async def send_ifc(self) -> None: """Asserts GPIB IFC signal""" await self.send_command("++ifc") async def send_llo(self) -> None: """Send LLO (disable front panel) to currently specified address""" await self.send_command("++llo") async def send_loc(self) -> None: """Send LOC (enable front panel) to currently specified address""" await self.send_command("++loc") async def get_srq(self) -> int: """Get SRQ assertion status""" resp = await self.send_and_read("++srq") return int(resp) async def poll(self) -> int: """Do serial poll on the selected device""" resp = await self.send_and_read("++spoll") return int(resp) async def send_group_trig(self, addresses: Optional[Sequence[int]] = None) -> None: # pylint: disable=W0221 """Send trigger to listed addresses For some reason Prologix does not trigger the whole bus but only listed devices (if none listed then the currently selected device is used)""" if addresses is None: return await self.send_command("++trg") await self.send_command("++trg " + " ".join((str(x) for x in addresses))) async def scan_devices(self) -> Sequence[Tuple[int, str]]: """Scan for devices in the bus. Returns list of addresses and identifiers for found primary addresses (0-30)""" if not self._serialhandler: raise RuntimeError("Serialhandler isn't") found_addresses: List[int] = [] # We do not lock on this level since the commands we use need to manipulate the lock prev_addr = await self.query_address() prev_read_tmo_ms = int(await self.send_and_read("++read_tmo_ms")) new_read_tmo_ms = int((SCAN_DEVICE_TIMEOUT / 2) * 1000) self._serialhandler.protocol.write_line(f"++read_tmo_ms {new_read_tmo_ms:d}") for addr in range(0, 31): # 0-30 inclusive async def _scan_addr(addr: int) -> None: """Sacn single address""" nonlocal found_addresses, self await self.set_address(addr) await self.poll() found_addresses.append(addr) try: await asyncio.wait_for(_scan_addr(addr), timeout=SCAN_DEVICE_TIMEOUT) except (asyncio.TimeoutError, asyncio.CancelledError): pass self._serialhandler.protocol.write_line(f"++read_tmo_ms {prev_read_tmo_ms:d}") # Wait a moment for things to settle await asyncio.sleep(float(prev_read_tmo_ms) / 1000) # Get ids for the devices we found ret = [] for addr in found_addresses: await self.set_address(addr) await self.send_command("*IDN?") idstr = await self.get_response() ret.append((addr, idstr)) await self.set_address(*prev_addr) return ret async def abort_command(self) -> None: """Not implemented for prologix""" LOGGER.debug("not implemented on PrologixGPIBTransport") def get(serial_url: str, **serial_kwargs: Any) -> PrologixGPIBTransport: """Shorthand for creating the port from url and initializing the transport""" port = serial.serial_for_url(serial_url, **serial_kwargs) return PrologixGPIBTransport(serialdevice=port) python-scpi-2.4.0/src/scpi/transports/rs232.py000066400000000000000000000100031404124601200211470ustar00rootroot00000000000000"""Serial port transport layer""" from __future__ import annotations from typing import Optional, Any, Dict, cast import asyncio import logging from dataclasses import field, dataclass import serial # type: ignore import serial.threaded # type: ignore from .baseclass import BaseTransport LOGGER = logging.getLogger(__name__) WRITE_TIMEOUT = 1.0 class RS232SerialProtocol(serial.threaded.LineReader): # type: ignore """PySerial "protocol" class for handling stuff""" ENCODING = "ascii" def connection_made(self, transport: RS232Transport) -> None: """Overridden to make sure we have write_timeout set""" super().connection_made(transport) # Make sure we have a write timeout of expected size self.transport.write_timeout = WRITE_TIMEOUT def handle_line(self, line: str) -> None: raise RuntimeError("This should have been overloaded by RS232Transport") @dataclass class RS232Transport(BaseTransport): """Uses PySerials ReaderThread in the background to save us some pain""" serialdevice: Optional[serial.SerialBase] = field(default=None) _serialhandler: Optional[serial.threaded.ReaderThread] = field(default=None, repr=False) def __post_init__(self) -> None: """Initialize the transport""" if not self.serialdevice: raise ValueError("serialdevice must be given") self._serialhandler = serial.threaded.ReaderThread(self.serialdevice, RS232SerialProtocol) self._serialhandler.start() self._serialhandler.protocol.handle_line = self.message_received async def send_command(self, command: str) -> None: """Wrapper for write_line on the protocol with some sanity checks""" if not self._serialhandler or not self._serialhandler.is_alive(): raise RuntimeError("Serial handler not ready") async with self.lock: self._serialhandler.protocol.write_line(command) async def get_response(self) -> str: """Serial devices send responses without needing to be told to, just reads it""" # TODO: we probably have a race-condition possibility here, maybe always put all received # messages to a stack and return popleft ?? async with self.lock: response: Optional[str] = None def set_response(message: str) -> None: """Callback for setting the response""" nonlocal response, self response = message self.blevent.set() self.blevent.clear() self.message_callback = set_response await asyncio.get_event_loop().run_in_executor(None, self.blevent.wait) self.message_callback = None return cast(str, response) async def abort_command(self) -> None: """Uses the break-command to issue "Device clear", from the SCPI documentation (for HP6632B): The status registers, the error queue, and all configuration states are left unchanged when a device clear message is received. Device clear performs the following actions: - The input and output buffers of the dc source are cleared. - The dc source is prepared to accept a new command string.""" if not self._serialhandler: raise RuntimeError("No serialhandler") if not self._serialhandler.serial: raise RuntimeError("No serialhandler.serial") async with self.lock: self._serialhandler.serial.send_break() async def quit(self) -> None: """Closes the port and background threads""" if not self._serialhandler: raise RuntimeError("No serialhandler") if not self._serialhandler.serial: raise RuntimeError("No serialhandler.serial") self._serialhandler.close() def get(serial_url: str, **serial_kwargs: Dict[str, Any]) -> RS232Transport: """Shorthand for creating the port from url and initializing the transport""" port = serial.serial_for_url(serial_url, **serial_kwargs) return RS232Transport(serialdevice=port) python-scpi-2.4.0/src/scpi/transports/tcp.py000066400000000000000000000046561404124601200211030ustar00rootroot00000000000000"""TCP based transport""" from typing import Optional import asyncio from dataclasses import dataclass, field import logging from .baseclass import BaseTransport LOGGER = logging.getLogger(__name__) @dataclass class TCPTransport(BaseTransport): """TCP based transport""" ipaddr: Optional[str] = field(default=None) port: Optional[int] = field(default=None) reader: Optional[asyncio.StreamReader] = field(default=None) writer: Optional[asyncio.StreamWriter] = field(default=None) async def open_connection(self, ipaddr: str, port: int) -> None: """Open a connection (also update the IP/port)""" self.reader, self.writer = await asyncio.open_connection(ipaddr, port, loop=asyncio.get_event_loop()) self.ipaddr = ipaddr self.port = port def __post_init__(self) -> None: """Call open_connection in an eventloop""" if self.ipaddr is None or self.port is None: raise ValueError("ipaddr and port must be given") loop = asyncio.get_event_loop() loop.run_until_complete(self.open_connection(self.ipaddr, self.port)) async def send_command(self, command: str) -> None: """Write command to the stream""" if not self.writer: raise RuntimeError("Writer not set") async with self.lock: LOGGER.debug("sending command: {}".format(command)) self.writer.write((command + "\r\n").encode()) await asyncio.sleep(0.05) await self.writer.drain() async def get_response(self) -> str: """Get response from the stream""" if not self.reader: raise RuntimeError("Reader not set") async with self.lock: data = await self.reader.readline() res = data.decode() LOGGER.debug("Got response: {}".format(res.strip())) return res async def quit(self) -> None: """Closes the connection and background threads""" if not self.writer: raise RuntimeError("Writer not set") self.writer.close() await self.writer.wait_closed() async def abort_command(self) -> None: """This does not apply on TCP transport""" LOGGER.debug("TCP transport does not know what to do here") def get(ipaddr: str, port: int) -> TCPTransport: """Shorthand for creating the port from ip and port and initializing the transport""" return TCPTransport(ipaddr=ipaddr, port=port) python-scpi-2.4.0/src/scpi/wrapper.py000066400000000000000000000035731404124601200175530ustar00rootroot00000000000000"""Helper class to allow using of device in traditional blocking style without having to deal with the ioloop""" from typing import Any import asyncio import functools import inspect import logging LOGGER = logging.getLogger(__name__) class AIOWrapper: # pylint: disable=R0903 """Wraps all coroutine methods into asyncio run_until_complete calls""" def __init__(self, to_be_wrapped: Any) -> None: """Init wrapper for device""" self._device = to_be_wrapped self._loop = asyncio.get_event_loop() for attr in functools.WRAPPER_ASSIGNMENTS: try: setattr(self, attr, getattr(self._device, attr)) except AttributeError: try: setattr(self.__class__, attr, getattr(self._device.__class__, attr)) except AttributeError: LOGGER.debug("Could not copy {}".format(attr)) def __getattr__(self, item: str) -> Any: """Get a memeber, if it's a coroutine autowrap it to eventloop run""" orig = getattr(self._device, item) if inspect.iscoroutinefunction(orig): @functools.wraps(orig) def wrapped(*args: Any, **kwargs: Any) -> Any: """Gets the waitable and tells the event loop to run it""" nonlocal self waitable = orig(*args, **kwargs) return self._loop.run_until_complete(waitable) return wrapped return orig def __dir__(self) -> Any: """Proxy the dir on the device""" return dir(self._device) def quit(self) -> None: """Calls the device.quit via loop and closes the loop""" self._loop.run_until_complete(self._device.quit()) self._loop.close() class DeviceWrapper(AIOWrapper): # pylint: disable=R0903 """Legacy name for the AsyncIO wrapper class for backwards compatibility""" python-scpi-2.4.0/tests/000077500000000000000000000000001404124601200151265ustar00rootroot00000000000000python-scpi-2.4.0/tests/__init__.py000066400000000000000000000000451404124601200172360ustar00rootroot00000000000000"""Tests for datastreamservicelib""" python-scpi-2.4.0/tests/conftest.py000066400000000000000000000000301404124601200173160ustar00rootroot00000000000000"""pytest automagics""" python-scpi-2.4.0/tests/test_scpi.py000066400000000000000000000002441404124601200174750ustar00rootroot00000000000000"""Package level tests""" from scpi import __version__ def test_version() -> None: """Make sure version matches expected""" assert __version__ == "2.4.0" python-scpi-2.4.0/tox.ini000066400000000000000000000007121404124601200152770ustar00rootroot00000000000000[tox] isolated_build = true ;pylint has issue with Optional and Union type hints on 3.9, re-enable when fixed ;envlist = py39,py38,py37 envlist = py38,py37 [testenv] whitelist_externals = poetry passenv = SSH_AUTH_SOCK SKIP commands = poetry install -v poetry run docker/pre_commit_init.sh # this also checks all files poetry run pytest --junitxml=pytest-{envname}.xml tests/ poetry run mypy --strict src tests poetry run bandit -r src