Compare commits

...

40 Commits

Author SHA1 Message Date
Brett Rowan
d67de98bd0 version: bump version number 2024-11-01 10:52:04 -06:00
Brett Rowan
fd1a3e459b bug: reset betterproto version so that it can be built for pypi 2024-11-01 10:51:49 -06:00
Brett Rowan
adcab694b5 version: bump version number. 2024-11-01 10:46:59 -06:00
Brett Rowan
2bb097272f fix: use git version of better proto for compatibility with home assistant 2024-11-01 10:46:45 -06:00
Upstream Data
896968dded version: bump version number 2024-10-30 14:48:41 -06:00
Upstream Data
56b8f7c5b3 feature: parse iceriver config 2024-10-30 14:48:15 -06:00
Upstream Data
0ed7559aef version: bump version number 2024-10-30 14:37:47 -06:00
Upstream Data
275d87e4fe bug: fix AntminerOld board parsing 2024-10-30 14:37:19 -06:00
Upstream Data
c3ab814d77 version: bump version number 2024-10-30 14:08:38 -06:00
Upstream Data
05a8569205 bug: fix some calcuation errors with pool info 2024-10-30 14:08:22 -06:00
Upstream Data
b098cb8136 version: bump version number 2024-10-30 13:40:44 -06:00
Upstream Data
75fe7857e4 feature: add iceriver pool metrics 2024-10-30 13:39:22 -06:00
Upstream Data
66797aced1 ci: remove deprecated stages arg from pre-commit 2024-10-30 13:20:41 -06:00
Upstream Data
4a71e38078 ci: fix test framework name 2024-10-30 13:19:46 -06:00
Upstream Data
9fb07e4fa3 ci: skip pre-commit CI running pytest 2024-10-30 13:19:12 -06:00
Upstream Data
74792771ec version: bump version number 2024-10-30 13:16:22 -06:00
Upstream Data
fa6e8a976d bug: fix iceriver userpanel parsing 2024-10-30 13:16:07 -06:00
James Hilliard
f20531cff5 Run pre-commit formatting on all files 2024-10-30 13:07:57 -06:00
Upstream Data
8b1cbed9ce version: bump version number. 2024-10-30 12:25:47 -06:00
Upstream Data
0194e13427 bug: update A11MX chip count 2024-10-30 12:25:32 -06:00
Upstream Data
82d71abf54 version: bump version number 2024-10-30 12:23:08 -06:00
Upstream Data
e71cfadf6e bug: add model parser into map for iceriver 2024-10-30 12:22:44 -06:00
Upstream Data
18931c4e98 version: bump version number 2024-10-30 12:18:53 -06:00
Upstream Data
8622c080aa bug: fix inno pool parsing 2024-10-30 12:18:27 -06:00
Upstream Data
cb71b2a593 docs: update supported miners 2024-10-30 10:44:37 -06:00
Upstream Data
ff5956da41 version: bump version number 2024-10-30 10:32:48 -06:00
Upstream Data
acdafc2efd bug: fix hashboard innosilicon model selection 2024-10-30 10:32:29 -06:00
Upstream Data
b8874092ad bug: fix hashboard count on A11MX 2024-10-30 10:07:59 -06:00
Upstream Data
ad28ba0b3e version: bump version number 2024-10-30 09:27:54 -06:00
Upstream Data
0d90b60eef feature: add the rest of the iceriver models 2024-10-30 09:27:12 -06:00
Upstream Data
7c18c9f69c feature: improve iceriver support and add support for KS3M 2024-10-30 09:21:12 -06:00
Upstream Data
975560f46f feature: add iceriver type parsing 2024-10-30 09:12:54 -06:00
Upstream Data
bfe9cbf7d9 bug: fix iceriver miners being identified as bitaxe 2024-10-30 08:54:27 -06:00
Upstream Data
ccb5eb73db version: bump version number 2024-10-30 08:49:25 -06:00
Upstream Data
d143667bd6 feature: add warning message when instantiating an unsupported miner type 2024-10-30 08:49:01 -06:00
Upstream Data
87d809abc0 bug: update KS3 fan count 2024-10-30 08:44:38 -06:00
Upstream Data
4dc5b1a541 feature: add chip count for WM M60VK20 2024-10-30 08:44:08 -06:00
Upstream Data
ddd3e867f9 feature: add stratum+ssl pool url scheme 2024-10-30 08:41:35 -06:00
Upstream Data
77480d3d69 feature: add support for antminer KS5 2024-10-30 08:39:05 -06:00
Upstream Data
0767c93002 feature: add support for antminer KS3 2024-10-30 08:33:59 -06:00
57 changed files with 598 additions and 121 deletions

View File

@@ -1,3 +1,6 @@
ci:
skip:
- unittest
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
@@ -24,4 +27,3 @@ repos:
'types': [python]
args: ["-p '*test.py'"] # Probably this option is absolutely not needed.
pass_filenames: false
stages: [commit]

View File

