Compare commits

...

46 Commits

Author SHA1 Message Date
Upstream Data
25d971b699 version: bump version number. 2023-03-01 21:46:03 -07:00
Upstream Data
cd16ef3a25 feature: add Z15 support. 2023-03-01 21:45:37 -07:00
Upstream Data
b70010272f feature: add L7 support. 2023-03-01 20:32:03 -07:00
Upstream Data
140a457445 Merge branch 'dev' 2023-03-01 19:56:27 -07:00
Upstream Data
f4775e6311 bug: better fix for the issue with whatsminer pools. 2023-03-01 19:37:51 -07:00
Upstream Data
a4ecda93a2 bug: Fix an issue with sending None to whatsminers. 2023-03-01 19:34:14 -07:00
Arceris
ba90f2f082 Add VH40 and VH20 chip counts to M50.py 2023-03-01 18:56:41 -07:00
UpstreamData
44ac958bbb version: bump version number. 2023-02-28 16:23:36 -07:00
UpstreamData
e9bcf2ec9f bug: fix an issue with missing a check for bosminer config when getting data. 2023-02-28 16:23:04 -07:00
UpstreamData
c73dfad01a format: move vnish over to the new web API style. 2023-02-28 11:32:42 -07:00
UpstreamData
d222912e30 format: update a bunch of formatting and remove a lot of useless imports. 2023-02-28 09:35:11 -07:00
UpstreamData
6f1c1e0290 format: swap X9 and Innosilicon over to the new web api format. 2023-02-27 16:07:34 -07:00
UpstreamData
ba0bb73aa3 format: swap X17 over to the new web api format. 2023-02-27 15:44:58 -07:00
UpstreamData
13fcf1d4aa format: swap X19 over to the new web api format. 2023-02-27 15:32:02 -07:00
UpstreamData
6be1e94216 version: bump version number. 2023-02-24 12:32:22 -07:00
UpstreamData
709b3efa81 bug: fix mis-identification of X19 on some new versions of stock. 2023-02-24 12:31:55 -07:00
UpstreamData
5ac5770331 version: bump version number. 2023-02-24 08:58:11 -07:00
UpstreamData
f131ebbdf5 bug: fix miner mode needing to be parsed as an int not a str. 2023-02-24 08:57:28 -07:00
UpstreamData
5441e50f73 version: bump version number. 2023-02-24 08:49:29 -07:00
UpstreamData
dc6a952de4 bug: fix backwards modes for X19. 2023-02-24 08:49:13 -07:00
UpstreamData
b781d215fb version: bump version number. 2023-02-24 08:44:20 -07:00
UpstreamData
086b31ba23 feature: add enum for miner mode in config. 2023-02-24 08:43:52 -07:00
UpstreamData
46b7352769 version: bump version number. 2023-02-23 15:39:37 -07:00
UpstreamData
e218c5039d bug: fix wrong LPM mode number for X19. 2023-02-23 15:39:08 -07:00
UpstreamData
3bb392980e version: bump version number. 2023-02-23 15:22:49 -07:00
UpstreamData
92264619d2 bug: fix missing autotuning wattage. 2023-02-23 15:22:23 -07:00
UpstreamData
3e21829fae version: bump version number. 2023-02-23 15:16:23 -07:00
UpstreamData
d3619b0e48 bug: fix insane amount of warnings when getting config. 2023-02-23 15:15:58 -07:00
UpstreamData
0fbb05d62d version: bump version number. 2023-02-23 15:05:29 -07:00
UpstreamData
01fc2591ad bug: fix a missed check for missing pool info. 2023-02-23 15:03:54 -07:00
UpstreamData
c08d7fa5cd version: bump version number. 2023-02-23 14:47:30 -07:00
UpstreamData
91c0e1c125 feature: add low power mode ability for X19 possibly. 2023-02-23 14:44:43 -07:00
UpstreamData
3d137f95d9 version: bump version number. 2023-02-23 12:16:56 -07:00
UpstreamData
d5e8062f09 bug: make sure self.config is set in all send or get config methods. 2023-02-23 12:16:19 -07:00
UpstreamData
2dc4b2cb8a tests: Update unittests. 2023-02-23 11:42:20 -07:00
UpstreamData
5ff0074d33 bug: update pyproject.toml and poetry.lock to be on recent versions and consistent. 2023-02-23 09:42:55 -07:00
UpstreamData
bbc88e175c version: bump version number. 2023-02-22 16:14:23 -07:00
UpstreamData
ff54bea0ed feature: add miner load balancer. 2023-02-22 16:13:48 -07:00
UpstreamData
bcfa807e12 version: bump version number. 2023-02-22 13:00:29 -07:00
UpstreamData
728e94f120 bug: fix OSError.winerror missing on some systems with errno. 2023-02-22 13:00:09 -07:00
UpstreamData
5f2832d632 version: bump version number. 2023-02-22 12:11:37 -07:00
UpstreamData
59e5be280f bug: fix a division by 0 error edge case in MinerData. 2023-02-22 12:10:53 -07:00
UpstreamData
e6424acf5f version: bump version number. 2023-02-22 11:43:42 -07:00
UpstreamData
34389e9133 feature: add M30S++ VH70 chip count. 2023-02-22 11:43:22 -07:00
UpstreamData
b0f7629607 version: bump version number. 2023-02-22 11:22:29 -07:00
UpstreamData
6e50f1f769 feature: add chip counts for M30S++VH50. 2023-02-22 11:21:49 -07:00
63 changed files with 2428 additions and 798 deletions

442
poetry.lock generated
View File

