From 84faa8633934f5ea69d60bc9bb583d00dc280999 Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Fri, 18 Apr 2025 00:12:01 +0200 Subject: [PATCH 1/4] Improve TinyLink to improve resiliency - Add async support to library - Add escaping of header and body - Remove reserved flags --- .flake8 | 4 +- .gitignore | 1 + poetry.lock | 384 +++++++++++++++----------------- pyproject.toml | 19 +- tests/test_tinylink.py | 218 ++++++++++++------ tinylink/__init__.py | 2 +- tinylink/cli.py | 176 +++++++-------- tinylink/consts.py | 16 +- tinylink/link.py | 493 ++++++++++++++++++++++++++--------------- tinylink/types.py | 45 ++++ tinylink/utils.py | 61 +++-- 11 files changed, 841 insertions(+), 578 deletions(-) create mode 100644 tinylink/types.py diff --git a/.flake8 b/.flake8 index 541e00a..235fbcd 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,5 @@ [flake8] -ignore = E203, W503 max-line-length = 88 + +# Necessary for compatibility with the Black formatter. +ignore = E203, W503 diff --git a/.gitignore b/.gitignore index b6db07d..804288b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ +.history .mypy_cache/ .pytest_cache/ .vscode/ diff --git a/poetry.lock b/poetry.lock index ed3b160..43827b8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,65 +1,47 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. - -[[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] - -[package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "black" -version = "22.10.0" +version = "25.1.0" description = "The uncompromising code formatter." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, - {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, - {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, - {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, - {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, - {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, - {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, - {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, - {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, - {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, - {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, - {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, - {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, - {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, - {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, - {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, - {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, - {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, - {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, - {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, - {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -67,9 +49,9 @@ uvloop = ["uvloop (>=0.15.2)"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -82,70 +64,39 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "exceptiongroup" -version = "1.0.1" -description = "Backport of PEP 654 (exception groups)" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"}, - {file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "flake8" -version = "6.0.0" +version = "7.1.2" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.8.1" +groups = ["dev"] files = [ - {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, - {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, + {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, + {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.10.0,<2.11.0" -pyflakes = ">=3.0.0,<3.1.0" - -[[package]] -name = "flake8-mypy" -version = "17.8.0" -description = "A plugin for flake8 integrating mypy." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-mypy-17.8.0.tar.gz", hash = "sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18"}, - {file = "flake8_mypy-17.8.0-py35.py36-none-any.whl", hash = "sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc"}, -] - -[package.dependencies] -attrs = "*" -flake8 = ">=3.0.0" -mypy = "*" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -153,29 +104,27 @@ files = [ [[package]] name = "isort" -version = "5.12.0" +version = "6.0.1" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" +groups = ["dev"] files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, ] [package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +colors = ["colorama"] plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -183,89 +132,88 @@ files = [ [[package]] name = "mypy" -version = "0.990" +version = "1.15.0" description = "Optional static typing for Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "mypy-0.990-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aaf1be63e0207d7d17be942dcf9a6b641745581fe6c64df9a38deb562a7dbafa"}, - {file = "mypy-0.990-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d555aa7f44cecb7ea3c0ac69d58b1a5afb92caa017285a8e9c4efbf0518b61b4"}, - {file = "mypy-0.990-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f694d6d09a460b117dccb6857dda269188e3437c880d7b60fa0014fa872d1e9"}, - {file = "mypy-0.990-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:269f0dfb6463b8780333310ff4b5134425157ef0d2b1d614015adaf6d6a7eabd"}, - {file = "mypy-0.990-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8798c8ed83aa809f053abff08664bdca056038f5a02af3660de00b7290b64c47"}, - {file = "mypy-0.990-cp310-cp310-win_amd64.whl", hash = "sha256:47a9955214615108c3480a500cfda8513a0b1cd3c09a1ed42764ca0dd7b931dd"}, - {file = "mypy-0.990-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a8a6c10f4c63fbf6ad6c03eba22c9331b3946a4cec97f008e9ffb4d3b31e8e2"}, - {file = "mypy-0.990-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd2dd3730ba894ec2a2082cc703fbf3e95a08479f7be84912e3131fc68809d46"}, - {file = "mypy-0.990-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7da0005e47975287a92b43276e460ac1831af3d23032c34e67d003388a0ce8d0"}, - {file = "mypy-0.990-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:262c543ef24deb10470a3c1c254bb986714e2b6b1a67d66daf836a548a9f316c"}, - {file = "mypy-0.990-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ff201a0c6d3ea029d73b1648943387d75aa052491365b101f6edd5570d018ea"}, - {file = "mypy-0.990-cp311-cp311-win_amd64.whl", hash = "sha256:1767830da2d1afa4e62b684647af0ff79b401f004d7fa08bc5b0ce2d45bcd5ec"}, - {file = "mypy-0.990-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6826d9c4d85bbf6d68cb279b561de6a4d8d778ca8e9ab2d00ee768ab501a9852"}, - {file = "mypy-0.990-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46897755f944176fbc504178422a5a2875bbf3f7436727374724842c0987b5af"}, - {file = "mypy-0.990-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0680389c34284287fe00e82fc8bccdea9aff318f7e7d55b90d967a13a9606013"}, - {file = "mypy-0.990-cp37-cp37m-win_amd64.whl", hash = "sha256:b08541a06eed35b543ae1a6b301590eb61826a1eb099417676ddc5a42aa151c5"}, - {file = "mypy-0.990-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:be88d665e76b452c26fb2bdc3d54555c01226fba062b004ede780b190a50f9db"}, - {file = "mypy-0.990-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8f4a8213b1fd4b751e26b59ae0e0c12896568d7e805861035c7a15ed6dc9eb"}, - {file = "mypy-0.990-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b6f85c2ad378e3224e017904a051b26660087b3b76490d533b7344f1546d3ff"}, - {file = "mypy-0.990-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee5f99817ee70254e7eb5cf97c1b11dda29c6893d846c8b07bce449184e9466"}, - {file = "mypy-0.990-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49082382f571c3186ce9ea0bd627cb1345d4da8d44a8377870f4442401f0a706"}, - {file = "mypy-0.990-cp38-cp38-win_amd64.whl", hash = "sha256:aba38e3dd66bdbafbbfe9c6e79637841928ea4c79b32e334099463c17b0d90ef"}, - {file = "mypy-0.990-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9d851c09b981a65d9d283a8ccb5b1d0b698e580493416a10942ef1a04b19fd37"}, - {file = "mypy-0.990-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d847dd23540e2912d9667602271e5ebf25e5788e7da46da5ffd98e7872616e8e"}, - {file = "mypy-0.990-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc6019808580565040cd2a561b593d7c3c646badd7e580e07d875eb1bf35c695"}, - {file = "mypy-0.990-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a3150d409609a775c8cb65dbe305c4edd7fe576c22ea79d77d1454acd9aeda8"}, - {file = "mypy-0.990-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3227f14fe943524f5794679156488f18bf8d34bfecd4623cf76bc55958d229c5"}, - {file = "mypy-0.990-cp39-cp39-win_amd64.whl", hash = "sha256:c76c769c46a1e6062a84837badcb2a7b0cdb153d68601a61f60739c37d41cc74"}, - {file = "mypy-0.990-py3-none-any.whl", hash = "sha256:8f1940325a8ed460ba03d19ab83742260fa9534804c317224e5d4e5aa588e2d6"}, - {file = "mypy-0.990.tar.gz", hash = "sha256:72382cb609142dba3f04140d016c94b4092bc7b4d98ca718740dc989e5271b8d"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, + {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, + {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, + {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, + {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, + {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, + {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, + {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, + {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, + {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, + {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, + {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, + {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +mypy_extensions = ">=1.0.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = "*" +python-versions = ">=3.5" +groups = ["dev"] files = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "packaging" -version = "21.3" +version = "24.2" description = "Core utilities for Python packages" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - [[package]] name = "pathspec" version = "0.10.2" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"}, {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"}, @@ -275,9 +223,9 @@ files = [ name = "platformdirs" version = "2.5.4" 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" +groups = ["dev"] files = [ {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, @@ -289,108 +237,142 @@ test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock [[package]] name = "pluggy" -version = "1.0.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "prompt-toolkit" +version = "3.0.50" +description = "Library for building powerful interactive command lines in Python" +optional = true +python-versions = ">=3.8.0" +groups = ["main"] +markers = "extra == \"cli\"" +files = [ + {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, + {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "pycodestyle" -version = "2.10.0" +version = "2.12.1" description = "Python style guide checker" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, - {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] [[package]] name = "pyflakes" -version = "3.0.1" +version = "3.2.0" description = "passive checker of Python programs" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] [[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" +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"cli\"" files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, ] [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +cp2110 = ["hidapi"] + +[[package]] +name = "pyserial-asyncio" +version = "0.6" +description = "Python Serial Port Extension - Asynchronous I/O support" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"cli\"" +files = [ + {file = "pyserial-asyncio-0.6.tar.gz", hash = "sha256:b6032923e05e9d75ec17a5af9a98429c46d2839adfaf80604d52e0faacd7a32f"}, + {file = "pyserial_asyncio-0.6-py3-none-any.whl", hash = "sha256:de9337922619421b62b9b1a84048634b3ac520e1d690a674ed246a2af7ce1fc5"}, +] + +[package.dependencies] +pyserial = "*" [[package]] name = "pytest" -version = "7.2.0" +version = "8.3.5" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, - {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] [[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" -optional = false -python-versions = ">=3.7" +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"cli\"" files = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[extras] +cli = ["prompt-toolkit", "pyserial-asyncio"] + [metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "2dff72ad2f9ae813ff6ad7a10660b51c5a575fa76e475700b4c84c3bbb6f25c7" +lock-version = "2.1" +python-versions = "^3.12" +content-hash = "add51becfd2262f8681840316f4ea4d4dd11b1680829a99fe4a5fa2fa22733fa" diff --git a/pyproject.toml b/pyproject.toml index 41c470b..efd8fe4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ exclude = ''' ) ''' line-length = 88 -target-version = ["py39"] +target-version = ["py312"] [tool.isort] profile = "black" @@ -36,14 +36,19 @@ repository = "https://github.com/basilfx/python-tinylink" version = "2.0.0" [tool.poetry.dependencies] -python = "^3.9" +prompt-toolkit = { version = "^3.0.50", optional = true } +pyserial-asyncio = { version = "^0.6", optional = true } +python = "^3.12" [tool.poetry.group.dev.dependencies] -black = "^22.10.0" -isort = "^5.12.0" -flake8 = "^6.0.0" -flake8-mypy = "^17.8.0" -pytest = "^7.2.0" +black = "^25.0.0" +flake8 = "^7.1.0" +isort = "^6.0.0" +pytest = "^8.3.0" +mypy = "^1.15.0" + +[tool.poetry.extras] +cli = ["pyserial-asyncio", "prompt-toolkit"] [tool.poetry.scripts] tinylink = 'tinylink.cli:run' diff --git a/tests/test_tinylink.py b/tests/test_tinylink.py index a1ccfc2..424f373 100644 --- a/tests/test_tinylink.py +++ b/tests/test_tinylink.py @@ -5,27 +5,27 @@ class DummyHandle(object): """ - Dummy handler, so the TinyLink class can exchange data with itself. + Dummy handle, so the TinyLink class can exchange data with itself. """ - stream: bytes + buffer: bytes index: int length: int def __init__(self) -> None: - self.stream = bytearray() + self.buffer = bytearray() self.index = 0 self.length = 0 def read(self, size: int) -> bytes: - data = self.stream[self.index : min(self.length, self.index + size)] + data = self.buffer[self.index : min(self.length, self.index + size)] self.index += len(data) # Return data. return bytes(data) def write(self, data: bytes) -> int: - self.stream.extend(data) + self.buffer.extend(data) self.length += len(data) # Return number of bytes written. @@ -45,45 +45,145 @@ def test_basic(self): handle = DummyHandle() link = tinylink.TinyLink(handle) - message = b"Hello, this is a test" - size = link.write(message) + flags = 123 + payload = b"Hello, this is a test" + link.write_frame(tinylink.Frame(flags=flags, payload=payload)) self.assertEqual( - size, + len(handle.buffer), tinylink.LEN_PREAMBLE + tinylink.LEN_HEADER + tinylink.LEN_BODY - + len(message), + + len(payload), ) - # Read `size` bytes to receive the full frame, test it partially. - link.read(1) - link.read(1) - link.read(1) - frames = link.read(size - 3) + frame = link.read_frame() - self.assertEqual(len(frames), 1) - self.assertEqual(frames[0].data, message) + self.assertEqual(frame.flags, flags) + self.assertEqual(frame.payload, payload) - def test_multiple(self): + def test_flags_only(self): """ - Test multiple messages. + Test frame with only a flags and no payload. """ handle = DummyHandle() link = tinylink.TinyLink(handle) - size = 0 + flags = 123 + link.write_frame(tinylink.Frame(flags=flags)) - for i in range(5): - size += link.write(bytes([97 + i])) + self.assertEqual( + len(handle.buffer), + tinylink.LEN_PREAMBLE + tinylink.LEN_HEADER + tinylink.LEN_BODY, + ) + + frame = link.read_frame() + + self.assertEqual(frame.flags, flags) + + def test_escaping(self): + """ + Test escaping of flag (first byte of preamble) inside the header and + body. + """ + + handle = DummyHandle() + link = tinylink.TinyLink(handle) + + flags = tinylink.consts.FLAG + payload = bytes([tinylink.consts.ESCAPE]) + link.write_frame(tinylink.Frame(flags=flags, payload=payload)) + + # The frame will contain the flag five times: once in the header and + # once in the body. + overhead = 2 + + self.assertEqual( + len(handle.buffer), + tinylink.LEN_PREAMBLE + + tinylink.LEN_HEADER + + tinylink.LEN_BODY + + len(payload) + + overhead, + ) - frames = link.read(size) + frame = link.read_frame() - self.assertEqual(len(frames), 5) + self.assertEqual(frame.flags, flags) + self.assertEqual(frame.payload, payload) + + def test_escaping_last_byte(self): + """ + Test frame where the very last byte requires escaping (and the logic + could assume the frame is complete. This happens to be the case for + flag value 10. + """ + + handle = DummyHandle() + link = tinylink.TinyLink(handle) + + flags = 10 + link.write_frame(tinylink.Frame(flags=flags)) + + # The frame with flag value 10 and no payload requires the last frame + # checksum byte to be escaped. The overhead will be 1. + overhead = 1 + + self.assertEqual( + len(handle.buffer), + tinylink.LEN_PREAMBLE + tinylink.LEN_HEADER + tinylink.LEN_BODY + overhead, + ) + + frame = link.read_frame() + + self.assertEqual(frame.flags, flags) + + def test_escaping_max_payload_length(self): + """ + Test escaping with maximum payload length (which means that the + internal buffer should not overflow). + """ + + handle = DummyHandle() + link = tinylink.TinyLink(handle) + + flags = 123 + payload = bytes([tinylink.consts.FLAG] * link.max_payload_length) + link.write_frame(tinylink.Frame(flags=flags, payload=payload)) + + # Every payload byte will be esacped, so the overhead will be twice the + # number of payload bytes. + overhead = len(payload) + + self.assertEqual( + len(handle.buffer), + tinylink.LEN_PREAMBLE + + tinylink.LEN_HEADER + + tinylink.LEN_BODY + + len(payload) + + overhead, + ) + + frame = link.read_frame() + + self.assertEqual(frame.flags, flags) + self.assertEqual(frame.payload, payload) + + def test_multiple(self): + """ + Test multiple payloads. + """ + + handle = DummyHandle() + link = tinylink.TinyLink(handle) + + for i in range(5): + link.write(bytes([97 + i])) for i in range(5): - self.assertEqual(frames[i].data, bytes([97 + i])) + frame = link.read_frame() + self.assertEqual(frame.payload, bytes([97 + i])) def test_sync(self): """ @@ -94,13 +194,13 @@ def test_sync(self): link = tinylink.TinyLink(handle) garbage = b"Garbage here that does not synchronize." - message = b"Hi!" + payload = b"Hi!" - size = handle.write(garbage) + link.write(message) - frames = link.read(size) + handle.write(garbage) + link.write_frame(tinylink.Frame(payload=payload)) + frame = link.read_frame() - self.assertEqual(len(frames), 1) - self.assertEqual(frames[0].data, message) + self.assertEqual(frame.payload, payload) def test_sync_small(self): """ @@ -108,16 +208,16 @@ def test_sync_small(self): """ handle = DummyHandle() - link = tinylink.TinyLink(handle, max_length=4) + link = tinylink.TinyLink(handle, max_payload_length=4) garbage = b"Garbage here that does not synchronize." - message = b"Hi!" + payload = b"Hi!" - size = handle.write(garbage) + link.write(message) - frames = link.read(size) + handle.write(garbage) + link.write_frame(tinylink.Frame(payload=payload)) + frame = link.read_frame() - self.assertEqual(len(frames), 1) - self.assertEqual(frames[0].data, message) + self.assertEqual(frame.payload, payload) def test_size_fit(self): """ @@ -125,15 +225,14 @@ def test_size_fit(self): """ handle = DummyHandle() - link = tinylink.TinyLink(handle, max_length=4) + link = tinylink.TinyLink(handle, max_payload_length=4) - message = b"blub" + payload = b"blub" - size = link.write(message) - frames = link.read(size) + link.write_frame(tinylink.Frame(payload=payload)) + frame = link.read_frame() - self.assertEqual(len(frames), 1) - self.assertEqual(frames[0].data, message) + self.assertEqual(frame.payload, payload) def test_size_no_fit(self): """ @@ -141,33 +240,14 @@ def test_size_no_fit(self): """ handle = DummyHandle() - link = tinylink.TinyLink(handle, max_length=2) + link = tinylink.TinyLink(handle, max_payload_length=2) - message = b"blub" + payload = b"blub" with self.assertRaises(ValueError): - link.write(message) - - def test_damaged_a(self): - """ - Test damaged frame (in total) that will return a Frame with damages is - True. - """ - - handle = DummyHandle() - link = tinylink.TinyLink(handle) - - message = b"Hello, this is a test" - - size = link.write(message) - handle.stream[-tinylink.LEN_CRC :] = [0x00] * tinylink.LEN_CRC - frames = link.read(size) - - self.assertEqual(len(frames), 1) - self.assertEqual(frames[0].data, message) - self.assertEqual(frames[0].damaged, True) + link.write_frame(tinylink.Frame(payload=payload)) - def test_damaged_b(self): + def test_damaged(self): """ Test damaged frame (header) that will not return anything. """ @@ -175,10 +255,10 @@ def test_damaged_b(self): handle = DummyHandle() link = tinylink.TinyLink(handle) - message = b"Hello, this is a test" + payload = b"Hello, this is a test" - size = link.write(message) - handle.stream[tinylink.LEN_PREAMBLE + tinylink.LEN_HEADER - 1] = 0x00 - frames = link.read(size) + link.write_frame(tinylink.Frame(payload=payload)) + handle.buffer[tinylink.LEN_PREAMBLE + tinylink.LEN_HEADER - 1] = 0x00 + frame = link.read_frame() - self.assertEqual(len(frames), 0) + self.assertEqual(frame, None) diff --git a/tinylink/__init__.py b/tinylink/__init__.py index b419def..cefb562 100644 --- a/tinylink/__init__.py +++ b/tinylink/__init__.py @@ -1,4 +1,4 @@ from .consts import * # noqa -from .link import Frame, TinyLink # noqa +from .link import AsyncTinyLink, Frame, TinyLink # noqa __version__ = "2.0.0" diff --git a/tinylink/cli.py b/tinylink/cli.py index 6fadcdd..43c5135 100644 --- a/tinylink/cli.py +++ b/tinylink/cli.py @@ -1,18 +1,19 @@ import argparse +import asyncio import csv -import select import struct import sys -import time from io import StringIO from typing import Optional -import tinylink +import serial_asyncio +from prompt_toolkit import PromptSession +from prompt_toolkit.completion import WordCompleter +from prompt_toolkit.history import FileHistory +from prompt_toolkit.patch_stdout import patch_stdout -try: - import serial -except ImportError: - serial = None +import tinylink +from tinylink.utils import create_async_handle def parse_arguments(argv: list[str]) -> argparse.Namespace: @@ -24,16 +25,18 @@ def parse_arguments(argv: list[str]) -> argparse.Namespace: # Add options. parser.add_argument("port", type=str, help="serial port") - parser.add_argument("baudrate", type=int, default=9600, help="serial baudrate") parser.add_argument( - "--length", type=int, default=2**16, help="maximum length of frame" + "baudrate", type=int, nargs="?", default=9600, help="serial baudrate" + ) + parser.add_argument( + "--length", type=int, default=2**16, help="maximum length of payload" ) parser.add_argument( "--endianness", type=str, default="little", choices=["big", "little"], - help="maximum length of frame", + help="endianness of link", ) # Parse command line. @@ -69,93 +72,113 @@ def dump(prefix: str, data: bytes) -> str: return "\n".join(result) -def process_link(link: tinylink.TinyLink) -> None: +async def handle_link(link: tinylink.AsyncTinyLink) -> None: """ - Process incoming link data. + Process incoming frames. """ - frames = link.read() + while True: + frame = await link.read_frame() - # Print received frames. - for frame in frames: - sys.stdout.write("### Type = %s\n" % frame.__class__.__name__) - sys.stdout.write("### Flags = 0x%04x\n" % frame.flags) + if frame: + sys.stdout.write(">>> # Flags = 0x%04x\n" % frame.flags) - sys.stdout.write("### Length = %d\n" % len(frame.data)) - sys.stdout.write(dump("<<<", frame.data) + "\n\n") + if frame.payload: + sys.stdout.write(">>> # Length = %d\n" % len(frame.payload)) + sys.stdout.write(dump(">>>", frame.payload) + "\n\n") -def process_stdin(link: tinylink.TinyLink) -> Optional[bool]: +async def handle_console(link: tinylink.AsyncTinyLink) -> Optional[bool]: """ - Process stdin commands. + Process console inputs. """ - command = sys.stdin.readline() + completer = WordCompleter(["\\flags=", "\\pack=", "\\wait=", "\\repeat="]) + + session = PromptSession(history=FileHistory(".history")) + + while True: + with patch_stdout(): + try: + line = await session.prompt_async("--> ", completer=completer) + except KeyboardInterrupt: + continue + + if not line: + continue + + await parse_line(link, line) - # End of file. - if len(command) == 0: - return False +async def parse_line(link: tinylink.AsyncTinyLink, line: str) -> None: # Abuse the CSV module as a command parser, because CSV-like arguments are # possible. - items = list(csv.reader(StringIO(command.strip()), delimiter=" ")) + items = list(csv.reader(StringIO(line.strip()), delimiter=" ")) if not items: return - # Initialize state and start parsing. frame = tinylink.Frame() repeat = 1 pack = "B" - try: - for item in items[0]: - if item[0] == "\\": + for item in items[0]: + if item == "": + continue + elif item[0] == "\\": + try: k, v = item[1:].split("=") if k == "flags": - frame.flags = int(v, 0) + frame.flags = int(v) & 0xFFFF elif k == "pack": pack = v elif k == "wait": - time.sleep(float(v)) + await asyncio.sleep(float(v)) elif k == "repeat": repeat = int(v) else: - raise ValueError("Unkown option: %s" % k) - else: + raise ValueError(f"Unknown modifier: {k}") + except Exception as e: # noqa + sys.stdout.write(f"Unable to parse modifier: {e}\n") + return + else: + try: + # Assume it is a float. + value = struct.pack(link.endianness + pack, float(item)) + except: # noqa try: - # Assume it is a float. - value = struct.pack(link.endianness + pack, float(item)) + # Assume it is an int. + value = struct.pack(link.endianness + pack, int(item, 0)) except: # noqa try: - # Assume it is an int. - value = struct.pack(link.endianness + pack, int(item, 0)) - except ValueError: # Assume it is a byte string. item_bytes = item.encode("ascii") value = struct.pack( link.endianness + str(len(item_bytes)) + "s", item_bytes ) + except Exception as e: # noqa + sys.stdout.write( + "Unable to parse input as float, integer or byte\n" + ) + return - # Concat to frame. - frame.data = (frame.data or bytes()) + value - except Exception as e: - sys.stdout.write("Parse exception: %s\n" % e) + # Concat to frame. + frame.payload = (frame.payload or bytes()) + value - # Output the data. - for i in range(repeat): - sys.stdout.write("### Flags = 0x%04x\n" % frame.flags) + # Output the frame. + for _ in range(repeat): + sys.stdout.write("<<< # Flags = 0x%04x\n" % frame.flags) - if frame.data: - sys.stdout.write("### Length = %d\n" % len(frame.data)) - sys.stdout.write(dump(">>>", frame.data) + "\n\n") + if frame.payload: + sys.stdout.write("<<< # Length = %d\n" % len(frame.payload)) + sys.stdout.write(dump("<<<", frame.payload) + "\n\n") - # Send the frame. + # Write the frame. try: - link.write_frame(frame) + await link.write_frame(frame) except ValueError as e: - sys.stdout.write("Could not send frame: %s\n" % e) + sys.stdout.write(f"Could not write frame: {e}\n") return @@ -164,21 +187,14 @@ def run() -> None: Entry point for console script. """ - sys.exit(main(sys.argv)) + sys.exit(asyncio.run(main(sys.argv))) -def main(argv: list[str]) -> int: +async def main(argv: list[str]) -> int: """ Main entry point. """ - if serial is None: - sys.stdout.write( - "TinyLink CLI uses PySerial, but it is not installed. Please " - "install this first.\n" - ) - return 1 - # Parse arguments. arguments = parse_arguments(argv) @@ -188,32 +204,18 @@ def main(argv: list[str]) -> int: endianness = tinylink.BIG_ENDIAN # Open serial port and create link. - handle = serial.Serial(arguments.port, baudrate=arguments.baudrate) - link = tinylink.TinyLink(handle, max_length=arguments.length, endianness=endianness) - - # Loop until finished. - try: - # Input indicator. - sys.stdout.write("--> ") - sys.stdout.flush() - - while True: - readables, _, _ = select.select([handle, sys.stdin], [], []) - - # Read from serial port. - if handle in readables: - process_link(link) - - # Read from stdin. - if sys.stdin in readables: - if process_stdin(link) is False: - break - - # Input indicator. - sys.stdout.write("--> ") - sys.stdout.flush() - except KeyboardInterrupt: - handle.close() + reader, writer = await serial_asyncio.open_serial_connection( + url=arguments.port, baudrate=arguments.baudrate + ) + + link = tinylink.AsyncTinyLink( + create_async_handle(reader, writer), + max_payload_length=arguments.length, + endianness=endianness, + ) + + # Start co-routines and wait until finished. + await asyncio.gather(handle_console(link), handle_link(link)) # Done. return 0 diff --git a/tinylink/consts.py b/tinylink/consts.py index e83cf85..1cb525b 100644 --- a/tinylink/consts.py +++ b/tinylink/consts.py @@ -1,6 +1,10 @@ -# This can be anything, and is used to synchronize a frame. +# This can be anything, but must be an alternating pattern. PREAMBLE = 0xAA55AA55 +# The escape character is used for byte-stuffing of the header and body. +FLAG = 0xAA +ESCAPE = 0x1B + # Endianness. LITTLE_ENDIAN = "<" BIG_ENDIAN = ">" @@ -10,17 +14,13 @@ WAITING_FOR_HEADER = 2 WAITING_FOR_BODY = 3 -# Message flags (reserved). -FLAG_NONE = 0x00 -FLAG_RESET = 0x01 -FLAG_ERROR = 0x02 -FLAG_PRIORITY = 0x04 - # Do not change these values! LEN_PREAMBLE = 4 + LEN_FLAGS = 2 LEN_LENGTH = 2 LEN_XOR = 1 -LEN_CRC = 4 LEN_HEADER = LEN_FLAGS + LEN_LENGTH + LEN_XOR + +LEN_CRC = 4 LEN_BODY = LEN_CRC diff --git a/tinylink/link.py b/tinylink/link.py index a25d8f9..0b0e20c 100644 --- a/tinylink/link.py +++ b/tinylink/link.py @@ -1,85 +1,72 @@ import struct -from typing import Protocol +from typing import Optional -from . import consts, utils +from . import consts, types, utils -class Handle(Protocol): - """ - Protocol for a handler. - """ +class Frame: + """Represents a frame. A frame contains flags and an optional payload.""" - def read(self, size: int) -> bytes: - """ " - Read up to `size` bytes. - """ + flags: int + payload: Optional[bytes] - def write(self, data: bytes) -> int: - """ - Write data and return the number of bytes written. + def __init__(self, flags: int = 0x0000, payload: Optional[bytes] = None) -> None: + """Initialize a new frame with optional flags and payload. + + Args: + flags: The frame flags (default: 0x0000). + payload: Optional payload data (default: None). """ + if flags & 0xFFFF != flags: + raise ValueError("Flags must be in range 0 - 65535.") + self.flags = flags + self.payload = payload -class Frame: - """ - Represents a frame. - """ + def __repr__(self) -> str: + """Return a string representation of the frame. - data: bytes - flags: int - damaged: int + Returns: + A string containing the frame's flags and payload if present. + """ + class_name = self.__class__.__name__ - def __init__( - self, data: bytes = None, flags: int = consts.FLAG_NONE, damaged: bool = False - ) -> None: - if data is not None: - if type(data) is not bytes: - raise ValueError("Provided data must be encoded as bytes.") + if self.payload is None: + return f"{class_name}(flags={self.flags:04x})" else: - data = bytes() + return f"{class_name}({self.payload}, flags={self.flags:04x})" - self.data = data - self.flags = flags - self.damaged = damaged - def __repr__(self) -> str: - return "%s(%s, flags=%d, damaged=%s)" % ( - self.__class__.__name__, - repr(self.data), - self.flags, - self.damaged, - ) +class BaseTinyLink: + """TinyLink state machine for streaming communication with low-speed + embedded applications that only use RX/TX. + A link exchanges frames. Frames exist out of a preamble, a header and a + body. The header contain flags and length information. The body contains + the optional payload. Checksums are included to detect errors as fast as + possible (this can happen when you jump right into a stream of bytes, + without being synchronized). The header and body are escaped. -class TinyLink: - """ - TinyLink state machine for streaming communication with low-speed embedded - applications that only use RX/TX. Every message is encapsulated in a frame. - A frame has a header checksum and a frame checksum, to detect errors as - fast as possible (this can happen when you jump right into a stream of - packets, without being synchronized). + The payload can be up to 65536 bytes. A minimal frame (without payload and + escaping) is 13 bytes. - A typical frame has 13 bytes overhead, and can have a data payload up to - 65536 bytes. - - It does not provide error correction and the bytes are not aligned. + The protocol favors reliability and simplicity over speed and complexity. """ - handle: Handle endianness: str - max_length: int - ignore_damaged: bool + max_payload_length: int + + state: int + buffer: bytearray + index: int + unescaping: bool def __init__( self, - handle: Handle, endianness: str = consts.LITTLE_ENDIAN, - max_length: int = 2 ** (consts.LEN_LENGTH * 8), - ignore_damaged: bool = False, + max_payload_length: int = 2 ** (consts.LEN_LENGTH * 8) - 1, ) -> None: - """ - Construct a new TinyLink state machine. A state machine takes a handle, - which provides a `read` and `write` method. + """Construct a new TinyLink state machine. The endianness is either `consts.LITTLE_ENDIAN` or `consts.BIG_ENDIAN`. While big endian is common for networking, little endian is directly @@ -87,160 +74,306 @@ def __init__( have to perform conversion of endianness. Both microcontroller and this instance should agree upon the value of - `max_lengthz. In case a message is received that exceeds this value, it - will be silently ignored. - - By default, if a fully received frame is damaged, it will be returned - as an instance of `DamagedFrame` instance, unless `ignored_damaged` is - set to `True`. + `max_payload_length`. In case a frame is received that exceeds this + value, it will be silently ignored. """ - self.handle = handle self.endianness = endianness - self.max_length = max_length - self.ignore_damaged = ignore_damaged + self.max_payload_length = max_payload_length - # Set initial state + # Set initial state. self.state = consts.WAITING_FOR_PREAMBLE - # Pre-allocate buffer that fits header + body. The premable will be - # cleared when it is detected, so it does not need space. - self.buffer = bytearray(max_length + consts.LEN_HEADER + consts.LEN_BODY) + # Pre-allocate byte buffer that fits header + body. The premable will + # be cleared when it is detected, so it does not need space. + self.buffer = bytearray( + max_payload_length + consts.LEN_HEADER + consts.LEN_BODY + ) self.index = 0 + self.unescaping = False - def write_frame(self, frame: Frame) -> int: - """ - Write a frame via the handle. + def _write_frame(self, frame: Frame) -> bytes: + """Construct the bytes of a frame to write to a handle. + + Args: + frame: The frame to write. + + Raises: + ValueError: If the payload length exceeds the maximum length. + + Returns: + The bytes to write to a handle. """ - result = bytearray() - length = len(frame.data or []) + flags = frame.flags + payload = frame.payload or bytes() + length = len(payload) - # Check length of message. - if length > self.max_length: + # Check length of payload. + if length > self.max_payload_length: raise ValueError( - "Message length %d exceeds max length %d" % (length, self.max_length) + "Message length of %d bytes exceeds maximum payload length of %d bytes" + % (length, self.max_payload_length) ) + # pack preamble. + preamble = struct.pack(self.endianness + "I", consts.PREAMBLE) + # Pack header. - checksum_header = utils.checksum_header(frame.flags, length) - result += struct.pack( - self.endianness + "IHHB", - consts.PREAMBLE, - frame.flags, + checksum_header = self._checksum_header(flags, length) + header = struct.pack( + self.endianness + "HHB", + flags, length, checksum_header, ) - # Pack data. - if frame.data is not None: - checksum_frame = utils.checksum_frame(frame.data, checksum_header) - result += struct.pack( - self.endianness + str(length) + "sI", frame.data, checksum_frame - ) + # Pack body. + checksum_frame = self._checksum_frame(header, payload) + body = struct.pack( + self.endianness + str(length) + "sI", payload, checksum_frame + ) - # Write to file. - return self.handle.write(result) + # Done. + return preamble + self._escape(header + body) - def write(self, data: bytes, flags: int = consts.FLAG_NONE) -> int: - """ - Shorthand for `write_frame(Frame(data, flags=flags))`. - """ + def _read_byte(self, byte: bytes) -> Optional[Frame]: + """Process a byte that was read. - return self.write_frame(Frame(data, flags=flags)) + Args: + byte: The byte read from a source. - def read(self, limit: int = 1) -> list[Frame]: - """ - Read up to `limit` bytes from the handle and process it. Returns a list - of received frames, if any. + Returns: + A `Frame`, if the frame is complete (i.e. all bytes received), or + `None` if not yet complete. """ - # List of frames received. - frames = [] - - # Bytes are added one at a time. - while limit: - char = self.handle.read(1) - - if not char: - return [] - - # Append to buffer. - self.buffer[self.index] = ord(char) - self.index += 1 - - # Decide what to do. - if self.state == consts.WAITING_FOR_PREAMBLE: - if self.index >= consts.LEN_PREAMBLE: - (start,) = struct.unpack_from( - self.endianness + "I", self.buffer, self.index - 4 - ) - - if start == consts.PREAMBLE: - # Advance to next state. - self.index = 0 - self.state = consts.WAITING_FOR_HEADER - elif ( - self.index - == self.max_length + consts.LEN_HEADER + consts.LEN_BODY - ): - # Preamble not found and buffer is full. Copy last four - # bytes, because the next byte may form the preamble - # together with the last three bytes. - self.buffer[0:4] = self.buffer[-4:] - self.index = 4 - - elif self.state == consts.WAITING_FOR_HEADER: - if self.index == consts.LEN_HEADER: - flags, length, checksum = struct.unpack_from( - self.endianness + "HHB", self.buffer - ) - - # Verify checksum. - if ( - checksum == utils.checksum_header(flags, length) - and length <= self.max_length - ): - - if length > 0: - self.state = consts.WAITING_FOR_BODY - else: - # Frame without body. - frames.append(Frame(flags=flags)) - - self.index = 0 - self.state = consts.WAITING_FOR_PREAMBLE - else: - # Reset to start state. - self.index = 0 - self.state = consts.WAITING_FOR_PREAMBLE - - elif self.state == consts.WAITING_FOR_BODY: - # Unpack header. - flags, length, checksum_a = struct.unpack_from( - self.endianness + "HHB", self.buffer - ) + result = None - if self.index == consts.LEN_HEADER + length + consts.LEN_CRC: - # Unpack body. - result, checksum_b = struct.unpack_from( - self.endianness + str(length) + "sI", - self.buffer, - consts.LEN_HEADER, - ) + # Unescape and append to buffer. + if self.state in {consts.WAITING_FOR_HEADER, consts.WAITING_FOR_BODY}: + if self.unescaping: + self.index = self.index - 1 + self.unescaping = False + else: + if byte[0] == consts.ESCAPE: + self.unescaping = True - # Verify checksum. - if checksum_b == utils.checksum_frame(result, checksum_a): - frames.append(Frame(result, flags=flags)) - elif not self.ignore_damaged: - frames.append(Frame(result, flags=flags, damaged=True)) + self.buffer[self.index] = byte[0] + self.index += 1 + if self.unescaping: + return + + # Decide what to do. + if self.state == consts.WAITING_FOR_PREAMBLE: + if self.index >= consts.LEN_PREAMBLE: + (preamble,) = struct.unpack_from( + self.endianness + "I", self.buffer[self.index - 4 : self.index] + ) + + if preamble == consts.PREAMBLE: + # Preamble found. Start reading the header. + self.index = 0 + self.state = consts.WAITING_FOR_HEADER + elif ( + self.index + == self.max_payload_length + consts.LEN_HEADER + consts.LEN_BODY + ): + # Preamble not found and buffer is full. Copy last four + # bytes, because the next byte may form the preamble + # together with the last three bytes. + self.buffer[0:4] = self.buffer[-4:] + self.index = 4 + + elif self.state == consts.WAITING_FOR_HEADER: + if self.index == consts.LEN_HEADER: + flags, length, checksum_header = struct.unpack_from( + self.endianness + "HHB", self.buffer + ) + + # Verify checksum. + if ( + checksum_header == self._checksum_header(flags, length) + and length <= self.max_payload_length + ): + self.state = consts.WAITING_FOR_BODY + else: # Reset to start state. self.index = 0 self.state = consts.WAITING_FOR_PREAMBLE - # Decrement number of bytes to read. - limit -= 1 + elif self.state == consts.WAITING_FOR_BODY: + flags, length, _ = struct.unpack_from(self.endianness + "HHB", self.buffer) + + if self.index == length + consts.LEN_HEADER + consts.LEN_CRC: + payload, checksum_frame = struct.unpack_from( + self.endianness + str(length) + "sI", + self.buffer, + consts.LEN_HEADER, + ) + + # Verify checksum. + if checksum_frame == self._checksum_frame( + self.buffer[: consts.LEN_HEADER], payload + ): + result = Frame(flags=flags, payload=payload) + + # Reset to start state. + self.index = 0 + self.state = consts.WAITING_FOR_PREAMBLE # Done. - return frames + return result + + def _checksum_header(self, flags, length) -> int: + """Calculate the header checksum. + + Args: + flags: The flags field to checksum. + length: The length field to checksum. + + Returns: + A single-byte checksum of the header fields. + """ + + a = (flags & 0x00FF) >> 0 + b = (flags & 0xFF00) >> 8 + c = (length & 0x00FF) >> 0 + d = (length & 0xFF00) >> 8 + + return a ^ b ^ c ^ d + + def _checksum_frame(self, header: bytes, payload: bytes) -> int: + """Calculate the frame checksum (header and payload). + + Args: + header: The frame header. + payload: The frame payload. + + Returns: + A four-byte CRC32 checksum of the header and body. + """ + + return utils.crc32(header + payload) & 0xFFFFFFFF + + def _escape(self, data: bytes) -> bytes: + """Escape the data using byte-stuffing, so that the data can safely + contain the preamble. + + Args: + data: The data to escape. + + Returns: + The escaped data. + """ + + buffer = bytearray(len(data) * 2) + index = 0 + + for byte in data: + if byte == consts.FLAG: + buffer[index] = consts.ESCAPE + buffer[index + 1] = consts.FLAG + index += 2 + elif byte == consts.ESCAPE: + buffer[index] = consts.ESCAPE + buffer[index + 1] = consts.ESCAPE + index += 2 + else: + buffer[index] = byte + index += 1 + + return bytes(buffer[:index]) + + +class AsyncTinyLink(BaseTinyLink): + + handle: types.AsyncHandle + + def __init__(self, handle: types.AsyncHandle, *args, **kwargs) -> None: + """Construct a new asynchronous TinyLink instance. + + See `TinyLink.__init__` for more information. + + Args: + handle: A handle that provides a `read` and `write` method. Note + that the handle is just a wrapper and does not 'own' it. + """ + super().__init__(*args, **kwargs) + + self.handle = handle + + async def read_frame(self) -> Optional[Frame]: + while True: + byte = await self.handle.read(1) + + # Handle end-of-file. + if not byte: + return None + + result = self._read_byte(byte) + + if result: + return result + + async def read(self) -> bytes: + frame = await self.read_frame() + + # Handle end-of-file. + if not frame: + return bytes() + + return frame.payload or bytes() + + async def write_frame(self, frame: Frame) -> None: + await self.handle.write(self._write_frame(frame)) + + async def write(self, payload: bytearray, flags: int = 0x0000) -> None: + await self.write_frame(Frame(payload=payload, flags=flags)) + + +class TinyLink(BaseTinyLink): + + handle: types.Handle + + def __init__(self, handle: types.Handle, *args, **kwargs) -> None: + """Construct a new synchronous TinyLink instance. + + See `TinyLink.__init__` for more information. + + Args: + handle: A handle that provides a `read` and `write` method. Note + that the handle is just a wrapper and does not 'own' it. + """ + super().__init__(*args, **kwargs) + + self.handle = handle + + def read_frame(self) -> Optional[Frame]: + while True: + byte = self.handle.read(1) + + # Handle end-of-file. + if not byte: + return None + + result = self._read_byte(byte) + + if result: + return result + + def read(self) -> bytes: + frame = self.read_frame() + + # Handle end-of-file. + if not frame: + return bytes() + + return frame.payload or bytes() + + def write_frame(self, frame: Frame) -> None: + self.handle.write(self._write_frame(frame)) + + def write(self, payload: bytes, flags: int = 0x0000) -> None: + self.write_frame(Frame(payload=payload, flags=flags)) diff --git a/tinylink/types.py b/tinylink/types.py new file mode 100644 index 0000000..d3c5133 --- /dev/null +++ b/tinylink/types.py @@ -0,0 +1,45 @@ +from typing import Protocol + + +class AsyncHandle(Protocol): + """Protocol for an asynchronous handle.""" + + async def read(self, size: int) -> bytes: + """Read up to `size` bytes from the handle. + + Args: + size: Maximum number of bytes to read. + + Returns: + The bytes read from the handle. May be less than `size` if fewer + bytes are available. + """ + + async def write(self, data: bytes) -> None: + """Write data to the handle. + + Args: + data: The bytes to write to the handle. + """ + + +class Handle(Protocol): + """Protocol for a synchronous handle.""" + + def read(self, size: int) -> bytes: + """Read up to `size` bytes from the handle. + + Args: + size: Maximum number of bytes to read. + + Returns: + The bytes read from the handle. May be less than `size` if fewer + bytes are available. + """ + + def write(self, data: bytes) -> None: + """Write data to the handle. + + Args: + data: The bytes to write to the handle. + """ diff --git a/tinylink/utils.py b/tinylink/utils.py index 69c0773..1f392a8 100644 --- a/tinylink/utils.py +++ b/tinylink/utils.py @@ -1,25 +1,34 @@ +import asyncio + +from . import types + CRC32_POLYNOMIAL = 0xEDB88320 CRC32_INITIAL = 0x00000000 -def crc32(buf): - """ - Calculate CRC32 of given input. +def crc32(buf) -> int: + """Calculate CRC32 of given input. + + Args: + buf: the buffer to calculate CRC32 over. + + Returns: + A four-byte CRC32 checksum. """ result = CRC32_INITIAL def crc32_value(c): - ulTemp1 = (result >> 8) & 0x00FFFFFF - ulCRC = (result ^ c) & 0xFF + temp = (result >> 8) & 0x00FFFFFF + crc = (result ^ c) & 0xFF - for i in range(8): - if ulCRC & 0x01: - ulCRC = (ulCRC >> 1) ^ CRC32_POLYNOMIAL + for _ in range(8): + if crc & 0x01: + crc = (crc >> 1) ^ CRC32_POLYNOMIAL else: - ulCRC = ulCRC >> 1 + crc = crc >> 1 - return ulTemp1 ^ ulCRC + return temp ^ crc # Execute function for each byte. for b in buf: @@ -28,22 +37,26 @@ def crc32_value(c): return result -def checksum_header(flags, length): - """ - Calculate checksum over the header. - """ +def create_async_handle( + reader: asyncio.StreamReader, writer: asyncio.StreamWriter +) -> types.AsyncHandle: + """Create a handle from a `asyncio.StreamReader` and asyncio.StreamWriter` + pair. - a = (flags & 0x00FF) >> 0 - b = (flags & 0xFF00) >> 8 - c = (length & 0x00FF) >> 0 - d = (length & 0xFF00) >> 8 + Args: + reader: The reader instance. + writer: The writer instance. - return a ^ b ^ c ^ d + Returns: + A handle that can be used with `link.AsyncTinyLink`. + """ + class Handle: + async def read(self, size: int) -> bytes: + return await reader.read(size) -def checksum_frame(data, checksum_header): - """ - Calculate checksum of both the checksum header and the data. - """ + async def write(self, data: bytes) -> None: + writer.write(data) + await writer.drain() - return crc32(memoryview(data).tobytes() + bytearray([checksum_header])) & 0xFFFFFFFF + return Handle() From ac19a9448eeeb30b39242c86972e2bc08c9f9d1e Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Fri, 18 Apr 2025 00:13:08 +0200 Subject: [PATCH 2/4] Update documentation --- README.md | 31 ++++++++++++++----------------- docs/statechart.dot | 12 ++++-------- docs/statechart.png | Bin 54659 -> 45622 bytes 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 167cc47..0f28283 100644 --- a/README.md +++ b/README.md @@ -10,30 +10,26 @@ streaming protocol for low-speed embedded applications, such as serial connected devices. It allowes the receiver to 'jump into' a stream of data frames. Every frame starts with a preamble, so the receiver can synchronize. -A payload is optional. - The format of a frame is as follows: ``` +| Preamble | Header | Body | | 0xAA 0x55 0xAA 0x55 | AA AA BB BB CC | XX XX .. .. .. .. XX XX YY YY YY YY | -| Preamble | Header | Body (optional) | Fields: -A = Length -B = Flags +A = Flags +B = Length C = XOR checksum over header -X = Body payload (max. 65536 bytes) -Y = CRC32 checksum over header + body +X = Payload (max. 65536 bytes) +Y = CRC32 checksum over header + payload ``` -The flags field can have arbitrary values, but the following flags are -reserved. +The flags field can be used for arbitrary purposes. The payload is optional. -* `0x01 = RESET` -* `0x02 = ERROR` -* `0x04 = PRIORITY` +Escaping of the header and body are performed using byte-stuffing, to ensure +that the header and body can contain bytes of the preamble. -Error correction is not implemented and the bytes are not aligned. The +Error correction is not implemented and the bytes are not strictly aligned. The endianness is customizable. ## State chart diagram @@ -45,15 +41,16 @@ The latest development version can be installed via `pip install git+https://github.com/basilfx/python-tinylink`. ## CLI -A simple serial CLI is included. When installed, run +A CLI is included to experiment with TinyLink. When installed, run `tinylink /dev/tty.PORT_HERE` to start it. You can use it to send raw bytes via the link and display what comes back. The CLI supports so-called modifiers to modify the outgoing data. For example, -the input `\flags=1 hello world` would send a reset frame with the value -'hello world'. +the input `\flags=16 hello world` would send a frame with the flags equal to 16 +and the payload 'hello world' -PySerial is required to run this CLI. +The CLI requires additional dependencies, that are installed using the `cli` +dependency specification (`poetry install --extras cli`). ## Tests To run the tests, please clone this repository and run `poetry run pytest`. diff --git a/docs/statechart.dot b/docs/statechart.dot index 61a3537..6fb994d 100644 --- a/docs/statechart.dot +++ b/docs/statechart.dot @@ -8,8 +8,7 @@ digraph G { receiving_header[shape=box, style=rounded, label="Receiving\nheader"] receiving_body[shape=box, style=rounded, label="Receiving\nbody"] - output_reset_frame[shape=box, style=rounded, label="Output\nreset frame"] - output_data_frame[shape=box, style=rounded, label="Output\ndata frame"] + output_frame[shape=box, style=rounded, label="Output frame"] start -> waiting_for_preamble @@ -17,15 +16,12 @@ digraph G { waiting_for_preamble -> receiving_header[label="Preamble detected"] receiving_header -> receiving_header[label="Byte received"] - receiving_header -> receiving_body[label="Header complete,\nbody expected"] + receiving_header -> receiving_body[label="Header complete,\nchecksum OK"] receiving_header -> waiting_for_preamble[label="Header invalid"] - receiving_header -> output_reset_frame[label="Header complete,\nreset frame"] - output_reset_frame -> waiting_for_preamble - receiving_body -> receiving_body[label="Byte received"] receiving_body -> waiting_for_preamble[label="Body invalid"] - receiving_body -> output_data_frame[label="Body complete,\ndata frame"] - output_data_frame -> waiting_for_preamble + receiving_body -> output_frame[label="Body complete,\nchecksum OK"] + output_frame -> waiting_for_preamble } \ No newline at end of file diff --git a/docs/statechart.png b/docs/statechart.png index ad2362f702e3640ab3cbebc9e4e70f85ea800976..af20959c261fabf83aa689dd86fa3c533951b50a 100644 GIT binary patch literal 45622 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 a`p?h&N=vwY#rF^fAn Date: Fri, 18 Apr 2025 00:16:43 +0200 Subject: [PATCH 3/4] Update CHANGELOG.md --- CHANGELOG.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b26d1..0be5d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,25 @@ # Changelog +## Unreleased + +Highlights: +* Added: support for asynchronous handles. +* Added: escaping of the header and body (protocol). +* Changed: minimal Python version is Python 3.12. +* Changed: body checksum is always included (protocol). +* Changed: body checksum is now calculated over header and payload (protocol). +* Fixed: preamble is now read as string of bytes. +* Improved: CLI is more interactive. +* Improved: consistent terminology and naming. +* Removed: reserved flags (protocol). + ## v2.0.0 Released 17 September 2015 Highlights: * Added: Python 3.4 support. -* Improved: More strict handling of bytes. -* Improved: Formatted code according to PEP8. +* Improved: more strict handling of bytes. +* Improved: formatted code according to PEP8. * Changed: ResetFrame is now a Frame, with correct flags set. * Changed: DamagedFrame is now a property on a Frame. From 1c1204368aa2e5f0c712d4e9f52f8b6859f03f9d Mon Sep 17 00:00:00 2001 From: Bas Stottelaar Date: Fri, 18 Apr 2025 00:16:50 +0200 Subject: [PATCH 4/4] Update actions --- .github/workflows/lint.yml | 14 +++++++------- .github/workflows/test.yml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0aedf84..e839aed 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,24 +10,24 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: ~/.local - key: poetry-1.2.2 + key: poetry-1.8.0 - uses: snok/install-poetry@v1 with: - version: 1.2.2 + version: 1.8.0 virtualenvs-create: true virtualenvs-in-project: true - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: cache-deps with: path: .venv diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94a09a1..ff683ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,24 +10,24 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: ~/.local - key: poetry-1.2.2 + key: poetry-1.8.0 - uses: snok/install-poetry@v1 with: - version: 1.2.2 + version: 1.8.0 virtualenvs-create: true virtualenvs-in-project: true - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: cache-deps with: path: .venv