@@ -29,6 +29,13 @@
show_root_heading: false
heading_level: 4
## T21 (BOS+)
::: pyasic.miners.antminer.bosminer.X21.T21.BOSMinerT21
handler: python
options:
show_root_heading: false
heading_level: 4
## S21 (VNish)
::: pyasic.miners.antminer.vnish.X21.S21.VNishS21
handler: python

View File

@@ -22,6 +22,20 @@
show_root_heading: false
heading_level: 4
## KA3 (Stock)
::: pyasic.miners.antminer.bmminer.X3.KA3.BMMinerKA3
handler: python
options:
show_root_heading: false
heading_level: 4
## KS3 (Stock)
::: pyasic.miners.antminer.bmminer.X3.KS3.BMMinerKS3
handler: python
options:
show_root_heading: false
heading_level: 4
## L3+ (VNish)
::: pyasic.miners.antminer.vnish.X3.L3.VnishL3Plus
handler: python

View File

@@ -8,3 +8,10 @@
show_root_heading: false
heading_level: 4
## KS5 (Stock)
::: pyasic.miners.antminer.bmminer.X5.KS5.BMMinerKS5
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -1,6 +1,20 @@
# pyasic
## KSX Models
## KS0 (Stock)
::: pyasic.miners.iceriver.iceminer.KSX.KS0.IceRiverKS0
handler: python
options:
show_root_heading: false
heading_level: 4
## KS1 (Stock)
::: pyasic.miners.iceriver.iceminer.KSX.KS1.IceRiverKS1
handler: python
options:
show_root_heading: false
heading_level: 4
## KS2 (Stock)
::: pyasic.miners.iceriver.iceminer.KSX.KS2.IceRiverKS2
handler: python
@@ -8,3 +22,24 @@
show_root_heading: false
heading_level: 4
## KS3 (Stock)
::: pyasic.miners.iceriver.iceminer.KSX.KS3.IceRiverKS3
handler: python
options:
show_root_heading: false
heading_level: 4
## KS3L (Stock)
::: pyasic.miners.iceriver.iceminer.KSX.KS3.IceRiverKS3L
handler: python
options:
show_root_heading: false
heading_level: 4
## KS3M (Stock)
::: pyasic.miners.iceriver.iceminer.KSX.KS3.IceRiverKS3M
handler: python
options:
show_root_heading: false
heading_level: 4

View File

