From b5d1b7612fc3ffcfc29a2b9d613a3f035b81909b Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Mon, 3 Jun 2024 17:55:24 +0200 Subject: [PATCH 01/13] Replace timeago Replace the timeago library with a simple function --- meshtastic/__init__.py | 2 +- meshtastic/mesh_interface.py | 26 +++++++++++++++++++++----- setup.py | 1 - 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/meshtastic/__init__.py b/meshtastic/__init__.py index a5075bc..313cb42 100644 --- a/meshtastic/__init__.py +++ b/meshtastic/__init__.py @@ -76,7 +76,7 @@ from typing import * import google.protobuf.json_format import serial # type: ignore[import-untyped] -import timeago # type: ignore[import-untyped] +from dotmap import DotMap # type: ignore[import-untyped] from google.protobuf.json_format import MessageToJson from pubsub import pub # type: ignore[import-untyped] from tabulate import tabulate diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 5b3403c..bb04b74 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -14,7 +14,6 @@ from decimal import Decimal from typing import Any, Callable, Dict, List, Optional, Union import google.protobuf.json_format -import timeago # type: ignore[import-untyped] from pubsub import pub # type: ignore[import-untyped] from tabulate import tabulate @@ -158,11 +157,28 @@ class MeshInterface: # pylint: disable=R0902 def getTimeAgo(ts) -> Optional[str]: """Format how long ago have we heard from this node (aka timeago).""" - return ( - timeago.format(datetime.fromtimestamp(ts), datetime.now()) - if ts - else None + if ts is None: + return None + delta = datetime.now() - datetime.fromtimestamp(ts) + delta_secs = int(delta.total_seconds()) + if delta_secs < 0: + return None # not handling a timestamp from the future + intervals = ( + ("year", 60 * 60 * 24 * 365), + ("month", 60 * 60 * 24 * 30), + ("day", 60 * 60 * 24), + ("hour", 60 * 60), + ("min", 60), + ("sec", 1), ) + for name, interval_duration in intervals: + if delta_secs < interval_duration: + continue + x = delta_secs // interval_duration + plur = "s" if x > 1 else "" + return f"{x} {name}{plur} ago" + + return "now" rows: List[Dict[str, Any]] = [] if self.nodesByNum: diff --git a/setup.py b/setup.py index 7aaacf9..eed7c1c 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,6 @@ setup( "dotmap>=1.3.14", "pyqrcode>=1.2.1", "tabulate>=0.8.9", - "timeago>=1.0.15", "pyyaml", "bleak>=0.21.1", "packaging", From c34d08b0e53cd9c6e252d5e92b456f0566779ffc Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Fri, 21 Jun 2024 10:28:45 +0200 Subject: [PATCH 02/13] Refactor timeago and add tests _timeago is not specialized for mesh interfaces so it is factored out into a private function --- meshtastic/mesh_interface.py | 40 +++++++++++++++---------- meshtastic/tests/test_mesh_interface.py | 13 +++++++- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index bb04b74..66ce7f2 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -41,6 +41,29 @@ from meshtastic.util import ( ) +def _timeago(delta_secs: int) -> str: + """Convert a number of seconds in the past into a short, friendly string + e.g. "now", "30 sec ago", "1 hour ago" + Zero or negative intervals simply return "now" + """ + intervals = ( + ("year", 60 * 60 * 24 * 365), + ("month", 60 * 60 * 24 * 30), + ("day", 60 * 60 * 24), + ("hour", 60 * 60), + ("min", 60), + ("sec", 1), + ) + for name, interval_duration in intervals: + if delta_secs < interval_duration: + continue + x = delta_secs // interval_duration + plur = "s" if x > 1 else "" + return f"{x} {name}{plur} ago" + + return "now" + + class MeshInterface: # pylint: disable=R0902 """Interface class for meshtastic devices @@ -163,22 +186,7 @@ class MeshInterface: # pylint: disable=R0902 delta_secs = int(delta.total_seconds()) if delta_secs < 0: return None # not handling a timestamp from the future - intervals = ( - ("year", 60 * 60 * 24 * 365), - ("month", 60 * 60 * 24 * 30), - ("day", 60 * 60 * 24), - ("hour", 60 * 60), - ("min", 60), - ("sec", 1), - ) - for name, interval_duration in intervals: - if delta_secs < interval_duration: - continue - x = delta_secs // interval_duration - plur = "s" if x > 1 else "" - return f"{x} {name}{plur} ago" - - return "now" + return _timeago(delta_secs) rows: List[Dict[str, Any]] = [] if self.nodesByNum: diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 037e0bf..22950e5 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch import pytest from .. import mesh_pb2, config_pb2, BROADCAST_ADDR, LOCAL_ADDR -from ..mesh_interface import MeshInterface +from ..mesh_interface import MeshInterface, _timeago from ..node import Node # TODO @@ -684,3 +684,14 @@ def test_waitConnected_isConnected_timeout(capsys): out, err = capsys.readouterr() assert re.search(r"warn about something", err, re.MULTILINE) assert out == "" + + +@pytest.mark.unit +def test_timeago(): + assert _timeago(0) == "now" + assert _timeago(1) == "1 sec ago" + assert _timeago(15) == "15 secs ago" + assert _timeago(333) == "5 mins ago" + assert _timeago(99999) == "1 day ago" + assert _timeago(9999999) == "3 months ago" + assert _timeago(-999) == "now" From ccfb04720f95f5052409540f684e4f2eb96580e2 Mon Sep 17 00:00:00 2001 From: geeksville Date: Wed, 19 Jun 2024 14:45:59 -0700 Subject: [PATCH 03/13] Add a whitelist of known meshtastic USB VIDs to use a default serial ports. Initially only RAK4631 and heltec tracker are listed --- meshtastic/util.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/meshtastic/util.py b/meshtastic/util.py index 14f6a54..86de1e9 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -24,8 +24,14 @@ import serial.tools.list_ports # type: ignore[import-untyped] from meshtastic.supported_device import supported_devices from meshtastic.version import get_active_version -"""Some devices such as a seger jlink we never want to accidentally open""" -blacklistVids = dict.fromkeys([0x1366]) +"""Some devices such as a seger jlink or st-link we never want to accidentally open""" +blacklistVids = dict.fromkeys([0x1366, 0x0483]) + +"""Some devices are highly likely to be meshtastic. +0x239a RAK4631 +0x303a Heltec tracker""" +whitelistVids = dict.fromkeys([0x239a, 0x303a]) + def quoteBooleans(a_string): """Quote booleans @@ -130,19 +136,35 @@ def findPorts(eliminate_duplicates: bool=False) -> List[str]: Returns: list -- a list of device paths """ - l = list( + all_ports = serial.tools.list_ports.comports() + + # look for 'likely' meshtastic devices + ports = list( map( lambda port: port.device, filter( - lambda port: port.vid is not None and port.vid not in blacklistVids, - serial.tools.list_ports.comports(), + lambda port: port.vid is not None and port.vid in whitelistVids, + all_ports, ), ) ) - l.sort() + + # if no likely devices, just list everything not blacklisted + if len(ports) == 0: + ports = list( + map( + lambda port: port.device, + filter( + lambda port: port.vid is not None and port.vid not in blacklistVids, + all_ports, + ), + ) + ) + + ports.sort() if eliminate_duplicates: - l = eliminate_duplicate_port(l) - return l + ports = eliminate_duplicate_port(ports) + return ports class dotdict(dict): From 8456f36c6bd1e386571099e512232f9d789774fd Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sun, 23 Jun 2024 17:18:04 -0700 Subject: [PATCH 04/13] add NordicSemi Power Profiler Kit 2 device to the USB blacklist --- meshtastic/util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/meshtastic/util.py b/meshtastic/util.py index 86de1e9..3193802 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -24,8 +24,10 @@ import serial.tools.list_ports # type: ignore[import-untyped] from meshtastic.supported_device import supported_devices from meshtastic.version import get_active_version -"""Some devices such as a seger jlink or st-link we never want to accidentally open""" -blacklistVids = dict.fromkeys([0x1366, 0x0483]) +"""Some devices such as a seger jlink or st-link we never want to accidentally open +0x1915 NordicSemi (PPK2) +""" +blacklistVids = dict.fromkeys([0x1366, 0x0483, 0x1915]) """Some devices are highly likely to be meshtastic. 0x239a RAK4631 From b30cde979caea2bb899a222bffe2b5a3c36a2b1d Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 25 Jun 2024 11:31:02 -0700 Subject: [PATCH 05/13] fix bitrot in an old sanity test - use correct namespace --- tests/hello_world.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/hello_world.py b/tests/hello_world.py index 8fd8f79..8c25f8f 100644 --- a/tests/hello_world.py +++ b/tests/hello_world.py @@ -1,9 +1,7 @@ -import time - -import meshtastic +import meshtastic.serial_interface interface = ( - meshtastic.SerialInterface() + meshtastic.serial_interface.SerialInterface() ) # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0 interface.sendText("hello mesh") interface.close() From 9ab1b32bdb0dcc0b05ed2cae9483a880c6ac2e48 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 25 Jun 2024 18:09:20 -0700 Subject: [PATCH 06/13] make pylint happy with a docstring --- meshtastic/tests/test_mesh_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 22950e5..810138c 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -688,6 +688,7 @@ def test_waitConnected_isConnected_timeout(capsys): @pytest.mark.unit def test_timeago(): + """Test that the _timeago function returns sane values""" assert _timeago(0) == "now" assert _timeago(1) == "1 sec ago" assert _timeago(15) == "15 secs ago" From 267923fdc5a7418254150904af6dc6f8a1461e35 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 25 Jun 2024 18:14:07 -0700 Subject: [PATCH 07/13] Add hypothesis fuzzing test for _timeago --- meshtastic/tests/test_mesh_interface.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/meshtastic/tests/test_mesh_interface.py b/meshtastic/tests/test_mesh_interface.py index 810138c..5e8441c 100644 --- a/meshtastic/tests/test_mesh_interface.py +++ b/meshtastic/tests/test_mesh_interface.py @@ -5,6 +5,7 @@ import re from unittest.mock import MagicMock, patch import pytest +from hypothesis import given, strategies as st from .. import mesh_pb2, config_pb2, BROADCAST_ADDR, LOCAL_ADDR from ..mesh_interface import MeshInterface, _timeago @@ -696,3 +697,9 @@ def test_timeago(): assert _timeago(99999) == "1 day ago" assert _timeago(9999999) == "3 months ago" assert _timeago(-999) == "now" + +@given(seconds=st.integers()) +def test_timeago_fuzz(seconds): + """Fuzz _timeago to ensure it works with any integer""" + val = _timeago(seconds) + assert re.match(r"(now|\d+ (secs?|mins?|hours?|days?|months?|years?))", val) From 195f0c9d905f559342a8d13379ee75906e3000ba Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 25 Jun 2024 18:24:04 -0700 Subject: [PATCH 08/13] drop timeago dep, concurrent PR --- poetry.lock | 13 ++----------- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0e4c244..dd7228f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1098,6 +1098,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1193,16 +1194,6 @@ files = [ [package.extras] widechars = ["wcwidth"] -[[package]] -name = "timeago" -version = "1.0.16" -description = "A very simple python library, used to format datetime with `*** time ago` statement. eg: \"3 hours ago\"." -optional = false -python-versions = "*" -files = [ - {file = "timeago-1.0.16-py3-none-any.whl", hash = "sha256:9b8cb2e3102b329f35a04aa4531982d867b093b19481cfbb1dac7845fa2f79b0"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -1561,4 +1552,4 @@ tunnel = [] [metadata] lock-version = "2.0" python-versions = "^3.8,<3.13" -content-hash = "ad7784653845f37d34feab43aa3947b8d6e51d7f4e51679a8730e26b98dbe453" +content-hash = "8548a8b432a3f62db158f5b35254b05b2599aafe75ef12100471937fd4603e3c" diff --git a/pyproject.toml b/pyproject.toml index 1ce1e4e..a22ddd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ dotmap = "^1.3.30" pexpect = "^4.9.0" pyqrcode = "^1.2.1" tabulate = "^0.9.0" -timeago = "^1.0.16" webencodings = "^0.5.1" requests = "^2.31.0" pyparsing = "^3.1.2" From 1b14b1ef2068cab2a27d95b7d611739b2440256d Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 25 Jun 2024 18:58:27 -0700 Subject: [PATCH 09/13] Use poetry version --short for a valid tag name --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fb71c3..8cdf3da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: - name: Get version id: get_version run: >- - poetry version + poetry version --short - name: Create GitHub release uses: actions/create-release@v1 From 96afa703badc3dc083b6bfffb2d4526eeff6b8cb Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 25 Jun 2024 19:07:35 -0700 Subject: [PATCH 10/13] output version number in correct format for github actions, hopefully --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8cdf3da..807b1fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: - name: Get version id: get_version run: >- - poetry version --short + poetry version --short | sed 's/^/::set-output name=version::/' - name: Create GitHub release uses: actions/create-release@v1 From c3dcafb5ef9285922648cfd5d5b2c2a003f14b96 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 26 Jun 2024 02:08:30 +0000 Subject: [PATCH 11/13] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a22ddd0..dc1f2ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "meshtastic" -version = "2.3.11" +version = "2.3.12" description = "Python API & client shell for talking to Meshtastic devices" authors = ["Meshtastic Developers "] license = "GPL-3.0-only" From f5febc566fa9fe5477a09f9c00465cd8cb11c000 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Tue, 25 Jun 2024 19:13:23 -0700 Subject: [PATCH 12/13] comment out windows build for next release, we've been deleting it out of the releases anyway --- .github/workflows/release.yml | 50 +++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 807b1fb..889bb3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -152,31 +152,31 @@ jobs: asset_name: readme.txt asset_content_type: text/plain - build-and-publish-windows: - runs-on: windows-latest - needs: release_create - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ needs.release_create.outputs.new_sha }} + # build-and-publish-windows: + # runs-on: windows-latest + # needs: release_create + # steps: + # - name: Checkout + # uses: actions/checkout@v4 + # with: + # ref: ${{ needs.release_create.outputs.new_sha }} - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: "3.9" + # - name: Set up Python 3.9 + # uses: actions/setup-python@v5 + # with: + # python-version: "3.9" - - name: Build - run: | - pip install poetry - bin/build-bin.sh + # - name: Build + # run: | + # pip install poetry + # bin/build-bin.sh - - name: Add windows to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.release_create.outputs.upload_url }} - asset_path: dist/meshtastic.exe - asset_name: meshtastic_windows - asset_content_type: application/zip + # - name: Add windows to release + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # upload_url: ${{ needs.release_create.outputs.upload_url }} + # asset_path: dist/meshtastic.exe + # asset_name: meshtastic_windows + # asset_content_type: application/zip From 897adfb8c2786bfc846762282b78547a99dd6b79 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 29 Jun 2024 09:41:06 -0500 Subject: [PATCH 13/13] Adds support for ble logging characteristic --- meshtastic/ble_interface.py | 18 ++++++++++++++++++ poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/meshtastic/ble_interface.py b/meshtastic/ble_interface.py index 1c12758..d1c9fd5 100644 --- a/meshtastic/ble_interface.py +++ b/meshtastic/ble_interface.py @@ -6,6 +6,7 @@ import struct import asyncio from threading import Thread, Event from typing import Optional +from print_color import print from bleak import BleakScanner, BleakClient @@ -16,6 +17,8 @@ SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd" TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7" FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002" FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453" +LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2" + class BLEInterface(MeshInterface): @@ -70,6 +73,7 @@ class BLEInterface(MeshInterface): logging.debug("Register FROMNUM notify callback") self.client.start_notify(FROMNUM_UUID, self.from_num_handler) + self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler) async def from_num_handler(self, _, b): # pylint: disable=C0116 @@ -77,6 +81,20 @@ class BLEInterface(MeshInterface): logging.debug(f"FROMNUM notify: {from_num}") self.should_read = True + async def log_radio_handler(self, _, b): # pylint: disable=C0116 + log_radio = b.decode('utf-8').replace('\n', '') + if log_radio.startswith("DEBUG"): + print(log_radio, color="cyan", end=None) + elif log_radio.startswith("INFO"): + print(log_radio, color="white", end=None) + elif log_radio.startswith("WARN"): + print(log_radio, color="yellow", end=None) + elif log_radio.startswith("ERROR"): + print(log_radio, color="red", end=None) + else: + print(log_radio, end=None) + + self.should_read = False def scan(self): "Scan for available BLE devices" diff --git a/poetry.lock b/poetry.lock index dd7228f..915c63f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -762,6 +762,17 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "print-color" +version = "0.4.6" +description = "A simple package to print in color to the terminal" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "print_color-0.4.6-py3-none-any.whl", hash = "sha256:494bd1cdb84daf481f0e63bd22b3c32f7d52827d8f5d9138a96bb01ca8ba9299"}, + {file = "print_color-0.4.6.tar.gz", hash = "sha256:d3aafc1666c8d31a85fffa6ee8e4f269f5d5e338d685b4e6179915c71867c585"}, +] + [[package]] name = "protobuf" version = "5.27.1" @@ -1552,4 +1563,4 @@ tunnel = [] [metadata] lock-version = "2.0" python-versions = "^3.8,<3.13" -content-hash = "8548a8b432a3f62db158f5b35254b05b2599aafe75ef12100471937fd4603e3c" +content-hash = "8e82c70af84ffd1525ece9c446bf06c9a1a1235cdf3bb6c563413daf389de353" diff --git a/pyproject.toml b/pyproject.toml index dc1f2ae..730ca20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ pyyaml = "^6.0.1" pypubsub = "^4.0.3" bleak = "^0.21.1" packaging = "^24.0" +print-color = "^0.4.6" [tool.poetry.group.dev.dependencies] hypothesis = "^6.103.2"