@@ -1,6 +1,6 @@
[[package]]
name = "anyio"
version = "3.6.1"
version = "3.6.2"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
@@ -13,11 +13,11 @@ sniffio = ">=1.1"
[package.extras]
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16)"]
trio = ["trio (>=0.16,<0.22)"]
[[package]]
name = "asyncssh"
version = "2.12.0"
version = "2.13.1"
description = "AsyncSSH: Asynchronous SSHv2 client and server library"
category = "main"
optional = false
@@ -38,7 +38,7 @@ pywin32 = ["pywin32 (>=227)"]
[[package]]
name = "certifi"
version = "2022.9.24"
version = "2022.12.7"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
@@ -76,15 +76,15 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.5"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]]
name = "cryptography"
version = "38.0.1"
version = "39.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
@@ -94,12 +94,14 @@ python-versions = ">=3.6"
cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"]
sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"]
test-randomorder = ["pytest-randomly"]
tox = ["tox"]
[[package]]
name = "distlib"
@@ -111,15 +113,15 @@ python-versions = "*"
[[package]]
name = "filelock"
version = "3.8.0"
version = "3.9.0"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]
docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
[[package]]
name = "ghp-import"
@@ -137,35 +139,38 @@ dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "griffe"
version = "0.22.2"
version = "0.25.5"
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = ">=0.4"
[package.extras]
async = ["aiofiles (>=0.7,<1.0)"]
[[package]]
name = "h11"
version = "0.12.0"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[[package]]
name = "httpcore"
version = "0.15.0"
version = "0.16.3"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
anyio = ">=3.0.0,<4.0.0"
anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.11,<0.13"
h11 = ">=0.13,<0.15"
sniffio = ">=1.0.0,<2.0.0"
[package.extras]
@@ -174,7 +179,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
version = "0.23.0"
version = "0.23.3"
description = "The next generation HTTP client."
category = "main"
optional = false
@@ -182,7 +187,7 @@ python-versions = ">=3.7"
[package.dependencies]
certifi = "*"
httpcore = ">=0.15.0,<0.16.0"
httpcore = ">=0.15.0,<0.17.0"
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
sniffio = "*"
@@ -194,7 +199,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "identify"
version = "2.5.5"
version = "2.5.18"
description = "File identification library for Python"
category = "dev"
optional = false
@@ -213,7 +218,7 @@ python-versions = ">=3.5"
[[package]]
name = "importlib-metadata"
version = "4.12.0"
version = "6.0.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
@@ -223,9 +228,23 @@ python-versions = ">=3.7"
zipp = ">=0.5"
[package.extras]
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
name = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.8.0"
[package.extras]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "Jinja2"
@@ -257,7 +276,7 @@ testing = ["coverage", "pyyaml"]
[[package]]
name = "MarkupSafe"
version = "2.1.1"
version = "2.1.2"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
@@ -273,7 +292,7 @@ python-versions = ">=3.6"
[[package]]
name = "mkdocs"
version = "1.4.0"
version = "1.4.2"
description = "Project documentation with Markdown."
category = "dev"
optional = false
@@ -281,18 +300,20 @@ python-versions = ">=3.7"
[package.dependencies]
click = ">=7.0"
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
ghp-import = ">=1.0"
importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
Jinja2 = ">=2.11.1"
Markdown = ">=3.2.1,<3.4"
jinja2 = ">=2.11.1"
markdown = ">=3.2.1,<3.4"
mergedeep = ">=1.3.4"
packaging = ">=20.5"
PyYAML = ">=5.1"
pyyaml = ">=5.1"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"]
[[package]]
name = "mkdocs-autorefs"
@@ -308,7 +329,7 @@ mkdocs = ">=1.1"
[[package]]
name = "mkdocstrings"
version = "0.19.0"
version = "0.20.0"
description = "Automatic documentation from sources, for MkDocs."
category = "dev"
optional = false
@@ -330,14 +351,14 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
[[package]]
name = "mkdocstrings-python"
version = "0.7.1"
version = "0.8.3"
description = "A Python handler for mkdocstrings."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
griffe = ">=0.11.1"
griffe = ">=0.24"
mkdocstrings = ">=0.19"
[[package]]
@@ -353,14 +374,11 @@ setuptools = "*"
[[package]]
name = "packaging"
version = "21.3"
version = "23.0"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
python-versions = ">=3.7"
[[package]]
name = "passlib"
@@ -378,31 +396,30 @@ totp = ["cryptography"]
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
version = "3.0.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
[[package]]
name = "pre-commit"
version = "2.20.0"
version = "3.1.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
toml = "*"
virtualenv = ">=20.0.8"
virtualenv = ">=20.10.0"
[[package]]
name = "pyaml"
@@ -425,7 +442,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pymdown-extensions"
version = "9.6"
version = "9.9.2"
description = "Extension pack for Python Markdown."
category = "dev"
optional = false
@@ -434,17 +451,6 @@ python-versions = ">=3.7"
[package.dependencies]
markdown = ">=3.2"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
@@ -491,15 +497,15 @@ idna2008 = ["idna"]
[[package]]
name = "setuptools"
version = "65.4.1"
version = "67.4.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
@@ -528,7 +534,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typing-extensions"
version = "4.3.0"
version = "4.5.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
@@ -536,24 +542,24 @@ python-versions = ">=3.7"
[[package]]
name = "virtualenv"
version = "20.16.5"
version = "20.19.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
[package.dependencies]
distlib = ">=0.3.5,<1"
distlib = ">=0.3.6,<1"
filelock = ">=3.4.1,<4"
platformdirs = ">=2.4,<3"
platformdirs = ">=2.4,<4"
[package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
[[package]]
name = "watchdog"
version = "2.1.9"
version = "2.2.1"
description = "Filesystem events monitoring"
category = "dev"
optional = false
@@ -564,33 +570,33 @@ watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "zipp"
version = "3.8.1"
version = "3.14.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "98c3026a6f27c29c0357bbfa07166d6d8b604a869f3a802adc3bb3610f86964c"
python-versions = "^3.8"
content-hash = "44b511a675e63d02eaf82f3ec78baa2364e5116e990f9f0a6fc2cb8f21cf616f"
[metadata.files]
anyio = [
{file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"},
{file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"},
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
]
asyncssh = [
{file = "asyncssh-2.12.0-py3-none-any.whl", hash = "sha256:6841c4242c606fd51188c974ec2f4887efeec67ecdfa5b84140711dacd985ab3"},
{file = "asyncssh-2.12.0.tar.gz", hash = "sha256:274101322c4b941823aeed8e1ab6e7be5191686c6db2d2bd35afeba30505e780"},
{file = "asyncssh-2.13.1-py3-none-any.whl", hash = "sha256:c90eb5e2b4f9a7cc6e6af01fd844563d722c0d667f8c5f51fe5b3c2a79fa0575"},
{file = "asyncssh-2.13.1.tar.gz", hash = "sha256:ebbb83c05c0b45cf230de1ef2f06059e360f9afa5c3ddf60fc92faf7b94ff887"},
]
certifi = [
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
{file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
{file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
]
cffi = [
{file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
@@ -667,76 +673,77 @@ click = [
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
cryptography = [
{file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"},
{file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"},
{file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"},
{file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"},
{file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"},
{file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"},
{file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"},
{file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"},
{file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"},
{file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"},
{file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"},
{file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"},
{file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"},
{file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"},
{file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"},
{file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"},
{file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"},
{file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"},
{file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"},
{file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"},
{file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"},
]
distlib = [
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
filelock = [
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
{file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
{file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
]
ghp-import = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
griffe = [
{file = "griffe-0.22.2-py3-none-any.whl", hash = "sha256:cea5415ac6a92f4a22638e3f1f2e661402bac09fb8e8266936d67185a7e0d0fb"},
{file = "griffe-0.22.2.tar.gz", hash = "sha256:1408e336a4155392bbd81eed9f2f44bf144e71b9c664e905630affe83bbc088e"},
{file = "griffe-0.25.5-py3-none-any.whl", hash = "sha256:1fb9edff48e66d4873014a2ebf21aca5f271d0006a4c937826e3cf592ffb3706"},
{file = "griffe-0.25.5.tar.gz", hash = "sha256:11ea3403ef0560a1cbcf7f302eb5d21cf4c1d8ed3f8a16a75aa9f6f458caf3f1"},
]
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
httpcore = [
{file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"},
{file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"},
{file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
{file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
]
httpx = [
{file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
{file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
{file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
{file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
]
identify = [
{file = "identify-2.5.5-py2.py3-none-any.whl", hash = "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97"},
{file = "identify-2.5.5.tar.gz", hash = "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6"},
{file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"},
{file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"},
]
idna = [
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
importlib-metadata = [
{file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
{file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
{file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"},
{file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"},
]
isort = [
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
Jinja2 = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
@@ -747,86 +754,96 @@ Markdown = [
{file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"},
]
MarkupSafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
{file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
{file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
{file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
{file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
{file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
{file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
{file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
{file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
{file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
]
mergedeep = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
mkdocs = [
{file = "mkdocs-1.4.0-py3-none-any.whl", hash = "sha256:ce057e9992f017b8e1496b591b6c242cbd34c2d406e2f9af6a19b97dd6248faa"},
{file = "mkdocs-1.4.0.tar.gz", hash = "sha256:e5549a22d59e7cb230d6a791edd2c3d06690908454c0af82edc31b35d57e3069"},
{file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"},
{file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"},
]
mkdocs-autorefs = [
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
]
mkdocstrings = [
{file = "mkdocstrings-0.19.0-py3-none-any.whl", hash = "sha256:3217d510d385c961f69385a670b2677e68e07b5fea4a504d86bf54c006c87c7d"},
{file = "mkdocstrings-0.19.0.tar.gz", hash = "sha256:efa34a67bad11229d532d89f6836a8a215937548623b64f3698a1df62e01cc3e"},
{file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"},
{file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"},
]
mkdocstrings-python = [
{file = "mkdocstrings-python-0.7.1.tar.gz", hash = "sha256:c334b382dca202dfa37071c182418a6df5818356a95d54362a2b24822ca3af71"},
{file = "mkdocstrings_python-0.7.1-py3-none-any.whl", hash = "sha256:a22060bfa374697678e9af4e62b020d990dad2711c98f7a9fac5c0345bef93c7"},
{file = "mkdocstrings-python-0.8.3.tar.gz", hash = "sha256:9ae473f6dc599339b09eee17e4d2b05d6ac0ec29860f3fc9b7512d940fc61adf"},
{file = "mkdocstrings_python-0.8.3-py3-none-any.whl", hash = "sha256:4e6e1cd6f37a785de0946ced6eb846eb2f5d891ac1cc2c7b832943d3529087a7"},
]
nodeenv = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
]
passlib = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
]
pre-commit = [
{file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
{file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
{file = "pre_commit-3.1.0-py2.py3-none-any.whl", hash = "sha256:7001dfcd174540658822b1fd3630ceadf4f41375a5d1844b5c3b3830f227348c"},
{file = "pre_commit-3.1.0.tar.gz", hash = "sha256:61bd9f1b96d3d1e763f2a9a0f8522aed341646800642ff6803c73fac5781f5b7"},
]
pyaml = [
{file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"},
@@ -837,12 +854,8 @@ pycparser = [
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
pymdown-extensions = [
{file = "pymdown_extensions-9.6-py3-none-any.whl", hash = "sha256:1e36490adc7bfcef1fdb21bb0306e93af99cff8ec2db199bd17e3bf009768c11"},
{file = "pymdown_extensions-9.6.tar.gz", hash = "sha256:b956b806439bbff10f726103a941266beb03fbe99f897c7d5e774d7170339ad9"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
{file = "pymdown_extensions-9.9.2-py3-none-any.whl", hash = "sha256:c3d804eb4a42b85bafb5f36436342a5ad38df03878bb24db8855a4aa8b08b765"},
{file = "pymdown_extensions-9.9.2.tar.gz", hash = "sha256:ebb33069bafcb64d5f5988043331d4ea4929325dc678a6bcf247ddfcf96499f8"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
@@ -899,8 +912,8 @@ rfc3986 = [
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
setuptools = [
{file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"},
{file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"},
{file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"},
{file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -915,41 +928,44 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typing-extensions = [
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
{file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
{file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
]
virtualenv = [
{file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"},
{file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"},
{file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"},
{file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"},
]
watchdog = [
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"},
{file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"},
{file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"},
{file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"},
{file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"},
{file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"},
{file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"},
{file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"},
{file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"},
{file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"},
{file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"},
{file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"},
{file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"},
{file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"},
{file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"},
{file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"},
{file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"},
{file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"},
{file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"},
{file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"},
{file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"},
{file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:102a60093090fc3ff76c983367b19849b7cc24ec414a43c0333680106e62aae1"},
{file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:748ca797ff59962e83cc8e4b233f87113f3cf247c23e6be58b8a2885c7337aa3"},
{file = "watchdog-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ccd8d84b9490a82b51b230740468116b8205822ea5fdc700a553d92661253a3"},
{file = "watchdog-2.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e01d699cd260d59b84da6bda019dce0a3353e3fcc774408ae767fe88ee096b7"},
{file = "watchdog-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8586d98c494690482c963ffb24c49bf9c8c2fe0589cec4dc2f753b78d1ec301d"},
{file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:adaf2ece15f3afa33a6b45f76b333a7da9256e1360003032524d61bdb4c422ae"},
{file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83a7cead445008e880dbde833cb9e5cc7b9a0958edb697a96b936621975f15b9"},
{file = "watchdog-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8ac23ff2c2df4471a61af6490f847633024e5aa120567e08d07af5718c9d092"},
{file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0f29fd9f3f149a5277929de33b4f121a04cf84bb494634707cfa8ea8ae106a8"},
{file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:967636031fa4c4955f0f3f22da3c5c418aa65d50908d31b73b3b3ffd66d60640"},
{file = "watchdog-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96cbeb494e6cbe3ae6aacc430e678ce4b4dd3ae5125035f72b6eb4e5e9eb4f4e"},
{file = "watchdog-2.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61fdb8e9c57baf625e27e1420e7ca17f7d2023929cd0065eb79c83da1dfbeacd"},
{file = "watchdog-2.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb5ecc332112017fbdb19ede78d92e29a8165c46b68a0b8ccbd0a154f196d5e"},
{file = "watchdog-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a480d122740debf0afac4ddd583c6c0bb519c24f817b42ed6f850e2f6f9d64a8"},
{file = "watchdog-2.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:978a1aed55de0b807913b7482d09943b23a2d634040b112bdf31811a422f6344"},
{file = "watchdog-2.2.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:8c28c23972ec9c524967895ccb1954bc6f6d4a557d36e681a36e84368660c4ce"},
{file = "watchdog-2.2.1-py3-none-manylinux2014_i686.whl", hash = "sha256:c27d8c1535fd4474e40a4b5e01f4ba6720bac58e6751c667895cbc5c8a7af33c"},
{file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d6b87477752bd86ac5392ecb9eeed92b416898c30bd40c7e2dd03c3146105646"},
{file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cece1aa596027ff56369f0b50a9de209920e1df9ac6d02c7f9e5d8162eb4f02b"},
{file = "watchdog-2.2.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:8b5cde14e5c72b2df5d074774bdff69e9b55da77e102a91f36ef26ca35f9819c"},
{file = "watchdog-2.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e038be858425c4f621900b8ff1a3a1330d9edcfeaa1c0468aeb7e330fb87693e"},
{file = "watchdog-2.2.1-py3-none-win32.whl", hash = "sha256:bc43c1b24d2f86b6e1cc15f68635a959388219426109233e606517ff7d0a5a73"},
{file = "watchdog-2.2.1-py3-none-win_amd64.whl", hash = "sha256:17f1708f7410af92ddf591e94ae71a27a13974559e72f7e9fde3ec174b26ba2e"},
{file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"},
{file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"},
]
zipp = [
{file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"},
{file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"},
{file = "zipp-3.14.0-py3-none-any.whl", hash = "sha256:188834565033387710d046e3fe96acfc9b5e86cbca7f39ff69cf21a4128198b7"},
{file = "zipp-3.14.0.tar.gz", hash = "sha256:9e5421e176ef5ab4c0ad896624e87a7b2f07aca746c9b2aa305952800cb8eecb"},
]

View File

@@ -92,7 +92,9 @@ class BaseMinerAPI:
async def send_privileged_command(self, *args, **kwargs) -> dict:
return await self.send_command(*args, **kwargs)
async def multicommand(self, *commands: str, allow_warning: bool = True) -> dict:
async def multicommand(
self, *commands: str, ignore_errors: bool = False, allow_warning: bool = True
) -> dict:
"""Creates and sends multiple commands as one command to the miner.
Parameters:
@@ -107,7 +109,9 @@ class BaseMinerAPI:
# standard format doesn't work for X19
command = "+".join(commands)
try:
data = await self.send_command(command, allow_warning=allow_warning)
data = await self.send_command(
command, allow_warning=allow_warning, ignore_errors=ignore_errors
)
except APIError:
return {command: [{}] for command in commands}
logging.debug(f"{self} - (Multicommand) - Received data")
@@ -162,7 +166,7 @@ If you are sure you want to use this command please use API.send_command("{comma
reader, writer = await asyncio.open_connection(str(self.ip), self.port)
# handle OSError 121
except OSError as e:
if getattr(e, "winerror") == "121":
if e.errno == 121:
logging.warning(
f"{self} - ([Hidden] Send Bytes) - Semaphore timeout expired."
)

View File

@@ -246,7 +246,7 @@ class BTMinerAPI(BaseMinerAPI):
logging.debug(f"{self} - (Send Privileged Command) - Sending")
try:
data = await self._send_bytes(enc_command, timeout)
except (asyncio.CancelledError, asyncio.TimeoutError) as e:
except (asyncio.CancelledError, asyncio.TimeoutError):
if ignore_errors:
return {}
raise APIError("No data was returned from the API.")

View File

@@ -62,7 +62,7 @@ class CGMinerAPI(BaseMinerAPI):
for cmd in commands:
data[cmd] = []
data[cmd].append(await self.send_command(cmd, allow_warning=True))
except APIError as e:
except APIError:
pass
except Exception as e:
logging.warning(

View File

@@ -14,18 +14,24 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
import logging
import random
import string
import time
from dataclasses import asdict, dataclass, fields
from typing import Dict, List, Literal
from enum import IntEnum
from typing import List, Literal
import toml
import yaml
class X19PowerMode(IntEnum):
Normal = 0
Sleep = 1
LPM = 3
@dataclass
class _Pool:
"""A dataclass for pool information.
@@ -85,6 +91,19 @@ class _Pool:
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_x15(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an X19 device.
Parameters:
user_suffix: The suffix to append to username.
"""
username = self.username
if user_suffix:
username = f"{username}{user_suffix}"
pool = {"url": self.url, "user": username, "pass": self.password}
return pool
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a dict usable by an Innosilicon device.
@@ -182,6 +201,34 @@ class _PoolGroup:
pools.append(pool.as_x19(user_suffix=user_suffix))
return pools
def as_x15(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an X15 device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = {
"_ant_pool1url": "",
"_ant_pool1user": "",
"_ant_pool1pw": "",
"_ant_pool2url": "",
"_ant_pool2user": "",
"_ant_pool2pw": "",
"_ant_pool3url": "",
"_ant_pool3user": "",
"_ant_pool3pw": "",
}
for idx, pool in enumerate(self.pools[:3]):
pools[f"_ant_pool{idx+1}url"] = pool.as_x15(user_suffix=user_suffix)["url"]
pools[f"_ant_pool{idx+1}user"] = pool.as_x15(user_suffix=user_suffix)[
"user"
]
pools[f"_ant_pool{idx+1}pw"] = pool.as_x15(user_suffix=user_suffix)[
"pass"
]
return pools
def as_inno(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by an Innosilicon device.
@@ -195,17 +242,23 @@ class _PoolGroup:
pools[f"{key}{idx+1}"] = pool_data[key]
return pools
def as_wm(self, user_suffix: str = None) -> List[dict]:
def as_wm(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a list usable by a Whatsminer device.
Parameters:
user_suffix: The suffix to append to username.
"""
pools = []
for pool in self.pools[:3]:
pools.append(pool.as_wm(user_suffix=user_suffix))
while len(pools) < 3:
pools.append({"url": None, "user": None, "pass": None})
pools = {}
for i in range(1, 4):
if i <= len(self.pools):
pool_wm = self.pools[i - 1].as_wm(user_suffix)
pools[f"pool_{i}"] = pool_wm["url"]
pools[f"worker_{i}"] = pool_wm["user"]
pools[f"passwd_{i}"] = pool_wm["pass"]
else:
pools[f"pool_{i}"] = ""
pools[f"worker_{i}"] = ""
pools[f"passwd_{i}"] = ""
return pools
def as_avalon(self, user_suffix: str = None) -> str:
@@ -267,6 +320,7 @@ class MinerConfig:
asicboost: bool = None
miner_mode: IntEnum = X19PowerMode.Normal
autotuning_enabled: bool = True
autotuning_mode: Literal["power", "hashrate"] = None
autotuning_wattage: int = None
@@ -287,6 +341,8 @@ class MinerConfig:
logging.debug(f"MinerConfig - (To Dict) - Dumping Dict config")
data_dict = asdict(self)
for key in asdict(self).keys():
if isinstance(data_dict[key], IntEnum):
data_dict[key] = data_dict[key].value
if data_dict[key] is None:
del data_dict[key]
return data_dict
@@ -324,8 +380,7 @@ class MinerConfig:
self.fan_speed = int(data["bitmain-fan-pwm"])
elif key == "bitmain-work-mode":
if data[key]:
if data[key] == 1:
self.autotuning_wattage = 0
self.miner_mode = X19PowerMode(int(data[key]))
elif key == "fan_control":
for _key in data[key].keys():
if _key == "min_fans":
@@ -425,7 +480,7 @@ class MinerConfig:
logging.debug(f"MinerConfig - (From YAML) - Loading YAML config")
return self.from_dict(yaml.load(data, Loader=yaml.SafeLoader))
def as_wm(self, user_suffix: str = None) -> Dict[str, int]:
def as_wm(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by a Whatsminer device.
Parameters:
@@ -446,7 +501,7 @@ class MinerConfig:
logging.debug(f"MinerConfig - (As Inno) - Generating Innosilicon config")
return self.pool_groups[0].as_inno(user_suffix=user_suffix)
def as_x19(self, user_suffix: str = None) -> str:
def as_x19(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an X19 device.
Parameters:
@@ -457,10 +512,8 @@ class MinerConfig:
"pools": self.pool_groups[0].as_x19(user_suffix=user_suffix),
"bitmain-fan-ctrl": False,
"bitmain-fan-pwn": 100,
"miner-mode": 0, # Normal Mode
"miner-mode": self.miner_mode.value,
}
if self.autotuning_wattage == 0:
cfg["miner-mode"] = 1 # Sleep Mode
if not self.temp_mode == "auto":
cfg["bitmain-fan-ctrl"] = True
@@ -468,7 +521,17 @@ class MinerConfig:
if self.fan_speed:
cfg["bitmain-fan-ctrl"] = str(self.fan_speed)
return json.dumps(cfg)
return cfg
def as_x15(self, user_suffix: str = None) -> dict:
"""Convert the data in this class to a config usable by an X15 device.
Parameters:
user_suffix: The suffix to append to username.
"""
cfg = self.pool_groups[0].as_x15(user_suffix=user_suffix)
return cfg
def as_avalon(self, user_suffix: str = None) -> str:
"""Convert the data in this class to a config usable by an Avalonminer device.

View File

@@ -403,6 +403,8 @@ class MinerData:
@property
def percent_ideal(self): # noqa - Skip PyCharm inspection
if self.total_chips == 0 or self.ideal_chips == 0:
return 0
return round((self.total_chips / self.ideal_chips) * 100)
@percent_ideal.setter
@@ -427,7 +429,7 @@ class MinerData:
@property
def efficiency(self): # noqa - Skip PyCharm inspection
if self.hashrate == 0:
if self.hashrate == 0 or self.wattage == -1:
return 0
return round(self.wattage / self.hashrate)

View File

@@ -24,11 +24,27 @@ class APIError(Exception):
def __str__(self):
if self.message:
if self.message == "can't access write cmd":
return f"{self.message}, please make sure your miner has been unlocked."
return f"{self.message}"
else:
return "Incorrect API parameters."
class PhaseBalancingError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
if self.message:
return f"{self.message}"
else:
return "Failed to balance phase."
class APIWarning(Warning):
def __init__(self, *args):
if args:

344
pyasic/load/__init__.py Normal file
View File

@@ -0,0 +1,344 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
from typing import List, Union
from pyasic.errors import APIError
from pyasic.miners import AnyMiner
from pyasic.miners._backends import ( # noqa - Ignore access to _module
X19,
BOSMiner,
BTMiner,
)
from pyasic.miners._types import ( # noqa - Ignore access to _module
S9,
S17,
T17,
S17e,
S17Plus,
S17Pro,
T17e,
T17Plus,
)
FAN_USAGE = 50 # 50 W per fan
class MinerLoadBalancer:
"""A load balancer for miners. Can be passed a list of `AnyMiner`, or a list of phases (lists of `AnyMiner`)."""
def __init__(
self,
phases: Union[List[List[AnyMiner]], None] = None,
):
self.phases = [_MinerPhaseBalancer(phase) for phase in phases]
async def balance(self, wattage: int) -> int:
phase_wattage = wattage // len(self.phases)
setpoints = await asyncio.gather(
*[phase.get_balance_setpoints(phase_wattage) for phase in self.phases]
)
tasks = []
total_wattage = 0
for setpoint in setpoints:
wattage_set = 0
for miner in setpoint:
if setpoint[miner]["set"] == "on":
wattage_set += setpoint[miner]["max"]
tasks.append(setpoint[miner]["miner"].resume_mining())
elif setpoint[miner]["set"] == "off":
wattage_set += setpoint[miner]["min"]
tasks.append(setpoint[miner]["miner"].stop_mining())
else:
wattage_set += setpoint[miner]["set"]
tasks.append(
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
)
total_wattage += wattage_set
await asyncio.gather(*tasks)
return total_wattage
class _MinerPhaseBalancer:
def __init__(self, miners: List[AnyMiner]):
self.miners = {
str(miner.ip): {
"miner": miner,
"set": 0,
"min": miner.fan_count * FAN_USAGE,
}
for miner in miners
}
for miner in miners:
if (
isinstance(miner, BTMiner)
and not (miner.model.startswith("M2") if miner.model else True)
) or isinstance(miner, BOSMiner):
if isinstance(miner, S9):
self.miners[str(miner.ip)]["tune"] = True
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 1400
elif True in [
isinstance(miner, x)
for x in [S17, S17Plus, S17Pro, S17e, T17, T17Plus, T17e]
]:
self.miners[str(miner.ip)]["tune"] = True
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 2400
else:
self.miners[str(miner.ip)]["tune"] = True
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 3600
elif isinstance(miner, X19):
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 3600
elif isinstance(miner, BTMiner):
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 3600
if miner.model:
if miner.model.startswith("M2"):
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = True
self.miners[str(miner.ip)]["max"] = 2400
else:
self.miners[str(miner.ip)]["tune"] = False
self.miners[str(miner.ip)]["shutdown"] = False
self.miners[str(miner.ip)]["max"] = 3600
self.miners[str(miner.ip)]["min"] = 3600
async def balance(self, wattage: int) -> int:
setpoint = await self.get_balance_setpoints(wattage)
wattage_set = 0
tasks = []
for miner in setpoint:
if setpoint[miner]["set"] == "on":
wattage_set += setpoint[miner]["max"]
tasks.append(setpoint[miner]["miner"].resume_mining())
elif setpoint[miner]["set"] == "off":
wattage_set += setpoint[miner]["min"]
tasks.append(setpoint[miner]["miner"].stop_mining())
else:
wattage_set += setpoint[miner]["set"]
tasks.append(
setpoint[miner]["miner"].set_power_limit(setpoint[miner]["set"])
)
await asyncio.gather(*tasks)
return wattage_set
async def get_balance_setpoints(self, wattage: int) -> dict:
# gather data needed to optimize shutdown only miners
dp = ["hashrate", "wattage", "wattage_limit"]
data = await asyncio.gather(
*[
self.miners[miner]["miner"].get_data(data_to_get=dp)
for miner in self.miners
]
)
for data_point in data:
if (not self.miners[data_point.ip]["tune"]) and (
not self.miners[data_point.ip]["shutdown"]
):
# cant do anything with it so need to find a semi-accurate power limit
if not data_point.wattage_limit == -1:
self.miners[data_point.ip]["max"] = int(data_point.wattage_limit)
self.miners[data_point.ip]["min"] = int(data_point.wattage_limit)
elif not data_point.wattage == -1:
self.miners[data_point.ip]["max"] = int(data_point.wattage)
self.miners[data_point.ip]["min"] = int(data_point.wattage)
max_tune_wattage = sum(
[miner["max"] for miner in self.miners.values() if miner["tune"]]
)
max_shutdown_wattage = sum(
[
miner["max"]
for miner in self.miners.values()
if (not miner["tune"]) and (miner["shutdown"])
]
)
max_other_wattage = sum(
[
miner["max"]
for miner in self.miners.values()
if (not miner["tune"]) and (not miner["shutdown"])
]
)
min_tune_wattage = sum(
[miner["min"] for miner in self.miners.values() if miner["tune"]]
)
min_shutdown_wattage = sum(
[
miner["min"]
for miner in self.miners.values()
if (not miner["tune"]) and (miner["shutdown"])
]
)
# min_other_wattage = sum([miner["min"] for miner in self.miners.values() if (not miner["tune"]) and (not miner["shutdown"])])
# make sure wattage isnt set too high
if wattage > (max_tune_wattage + max_shutdown_wattage + max_other_wattage):
raise APIError(
f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W"
) # PhaseBalancingError(f"Wattage setpoint is too high, setpoint: {wattage}W, max: {max_tune_wattage + max_shutdown_wattage + max_other_wattage}W")
# should now know wattage limits and which can be tuned/shutdown
# check if 1/2 max of the miners which can be tuned is low enough
if (max_tune_wattage / 2) + max_shutdown_wattage + max_other_wattage < wattage:
useable_wattage = wattage - (max_other_wattage + max_shutdown_wattage)
useable_miners = len(
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
)
if not useable_miners == 0:
watts_per_miner = useable_wattage // useable_miners
# loop through and set useable miners to wattage
for miner in self.miners:
if (self.miners[miner]["set"] == 0) and (
self.miners[miner]["tune"]
):
self.miners[miner]["set"] = watts_per_miner
elif self.miners[miner]["set"] == 0 and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "on"
# check if shutting down miners will help
elif (
max_tune_wattage / 2
) + min_shutdown_wattage + max_other_wattage < wattage:
# tuneable inclusive since could be S9 BOS+ and S19 Stock, would rather shut down the S9, tuneable should always support shutdown
useable_wattage = wattage - (
min_tune_wattage + max_other_wattage + min_shutdown_wattage
)
for miner in sorted(
[miner for miner in self.miners.values() if miner["shutdown"]],
key=lambda x: x["max"],
reverse=True,
):
if miner["tune"]:
miner_min_watt_use = miner["max"] / 2
useable_wattage -= miner_min_watt_use - miner["min"]
if useable_wattage < 0:
useable_wattage += miner_min_watt_use - miner["min"]
self.miners[str(miner["miner"].ip)]["set"] = "off"
else:
miner_min_watt_use = miner["max"]
useable_wattage -= miner_min_watt_use - miner["min"]
if useable_wattage < 0:
useable_wattage += miner_min_watt_use - miner["min"]
self.miners[str(miner["miner"].ip)]["set"] = "off"
new_shutdown_wattage = sum(
[
miner["max"] if miner["set"] == 0 else miner["min"]
for miner in self.miners.values()
if miner["shutdown"] and not miner["tune"]
]
)
new_tune_wattage = sum(
[
miner["min"]
for miner in self.miners.values()
if miner["tune"] and miner["set"] == "off"
]
)
useable_wattage = wattage - (
new_tune_wattage + max_other_wattage + new_shutdown_wattage
)
useable_miners = len(
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
)
if not useable_miners == 0:
watts_per_miner = useable_wattage // useable_miners
# loop through and set useable miners to wattage
for miner in self.miners:
if (self.miners[miner]["set"] == 0) and (
self.miners[miner]["tune"]
):
self.miners[miner]["set"] = watts_per_miner
elif self.miners[miner]["set"] == 0 and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "on"
# check if shutting down tuneable miners will do it
elif min_tune_wattage + min_shutdown_wattage + max_other_wattage < wattage:
# all miners that can be shutdown need to be
for miner in self.miners:
if (not self.miners[miner]["tune"]) and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "off"
# calculate wattage usable by tuneable miners
useable_wattage = wattage - (
min_tune_wattage + max_other_wattage + min_shutdown_wattage
)
# loop through miners to see how much is actually useable
# sort the largest first
for miner in sorted(
[
miner
for miner in self.miners.values()
if miner["tune"] and miner["shutdown"]
],
key=lambda x: x["max"],
reverse=True,
):
# add min to useable wattage since it was removed earlier, and remove 1/2 tuner max
useable_wattage -= (miner["max"] / 2) - miner["min"]
if useable_wattage < 0:
useable_wattage += (miner["max"] / 2) - miner["min"]
self.miners[str(miner["miner"].ip)]["set"] = "off"
new_tune_wattage = sum(
[
miner["min"]
for miner in self.miners.values()
if miner["tune"] and miner["set"] == "off"
]
)
useable_wattage = wattage - (
new_tune_wattage + max_other_wattage + min_shutdown_wattage
)
useable_miners = len(
[m for m in self.miners.values() if (m["set"] == 0) and (m["tune"])]
)
if not useable_miners == 0:
watts_per_miner = useable_wattage // useable_miners
# loop through and set useable miners to wattage
for miner in self.miners:
if (self.miners[miner]["set"] == 0) and (
self.miners[miner]["tune"]
):
self.miners[miner]["set"] = watts_per_miner
elif self.miners[miner]["set"] == 0 and (
self.miners[miner]["shutdown"]
):
self.miners[miner]["set"] = "on"
else:
raise APIError(
f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W"
) # PhaseBalancingError(f"Wattage setpoint is too low, setpoint: {wattage}W, min: {min_tune_wattage + min_shutdown_wattage + max_other_wattage}W")
return self.miners

View File

@@ -0,0 +1,176 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import Union
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.web.X15 import X15WebAPI
from typing import List
from pyasic.data import Fan, HashBoard
from pyasic.errors import APIError
from pyasic.config import MinerConfig, X19PowerMode
class X15(CGMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver=api_ver)
self.ip = ip
self.web = X15WebAPI(ip)
async def get_config(self) -> MinerConfig:
data = await self.web.get_miner_conf()
if data:
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
await self.web.set_miner_conf(config.as_x15(user_suffix=user_suffix))
async def get_mac(self) -> Union[str, None]:
try:
data = await self.web.get_system_info()
if data:
return data["macaddr"]
except KeyError:
pass
async def fault_light_on(self) -> bool:
# this should time out, after it does do a check
await self.web.blink(blink=True)
try:
data = await self.web.get_blink_status()
if data:
if data["isBlinking"]:
self.light = True
except KeyError:
pass
return self.light
async def fault_light_off(self) -> bool:
await self.web.blink(blink=False)
try:
data = await self.web.get_blink_status()
if data:
if not data["isBlinking"]:
self.light = False
except KeyError:
pass
return self.light
async def reboot(self) -> bool:
data = await self.web.reboot()
if data:
return True
return False
async def get_fault_light(self) -> bool:
if self.light:
return self.light
try:
data = await self.web.get_blink_status()
if data:
self.light = data["isBlinking"]
except KeyError:
pass
return self.light
async def get_hostname(self) -> Union[str, None]:
try:
data = await self.web.get_system_info()
if data:
return data["hostname"]
except KeyError:
pass
async def get_fans(self, api_stats: dict = None) -> List[Fan]:
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
fans_data = [Fan(), Fan(), Fan(), Fan()]
if api_stats:
try:
fan_offset = -1
for fan_num in range(1, 8, 4):
for _f_num in range(4):
f = api_stats["STATS"][1].get(f"fan{fan_num + _f_num}")
if f and not f == 0 and fan_offset == -1:
fan_offset = fan_num + 2
if fan_offset == -1:
fan_offset = 3
for fan in range(self.fan_count):
fans_data[fan] = Fan(
api_stats["STATS"][1].get(f"fan{fan_offset+fan}")
)
except (KeyError, IndexError):
pass
return fans_data
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = []
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
board_offset = -1
boards = api_stats["STATS"]
if len(boards) > 1:
for board_num in range(1, 16, 5):
for _b_num in range(5):
b = boards[1].get(f"chain_acn{board_num + _b_num}")
if b and not b == 0 and board_offset == -1:
board_offset = board_num
if board_offset == -1:
board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
)
chip_temp = boards[1].get(f"temp{i}")
if chip_temp:
hashboard.chip_temp = round(chip_temp)
temp = boards[1].get(f"temp2_{i}")
if temp:
hashboard.temp = round(temp)
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = round(float(hashrate), 2)
chips = boards[1].get(f"chain_acn{i}")
if chips:
hashboard.chips = chips
hashboard.missing = False
if (not chips) or (not chips > 0):
hashboard.missing = True
hashboards.append(hashboard)
except (IndexError, KeyError, ValueError, TypeError):
pass
return hashboards

View File

@@ -15,56 +15,31 @@
# ------------------------------------------------------------------------------
import asyncio
import json
from typing import List, Optional, Union
import httpx
from pyasic.API import APIError
from pyasic.config import MinerConfig
from pyasic.config import MinerConfig, X19PowerMode
from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
from pyasic.web.X19 import X19WebAPI
class X19(BMMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver=api_ver)
self.ip = ip
self.uname = "root"
self.pwd = PyasicSettings().global_x19_password
async def send_web_command(
self, command: str, params: dict = None
) -> Optional[dict]:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.uname, self.pwd)
try:
async with httpx.AsyncClient() as client:
if params:
data = await client.post(url, data=params, auth=auth)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
self.web = X19WebAPI(ip)
async def get_config(self) -> MinerConfig:
data = await self.send_web_command("get_miner_conf")
data = await self.web.get_miner_conf()
if data:
self.config = MinerConfig().from_raw(data)
return self.config
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
self.config = config
conf = config.as_x19(user_suffix=user_suffix)
await self.send_web_command(
"set_miner_conf", params=conf # noqa: ignore conf being a str
)
await self.web.set_miner_conf(conf)
for i in range(7):
data = await self.get_config()
@@ -73,46 +48,40 @@ class X19(BMMiner):
await asyncio.sleep(1)
async def fault_light_on(self) -> bool:
data = await self.send_web_command(
"blink",
params=json.dumps({"blink": "true"}), # noqa - ignore params being a str
)
data = await self.web.blink(blink=True)
if data:
if data.get("code") == "B000":
self.light = True
return self.light
async def fault_light_off(self) -> bool:
data = await self.send_web_command(
"blink",
params=json.dumps({"blink": "false"}), # noqa - ignore params being a str
)
data = await self.web.blink(blink=False)
if data:
if data.get("code") == "B100":
self.light = True
return self.light
async def reboot(self) -> bool:
data = await self.send_web_command("reboot")
data = await self.web.reboot()
if data:
return True
return False
async def stop_mining(self) -> bool:
cfg = await self.get_config()
cfg.autotuning_wattage = 0
cfg.miner_mode = X19PowerMode.Sleep
await self.send_config(cfg)
return True
async def resume_mining(self) -> bool:
cfg = await self.get_config()
cfg.autotuning_wattage = 1
cfg.miner_mode = X19PowerMode.Normal
await self.send_config(cfg)
return True
async def get_hostname(self) -> Union[str, None]:
try:
data = await self.send_web_command("get_system_info")
data = await self.web.get_system_info()
if data:
return data["hostname"]
except KeyError:
@@ -120,14 +89,14 @@ class X19(BMMiner):
async def get_mac(self) -> Union[str, None]:
try:
data = await self.send_web_command("get_system_info")
data = await self.web.get_system_info()
if data:
return data["macaddr"]
except KeyError:
pass
try:
data = await self.send_web_command("get_network_info")
data = await self.web.get_network_info()
if data:
return data["macaddr"]
except KeyError:
@@ -135,7 +104,7 @@ class X19(BMMiner):
async def get_errors(self) -> List[MinerErrorData]:
errors = []
data = await self.send_web_command("summary")
data = await self.web.summary()
if data:
try:
for item in data["SUMMARY"][0]["status"]:
@@ -152,7 +121,7 @@ class X19(BMMiner):
if self.light:
return self.light
try:
data = await self.send_web_command("get_blink_status")
data = await self.web.get_blink_status()
if data:
self.light = data["blink"]
except KeyError:
@@ -192,42 +161,34 @@ class X19(BMMiner):
):
if not hostname:
hostname = await self.get_hostname()
payload = {
"ipAddress": ip,
"ipDns": dns,
"ipGateway": gateway,
"ipHost": hostname,
"ipPro": 2, # static
"ipSub": subnet_mask,
}
await self.send_web_command("set_network_conf", params=payload)
await self.web.set_network_conf(
ip=ip,
dns=dns,
gateway=gateway,
subnet_mask=subnet_mask,
hostname=hostname,
protocol=2,
)
async def set_dhcp(self, hostname: str = None):
if not hostname:
hostname = await self.get_hostname()
payload = {
"ipAddress": "",
"ipDns": "",
"ipGateway": "",
"ipHost": hostname,
"ipPro": 1, # DHCP
"ipSub": "",
}
await self.send_web_command("set_network_conf", params=payload)
await self.web.set_network_conf(
ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1
)
async def set_hostname(self, hostname: str):
cfg = await self.send_web_command("get_network_info")
cfg = await self.web.get_network_info()
dns = cfg["conf_dnsservers"]
gateway = cfg["conf_gateway"]
ip = cfg["conf_ipaddress"]
subnet_mask = cfg["conf_netmask"]
protocol = 1 if cfg["conf_nettype"] == "DHCP" else 2
payload = {
"ipAddress": ip,
"ipDns": dns,
"ipGateway": gateway,
"ipHost": hostname,
"ipPro": protocol,
"ipSub": subnet_mask,
}
await self.send_web_command("set_network_conf", params=payload)
await self.web.set_network_conf(
ip=ip,
dns=dns,
gateway=gateway,
subnet_mask=subnet_mask,
hostname=hostname,
protocol=protocol,
)

View File

@@ -0,0 +1,263 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
from typing import List, Optional, Union
from pyasic.API import APIError
from pyasic.config import MinerConfig, X19PowerMode
from pyasic.data.error_codes import MinerErrorData, X19Error
from pyasic.data import HashBoard
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.web.X7 import X7WebAPI
class X7(BMMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver=api_ver)
self.ip = ip
self.web = X7WebAPI(ip)
async def get_config(self) -> MinerConfig:
data = await self.web.get_miner_conf()
if data:
self.config = MinerConfig().from_raw(data)
return self.config
async def get_hashrate(self, api_summary: dict = None) -> Optional[float]:
# get hr from API
if not api_summary:
try:
api_summary = await self.api.summary()
except APIError:
pass
if api_summary:
try:
# not actually GHS, MHS.
return round(float(api_summary["SUMMARY"][0]["GHS 5s"] / 1000000), 5)
except (IndexError, KeyError, ValueError, TypeError):
pass
async def get_hashboards(self, api_stats: dict = None) -> List[HashBoard]:
hashboards = []
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
board_offset = -1
boards = api_stats["STATS"]
if len(boards) > 1:
for board_num in range(1, 16, 5):
for _b_num in range(5):
b = boards[1].get(f"chain_acn{board_num + _b_num}")
if b and not b == 0 and board_offset == -1:
board_offset = board_num
if board_offset == -1:
board_offset = 1
for i in range(board_offset, board_offset + self.ideal_hashboards):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.nominal_chips
)
chip_temp = boards[1].get(f"temp{i}")
if chip_temp:
hashboard.chip_temp = round(chip_temp)
temp = boards[1].get(f"temp2_{i}")
if temp:
hashboard.temp = round(temp)
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = round(float(hashrate) / 1000, 5)
chips = boards[1].get(f"chain_acn{i}")
if chips:
hashboard.chips = chips
hashboard.missing = False
if (not chips) or (not chips > 0):
hashboard.missing = True
hashboards.append(hashboard)
except (IndexError, KeyError, ValueError, TypeError):
pass
return hashboards
async def get_nominal_hashrate(self, api_stats: dict = None) -> Optional[float]:
# X19 method, not sure compatibility
if not api_stats:
try:
api_stats = await self.api.stats()
except APIError:
pass
if api_stats:
try:
ideal_rate = api_stats["STATS"][1]["total_rateideal"]
try:
rate_unit = api_stats["STATS"][1]["rate_unit"]
except KeyError:
rate_unit = "MH"
if rate_unit == "GH":
return round(ideal_rate, 2)
if rate_unit == "MH":
return round(ideal_rate / 1000000, 5)
else:
return round(ideal_rate, 2)
except (KeyError, IndexError):
pass
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
self.config = config
conf = config.as_x19(user_suffix=user_suffix)
await self.web.set_miner_conf(conf)
for i in range(7):
data = await self.get_config()
if data.as_x19() == conf:
break
await asyncio.sleep(1)
async def fault_light_on(self) -> bool:
data = await self.web.blink(blink=True)
if data:
if data.get("code") == "B000":
self.light = True
return self.light
async def fault_light_off(self) -> bool:
data = await self.web.blink(blink=False)
if data:
if data.get("code") == "B100":
self.light = True
return self.light
async def reboot(self) -> bool:
data = await self.web.reboot()
if data:
return True
return False
async def stop_mining(self) -> bool:
cfg = await self.get_config()
cfg.miner_mode = X19PowerMode.Sleep
await self.send_config(cfg)
return True
async def resume_mining(self) -> bool:
cfg = await self.get_config()
cfg.miner_mode = X19PowerMode.Normal
await self.send_config(cfg)
return True
async def get_hostname(self) -> Union[str, None]:
try:
data = await self.web.get_system_info()
if data:
return data["hostname"]
except KeyError:
pass
async def get_mac(self) -> Union[str, None]:
try:
data = await self.web.get_system_info()
if data:
return data["macaddr"]
except KeyError:
pass
try:
data = await self.web.get_network_info()
if data:
return data["macaddr"]
except KeyError:
pass
async def get_errors(self) -> List[MinerErrorData]:
errors = []
data = await self.web.summary()
if data:
try:
for item in data["SUMMARY"][0]["status"]:
try:
if not item["status"] == "s":
errors.append(X19Error(item["msg"]))
except KeyError:
continue
except (KeyError, IndexError):
pass
return errors
async def get_fault_light(self) -> bool:
if self.light:
return self.light
try:
data = await self.web.get_blink_status()
if data:
self.light = data["blink"]
except KeyError:
pass
return self.light
async def set_static_ip(
self,
ip: str,
dns: str,
gateway: str,
subnet_mask: str = "255.255.255.0",
hostname: str = None,
):
if not hostname:
hostname = await self.get_hostname()
await self.web.set_network_conf(
ip=ip,
dns=dns,
gateway=gateway,
subnet_mask=subnet_mask,
hostname=hostname,
protocol=2,
)
async def set_dhcp(self, hostname: str = None):
if not hostname:
hostname = await self.get_hostname()
await self.web.set_network_conf(
ip="", dns="", gateway="", subnet_mask="", hostname=hostname, protocol=1
)
async def set_hostname(self, hostname: str):
cfg = await self.web.get_network_info()
dns = cfg["conf_dnsservers"]
gateway = cfg["conf_gateway"]
ip = cfg["conf_ipaddress"]
subnet_mask = cfg["conf_netmask"]
protocol = 1 if cfg["conf_nettype"] == "DHCP" else 2
await self.web.set_network_conf(
ip=ip,
dns=dns,
gateway=gateway,
subnet_mask=subnet_mask,
hostname=hostname,
protocol=protocol,
)

View File

@@ -22,3 +22,5 @@ from .cgminer_avalon import CGMinerAvalon
from .hiveon import Hiveon
from .vnish import VNish
from .X19 import X19
from .X7 import X7
from .X15 import X15

View File

@@ -17,17 +17,16 @@
import ipaddress
import logging
from collections import namedtuple
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Tuple
import asyncssh
from pyasic.API.bmminer import BMMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class BMMiner(BaseMiner):

View File

@@ -26,11 +26,10 @@ import toml
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import BraiinsOSError, MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class BOSMiner(BaseMiner):
@@ -199,7 +198,9 @@ class BOSMiner(BaseMiner):
if conn:
async with conn:
# good ol' BBB compatibility :/
toml_data = toml.loads((await conn.run("cat /etc/bosminer.toml")).stdout)
toml_data = toml.loads(
(await conn.run("cat /etc/bosminer.toml")).stdout
)
logging.debug(f"{self}: Converting config file.")
cfg = MinerConfig().from_raw(toml_data)
self.config = cfg
@@ -208,6 +209,7 @@ class BOSMiner(BaseMiner):
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
logging.debug(f"{self}: Sending config.")
self.config = config
toml_conf = config.as_bos(
model=self.model.replace(" (BOS)", ""), user_suffix=user_suffix
)
@@ -217,7 +219,9 @@ class BOSMiner(BaseMiner):
return None
async with conn:
# BBB check because bitmain suxx
bbb_check = await conn.run("if [ ! -f /etc/init.d/bosminer ]; then echo '1'; else echo '0'; fi;")
bbb_check = await conn.run(
"if [ ! -f /etc/init.d/bosminer ]; then echo '1'; else echo '0'; fi;"
)
bbb = bbb_check.stdout.strip() == "1"
@@ -685,14 +689,15 @@ class BOSMiner(BaseMiner):
# need to use get_config, as this will never read perfectly as there are some bad edge cases
groups = []
cfg = await self.get_config()
for group in cfg.pool_groups:
pools = {"quota": group.quota}
for _i, _pool in enumerate(group.pools):
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
"stratum+tcp://", ""
).replace("stratum2+tcp://", "")
pools[f"pool_{_i + 1}_user"] = _pool.username
groups.append(pools)
if cfg:
for group in cfg.pool_groups:
pools = {"quota": group.quota}
for _i, _pool in enumerate(group.pools):
pools[f"pool_{_i + 1}_url"] = _pool.url.replace(
"stratum+tcp://", ""
).replace("stratum2+tcp://", "")
pools[f"pool_{_i + 1}_user"] = _pool.username
groups.append(pools)
return groups
else:
groups[0][f"pool_{i + 1}_url"] = (
@@ -834,7 +839,6 @@ class BOSMiner(BaseMiner):
api_devs = await self.api.devs()
except APIError:
pass
nom_hr = 0
if api_devs:
try:

View File

@@ -16,8 +16,7 @@
import ipaddress
import logging
from collections import namedtuple
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Tuple
import asyncssh
@@ -180,5 +179,5 @@ class BOSMinerOld(BaseMiner):
async def get_nominal_hashrate(self) -> Optional[float]:
return None
async def get_data(self, allow_warning: bool = False) -> MinerData:
async def get_data(self, allow_warning: bool = False, **kwargs) -> MinerData:
return MinerData(ip=str(self.ip))

View File

@@ -16,16 +16,16 @@
import ipaddress
import logging
import warnings
from collections import namedtuple
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Tuple
from pyasic.API.btminer import BTMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class BTMiner(BaseMiner):
@@ -115,21 +115,13 @@ class BTMiner(BaseMiner):
return False
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
self.config = config
conf = config.as_wm(user_suffix=user_suffix)
pools_conf = conf["pools"]
try:
await self.api.update_pools(
pools_conf[0]["url"],
pools_conf[0]["user"],
pools_conf[0]["pass"],
pools_conf[1]["url"],
pools_conf[1]["user"],
pools_conf[1]["pass"],
pools_conf[2]["url"],
pools_conf[2]["user"],
pools_conf[2]["pass"],
)
await self.api.update_pools(**pools_conf)
except APIError:
pass
try:
@@ -149,16 +141,25 @@ class BTMiner(BaseMiner):
summary = data["summary"][0]
except APIError as e:
logging.warning(e)
except LookupError:
pass
if pools:
if "POOLS" in pools:
cfg = cfg.from_api(pools["POOLS"])
else:
# somethings wrong with the miner
warnings.warn(
f"Failed to gather pool config for miner: {self}, miner did not return pool information."
)
if summary:
if "SUMMARY" in summary:
if wattage := summary["SUMMARY"][0].get("Power Limit"):
cfg.autotuning_wattage = wattage
return cfg
self.config = cfg
return self.config
async def set_power_limit(self, wattage: int) -> bool:
try:
@@ -534,8 +535,6 @@ class BTMiner(BaseMiner):
pass
async def get_fault_light(self, api_get_miner_info: dict = None) -> bool:
data = None
if not api_get_miner_info:
try:
api_get_miner_info = await self.api.get_miner_info()

View File

@@ -17,17 +17,16 @@
import ipaddress
import logging
from collections import namedtuple
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Tuple
import asyncssh
from pyasic.API.cgminer import CGMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class CGMiner(BaseMiner):
@@ -129,12 +128,8 @@ class CGMiner(BaseMiner):
else:
return True
async def get_config(self, api_pools: dict = None) -> MinerConfig:
# get pool data
try:
api_pools = await self.api.pools()
except APIError:
pass
async def get_config(self) -> MinerConfig:
api_pools = await self.api.pools()
if api_pools:
self.config = MinerConfig().from_api(api_pools["POOLS"])

View File

@@ -14,20 +14,15 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
import ipaddress
import logging
import re
from collections import namedtuple
from typing import List, Optional, Tuple, Union
from typing import List, Optional
from pyasic.API.cgminer import CGMinerAPI
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard, MinerData
from pyasic.data import Fan, HashBoard
from pyasic.data.error_codes import MinerErrorData
from pyasic.errors import APIError
from pyasic.miners._backends import CGMiner
from pyasic.miners.base import BaseMiner
from pyasic.settings import PyasicSettings
class CGMinerAvalon(CGMiner):
@@ -74,11 +69,12 @@ class CGMinerAvalon(CGMiner):
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
"""Configures miner with yaml config."""
self.config = config
return None
logging.debug(f"{self}: Sending config.") # noqa - This doesnt work...
conf = config.as_avalon(user_suffix=user_suffix)
try:
data = await self.api.ascset(
data = await self.api.ascset( # noqa
0, "setpool", f"root,root,{conf}"
) # this should work but doesn't
except APIError:

View File

@@ -14,92 +14,19 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
import logging
import warnings
from typing import Optional, Union
import httpx
from typing import Optional
from pyasic.errors import APIError
from pyasic.miners._backends.bmminer import BMMiner
from pyasic.settings import PyasicSettings
from pyasic.web.vnish import VNishWebAPI
class VNish(BMMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver)
self.api_type = "VNish"
self.uname = "root"
self.pwd = PyasicSettings().global_vnish_password
self.jwt = None
async def auth(self):
async with httpx.AsyncClient() as client:
try:
auth = await client.post(
f"http://{self.ip}/api/v1/unlock",
json={"pw": self.pwd},
)
except httpx.HTTPError:
warnings.warn(f"Could not authenticate web token with miner: {self}")
else:
if not auth.status_code == 200:
warnings.warn(
f"Could not authenticate web token with miner: {self}"
)
return None
json_auth = auth.json()
self.jwt = json_auth["token"]
return self.jwt
async def send_web_command(
self, command: str, data: Union[dict, None] = None, method: str = "GET"
):
if not self.jwt:
await self.auth()
if not data:
data = {}
async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries):
try:
auth = self.jwt
if command.startswith("system"):
auth = "Bearer " + self.jwt
if method == "GET":
response = await client.get(
f"http://{self.ip}/api/v1/{command}",
headers={"Authorization": auth},
timeout=5,
)
elif method == "POST":
if data:
response = await client.post(
f"http://{self.ip}/api/v1/{command}",
headers={"Authorization": auth},
timeout=5,
json=data,
)
else:
response = await client.post(
f"http://{self.ip}/api/v1/{command}",
headers={"Authorization": auth},
timeout=5,
)
else:
raise APIError("Bad method type.")
if not response.status_code == 200:
# refresh the token, retry
await self.auth()
continue
json_data = response.json()
if json_data:
return json_data
return True
except httpx.HTTPError:
pass
except json.JSONDecodeError:
pass
self.web = VNishWebAPI(ip)
async def get_model(self, api_stats: dict = None) -> Optional[str]:
# check if model is cached
@@ -122,16 +49,26 @@ class VNish(BMMiner):
pass
async def restart_backend(self) -> bool:
data = await self.send_web_command("mining/restart", method="POST")
return data
data = await self.web.restart_vnish()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def reboot(self) -> bool:
data = await self.send_web_command("system/reboot", method="POST")
return data
data = await self.web.reboot()
if data:
try:
return data["success"]
except KeyError:
pass
return False
async def get_mac(self, web_summary: dict = None) -> str:
if not web_summary:
web_info = await self.send_web_command("info")
web_info = await self.web.info()
if web_info:
try:
@@ -149,7 +86,7 @@ class VNish(BMMiner):
async def get_hostname(self, web_summary: dict = None) -> str:
if not web_summary:
web_info = await self.send_web_command("info")
web_info = await self.web.info()
if web_info:
try:
@@ -167,7 +104,7 @@ class VNish(BMMiner):
async def get_wattage(self, web_summary: dict = None) -> Optional[int]:
if not web_summary:
web_summary = await self.send_web_command("summary")
web_summary = await self.web.summary()
if web_summary:
try:
@@ -196,7 +133,7 @@ class VNish(BMMiner):
async def get_wattage_limit(self, web_settings: dict = None) -> Optional[int]:
if not web_settings:
web_settings = await self.send_web_command("summary")
web_settings = await self.web.summary()
if web_settings:
try:
@@ -207,7 +144,7 @@ class VNish(BMMiner):
async def get_fw_ver(self, web_summary: dict = None) -> Optional[str]:
if not web_summary:
web_summary = await self.send_web_command("summary")
web_summary = await self.web.summary()
if web_summary:
try:

View File

@@ -0,0 +1,26 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._types.makes import AntMiner
class Z15(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "Z15"
self.nominal_chips = 3
self.fan_count = 2

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Z15 import *

View File

@@ -0,0 +1,25 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._types.makes import AntMiner
class L7(AntMiner): # noqa - ignore ABC method implementation
def __init__(self, ip: str):
super().__init__()
self.ip = ip
self.model = "L7"
self.nominal_chips = 120
self.fan_count = 4

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .L7 import L7

View File

@@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .X7 import *
from .X9 import *
from .X15 import *
from .X17 import *
from .X19 import *

View File

@@ -174,10 +174,7 @@ class M30SPlusPlusVH50(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__()
self.ip = ip
self.model = "M30S++ VH50"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH50, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 74
self.fan_count = 2
@@ -195,10 +192,7 @@ class M30SPlusPlusVH70(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__()
self.ip = ip
self.model = "M30S++ VH70"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M30S++ VH70, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 70
self.fan_count = 2

View File

@@ -142,6 +142,7 @@ from .M31S_Plus import (
)
from .M31SE import M31SEV10, M31SEV20, M31SEV30
from .M32 import M32V10, M32V20
from .M32S import M32S
from .M33 import M33V10, M33V20, M33V30
from .M33S import M33SVG30
from .M33S_Plus import M33SPlusVH20, M33SPlusVH30

View File

@@ -48,10 +48,7 @@ class M50VH20(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__()
self.ip = ip
self.model = "M50 VH20"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M50 VH20, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 111
self.fan_count = 2
@@ -72,10 +69,7 @@ class M50VH40(WhatsMiner): # noqa - ignore ABC method implementation
super().__init__()
self.ip = ip
self.model = "M50 VH40"
self.nominal_chips = 0
warnings.warn(
"Unknown chip count for miner type M50 VH40, please open an issue on GitHub (https://github.com/UpstreamData/pyasic)."
)
self.nominal_chips = 84
self.fan_count = 2

View File

@@ -14,45 +14,21 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
from typing import Optional, Union
import httpx
from typing import Union
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
from pyasic.web.X17 import X17WebAPI
class BMMinerX17(BMMiner):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver=api_ver)
self.ip = ip
self.uname = "root"
self.pwd = PyasicSettings().global_x17_password
async def send_web_command(
self, command: str, params: dict = None
) -> Optional[dict]:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.uname, self.pwd)
try:
async with httpx.AsyncClient() as client:
if params:
data = await client.post(url, data=params, auth=auth)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
self.web = X17WebAPI(ip)
async def get_mac(self) -> Union[str, None]:
try:
data = await self.send_web_command("get_system_info")
data = await self.web.get_system_info()
if data:
return data["macaddr"]
except KeyError:
@@ -60,11 +36,9 @@ class BMMinerX17(BMMiner):
async def fault_light_on(self) -> bool:
# this should time out, after it does do a check
await self.send_web_command("blink", params={"action": "startBlink"})
await self.web.blink(blink=True)
try:
data = await self.send_web_command(
"blink", params={"action": "onPageLoaded"}
)
data = await self.web.get_blink_status()
if data:
if data["isBlinking"]:
self.light = True
@@ -73,11 +47,9 @@ class BMMinerX17(BMMiner):
return self.light
async def fault_light_off(self) -> bool:
await self.send_web_command("blink", params={"action": "stopBlink"})
await self.web.blink(blink=False)
try:
data = await self.send_web_command(
"blink", params={"action": "onPageLoaded"}
)
data = await self.web.get_blink_status()
if data:
if not data["isBlinking"]:
self.light = False
@@ -86,7 +58,7 @@ class BMMinerX17(BMMiner):
return self.light
async def reboot(self) -> bool:
data = await self.send_web_command("reboot")
data = await self.web.reboot()
if data:
return True
return False
@@ -95,9 +67,7 @@ class BMMinerX17(BMMiner):
if self.light:
return self.light
try:
data = await self.send_web_command(
"blink", params={"action": "onPageLoaded"}
)
data = await self.web.get_blink_status()
if data:
self.light = data["isBlinking"]
except KeyError:
@@ -106,7 +76,7 @@ class BMMinerX17(BMMiner):
async def get_hostname(self) -> Union[str, None]:
try:
data = await self.send_web_command("get_system_info")
data = await self.web.get_system_info()
if data:
return data["hostname"]
except KeyError:

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import S19 # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import S19Pro # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import S19XP # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import S19a # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import S19aPro # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import S19j # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import S19jPro # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -14,7 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X19
from pyasic.miners._backends import X19 # noqa - Ignore access to _module
from pyasic.miners._types import T19 # noqa - Ignore access to _module
# noqa - Ignore access to _module

View File

@@ -0,0 +1,24 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X7 # noqa - Ignore access to _module
from pyasic.miners._types import L7 # noqa - Ignore access to _module
# noqa - Ignore access to _module
class BMMinerL7(X7, L7):
pass

View File

@@ -0,0 +1,17 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .L7 import BMMinerL7

View File

@@ -14,53 +14,29 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
from typing import Optional, Union
import httpx
from typing import Union
from pyasic.miners._backends import BMMiner # noqa - Ignore access to _module
from pyasic.miners._types import S9 # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
from pyasic.web.S9 import S9WebAPI
class BMMinerS9(BMMiner, S9):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver=api_ver)
self.ip = ip
self.uname = "root"
self.pwd = PyasicSettings().global_x19_password
async def send_web_command(
self, command: str, params: dict = None
) -> Optional[dict]:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.uname, self.pwd)
try:
async with httpx.AsyncClient() as client:
if params:
data = await client.post(url, data=params, auth=auth)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
self.web = S9WebAPI(ip)
async def get_mac(self) -> Union[str, None]:
try:
data = await self.send_web_command("get_system_info")
data = await self.web.get_system_info()
if data:
return data["macaddr"]
except KeyError:
pass
try:
data = await self.send_web_command("get_network_info")
data = await self.web.get_network_info()
if data:
return data["macaddr"]
except KeyError:

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .X7 import *
from .X9 import *
from .X17 import *
from .X19 import *

View File

@@ -0,0 +1,22 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from pyasic.miners._backends import X15 # noqa - Ignore access to _module
from pyasic.miners._types import Z15 # noqa - Ignore access to _module
class CGMinerZ15(X15, Z15):
pass

View File

@@ -0,0 +1,16 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from .Z15 import CGMinerZ15

View File

@@ -15,5 +15,6 @@
# ------------------------------------------------------------------------------
from .X9 import *
from .X15 import *
from .X17 import *
from .X19 import *

View File

@@ -18,11 +18,10 @@ from typing import List, Optional
import asyncssh
from pyasic.data import HashBoard, MinerData
from pyasic.data import HashBoard
from pyasic.errors import APIError
from pyasic.miners._backends import Hiveon # noqa - Ignore access to _module
from pyasic.miners._types import T9 # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
class HiveonT9(Hiveon, T9):

View File

@@ -31,9 +31,10 @@ from pyasic.errors import APIError
class BaseMiner(ABC):
def __init__(self, *args, **kwargs) -> None:
self.ip = None
self.uname = "root"
self.pwd = "admin"
self.api = None
self.web = None
self.uname = None
self.pwd = None
self.api_type = None
self.api_ver = None
self.fw_ver = None
@@ -162,7 +163,7 @@ class BaseMiner(ABC):
@abstractmethod
async def resume_mining(self) -> bool:
"""Stop the mining process of the miner.
"""Resume the mining process of the miner.
Returns:
A boolean value of the success of resuming the mining process.
@@ -396,8 +397,13 @@ class BaseMiner(ABC):
web_data = {}
for command in web_params:
data = await self.send_web_command(command) # noqa: web only anyway
web_data[command] = data
try:
cmd_func = getattr(self.web, command)
data = await cmd_func() # noqa: web only anyway
except (LookupError, APIError):
pass
else:
web_data[command] = data
for data_name in data_to_get:
function = getattr(self, "get_" + data_name)
sig = inspect.signature(function)

View File

@@ -13,13 +13,8 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
import logging
import warnings
from collections import namedtuple
from typing import List, Optional, Tuple, Union
import httpx
from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import Fan, HashBoard
@@ -27,65 +22,14 @@ from pyasic.data.error_codes import InnosiliconError, MinerErrorData
from pyasic.errors import APIError
from pyasic.miners._backends import CGMiner # noqa - Ignore access to _module
from pyasic.miners._types import InnosiliconT3HPlus # noqa - Ignore access to _module
from pyasic.settings import PyasicSettings
from pyasic.web.Inno import InnosiliconWebAPI
class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
def __init__(self, ip: str, api_ver: str = "0.0.0") -> None:
super().__init__(ip, api_ver=api_ver)
self.ip = ip
self.uname = "admin"
self.pwd = PyasicSettings().global_innosilicon_password
self.jwt = None
async def auth(self):
async with httpx.AsyncClient() as client:
try:
auth = await client.post(
f"http://{self.ip}/api/auth",
data={"username": self.uname, "password": self.pwd},
)
except httpx.HTTPError:
warnings.warn(f"Could not authenticate web token with miner: {self}")
else:
json_auth = auth.json()
self.jwt = json_auth.get("jwt")
return self.jwt
async def send_web_command(self, command: str, data: Union[dict, None] = None):
if not self.jwt:
await self.auth()
if not data:
data = {}
async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries):
try:
response = await client.post(
f"http://{self.ip}/api/{command}",
headers={"Authorization": "Bearer " + self.jwt},
timeout=5,
data=data,
)
json_data = response.json()
if (
not json_data.get("success")
and "token" in json_data
and json_data.get("token") == "expired"
):
# refresh the token, retry
await self.auth()
continue
if not json_data.get("success"):
if json_data.get("msg"):
raise APIError(json_data["msg"])
elif json_data.get("message"):
raise APIError(json_data["message"])
raise APIError("Innosilicon web api command failed.")
return json_data
except httpx.HTTPError:
pass
except json.JSONDecodeError:
pass
self.web = InnosiliconWebAPI(ip)
async def fault_light_on(self) -> bool:
return False
@@ -108,7 +52,7 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
async def reboot(self) -> bool:
try:
data = await self.send_web_command("reboot")
data = await self.web.reboot()
except APIError:
pass
else:
@@ -116,7 +60,7 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
async def restart_cgminer(self) -> bool:
try:
data = await self.send_web_command("restartCgMiner")
data = await self.web.restart_cgminer()
except APIError:
pass
else:
@@ -126,29 +70,25 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
return await self.restart_cgminer()
async def send_config(self, config: MinerConfig, user_suffix: str = None) -> None:
await self.send_web_command(
"updatePools", data=config.as_inno(user_suffix=user_suffix)
)
self.config = config
await self.web.update_pools(config.as_inno(user_suffix=user_suffix))
##################################################
### DATA GATHERING FUNCTIONS (get_{some_data}) ###
##################################################
async def get_mac(
self,
web_getAll: dict = None, # noqa
web_overview: dict = None, # noqa: named this way for automatic functionality
self, web_get_all: dict = None, web_overview: dict = None
) -> Optional[str]:
web_all_data = web_getAll.get("all")
if not web_all_data and not web_overview:
if not web_get_all and not web_overview:
try:
web_overview = await self.send_web_command("overview")
web_overview = await self.web.overview()
except APIError:
pass
if web_all_data:
if web_get_all:
try:
mac = web_all_data["mac"]
mac = web_get_all["mac"]
return mac.upper()
except KeyError:
pass
@@ -167,7 +107,7 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
if not web_type:
try:
web_type = await self.send_web_command("type")
web_type = await self.web.type()
except APIError:
pass
@@ -179,21 +119,18 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
pass
async def get_hashrate(
self,
api_summary: dict = None,
web_getAll: dict = None, # noqa: named this way for automatic functionality
self, api_summary: dict = None, web_get_all: dict = None
) -> Optional[float]:
web_all_data = web_getAll.get("all")
if not api_summary and not web_all_data:
if not api_summary and not web_get_all:
try:
api_summary = await self.api.summary()
except APIError:
pass
if web_all_data:
if web_get_all:
try:
return round(
float(web_all_data["total_hash"]["Hash Rate H"] / 1000000000000), 2
float(web_get_all["total_hash"]["Hash Rate H"] / 1000000000000), 2
)
except KeyError:
pass
@@ -205,11 +142,8 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
pass
async def get_hashboards(
self,
api_stats: dict = None,
web_getAll: dict = None, # noqa: named this way for automatic functionality
self, api_stats: dict = None, web_get_all: dict = None
) -> List[HashBoard]:
web_all_data = web_getAll.get("all")
hashboards = [
HashBoard(slot=i, expected_chips=self.nominal_chips)
for i in range(self.ideal_hashboards)
@@ -221,13 +155,13 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
except APIError:
pass
if not web_all_data:
if not web_get_all:
try:
web_all_data = await self.send_web_command("getAll")
web_get_all = await self.web.get_all()
except APIError:
pass
else:
web_all_data = web_all_data["all"]
web_get_all = web_get_all["all"]
if api_stats:
if api_stats.get("STATS"):
@@ -241,9 +175,9 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
hashboards[idx].chips = chips
hashboards[idx].missing = False
if web_all_data:
if web_all_data.get("chain"):
for board in web_all_data["chain"]:
if web_get_all:
if web_get_all.get("chain"):
for board in web_get_all["chain"]:
idx = board.get("ASC")
if idx is not None:
temp = board.get("Temp min")
@@ -263,22 +197,19 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
return hashboards
async def get_wattage(
self,
web_getAll: dict = None,
api_stats: dict = None, # noqa: named this way for automatic functionality
self, web_get_all: dict = None, api_stats: dict = None
) -> Optional[int]:
web_all_data = web_getAll.get("all")
if not web_all_data:
if not web_get_all:
try:
web_all_data = await self.send_web_command("getAll")
web_get_all = await self.web.get_all()
except APIError:
pass
else:
web_all_data = web_all_data["all"]
web_get_all = web_get_all["all"]
if web_all_data:
if web_get_all:
try:
return web_all_data["power"]
return web_get_all["power"]
except KeyError:
pass
@@ -299,23 +230,19 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
wattage = int(wattage)
return wattage
async def get_fans(
self,
web_getAll: dict = None, # noqa: named this way for automatic functionality
) -> List[Fan]:
web_all_data = web_getAll.get("all")
if not web_all_data:
async def get_fans(self, web_get_all: dict = None) -> List[Fan]:
if not web_get_all:
try:
web_all_data = await self.send_web_command("getAll")
web_get_all = await self.web.get_all()
except APIError:
pass
else:
web_all_data = web_all_data["all"]
web_get_all = web_get_all["all"]
fan_data = [Fan(), Fan(), Fan(), Fan()]
if web_all_data:
if web_get_all:
try:
spd = web_all_data["fansSpeed"]
spd = web_get_all["fansSpeed"]
except KeyError:
pass
else:
@@ -352,21 +279,20 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
return groups
async def get_errors(
self, web_getErrorDetail: dict = None
self, web_get_error_detail: dict = None
) -> List[MinerErrorData]: # noqa: named this way for automatic functionality
web_error_details = web_getErrorDetail
errors = []
if not web_error_details:
if not web_get_error_detail:
try:
web_error_details = await self.send_web_command("getErrorDetail")
web_get_error_detail = await self.web.get_error_detail()
except APIError:
pass
if web_error_details:
if web_get_error_detail:
try:
# only 1 error?
# TODO: check if this should be a loop, can't remember.
err = web_error_details["code"]
err = web_get_error_detail["code"]
except KeyError:
pass
else:
@@ -375,19 +301,18 @@ class CGMinerInnosiliconT3HPlus(CGMiner, InnosiliconT3HPlus):
errors.append(InnosiliconError(error_code=err))
return errors
async def get_wattage_limit(self, web_getAll: dict = None) -> Optional[int]:
web_all_data = web_getAll.get("all")
if not web_all_data:
async def get_wattage_limit(self, web_get_all: dict = None) -> Optional[int]:
if not web_get_all:
try:
web_all_data = await self.send_web_command("getAll")
web_get_all = await self.web.get_all()
except APIError:
pass
else:
web_all_data = web_all_data["all"]
web_get_all = web_get_all["all"]
if web_all_data:
if web_get_all:
try:
level = web_all_data["running_mode"]["level"]
level = web_get_all["running_mode"]["level"]
except KeyError:
pass
else:

View File

@@ -42,6 +42,10 @@ from pyasic.misc import Singleton
from pyasic.settings import PyasicSettings
MINER_CLASSES = {
"ANTMINER L7": {
"Default": BMMinerL7,
"BMMiner": BMMinerL7,
},
"ANTMINER S9": {
"Default": BOSMinerS9,
"BOSMiner": BOSMinerOld,
@@ -59,6 +63,10 @@ MINER_CLASSES = {
"Hiveon": HiveonT9,
"CGMiner": CGMinerT9,
},
"ANTMINER Z15": {
"Default": CGMinerZ15,
"CGMiner": CGMinerZ15,
},
"ANTMINER S17": {
"Default": BMMinerS17,
"BOSMiner+": BOSMinerS17,
@@ -339,6 +347,10 @@ MINER_CLASSES = {
"10": BTMinerM32V10,
"20": BTMinerM32V20,
},
"M32S": {
"Default": BTMinerM32S,
"BTMiner": BTMinerM32S,
},
"M33": {
"Default": BTMinerM33V10,
"BTMiner": BTMinerM33V10,
@@ -777,6 +789,10 @@ class MinerFactory(metaclass=Singleton):
if version and not model:
try:
model = version["VERSION"][0]["Type"].upper()
if "ANTMINER BHB" in model:
# def antminer, get from web
sysinfo = await self.__get_system_info_from_web(str(ip))
model = sysinfo["minertype"].upper()
except KeyError:
pass

View File

@@ -27,7 +27,8 @@ class _MinerListener:
def connection_made(self, transport):
self.transport = transport
def datagram_received(self, data, _addr):
@staticmethod
def datagram_received(data, _addr):
m = data.decode()
if "," in m:
ip, mac = m.split(",")

View File

@@ -14,7 +14,6 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from collections import namedtuple
from typing import List, Optional, Tuple
from pyasic.API.unknown import UnknownAPI
@@ -26,7 +25,9 @@ from pyasic.miners.base import BaseMiner
class UnknownMiner(BaseMiner):
def __init__(self, ip: str, *args, **kwargs) -> None:
def __init__(
self, ip: str, *args, **kwargs
) -> None: # noqa - ignore *args and **kwargs for signature consistency
super().__init__()
self.ip = ip
self.api = UnknownAPI(ip)

View File

@@ -142,6 +142,7 @@ from .M31S_Plus import (
)
from .M31SE import BTMinerM31SEV10, BTMinerM31SEV20, BTMinerM31SEV30
from .M32 import BTMinerM32V10, BTMinerM32V20
from .M32S import BTMinerM32S
from .M33 import BTMinerM33V10, BTMinerM33V20, BTMinerM33V30
from .M33S import BTMinerM33SVG30
from .M33S_Plus import BTMinerM33SPlusVH20, BTMinerM33SPlusVH30

106
pyasic/web/Inno.py Normal file
View File

@@ -0,0 +1,106 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
import warnings
from typing import Union
import httpx
from pyasic.errors import APIError
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI
class InnosiliconWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.username = "admin"
self.pwd = PyasicSettings().global_innosilicon_password
self.jwt = None
async def auth(self):
async with httpx.AsyncClient() as client:
try:
auth = await client.post(
f"http://{self.ip}/api/auth",
data={"username": self.username, "password": self.pwd},
)
except httpx.HTTPError:
warnings.warn(f"Could not authenticate web token with miner: {self}")
else:
json_auth = auth.json()
self.jwt = json_auth.get("jwt")
return self.jwt
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
if not self.jwt:
await self.auth()
async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries):
try:
response = await client.post(
f"http://{self.ip}/api/{command}",
headers={"Authorization": "Bearer " + self.jwt},
timeout=5,
data=parameters,
)
json_data = response.json()
if (
not json_data.get("success")
and "token" in json_data
and json_data.get("token") == "expired"
):
# refresh the token, retry
await self.auth()
continue
if not json_data.get("success"):
if json_data.get("msg"):
raise APIError(json_data["msg"])
elif json_data.get("message"):
raise APIError(json_data["message"])
raise APIError("Innosilicon web api command failed.")
return json_data
except httpx.HTTPError:
pass
except json.JSONDecodeError:
pass
async def reboot(self) -> dict:
return await self.send_command("reboot")
async def restart_cgminer(self) -> dict:
return await self.send_command("restartCgMiner")
async def update_pools(self, conf: dict) -> dict:
return await self.send_command("updatePools", **conf)
async def overview(self) -> dict:
return await self.send_command("overview")
async def type(self) -> dict:
return await self.send_command("type")
async def get_all(self):
return await self.send_command("getAll")
async def get_error_detail(self):
return await self.send_command("getErrorDetail")

58
pyasic/web/S9.py Normal file
View File

@@ -0,0 +1,58 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
from typing import Union
import httpx
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI
class S9WebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.pwd = PyasicSettings().global_x17_password
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd)
try:
async with httpx.AsyncClient() as client:
if parameters:
data = await client.post(url, data=parameters, auth=auth)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
async def get_system_info(self) -> dict:
return await self.send_command("get_system_info")
async def get_network_info(self) -> dict:
return await self.send_command("get_network_info")

72
pyasic/web/X15.py Normal file
View File

@@ -0,0 +1,72 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
from typing import Union
import httpx
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI
class X15WebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.pwd = PyasicSettings().global_x17_password
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd)
try:
async with httpx.AsyncClient() as client:
if parameters:
data = await client.post(url, data=parameters, auth=auth)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
async def get_system_info(self) -> dict:
return await self.send_command("get_system_info")
async def blink(self, blink: bool) -> dict:
if blink:
return await self.send_command("blink", action="startBlink")
return await self.send_command("blink", action="stopBlink")
async def reboot(self) -> dict:
return await self.send_command("reboot")
async def get_blink_status(self) -> dict:
return await self.send_command("blink", action="onPageLoaded")
async def get_miner_conf(self) -> dict:
return await self.send_command("get_miner_conf")
async def set_miner_conf(self, conf: dict) -> dict:
return await self.send_command("set_miner_conf", **conf)

66
pyasic/web/X17.py Normal file
View File

@@ -0,0 +1,66 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
from typing import Union
import httpx
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI
class X17WebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.pwd = PyasicSettings().global_x17_password
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd)
try:
async with httpx.AsyncClient() as client:
if parameters:
data = await client.post(url, data=parameters, auth=auth)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
async def get_system_info(self) -> dict:
return await self.send_command("get_system_info")
async def blink(self, blink: bool) -> dict:
if blink:
return await self.send_command("blink", action="startBlink")
return await self.send_command("blink", action="stopBlink")
async def reboot(self) -> dict:
return await self.send_command("reboot")
async def get_blink_status(self) -> dict:
return await self.send_command("blink", action="onPageLoaded")

99
pyasic/web/X19.py Normal file
View File

@@ -0,0 +1,99 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
from typing import Union
import httpx
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI
class X19WebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.pwd = PyasicSettings().global_x19_password
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd)
try:
async with httpx.AsyncClient() as client:
if parameters:
data = await client.post(
url, data=json.dumps(parameters), auth=auth # noqa
)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
async def get_miner_conf(self) -> dict:
return await self.send_command("get_miner_conf")
async def set_miner_conf(self, conf: dict) -> dict:
return await self.send_command("set_miner_conf", **conf)
async def blink(self, blink: bool) -> dict:
if blink:
return await self.send_command("blink", blink="true")
return await self.send_command("blink", blink="false")
async def reboot(self) -> dict:
return await self.send_command("reboot")
async def get_system_info(self) -> dict:
return await self.send_command("get_system_info")
async def get_network_info(self) -> dict:
return await self.send_command("get_network_info")
async def summary(self) -> dict:
return await self.send_command("summary")
async def get_blink_status(self) -> dict:
return await self.send_command("get_blink_status")
async def set_network_conf(
self,
ip: str,
dns: str,
gateway: str,
subnet_mask: str,
hostname: str,
protocol: int,
) -> dict:
return await self.send_command(
"set_network_conf",
ipAddress=ip,
ipDns=dns,
ipGateway=gateway,
ipHost=hostname,
ipPro=protocol,
ipSub=subnet_mask,
)

99
pyasic/web/X7.py Normal file
View File

@@ -0,0 +1,99 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
from typing import Union
import httpx
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI
class X7WebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.pwd = PyasicSettings().global_x19_password
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
url = f"http://{self.ip}/cgi-bin/{command}.cgi"
auth = httpx.DigestAuth(self.username, self.pwd)
try:
async with httpx.AsyncClient() as client:
if parameters:
data = await client.post(
url, data=json.dumps(parameters), auth=auth # noqa
)
else:
data = await client.get(url, auth=auth)
except httpx.HTTPError:
pass
else:
if data.status_code == 200:
try:
return data.json()
except json.decoder.JSONDecodeError:
pass
async def get_miner_conf(self) -> dict:
return await self.send_command("get_miner_conf")
async def set_miner_conf(self, conf: dict) -> dict:
return await self.send_command("set_miner_conf", **conf)
async def blink(self, blink: bool) -> dict:
if blink:
return await self.send_command("blink", blink="true")
return await self.send_command("blink", blink="false")
async def reboot(self) -> dict:
return await self.send_command("reboot")
async def get_system_info(self) -> dict:
return await self.send_command("get_system_info")
async def get_network_info(self) -> dict:
return await self.send_command("get_network_info")
async def summary(self) -> dict:
return await self.send_command("summary")
async def get_blink_status(self) -> dict:
return await self.send_command("get_blink_status")
async def set_network_conf(
self,
ip: str,
dns: str,
gateway: str,
subnet_mask: str,
hostname: str,
protocol: int,
) -> dict:
return await self.send_command(
"set_network_conf",
ipAddress=ip,
ipDns=dns,
ipGateway=gateway,
ipHost=hostname,
ipPro=protocol,
ipSub=subnet_mask,
)

87
pyasic/web/__init__.py Normal file
View File

@@ -0,0 +1,87 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import ipaddress
import warnings
from abc import ABC, abstractmethod
from typing import Union
from pyasic.errors import APIWarning
class BaseWebAPI(ABC):
def __init__(self, ip: str) -> None:
# ip address of the miner
self.ip = ipaddress.ip_address(ip)
self.username = "root"
self.pwd = "root"
def __new__(cls, *args, **kwargs):
if cls is BaseWebAPI:
raise TypeError(f"Only children of '{cls.__name__}' may be instantiated")
return object.__new__(cls)
def __repr__(self):
return f"{self.__class__.__name__}: {str(self.ip)}"
@abstractmethod
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
pass
def _check_commands(self, *commands):
allowed_commands = self.get_commands()
return_commands = []
for command in [*commands]:
if command in allowed_commands:
return_commands.append(command)
else:
warnings.warn(
f"""Removing incorrect command: {command}
If you are sure you want to use this command please use WebAPI.send_command("{command}", ignore_errors=True) instead.""",
APIWarning,
)
return return_commands
@property
def commands(self) -> list:
return self.get_commands()
def get_commands(self) -> list:
"""Get a list of command accessible to a specific type of web API on the miner.
Returns:
A list of all web commands that the miner supports.
"""
return [
func
for func in
# each function in self
dir(self)
if not func == "commands"
if callable(getattr(self, func)) and
# no __ or _ methods
not func.startswith("__") and not func.startswith("_") and
# remove all functions that are in this base class
func
not in [
func for func in dir(BaseWebAPI) if callable(getattr(BaseWebAPI, func))
]
]

112
pyasic/web/vnish.py Normal file
View File

@@ -0,0 +1,112 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import json
import warnings
from typing import Union
import httpx
from pyasic.settings import PyasicSettings
from pyasic.web import BaseWebAPI
class VNishWebAPI(BaseWebAPI):
def __init__(self, ip: str) -> None:
super().__init__(ip)
self.username = "admin"
self.pwd = PyasicSettings().global_vnish_password
self.token = None
async def auth(self):
async with httpx.AsyncClient() as client:
try:
auth = await client.post(
f"http://{self.ip}/api/v1/unlock",
json={"pw": self.pwd},
)
except httpx.HTTPError:
warnings.warn(f"Could not authenticate web token with miner: {self}")
else:
if not auth.status_code == 200:
warnings.warn(
f"Could not authenticate web token with miner: {self}"
)
return None
json_auth = auth.json()
self.token = json_auth["token"]
return self.token
async def send_command(
self,
command: Union[str, bytes],
ignore_errors: bool = False,
allow_warning: bool = True,
**parameters: Union[str, int, bool],
) -> dict:
if not self.token:
await self.auth()
async with httpx.AsyncClient() as client:
for i in range(PyasicSettings().miner_get_data_retries):
try:
auth = self.token
if command.startswith("system"):
auth = "Bearer " + self.token
if parameters.get("post"):
parameters.pop("post")
response = await client.post(
f"http://{self.ip}/api/v1/{command}",
headers={"Authorization": auth},
timeout=5,
json=parameters,
)
elif not parameters == {}:
response = await client.post(
f"http://{self.ip}/api/v1/{command}",
headers={"Authorization": auth},
timeout=5,
json=parameters,
)
else:
response = await client.get(
f"http://{self.ip}/api/v1/{command}",
headers={"Authorization": auth},
timeout=5,
)
if not response.status_code == 200:
# refresh the token, retry
await self.auth()
continue
json_data = response.json()
if json_data:
return json_data
return {"success": True}
except httpx.HTTPError:
pass
except json.JSONDecodeError:
pass
async def restart_vnish(self) -> dict:
return await self.send_command("mining/restart", post=True)
async def reboot(self) -> dict:
return await self.send_command("system/reboot", post=True)
async def info(self):
return await self.send_command("info")
async def summary(self):
return await self.send_command("summary")

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.29.3"
version = "0.31.0"
description = "A set of modules for interfacing with many common types of ASIC bitcoin miners, using both their API and SSH."
authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic"
@@ -8,9 +8,9 @@ documentation = "https://pyasic.readthedocs.io/en/latest/"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.7"
asyncssh = "^2.11.0"
httpx = "^0.23.0"
python = "^3.8"
asyncssh = "^2.13.1"
httpx = "^0.23.3"
passlib = "^1.7.4"
pyaml = "^21.10.1"
toml = "^0.10.2"
@@ -19,15 +19,15 @@ toml = "^0.10.2"
optional = true
[tool.poetry.group.dev.dependencies]
pre-commit = "^2.20.0"
isort = "^5.10.1"
pre-commit = "^3.1.0"
isort = "^5.12.0"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
mkdocs = "^1.4.0"
mkdocstrings = {extras = ["python"], version = "^0.19.0"}
mkdocs = "^1.4.2"
mkdocstrings = {extras = ["python"], version = "^0.20.0"}
[build-system]

View File

@@ -16,7 +16,6 @@
import unittest
from tests.config_tests import ConfigTest
from tests.miners_tests import MinerFactoryTest, MinersTest
from tests.network_tests import NetworkTest

211
tests/api_tests/__init__.py Normal file
View File

@@ -0,0 +1,211 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import asyncio
import unittest
from unittest.mock import MagicMock, Mock, patch
from pyasic.API.bmminer import BMMinerAPI
from pyasic.API.bosminer import BOSMinerAPI
from pyasic.API.btminer import BTMinerAPI
from pyasic.API.cgminer import CGMinerAPI
class TestBMMinerAPI(unittest.IsolatedAsyncioTestCase):
async def create_mock_connection(self, reader_data: bytes = b""):
mock_reader = Mock(asyncio.StreamReader)
mock_reader.read.side_effect = [reader_data, None]
mock_writer = Mock(asyncio.StreamWriter)
mock_writer.write.return_value = None
mock_writer.drain.return_value = None
mock_writer.get_extra_info.return_value = (self.ip, self.port)
mock_connection = MagicMock()
mock_connection.__aenter__.return_value = (mock_reader, mock_writer)
mock_connection.__aexit__.return_value = None
return mock_connection
def setUp(self):
self.ip = "192.168.0.1"
self.port = 4028
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_send_command(self, mock_send_bytes):
miner_api = BMMinerAPI(ip=self.ip, port=self.port)
mock_send_bytes.return_value = b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"BMMiner"}], "SUMMARY":[{}], "id": 1}'
response = await miner_api.send_command("summary")
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_all_commands(self, mock_send_bytes):
miner_api = BMMinerAPI(ip=self.ip, port=self.port)
for command in miner_api.commands:
mock_send_bytes.return_value = (
b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"BMMiner"}], "'
+ command.upper().encode("utf-8")
+ b'":[{}], "id": 1}'
)
try:
command_func = getattr(miner_api, command)
except AttributeError:
pass
else:
try:
response = await command_func()
except TypeError:
# probably addpool or something
pass
else:
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
async def test_init(self):
miner_api = BMMinerAPI(ip=self.ip, port=self.port)
self.assertEqual(str(miner_api.ip), self.ip)
self.assertEqual(miner_api.port, self.port)
self.assertEqual(str(miner_api), "BMMinerAPI: 192.168.0.1")
class TestBTMinerAPI(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.ip = "192.168.0.1"
self.port = 4028
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_send_command(self, mock_send_bytes):
mock_send_bytes.return_value = b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"BTMiner"}], "SUMMARY":[{"sumamry": 1}], "id": 1}'
miner_api = BTMinerAPI(ip=self.ip, port=self.port)
response = await miner_api.send_command("summary")
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_all_commands(self, mock_send_bytes):
miner_api = BMMinerAPI(ip=self.ip, port=self.port)
for command in miner_api.commands:
mock_send_bytes.return_value = (
b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"BTMiner"}], "'
+ command.upper().encode("utf-8")
+ b'":[{}], "id": 1}'
)
try:
command_func = getattr(miner_api, command)
except AttributeError:
pass
else:
try:
response = await command_func()
except TypeError:
# probably addpool or something
pass
else:
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
async def test_init(self):
miner_api = BTMinerAPI(ip=self.ip, port=self.port)
self.assertEqual(str(miner_api.ip), self.ip)
self.assertEqual(miner_api.port, self.port)
self.assertEqual(str(miner_api), "BTMinerAPI: 192.168.0.1")
class TestCGMinerAPI(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.ip = "192.168.0.1"
self.port = 4028
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_send_command(self, mock_send_bytes):
mock_send_bytes.return_value = b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"CGMiner"}], "SUMMARY":[{"sumamry": 1}], "id": 1}'
miner_api = CGMinerAPI(ip=self.ip, port=self.port)
response = await miner_api.send_command("summary")
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_all_commands(self, mock_send_bytes):
miner_api = BMMinerAPI(ip=self.ip, port=self.port)
for command in miner_api.commands:
mock_send_bytes.return_value = (
b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"CGMiner"}], "'
+ command.upper().encode("utf-8")
+ b'":[{}], "id": 1}'
)
try:
command_func = getattr(miner_api, command)
except AttributeError:
pass
else:
try:
response = await command_func()
except TypeError:
# probably addpool or something
pass
else:
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
async def test_init(self):
miner_api = CGMinerAPI(ip=self.ip, port=self.port)
self.assertEqual(str(miner_api.ip), self.ip)
self.assertEqual(miner_api.port, self.port)
self.assertEqual(str(miner_api), "CGMinerAPI: 192.168.0.1")
class TestBOSMinerAPI(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.ip = "192.168.0.1"
self.port = 4028
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_send_command(self, mock_send_bytes):
mock_send_bytes.return_value = b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"BOSMiner"}], "SUMMARY":[{"sumamry": 1}], "id": 1}'
miner_api = BOSMinerAPI(ip=self.ip, port=self.port)
response = await miner_api.send_command("summary")
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
@patch("pyasic.API.BaseMinerAPI._send_bytes")
async def test_all_commands(self, mock_send_bytes):
miner_api = BMMinerAPI(ip=self.ip, port=self.port)
for command in miner_api.commands:
mock_send_bytes.return_value = (
b'{"STATUS":[{"STATUS":"S","When":1618486231,"Code":7,"Msg":"BOSMiner"}], "'
+ command.upper().encode("utf-8")
+ b'":[{}], "id": 1}'
)
try:
command_func = getattr(miner_api, command)
except AttributeError:
pass
else:
try:
response = await command_func()
except TypeError:
# probably addpool or something
pass
else:
self.assertIsInstance(response, dict)
self.assertEqual(response["STATUS"][0]["STATUS"], "S")
async def test_init(self):
miner_api = BOSMinerAPI(ip=self.ip, port=self.port)
self.assertEqual(str(miner_api.ip), self.ip)
self.assertEqual(miner_api.port, self.port)
self.assertEqual(str(miner_api), "BOSMinerAPI: 192.168.0.1")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,113 +0,0 @@
# ------------------------------------------------------------------------------
# Copyright 2022 Upstream Data Inc -
# -
# Licensed under the Apache License, Version 2.0 (the "License"); -
# you may not use this file except in compliance with the License. -
# You may obtain a copy of the License at -
# -
# http://www.apache.org/licenses/LICENSE-2.0 -
# -
# Unless required by applicable law or agreed to in writing, software -
# distributed under the License is distributed on an "AS IS" BASIS, -
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
import unittest
from pyasic.config import MinerConfig, _Pool, _PoolGroup # noqa
from tests.test_data import (
bosminer_api_pools,
bosminer_config_pools,
x19_api_pools,
x19_web_pools,
)
class ConfigTest(unittest.TestCase):
def setUp(self) -> None:
self.test_config = MinerConfig(
pool_groups=[
_PoolGroup(
quota=1,
group_name="TEST",
pools=[
_Pool(
url="stratum+tcp://pyasic.testpool_1.pool:3333",
username="pyasic.test",
password="123",
),
_Pool(
url="stratum+tcp://pyasic.testpool_2.pool:3333",
username="pyasic.test",
password="123",
),
],
)
],
temp_mode="auto",
temp_target=70.0,
temp_hot=80.0,
temp_dangerous=100.0,
fan_speed=None,
autotuning_enabled=True,
autotuning_wattage=900,
)
def test_config_from_raw(self):
bos_config = MinerConfig().from_raw(bosminer_config_pools)
bos_config.pool_groups[0].group_name = "TEST"
with self.subTest(
msg="Testing BOSMiner config file config.", bos_config=bos_config
):
self.assertEqual(bos_config, self.test_config)
x19_cfg = MinerConfig().from_raw(x19_web_pools)
x19_cfg.pool_groups[0].group_name = "TEST"
with self.subTest(msg="Testing X19 API config.", x19_cfg=x19_cfg):
self.assertEqual(x19_cfg, self.test_config)
def test_config_from_api(self):
bos_cfg = MinerConfig().from_api(bosminer_api_pools["POOLS"])
bos_cfg.pool_groups[0].group_name = "TEST"
with self.subTest(msg="Testing BOSMiner API config.", bos_cfg=bos_cfg):
self.assertEqual(bos_cfg, self.test_config)
x19_cfg = MinerConfig().from_api(x19_api_pools["POOLS"])
x19_cfg.pool_groups[0].group_name = "TEST"
with self.subTest(msg="Testing X19 API config.", x19_cfg=x19_cfg):
self.assertEqual(x19_cfg, self.test_config)
def test_config_as_types(self):
cfg = MinerConfig().from_api(bosminer_api_pools["POOLS"])
cfg.pool_groups[0].group_name = "TEST"
commands = [
func
for func in
# each function in self
dir(cfg)
if callable(getattr(cfg, func)) and
# no __ methods
not func.startswith("__")
]
for command in [cmd for cmd in commands if cmd.startswith("as_")]:
with self.subTest():
output = getattr(cfg, command)()
self.assertEqual(output, getattr(self.test_config, command)())
if f"from_{command.split('as_')[1]}" in commands:
self.assertEqual(
getattr(MinerConfig(), f"from_{command.split('as_')[1]}")(
output
),
self.test_config,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -17,6 +17,7 @@ import asyncio
import inspect
import sys
import unittest
import warnings
from pyasic.miners._backends import CGMiner # noqa
from pyasic.miners.base import BaseMiner
@@ -25,6 +26,7 @@ from pyasic.miners.miner_factory import MINER_CLASSES, MinerFactory
class MinersTest(unittest.TestCase):
def test_miner_model_creation(self):
warnings.filterwarnings("ignore")
for miner_model in MINER_CLASSES.keys():
for miner_api in MINER_CLASSES[miner_model].keys():
with self.subTest(
@@ -38,6 +40,8 @@ class MinersTest(unittest.TestCase):
)
def test_miner_backend_backup_creation(self):
warnings.filterwarnings("ignore")
backends = inspect.getmembers(
sys.modules["pyasic.miners._backends"], inspect.isclass
)
@@ -48,6 +52,8 @@ class MinersTest(unittest.TestCase):
self.assertTrue(isinstance(miner, miner_class))
def test_miner_type_creation_failure(self):
warnings.filterwarnings("ignore")
backends = inspect.getmembers(
sys.modules["pyasic.miners._types"], inspect.isclass
)
@@ -70,6 +76,8 @@ class MinersTest(unittest.TestCase):
class MinerFactoryTest(unittest.TestCase):
def test_miner_factory_creation(self):
warnings.filterwarnings("ignore")
self.assertDictEqual(MinerFactory().miners, {})
miner_factory = MinerFactory()
self.assertIs(MinerFactory(), miner_factory)
@@ -86,6 +94,8 @@ class MinerFactoryTest(unittest.TestCase):
self.assertListEqual(_miners, [])
def test_miner_selection(self):
warnings.filterwarnings("ignore")
for miner_model in MINER_CLASSES.keys():
with self.subTest():
miner = MinerFactory()._select_miner_from_classes(