@@ -21,12 +21,15 @@ details {
<li><a href="../antminer/X3#d3-stock">D3 (Stock)</a></li>
<li><a href="../antminer/X3#hs3-stock">HS3 (Stock)</a></li>
<li><a href="../antminer/X3#l3_1-stock">L3+ (Stock)</a></li>
<li><a href="../antminer/X3#ka3-stock">KA3 (Stock)</a></li>
<li><a href="../antminer/X3#ks3-stock">KS3 (Stock)</a></li>
</ul>
</details>
<details>
<summary>X5 Series:</summary>
<ul>
<li><a href="../antminer/X5#dr5-stock">DR5 (Stock)</a></li>
<li><a href="../antminer/X5#ks5-stock">KS5 (Stock)</a></li>
</ul>
</details>
<details>
@@ -378,6 +381,12 @@ details {
<li><a href="../avalonminer/A12X#avalon-1246-stock">Avalon 1246 (Stock)</a></li>
</ul>
</details>
<details>
<summary>nano Series:</summary>
<ul>
<li><a href="../avalonminer/nano#avalon-nano-3-stock">Avalon Nano 3 (Stock)</a></li>
</ul>
</details>
</ul>
</details>
<details>
@@ -395,6 +404,12 @@ details {
<li><a href="../innosilicon/A10X#a10x-stock">A10X (Stock)</a></li>
</ul>
</details>
<details>
<summary>A11X Series:</summary>
<ul>
<li><a href="../innosilicon/A11X#a11mx-stock">A11MX (Stock)</a></li>
</ul>
</details>
</ul>
</details>
<details>
@@ -470,6 +485,7 @@ details {
<summary>X21 Series:</summary>
<ul>
<li><a href="../antminer/X21#s21-bos_1">S21 (BOS+)</a></li>
<li><a href="../antminer/X21#t21-bos_1">T21 (BOS+)</a></li>
</ul>
</details>
</ul>
@@ -661,7 +677,12 @@ details {
<details>
<summary>KSX Series:</summary>
<ul>
<li><a href="../iceriver/KSX#ks0-stock">KS0 (Stock)</a></li>
<li><a href="../iceriver/KSX#ks1-stock">KS1 (Stock)</a></li>
<li><a href="../iceriver/KSX#ks2-stock">KS2 (Stock)</a></li>
<li><a href="../iceriver/KSX#ks3-stock">KS3 (Stock)</a></li>
<li><a href="../iceriver/KSX#ks3l-stock">KS3L (Stock)</a></li>
<li><a href="../iceriver/KSX#ks3m-stock">KS3M (Stock)</a></li>
</ul>
</details>
</ul>

24
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -62,21 +62,25 @@ name = "betterproto"
version = "2.0.0b7"
description = "A better Protobuf / gRPC generator & library"
optional = false
python-versions = "<4.0,>=3.7"
files = [
{file = "betterproto-2.0.0b7-py3-none-any.whl", hash = "sha256:401ab8055e2f814e77b9c88a74d0e1ae3d1e8a969cced6aeb1b59f71ad63fbd2"},
{file = "betterproto-2.0.0b7.tar.gz", hash = "sha256:1b1458ca5278d519bcd62556a4c236f998a91d503f0f71c67b0b954747052af2"},
]
python-versions = "^3.7"
files = []
develop = false
[package.dependencies]
grpclib = ">=0.4.1,<0.5.0"
python-dateutil = ">=2.8,<3.0"
typing-extensions = ">=4.7.1,<5.0.0"
grpclib = "^0.4.1"
python-dateutil = "^2.8"
typing-extensions = "^4.7.1"
[package.extras]
compiler = ["black (>=23.1.0)", "isort (>=5.11.5,<6.0.0)", "jinja2 (>=3.0.3)"]
rust-codec = ["betterproto-rust-codec (==0.1.1)"]
[package.source]
type = "git"
url = "https://github.com/danielgtaylor/python-betterproto"
reference = "v.2.0.0b7"
resolved_reference = "1538e156a1eadb88799eca009a238093d6bb40df"
[[package]]
name = "certifi"
version = "2024.8.30"
@@ -1183,4 +1187,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "d611b5e8b0c5611d6ee916cedfb7f07f20dfc90a675ebaed04188e8b3c96aabe"
content-hash = "503e85a8a0f1720b55634c5706cc6c2f4fbe8d14eb9b585ffce7e1e81811a70d"

View File

@@ -250,3 +250,9 @@ class MinerConfig:
pools=PoolConfig.from_bitaxe(web_system_info),
fan_mode=FanModeConfig.from_bitaxe(web_system_info),
)
@classmethod
def from_iceriver(cls, web_userpanel: dict) -> "MinerConfig":
return cls(
pools=PoolConfig.from_iceriver(web_userpanel),
)

View File

@@ -222,6 +222,14 @@ class Pool(MinerConfigValue):
password=web_system_info.get("stratumPassword", ""),
)
@classmethod
def from_iceriver(cls, web_pool: dict) -> "Pool":
return cls(
url=web_pool["addr"],
user=web_pool["user"],
password=web_pool["pass"],
)
@dataclass
class PoolGroup(MinerConfigValue):
@@ -402,6 +410,15 @@ class PoolGroup(MinerConfigValue):
def from_bitaxe(cls, web_system_info: dict) -> "PoolGroup":
return cls(pools=[Pool.from_bitaxe(web_system_info)])
@classmethod
def from_iceriver(cls, web_userpanel: dict) -> "PoolGroup":
return cls(
pools=[
Pool.from_iceriver(web_pool)
for web_pool in web_userpanel["data"]["pools"]
]
)
@dataclass
class PoolConfig(MinerConfigValue):
@@ -568,3 +585,7 @@ class PoolConfig(MinerConfigValue):
@classmethod
def from_bitaxe(cls, web_system_info: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_bitaxe(web_system_info)])
@classmethod
def from_iceriver(cls, web_userpanel: dict) -> "PoolConfig":
return cls(groups=[PoolGroup.from_iceriver(web_userpanel)])

View File

@@ -23,13 +23,13 @@ from typing import Any, List, Union
from pyasic.config import MinerConfig
from pyasic.config.mining import MiningModePowerTune
from pyasic.data.pools import PoolMetrics
from .boards import HashBoard
from .device import DeviceInfo
from .error_codes import BraiinsOSError, InnosiliconError, WhatsminerError, X19Error
from .fans import Fan
from .hashrate import AlgoHashRate, HashUnit
from pyasic.data.pools import PoolMetrics
@dataclass

View File

@@ -7,6 +7,7 @@ from urllib.parse import urlparse
class Scheme(Enum):
STRATUM_V1 = "stratum+tcp"
STRATUM_V2 = "stratum2+tcp"
STRATUM_V1_SSL = "stratum+ssl"
@dataclass
@@ -88,6 +89,8 @@ class PoolMetrics:
@staticmethod
def _calculate_percentage(value: int, total: int) -> float:
"""Calculate the percentage."""
if value is None or total is None:
return 0
if total == 0:
return 0
return (value / total) * 100

View File

@@ -26,6 +26,7 @@ class MinerMake(str, Enum):
AURADINE = "Auradine"
EPIC = "ePIC"
BITAXE = "BitAxe"
ICERIVER = "IceRiver"
def __str__(self):
return self.value

View File

@@ -6,7 +6,9 @@ class AntminerModels(str, Enum):
HS3 = "HS3"
L3Plus = "L3+"
KA3 = "KA3"
KS3 = "KS3"
DR5 = "DR5"
KS5 = "KS5"
L7 = "L7"
E9Pro = "E9Pro"
S9 = "S9"
@@ -343,7 +345,12 @@ class BitAxeModels(str, Enum):
class IceRiverModels(str, Enum):
KS0 = "KS0"
KS1 = "KS1"
KS2 = "KS2"
KS3 = "KS3"
KS3L = "KS3L"
KS3M = "KS3M"
def __str__(self):
return self.value

View File

@@ -23,6 +23,7 @@ from .S19 import (
BMMinerS19j,
BMMinerS19jNoPIC,
BMMinerS19jPro,
BMMinerS19KPro,
BMMinerS19L,
BMMinerS19Plus,
BMMinerS19Pro,
@@ -30,6 +31,5 @@ from .S19 import (
BMMinerS19ProPlus,
BMMinerS19ProPlusHydro,
BMMinerS19XP,
BMMinerS19KPro,
)
from .T19 import BMMinerT19

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 AntminerModern
from pyasic.miners.device.models.antminer import KS3
class BMMinerKS3(AntminerModern, KS3):
pass

View File

@@ -15,4 +15,5 @@
# ------------------------------------------------------------------------------
from .HS3 import BMMinerHS3
from .KA3 import BMMinerKA3
from .KS3 import BMMinerKS3
from .L3 import BMMinerL3Plus

View File

@@ -0,0 +1,21 @@
# ------------------------------------------------------------------------------
# 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 AntminerModern
from pyasic.miners.device.models import KS5
class BMMinerKS5(AntminerModern, KS5):
supports_shutdown = False

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 .KS5 import BMMinerKS5

View File

@@ -14,6 +14,7 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from .X3 import *
from .X5 import *
from .X7 import *
from .X9 import *
from .X17 import *

View File

@@ -15,7 +15,4 @@
# ------------------------------------------------------------------------------
from .S21 import ePICS21, ePICS21Pro
from .T21 import (
ePICT21,
)
from .T21 import ePICT21

View File

@@ -597,49 +597,44 @@ class AntminerOld(CGMiner):
pass
if rpc_stats is not None:
try:
board_offset = -1
boards = rpc_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}")
board_offset = -1
boards = rpc_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
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.expected_hashboards
):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.expected_chips
)
for i in range(board_offset, board_offset + self.expected_hashboards):
hashboard = HashBoard(
slot=i - board_offset, expected_chips=self.expected_chips
)
chip_temp = boards[1].get(f"temp{i}")
if chip_temp:
hashboard.chip_temp = round(chip_temp)
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)
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 = AlgoHashRate.SHA256(
hashrate, HashUnit.SHA256.GH
).into(self.algo.unit.default)
hashrate = boards[1].get(f"chain_rate{i}")
if hashrate:
hashboard.hashrate = AlgoHashRate.SHA256(
float(hashrate), HashUnit.SHA256.GH
).into(self.algo.unit.default)
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 (LookupError, ValueError, TypeError):
pass
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)
return hashboards

View File

@@ -193,7 +193,14 @@ class Auradine(StockFirmware):
for key in conf.keys():
await self.web.send_command(command=key, **conf[key])
async def upgrade_firmware(self, *, url: str = None, version: str = "latest", keep_settings: bool = False, **kwargs) -> bool:
async def upgrade_firmware(
self,
*,
url: str = None,
version: str = "latest",
keep_settings: bool = False,
**kwargs,
) -> bool:
"""
Upgrade the firmware of the Auradine device.
@@ -209,7 +216,9 @@ class Auradine(StockFirmware):
logging.info("Starting firmware upgrade process.")
if not url and not version:
raise ValueError("Either URL or version must be provided for firmware upgrade.")
raise ValueError(
"Either URL or version must be provided for firmware upgrade."
)
if url:
result = await self.web.firmware_upgrade(url=url)
@@ -220,11 +229,15 @@ class Auradine(StockFirmware):
logging.info("Firmware upgrade process completed successfully.")
return True
else:
logging.error(f"Firmware upgrade failed: {result.get('error', 'Unknown error')}")
logging.error(
f"Firmware upgrade failed: {result.get('error', 'Unknown error')}"
)
return False
except Exception as e:
logging.error(f"An error occurred during the firmware upgrade process: {str(e)}")
logging.error(
f"An error occurred during the firmware upgrade process: {str(e)}"
)
return False
##################################################

View File

@@ -18,11 +18,11 @@ from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware
from pyasic.rpc.bfgminer import BFGMinerRPCAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
BFGMINER_DATA_LOC = DataLocations(
**{
@@ -263,4 +263,4 @@ class BFGMiner(StockFirmware):
expected_rate, HashUnit.SHA256.from_str(rate_unit)
).into(self.algo.unit.default)
except LookupError:
pass
pass

View File

@@ -23,11 +23,11 @@ import aiofiles
from pyasic.config import MinerConfig, MiningModeConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.error_codes import MinerErrorData, WhatsminerError
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware
from pyasic.rpc.btminer import BTMinerRPCAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
BTMINER_DATA_LOC = DataLocations(
**{
@@ -113,7 +113,7 @@ BTMINER_DATA_LOC = DataLocations(
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")],
)
),
}
)

View File

@@ -14,15 +14,15 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import Optional, List
from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import AlgoHashRate, HashUnit
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, RPCAPICommand
from pyasic.miners.device.firmware import StockFirmware
from pyasic.rpc.cgminer import CGMinerRPCAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
CGMINER_DATA_LOC = DataLocations(
**{

View File

@@ -454,8 +454,9 @@ class ePIC(ePICFirmware):
except LookupError:
pass
async def upgrade_firmware(self, file: Path | str, keep_settings: bool = True) -> bool:
async def upgrade_firmware(
self, file: Path | str, keep_settings: bool = True
) -> bool:
"""
Upgrade the firmware of the ePIC miner device.
@@ -466,4 +467,4 @@ class ePIC(ePICFirmware):
Returns:
bool: Whether the firmware update succeeded.
"""
return await self.web.system_update(file=file, keep_settings=keep_settings)
return await self.web.system_update(file=file, keep_settings=keep_settings)

View File

@@ -1,6 +1,8 @@
from typing import List, Optional
from pyasic import MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.device import MinerAlgo
from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
@@ -41,6 +43,10 @@ ICERIVER_DATA_LOC = DataLocations(
"_get_uptime",
[WebAPICommand("web_userpanel", "userpanel")],
),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[WebAPICommand("web_userpanel", "userpanel")],
),
}
)
@@ -67,6 +73,11 @@ class IceRiver(StockFirmware):
return False
return True
async def get_config(self) -> MinerConfig:
web_userpanel = await self.web.userpanel()
return MinerConfig.from_iceriver(web_userpanel)
async def _get_fans(self, web_userpanel: dict = None) -> List[Fan]:
if web_userpanel is None:
try:
@@ -76,7 +87,7 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
return [Fan(spd) for spd in web_userpanel["fans"]]
return [Fan(spd) for spd in web_userpanel["userpanel"]["data"]["fans"]]
except (LookupError, ValueError, TypeError):
pass
@@ -89,7 +100,9 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
return web_userpanel["mac"].upper().replace("-", ":")
return (
web_userpanel["userpanel"]["data"]["mac"].upper().replace("-", ":")
)
except (LookupError, ValueError, TypeError):
pass
@@ -102,7 +115,7 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
return web_userpanel["host"]
return web_userpanel["userpanel"]["data"]["host"]
except (LookupError, ValueError, TypeError):
pass
@@ -115,9 +128,13 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
base_unit = web_userpanel["unit"]
base_unit = web_userpanel["userpanel"]["data"]["unit"]
return AlgoHashRate.SHA256(
float(web_userpanel["rtpow"].replace(base_unit, "")),
float(
web_userpanel["userpanel"]["data"]["rtpow"].replace(
base_unit, ""
)
),
unit=MinerAlgo.SHA256.unit.from_str(base_unit + "H"),
).into(MinerAlgo.SHA256.unit.default)
except (LookupError, ValueError, TypeError):
@@ -132,7 +149,7 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
return web_userpanel["locate"]
return web_userpanel["userpanel"]["data"]["locate"]
except (LookupError, ValueError, TypeError):
pass
return False
@@ -146,7 +163,7 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
return web_userpanel["powstate"]
return web_userpanel["userpanel"]["data"]["powstate"]
except (LookupError, ValueError, TypeError):
pass
@@ -164,14 +181,14 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
for board in web_userpanel["boards"]:
idx = board["no"] - 1
for board in web_userpanel["userpanel"]["data"]["boards"]:
idx = int(board["no"] - 1)
hb_list[idx].chip_temp = round(board["outtmp"])
hb_list[idx].temp = round(board["intmp"])
hb_list[idx].hashrate = AlgoHashRate.SHA256(
float(board["rtpow"].replace("G", "")), HashUnit.SHA256.GH
).into(self.algo.unit.default)
hb_list[idx].chips = board["chipnum"]
hb_list[idx].chips = int(board["chipnum"])
hb_list[idx].missing = False
except LookupError:
pass
@@ -186,7 +203,7 @@ class IceRiver(StockFirmware):
if web_userpanel is not None:
try:
runtime = web_userpanel["runtime"]
runtime = web_userpanel["userpanel"]["data"]["runtime"]
days, hours, minutes, seconds = runtime.split(":")
return (
(int(days) * 24 * 60 * 60)
@@ -196,3 +213,36 @@ class IceRiver(StockFirmware):
)
except (LookupError, ValueError, TypeError):
pass
async def _get_pools(self, web_userpanel: dict = None) -> List[PoolMetrics]:
if web_userpanel is None:
try:
web_userpanel = await self.web.userpanel()
except APIError:
pass
pools_data = []
if web_userpanel is not None:
try:
pools = web_userpanel["userpanel"]["data"]["pools"]
for pool_info in pools:
pool_num = pool_info.get("no")
if pool_num is not None:
pool_num = int(pool_num)
if pool_info["addr"] == "":
continue
url = pool_info.get("addr")
pool_url = PoolUrl.from_str(url) if url else None
pool_data = PoolMetrics(
accepted=pool_info.get("accepted"),
rejected=pool_info.get("rejected"),
active=pool_info.get("connect"),
alive=int(pool_info.get("state", 0)) == 1,
url=pool_url,
user=pool_info.get("user"),
index=pool_num,
)
pools_data.append(pool_data)
except LookupError:
pass
return pools_data

View File

@@ -19,6 +19,7 @@ from pyasic.config import MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.error_codes import MinerErrorData
from pyasic.data.error_codes.innosilicon import InnosiliconError
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.backends import CGMiner
from pyasic.miners.data import (
@@ -29,7 +30,6 @@ from pyasic.miners.data import (
WebAPICommand,
)
from pyasic.web.innosilicon import InnosiliconWebAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
INNOSILICON_DATA_LOC = DataLocations(
**{
@@ -92,9 +92,8 @@ INNOSILICON_DATA_LOC = DataLocations(
[RPCAPICommand("rpc_stats", "stats")],
),
str(DataOptions.POOLS): DataFunction(
"_get_pools",
[RPCAPICommand("rpc_pools", "pools")]
)
"_get_pools", [RPCAPICommand("rpc_pools", "pools")]
),
}
)
@@ -116,7 +115,7 @@ class Innosilicon(CGMiner):
except APIError:
return self.config
self.config = MinerConfig.from_inno([pools])
self.config = MinerConfig.from_inno(pools["pools"])
return self.config
async def reboot(self) -> bool:

View File

@@ -13,8 +13,8 @@
# See the License for the specific language governing permissions and -
# limitations under the License. -
# ------------------------------------------------------------------------------
from typing import List, Optional
import logging
from typing import List, Optional
from pyasic.config import MinerConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit

View File

@@ -3,13 +3,13 @@ from typing import List, Optional
from pyasic import MinerConfig
from pyasic.config import MiningModeConfig
from pyasic.data import AlgoHashRate, Fan, HashBoard, HashUnit
from pyasic.data.pools import PoolMetrics, PoolUrl
from pyasic.errors import APIError
from pyasic.miners.data import DataFunction, DataLocations, DataOptions, WebAPICommand
from pyasic.miners.device.firmware import MaraFirmware
from pyasic.misc import merge_dicts
from pyasic.rpc.marathon import MaraRPCAPI
from pyasic.web.marathon import MaraWebAPI
from pyasic.data.pools import PoolMetrics, PoolUrl
MARA_DATA_LOC = DataLocations(
**{
@@ -319,10 +319,13 @@ class MaraMiner(MaraFirmware):
return []
active_pool_index = None
highest_priority = float('inf')
highest_priority = float("inf")
for pool_info in web_pools:
if pool_info.get("status") == "Alive" and pool_info.get("priority", float('inf')) < highest_priority:
if (
pool_info.get("status") == "Alive"
and pool_info.get("priority", float("inf")) < highest_priority
):
highest_priority = pool_info.get["priority"]
active_pool_index = pool_info["index"]

View File

@@ -560,7 +560,14 @@ class BaseMiner(MinerProtocol):
if self._ssh_cls is not None:
self.ssh = self._ssh_cls(ip)
async def upgrade_firmware(self, *, file: str = None, url: str = None, version: str = None, keep_settings: bool = True) -> bool:
async def upgrade_firmware(
self,
*,
file: str = None,
url: str = None,
version: str = None,
keep_settings: bool = True,
) -> bool:
"""Upgrade the firmware of the miner.
Parameters:
@@ -574,4 +581,5 @@ class BaseMiner(MinerProtocol):
"""
return False
AnyMiner = TypeVar("AnyMiner", bound=BaseMiner)

View File

@@ -14,7 +14,4 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from .blockminer import (
ePICBlockMiner520i,
ePICBlockMiner720i,
)
from .blockminer import ePICBlockMiner520i, ePICBlockMiner720i

View File

@@ -43,4 +43,4 @@ class LuxOSFirmware(BaseMiner):
class MaraFirmware(BaseMiner):
firmware = MinerFirmware.MARATHON
firmware = MinerFirmware.MARATHON

View File

@@ -51,4 +51,4 @@ class BitAxeMake(BaseMiner):
class IceRiverMake(BaseMiner):
make = MinerMake.BITAXE
make = MinerMake.ICERIVER

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.device.models import MinerModel
from pyasic.miners.device.makes import AntMinerMake
class KS3(AntMinerMake):
raw_model = MinerModel.ANTMINER.KS3
expected_chips = 92
expected_fans = 2

View File

@@ -16,4 +16,5 @@
from .D3 import D3
from .HS3 import HS3
from .KA3 import KA3
from .KS3 import KS3
from .L3 import L3Plus

View File

@@ -0,0 +1,23 @@
# ------------------------------------------------------------------------------
# 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.device.models import MinerModel
from pyasic.miners.device.makes import AntMinerMake
class KS5(AntMinerMake):
raw_model = MinerModel.ANTMINER.KS5
expected_chips = 92

View File

@@ -14,3 +14,4 @@
# limitations under the License. -
# ------------------------------------------------------------------------------
from .DR5 import DR5
from .KS5 import KS5

View File

@@ -0,0 +1,23 @@
# ------------------------------------------------------------------------------
# Copyright 2024 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.device.models import MinerModel
from pyasic.miners.device.makes import IceRiverMake
class KS0(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS0
expected_fans = 0

View File

@@ -0,0 +1,23 @@
# ------------------------------------------------------------------------------
# Copyright 2024 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.device.models import MinerModel
from pyasic.miners.device.makes import IceRiverMake
class KS1(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS1
expected_fans = 4

View File

@@ -21,3 +21,4 @@ class KS2(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS2
expected_fans = 4
expected_chips = 18

View File

@@ -0,0 +1,36 @@
# ------------------------------------------------------------------------------
# Copyright 2024 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.device.models import MinerModel
from pyasic.miners.device.makes import IceRiverMake
class KS3(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS3
expected_fans = 4
class KS3L(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS3L
expected_fans = 4
class KS3M(IceRiverMake):
raw_model = MinerModel.ICERIVER.KS3M
expected_fans = 4
expected_chips = 18

View File

@@ -1 +1,4 @@
from .KS0 import KS0
from .KS1 import KS1
from .KS2 import KS2
from .KS3 import KS3, KS3L, KS3M

View File

@@ -19,3 +19,6 @@ from pyasic.miners.device.makes import InnosiliconMake
class A11MX(InnosiliconMake):
raw_model = MinerModel.INNOSILICON.A11MX
expected_hashboards = 4
expected_chips = 8

View File

@@ -24,6 +24,8 @@ class M60VK10(WhatsMinerMake):
class M60VK20(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M60VK20
expected_chips = 172
class M60VK30(WhatsMinerMake):
raw_model = MinerModel.WHATSMINER.M60VK30

View File

@@ -20,6 +20,7 @@ import enum
import ipaddress
import json
import re
import warnings
from typing import Any, AsyncGenerator, Callable
import anyio
@@ -67,7 +68,9 @@ MINER_CLASSES = {
"ANTMINER HS3": BMMinerHS3,
"ANTMINER L3+": BMMinerL3Plus,
"ANTMINER KA3": BMMinerKA3,
"ANTMINER KS3": BMMinerKS3,
"ANTMINER DR5": CGMinerDR5,
"ANTMINER KS5": BMMinerKS5,
"ANTMINER L7": BMMinerL7,
"ANTMINER E9 PRO": BMMinerE9Pro,
"ANTMINER S9": BMMinerS9,
@@ -459,7 +462,12 @@ MINER_CLASSES = {
},
MinerTypes.ICERIVER: {
None: type("IceRiverUnknown", (IceRiver, IceRiverMake), {}),
"KS0": IceRiverKS0,
"KS1": IceRiverKS1,
"KS2": IceRiverKS2,
"KS3": IceRiverKS3,
"KS3L": IceRiverKS3L,
"KS3M": IceRiverKS3M,
},
}
@@ -538,6 +546,7 @@ class MinerFactory:
MinerTypes.AURADINE: self.get_miner_model_auradine,
MinerTypes.MARATHON: self.get_miner_model_marathon,
MinerTypes.BITAXE: self.get_miner_model_bitaxe,
MinerTypes.ICERIVER: self.get_miner_model_iceriver,
}
fn = miner_model_fns.get(miner_type)
@@ -619,6 +628,8 @@ class MinerFactory:
return MinerTypes.WHATSMINER
if "Braiins OS" in web_text:
return MinerTypes.BRAIINS_OS
if "<TITLE>用户界面</TITLE>" in web_text:
return MinerTypes.ICERIVER
if "AxeOS" in web_text:
return MinerTypes.BITAXE
if "cloud-box" in web_text:
@@ -633,8 +644,6 @@ class MinerFactory:
return MinerTypes.INNOSILICON
if "Miner UI" in web_text:
return MinerTypes.AURADINE
if "<TITLE>用户界面</TITLE>" in web_text:
return MinerTypes.ICERIVER
async def _get_miner_socket(self, ip: str) -> MinerTypes | None:
commands = ["version", "devdetails"]
@@ -839,6 +848,10 @@ class MinerFactory:
return MINER_CLASSES[miner_type][str(miner_model).upper()](ip)
except LookupError:
if miner_type in MINER_CLASSES:
warnings.warn(
f"Partially supported miner found: {miner_model}, please open an issue with miner data "
f"and this model on GitHub (https://github.com/UpstreamData/pyasic/issues)."
)
return MINER_CLASSES[miner_type][None](ip)
return UnknownMiner(str(ip))
@@ -929,10 +942,19 @@ class MinerFactory:
async with httpx.AsyncClient(transport=settings.transport()) as session:
auth_req = await session.post(
f"http://{ip}/api/auth",
data={"username": "admin", "password": "admin"},
data={
"username": "admin",
"password": settings.get(
"default_innosilicon_web_password", "admin"
),
},
)
auth = auth_req.json()["jwt"]
except (httpx.HTTPError, LookupError):
return
try:
async with httpx.AsyncClient(transport=settings.transport()) as session:
web_data = (
await session.post(
f"http://{ip}/api/type",
@@ -943,6 +965,18 @@ class MinerFactory:
return web_data["type"]
except (httpx.HTTPError, LookupError):
pass
try:
async with httpx.AsyncClient(transport=settings.transport()) as session:
web_data = (
await session.post(
f"http://{ip}/overview",
headers={"Authorization": "Bearer " + auth},
data={},
)
).json()
return web_data["type"]
except (httpx.HTTPError, LookupError):
pass
async def get_miner_model_braiins_os(self, ip: str) -> str | None:
sock_json_data = await self.send_api_command(ip, "devdetails")
@@ -1056,6 +1090,39 @@ class MinerFactory:
except (TypeError, LookupError):
pass
async def get_miner_model_iceriver(self, ip: str) -> str | None:
async with httpx.AsyncClient(transport=settings.transport()) as client:
try:
# auth
await client.post(
f"http://{ip}/user/loginpost",
params={
"post": "6",
"user": "admin",
"pwd": settings.get(
"default_iceriver_web_password", "12345678"
),
},
)
except httpx.HTTPError:
return None
try:
resp = await client.post(
f"http://{ip}:/user/userpanel", params={"post": "4"}
)
if not resp.status_code == 200:
return
result = resp.json()
software_ver = result["data"]["softver1"]
split_ver = software_ver.split("_")
if split_ver[-1] == "miner":
miner_ver = split_ver[-2]
else:
miner_ver = split_ver[-1].replace("miner", "")
return miner_ver.upper()
except httpx.HTTPError:
pass
miner_factory = MinerFactory()

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.iceriver import IceRiver
from pyasic.miners.device.models import KS0
class IceRiverKS0(IceRiver, KS0):
pass

View File

@@ -0,0 +1,6 @@
from pyasic.miners.backends.iceriver import IceRiver
from pyasic.miners.device.models import KS1
class IceRiverKS1(IceRiver, KS1):
pass

View File

@@ -0,0 +1,14 @@
from pyasic.miners.backends.iceriver import IceRiver
from pyasic.miners.device.models.iceriver import KS3, KS3L, KS3M
class IceRiverKS3(IceRiver, KS3):
pass
class IceRiverKS3L(IceRiver, KS3L):
pass
class IceRiverKS3M(IceRiver, KS3M):
pass

View File

@@ -1 +1,4 @@
from .KS0 import IceRiverKS0
from .KS1 import IceRiverKS1
from .KS2 import IceRiverKS2
from .KS3 import IceRiverKS3, IceRiverKS3L, IceRiverKS3M

View File

@@ -22,8 +22,8 @@ import hashlib
import json
import logging
import re
from typing import Literal, Union
import struct
from typing import Literal, Union
import httpx
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

View File

@@ -757,4 +757,4 @@ class LUXMinerRPCAPI(BaseMinerRPCAPI):
Returns:
The response from the miner after sending the 'updaterun' command.
"""
return await self.send_command("updaterun")
return await self.send_command("updaterun")

View File

@@ -17,10 +17,11 @@ from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Any
import aiofiles
import httpx
from pathlib import Path
from pyasic import settings
from pyasic.web.base import BaseWebAPI
@@ -414,10 +415,7 @@ class AntminerOldWebAPI(BaseWebAPI):
parameters = {
"file": (file.name, file_content, "application/octet-stream"),
"filename": file.name,
"keep_settings": keep_settings
"keep_settings": keep_settings,
}
return await self.send_command(
command="upgrade",
**parameters
)
return await self.send_command(command="upgrade", **parameters)

View File

@@ -4,17 +4,12 @@
# This file has been @generated
from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
Dict,
Optional,
)
from typing import TYPE_CHECKING, Dict, Optional
import betterproto
import grpclib
from betterproto.grpc.grpclib_server import ServiceBase
if TYPE_CHECKING:
import grpclib.server
from betterproto.grpc.grpclib_client import MetadataLike

View File

@@ -5,19 +5,12 @@
import warnings
from dataclasses import dataclass
from datetime import datetime
from typing import (
TYPE_CHECKING,
AsyncIterator,
Dict,
List,
Optional,
)
from typing import TYPE_CHECKING, AsyncIterator, Dict, List, Optional
import betterproto
import grpclib
from betterproto.grpc.grpclib_server import ServiceBase
if TYPE_CHECKING:
import grpclib.server
from betterproto.grpc.grpclib_client import MetadataLike

View File

@@ -127,6 +127,9 @@ class InnosiliconWebAPI(BaseWebAPI):
async def get_all(self) -> dict:
return await self.send_command("getAll")
async def summary(self) -> dict:
return await self.send_command("summary")
async def get_error_detail(self) -> dict:
return await self.send_command("getErrorDetail")

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyasic"
version = "0.61.2"
version = "0.61.15"
description = "A simplified and standardized interface for Bitcoin ASICs."
authors = ["UpstreamData <brett@upstreamdata.ca>"]
repository = "https://github.com/UpstreamData/pyasic"
@@ -15,8 +15,8 @@ passlib = ">=1.7.4"
pyaml = ">=23.12.0"
tomli = { version = ">=2.0.1", python = "<3.11" }
tomli-w = "^1.0.0"
betterproto = "2.0.0b7"
aiofiles = ">=23.2.1"
betterproto = "2.0.0b7"
[tool.poetry.group.dev]
optional = true