From b0ee255acae95009114ebbf939711db20dd28733 Mon Sep 17 00:00:00 2001 From: Daniel Bayerlein Date: Fri, 7 Feb 2025 15:30:09 +0100 Subject: [PATCH 1/3] chore: update dependencies --- .github/dependabot.yml | 11 +- .github/workflows/ci.yml | 13 +- Pipfile | 22 +- Pipfile.lock | 432 +++++++++++++++++++--------------- README.md | 2 +- bin/sepicker | 2 +- sepicker/config.py | 6 +- sepicker/datastore/mysql.py | 2 +- sepicker/interface/can_bus.py | 6 +- tests/test_elster_frame.py | 11 +- 10 files changed, 285 insertions(+), 222 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eff7b5f..5e5c622 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,7 @@ version: 2 updates: -- package-ecosystem: pip - directory: "/" - schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 + - package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c09c8c..58b11c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,15 +10,16 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.11 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 + cache: "pipenv" + - name: Install pipenv + run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install pipenv pipenv install --dev - name: Lint run: pipenv run lint diff --git a/Pipfile b/Pipfile index 5538777..5bdc14d 100644 --- a/Pipfile +++ b/Pipfile @@ -4,21 +4,21 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -flake8 = "*" -flake8-bandit = "*" -flake8-bugbear = "*" -pep8-naming = "*" -pytest = "*" -pytest-cov = "*" +flake8 = "==7.1.1" +flake8-bandit = "==4.1.1" +flake8-bugbear = "==24.12.12" +pep8-naming = "==0.14.1" +pytest = "==8.3.4" +pytest-cov = "==6.0.0" [packages] -python-can = "*" -pyyaml = "*" -mysql-connector = "*" -python-dotenv = "*" +python-can = "==4.5.0" +pyyaml = "==6.0.2" +mysql-connector-python = "==9.2.0" +python-dotenv = "==1.0.1" [requires] -python_version = "3.11" +python_version = "3.13" [scripts] lint = "flake8" diff --git a/Pipfile.lock b/Pipfile.lock index 617b0ef..68fe759 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "ed53f2ad2024fa01048ab03fc2e3faa5c39e8434dd653ef629c85981832fcf07" + "sha256": "c8ebf026a145fa9311e1d9d36b1f6b34ba031c5c37e99738e984e0de9e3a1ba6" }, "pipfile-spec": 6, "requires": { - "python_version": "3.11" + "python_version": "3.13" }, "sources": [ { @@ -18,65 +18,73 @@ "default": { "msgpack": { "hashes": [ - "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", - "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d", - "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3", - "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", - "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0", - "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", - "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", - "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", - "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524", - "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819", - "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc", - "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc", - "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", - "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", - "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81", - "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", - "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", - "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2", - "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", - "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", - "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", - "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", - "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95", - "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f", - "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", - "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", - "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", - "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61", - "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", - "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", - "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d", - "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c", - "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", - "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", - "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", - "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", - "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", - "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", - "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", - "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f", - "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7", - "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", - "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", - "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", - "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf", - "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c", - "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", - "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", - "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", - "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", - "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", - "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad", - "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd", - "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7", - "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", - "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc" - ], - "markers": "platform_system != 'Windows'", - "version": "==1.0.7" + "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", + "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", + "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", + "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", + "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", + "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f", + "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", + "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", + "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", + "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", + "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", + "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", + "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", + "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", + "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", + "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", + "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", + "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7", + "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", + "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", + "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", + "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", + "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", + "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", + "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", + "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", + "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", + "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", + "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb", + "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", + "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", + "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", + "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", + "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", + "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", + "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", + "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", + "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", + "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", + "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", + "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb", + "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", + "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b", + "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", + "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", + "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", + "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", + "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", + "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", + "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", + "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", + "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", + "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec", + "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", + "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", + "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", + "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", + "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", + "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870", + "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", + "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96", + "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", + "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", + "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" }, "mysql-connector": { "hashes": [ @@ -85,22 +93,55 @@ "index": "pypi", "version": "==2.2.9" }, + "mysql-connector-python": { + "hashes": [ + "sha256:03fe54ca19c2dffa8f04ca0601a1217b126ff8306c76a9b2b4e555f8fbbd2178", + "sha256:0aee4e3a053672ded2134eda6fff3bc64796df88c9a9e5cab130b576830e14a8", + "sha256:2238ba121a30dd3a23d33ce3437642e38a1d4b86afaf7a487cdb0d7727db2010", + "sha256:292974916c6908e62bb0eb864c1c5679aa5c827209a200b5658aa4b4468c19a0", + "sha256:2a96bed643e901e91c05f421a37844939f5e2f65cc54a406ec5d95e704bda068", + "sha256:2c358b5732666043683445e88405d58232d5bb0977d24ec55e02a315b33fecbc", + "sha256:4142a0a4e997210558136ad2b623188c102996eda2d951a3933d6338b210f15e", + "sha256:417c538dfd64e6d415280067f4d7ce3f901ce85c548e609247a794d205769276", + "sha256:4d97485eb3f63ab26f6438570c4a392bb1f53c3ad9dca1bfd8b61b5ec1bba690", + "sha256:5c72ecad9007bc52d4f213e47d3c4cf17b3a3cabfeb2d36317fd4493adf5ed7d", + "sha256:6557942f6c6be3b41d2965456b53a244a7ce3e6fb81cb195c243549be72a6a24", + "sha256:65808b0a9998150416ee0b5781fdff456faea247e24d505f606aea2acaf21374", + "sha256:7c25dc2cb4fca82242d71a0bda2087bdfa3638e0c8175a747a6e765242763b4a", + "sha256:86c3217849857113912e8fc628e13182f00955ed1257cb7bfdc3d5ac3c5ff371", + "sha256:877552ff04800be4a53604bfee95b4078d10fcc072d54b9ea8dcd159c692742c", + "sha256:8a6cfb611bdfea41c67c5305e8fc0e30fdacd258c489dc619f566213cce8bba9", + "sha256:92ef9e5a37b25978a261b5a14fcfa0cf51cd96168dec4a52657bbfc1e1cb7d9a", + "sha256:b13b136c188363e18f9b3f6237a0e6eba8601df259ab0f8687cd689f433a391b", + "sha256:b7d149bcc455cf606a4aa604d45267fba16c8abf36056b804f8d16e8f5e753c1", + "sha256:bddecd023c0e92182eb3159abee7385a1d38b395a58ff4d94d3d6f1abeca6334", + "sha256:c2e973bcc6eab68e41e01438b706788e3f47b0b4d6a17fcb74583f7e0b990c5f", + "sha256:cc09ca64db620f7c898e1fb7c61a3d6f8f21694fcd276ab2635650ce216f8556", + "sha256:cf2684641084abc47be2dbba7bd42ce22c682bad4228a10bb12c343e1ecfc484", + "sha256:d007c05a48fc076e5ac7ad3d76e332fa5b0e885853c2da80071ade2d6e01aba5", + "sha256:e1afa12519671e024d80fbbf74606cc4fb96786b005d79ed959acf3584ba6af4", + "sha256:e8338a9babe6c67287080ec9f6a0fc245de24841bc532ef97dfd1f93caf4668b" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==9.2.0" + }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], - "markers": "python_version >= '3.7'", - "version": "==23.2" + "markers": "python_version >= '3.8'", + "version": "==24.2" }, "python-can": { "hashes": [ - "sha256:008bea1c12e31586e1aa76971126b831bf01056a900e91862a11360e011ab933", - "sha256:eebb68db388b8c997fc7c92dce311656946c968ff407706f5b5f9cc671879479" + "sha256:1eec66833c1ac76a7e3d636ee0f8b4ba2752e892bab1c56ce74308b2216b5445", + "sha256:d3684cebe5b028a148c1742b3a45cec4fcaf83a7f7c52d0680b2eaeaf52f8eb7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.3.1" + "version": "==4.5.0" }, "python-dotenv": { "hashes": [ @@ -113,49 +154,63 @@ }, "pyyaml": { "hashes": [ - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], "index": "pypi", - "version": "==6.0.1" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, "setuptools": { "hashes": [ @@ -168,87 +223,96 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.12.2" }, "wrapt": { "hashes": [ - "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", - "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", - "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", - "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e", - "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", - "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", - "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", - "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", - "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40", - "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", - "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", - "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", - "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41", - "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", - "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", - "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", - "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", - "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", - "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00", - "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", - "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", - "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", - "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", - "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966", - "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", - "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228", - "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", - "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", - "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292", - "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", - "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", - "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", - "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c", - "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5", - "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", - "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", - "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", - "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", - "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593", - "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39", - "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", - "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf", - "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", - "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", - "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c", - "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", - "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f", - "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", - "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465", - "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", - "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b", - "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", - "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", - "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8", - "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", - "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e", - "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", - "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c", - "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", - "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", - "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", - "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", - "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35", - "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", - "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3", - "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537", - "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", - "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", - "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a", - "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4" + "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f", + "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", + "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", + "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", + "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", + "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", + "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", + "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", + "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8", + "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", + "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061", + "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", + "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb", + "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", + "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", + "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", + "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", + "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", + "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7", + "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", + "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", + "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", + "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", + "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", + "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", + "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", + "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", + "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a", + "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", + "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", + "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9", + "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", + "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82", + "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9", + "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", + "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", + "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", + "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", + "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", + "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", + "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", + "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", + "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", + "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a", + "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3", + "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a", + "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", + "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", + "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", + "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", + "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", + "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", + "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", + "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", + "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", + "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", + "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2", + "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", + "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", + "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", + "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f", + "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9", + "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04", + "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", + "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9", + "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f", + "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", + "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", + "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", + "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", + "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", + "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", + "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", + "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6", + "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", + "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb", + "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119", + "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b", + "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58" ], - "markers": "python_version >= '3.6'", - "version": "==1.16.0" + "markers": "python_version >= '3.8'", + "version": "==1.17.2" } }, "develop": { diff --git a/README.md b/README.md index ce30172..3c56395 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ ## Requirements -- [Python 3.11](https://www.python.org) +- [Python 3.13](https://www.python.org) - [MySQL](https://www.mysql.com/) - [Grafana](https://grafana.com/) _(optional)_ - [Raspberry Pi](https://www.raspberrypi.org) + [CANable](https://canable.io) _(or similar devices)_ diff --git a/bin/sepicker b/bin/sepicker index 9adfb0b..d422423 100755 --- a/bin/sepicker +++ b/bin/sepicker @@ -11,6 +11,6 @@ PARENT_FOLDER = os.path.realpath( ) sys.path.insert(0, PARENT_FOLDER) -from sepicker.__main__ import main # noqa: E402 +from sepicker.__main__ import main main() diff --git a/sepicker/config.py b/sepicker/config.py index dfb24e3..fbeb952 100644 --- a/sepicker/config.py +++ b/sepicker/config.py @@ -2,9 +2,9 @@ import logging import sys import os -import dotenv +from dotenv import load_dotenv -dotenv.load_dotenv() +load_dotenv() LOGGER = logging.getLogger(__name__) CONFIG_FILE = 'config.yml' @@ -12,7 +12,7 @@ def _exists(key, value): if value is None: - LOGGER.error('Configuration value for "{key}" not found.') + LOGGER.error('Configuration value for "%s" not found.', key) sys.exit(1) return value diff --git a/sepicker/datastore/mysql.py b/sepicker/datastore/mysql.py index 897a39c..99c4c64 100644 --- a/sepicker/datastore/mysql.py +++ b/sepicker/datastore/mysql.py @@ -21,7 +21,7 @@ def __enter__(self): self.cursor = self.connection.cursor() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self): self.connection.commit() self.cursor.close() self.connection.close() diff --git a/sepicker/interface/can_bus.py b/sepicker/interface/can_bus.py index 4015b37..ca700b2 100644 --- a/sepicker/interface/can_bus.py +++ b/sepicker/interface/can_bus.py @@ -22,15 +22,15 @@ def __enter__(self): LOGGER.error('Can not connect to interface "%s".', self.interface) sys.exit(1) - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self): self.notifier.stop() self.bus.shutdown() def send(self, data): msg = can.Message( arbitration_id=self.sender, - data=data, - extended_id=False + is_extended_id=False, + data=data ) self.bus.send(msg) diff --git a/tests/test_elster_frame.py b/tests/test_elster_frame.py index ef778b5..ce9a128 100644 --- a/tests/test_elster_frame.py +++ b/tests/test_elster_frame.py @@ -3,12 +3,11 @@ class TestElsterFrame: - def setup(self): - self.elster_frame = ElsterFrame( - 'OUTSIDE_TEMPERATURE', - '180.000c', - 'dec_val' - ) + elster_frame = ElsterFrame( + 'OUTSIDE_TEMPERATURE', + '180.000c', + 'dec_val' + ) def test_message(self): assert self.elster_frame.message() == [49, 0, 250, 0, 12] From 9d2f178fda9f0456899cc9cf635a5f8662fe95ad Mon Sep 17 00:00:00 2001 From: Daniel Bayerlein Date: Fri, 7 Mar 2025 14:49:14 +0100 Subject: [PATCH 2/3] feat: add support for mqtt --- .env.example | 14 +++- Pipfile | 23 ++--- Pipfile.lock | 45 +++++++++- README.md | 61 +++++++++----- bin/sepicker | 2 +- sepicker/__main__.py | 29 ++++--- sepicker/config.py | 54 ++++++------ sepicker/{datastore => datasinks}/__init__.py | 0 sepicker/datasinks/mqtt.py | 75 +++++++++++++++++ sepicker/{datastore => datasinks}/mysql.py | 16 ++-- sepicker/elster/elster.py | 33 ++++---- sepicker/elster/elster_frame.py | 36 ++++---- sepicker/interface/can_bus.py | 37 +++++--- .../grafana.json => grafana/dashboard.json} | 0 .../resources/{datastore => mysql}/seed.sql | 0 tests/test_can_bus.py | 74 ++++++++++++++++ tests/test_config.py | 84 +++++++++++++++++++ tests/test_elster.py | 80 ++++++++++++++++++ tests/test_elster_frame.py | 11 +-- tests/test_mqtt.py | 83 ++++++++++++++++++ tests/test_mysql.py | 65 ++++++++++++++ tests/test_version.py | 7 ++ 22 files changed, 704 insertions(+), 125 deletions(-) rename sepicker/{datastore => datasinks}/__init__.py (100%) create mode 100644 sepicker/datasinks/mqtt.py rename sepicker/{datastore => datasinks}/mysql.py (64%) rename sepicker/resources/{dashboard/grafana.json => grafana/dashboard.json} (100%) rename sepicker/resources/{datastore => mysql}/seed.sql (100%) create mode 100644 tests/test_can_bus.py create mode 100644 tests/test_config.py create mode 100644 tests/test_elster.py create mode 100644 tests/test_mqtt.py create mode 100644 tests/test_mysql.py create mode 100644 tests/test_version.py diff --git a/.env.example b/.env.example index b4367c9..3573727 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,10 @@ -DB_DATABASE=heat-pump -DB_HOST=127.0.0.1 -DB_USER=root -DB_PASSWORD= +MYSQL_DATABASE=sepicker +MYSQL_HOST=127.0.0.1 +MYSQL_USER=root +MYSQL_PASSWORD= + +MQTT_HOST=127.0.0.1 +MQTT_PORT=1883 +MQTT_TOPIC=sepicker +MQTT_USER= +MQTT_PASSWORD= diff --git a/Pipfile b/Pipfile index 5bdc14d..4f60672 100644 --- a/Pipfile +++ b/Pipfile @@ -4,18 +4,21 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -flake8 = "==7.1.1" -flake8-bandit = "==4.1.1" -flake8-bugbear = "==24.12.12" -pep8-naming = "==0.14.1" -pytest = "==8.3.4" -pytest-cov = "==6.0.0" +flake8 = "*" +flake8-bandit = "*" +flake8-bugbear = "*" +pep8-naming = "*" +pytest = "*" +pytest-cov = "*" +pytest-mock = "*" +freezegun = "*" [packages] -python-can = "==4.5.0" -pyyaml = "==6.0.2" -mysql-connector-python = "==9.2.0" -python-dotenv = "==1.0.1" +python-can = "*" +pyyaml = "*" +mysql-connector-python = "*" +python-dotenv = "*" +paho-mqtt = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index 68fe759..67cf822 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c8ebf026a145fa9311e1d9d36b1f6b34ba031c5c37e99738e984e0de9e3a1ba6" + "sha256": "cbe76075928ecaea84d7250ca1cff8122b40b6dc8b48876145216e8164d0c1bb" }, "pipfile-spec": 6, "requires": { @@ -134,6 +134,15 @@ "markers": "python_version >= '3.8'", "version": "==24.2" }, + "paho-mqtt": { + "hashes": [ + "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", + "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.1.0" + }, "python-can": { "hashes": [ "sha256:1eec66833c1ac76a7e3d636ee0f8b4ba2752e892bab1c56ce74308b2216b5445", @@ -444,6 +453,15 @@ "markers": "python_version >= '3.7'", "version": "==3.1.41" }, + "freezegun": { + "hashes": [ + "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", + "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.5.1" + }, "iniconfig": { "hashes": [ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", @@ -549,6 +567,23 @@ "index": "pypi", "version": "==4.1.0" }, + "pytest-mock": { + "hashes": [ + "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", + "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.14.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.9.0.post0" + }, "pyyaml": { "hashes": [ "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", @@ -611,6 +646,14 @@ "markers": "python_version >= '3.7'", "version": "==5.0.1" }, + "six": { + "hashes": [ + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" + }, "stevedore": { "hashes": [ "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", diff --git a/README.md b/README.md index 3c56395..658dc68 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,9 @@ ## Table of Contents - [Requirements](#requirements) +- [Integrations](#integrations) - [Installation](#installation) - [Cron Job](#cron-job) - - [Database](#database) - - [Grafana (optional)](#grafana-optional) - [Config](#config) - [CAN bus](#can-bus) - [Example](#example) @@ -30,16 +29,29 @@ - [Data](#data) - [Example](#example-1) - [Description](#description-1) - - [Database](#database-1) + - [MySQL](#mysql) + - [Grafana](#grafana) + - [MQTT](#mqtt) - [Resources](#resources) - [License](#license) ## Requirements - [Python 3.13](https://www.python.org) -- [MySQL](https://www.mysql.com/) -- [Grafana](https://grafana.com/) _(optional)_ - [Raspberry Pi](https://www.raspberrypi.org) + [CANable](https://canable.io) _(or similar devices)_ +- Optional + - [MySQL](https://www.mysql.com) + - [MQTT](https://mqtt.org) + - [Grafana](https://grafana.com) + +## Integrations + +sepicker provides the following integrations: + +- MySQL +- MQTT + +Choose the right one for you. If you miss an integration, feel free to create an issue or PR. ## Installation @@ -64,14 +76,6 @@ To collect the data every _x_ minutes, it's necessary to create a cron job. This */2 * * * * /home/pi/sepicker/bin/sepicker ``` -### Database - -Execute the [seed file](./sepicker/resources/datastore/seed.sql) via MySQL command line or copy the query into your MySQL shell. - -### Grafana _(optional)_ - -With [Grafana](https://grafana.com/) you can create your own dashboard with widgets or use the existing [template](./sepicker/resources/dashboard/grafana.json). - ## Config The configuration for the CAN bus and for the data to be queried is located in [config.yml](./config.yml). The database configuration is located in [.env](./.env.example). @@ -126,17 +130,33 @@ data: - `mil_val` - `little_endian` -### Database +### MySQL + +Rename the [`.env.example`](./.env.example) file to `.env` or create it with the following content: + +``` +MYSQL_DATABASE=sepicker +MYSQL_HOST=127.0.0.1 +MYSQL_USER=root +MYSQL_PASSWORD= +``` + +Execute the [seed file](./sepicker/resources/mysql/seed.sql) via MySQL command line or copy the query into your MySQL shell. + +#### Grafana + +With [Grafana](https://grafana.com) you can create your own dashboard with widgets or use the existing [template](./sepicker/resources/grafana/dashboard.json). -File: [.env](./.env.example) +### MQTT -Rename the `.env.example` file to `.env` or create it with the following content: +Rename the [`.env.example`](./.env.example) file to `.env` or create it with the following content: ``` -DB_DATABASE=heat-pump -DB_HOST=127.0.0.1 -DB_USER=root -DB_PASSWORD= +MQTT_HOST=127.0.0.1 +MQTT_PORT=1883 +MQTT_TOPIC=sepicker +MQTT_USER= +MQTT_PASSWORD= ``` ## Resources @@ -144,6 +164,7 @@ DB_PASSWORD= - http://juerg5524.ch/list_data.php - https://github.com/andig/goelster - https://github.com/Andy2003/heat-pump-api +- https://elkement.art/2016/08/24/hacking-my-heat-pump-part-2-logging-energy-values/ ## License diff --git a/bin/sepicker b/bin/sepicker index d422423..9adfb0b 100755 --- a/bin/sepicker +++ b/bin/sepicker @@ -11,6 +11,6 @@ PARENT_FOLDER = os.path.realpath( ) sys.path.insert(0, PARENT_FOLDER) -from sepicker.__main__ import main +from sepicker.__main__ import main # noqa: E402 main() diff --git a/sepicker/__main__.py b/sepicker/__main__.py index 1bbecfc..b09e29d 100644 --- a/sepicker/__main__.py +++ b/sepicker/__main__.py @@ -3,22 +3,24 @@ from .elster.elster import Elster from .interface.can_bus import CanBus -from .datastore.mysql import Mysql as Datastore +from .datasinks.mysql import Mysql +from .datasinks.mqtt import Mqtt from .config import ( - CAN as CAN_CONFIG, - DATA, - DATABASE as DATABASE_CONFIG, - LOG_LEVEL + LOG_LEVEL, + read_config_file, + get_mqtt_config, + get_mysql_config ) logging.basicConfig(level=LOG_LEVEL) def main(): - elster = Elster(sender=CAN_CONFIG['sender'], items=DATA) + config = read_config_file() + elster = Elster(sender=config['can']['sender'], items=config['data']) - with CanBus(**CAN_CONFIG) as can_bus: - can_bus.notifier(elster.listener) + with CanBus(**config['can']) as can_bus: + can_bus.set_notifier(elster.listener) for frame in elster.frames: can_bus.send(frame.message()) @@ -35,8 +37,15 @@ def main(): break # Save result - with Datastore(**DATABASE_CONFIG) as db: - db.save(elster.values) + mysql_config = get_mysql_config() + if mysql_config['host']: + with Mysql(**mysql_config) as db: + db.save(elster.values) + + mqtt_config = get_mqtt_config() + if mqtt_config['host']: + with Mqtt(**mqtt_config) as mqtt: + mqtt.publish(elster.values) if __name__ == '__main__': diff --git a/sepicker/config.py b/sepicker/config.py index fbeb952..dd36845 100644 --- a/sepicker/config.py +++ b/sepicker/config.py @@ -6,20 +6,12 @@ load_dotenv() -LOGGER = logging.getLogger(__name__) -CONFIG_FILE = 'config.yml' +logger = logging.getLogger(__name__) +CONFIG_FILE: str = 'config.yml' -def _exists(key, value): - if value is None: - LOGGER.error('Configuration value for "%s" not found.', key) - sys.exit(1) - - return value - - -def _read_config_file(): - config = os.path.realpath( +def read_config_file() -> dict: + config: str = os.path.realpath( os.path.join( os.path.dirname(os.path.realpath(__file__)), '..', @@ -29,21 +21,31 @@ def _read_config_file(): try: with open(config, 'r') as file: return yaml.full_load(file) - except KeyError: - LOGGER.error('Configuration file "%s" not valid.', config) + except yaml.YAMLError as e: + logger.error(f'Configuration file "{config}" not valid: {e}') sys.exit(1) - except FileNotFoundError: - LOGGER.error('Configuration file "%s" not found.', config) + except FileNotFoundError as e: + logger.error(f'Configuration file "{config}" not found: {e}') sys.exit(1) -yaml_config = _read_config_file() -CAN = yaml_config['can'] -DATA = yaml_config['data'] -LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') -DATABASE = { - 'user': _exists('DB_USER', os.getenv('DB_USER')), - 'password': _exists('DB_PASSWORD', os.getenv('DB_PASSWORD')), - 'host': _exists('DB_HOST', os.getenv('DB_HOST')), - 'database': _exists('DB_DATABASE', os.getenv('DB_DATABASE')) -} +def get_mysql_config() -> dict: + return { + 'user': os.getenv('MYSQL_USER'), + 'password': os.getenv('MYSQL_PASSWORD'), + 'host': os.getenv('MYSQL_HOST'), + 'database': os.getenv('MYSQL_DATABASE') + } + + +def get_mqtt_config() -> dict: + return { + 'host': os.getenv('MQTT_HOST'), + 'port': int(os.getenv('MQTT_PORT', '1883')), + 'topic': os.getenv('MQTT_TOPIC'), + 'user': os.getenv('MQTT_USER'), + 'password': os.getenv('MQTT_PASSWORD'), + } + + +LOG_LEVEL: str = os.getenv('LOG_LEVEL', 'INFO') diff --git a/sepicker/datastore/__init__.py b/sepicker/datasinks/__init__.py similarity index 100% rename from sepicker/datastore/__init__.py rename to sepicker/datasinks/__init__.py diff --git a/sepicker/datasinks/mqtt.py b/sepicker/datasinks/mqtt.py new file mode 100644 index 0000000..07670f5 --- /dev/null +++ b/sepicker/datasinks/mqtt.py @@ -0,0 +1,75 @@ +from datetime import datetime +import logging +from typing import Optional +import paho.mqtt.client as paho +import json + +logger = logging.getLogger(__name__) + + +class Mqtt: + def __init__( + self, + host: str, + port: int, + topic: str, + user: Optional[str], + password: Optional[str] + ) -> None: + self.host: str = host + self.port: int = port + self.topic: str = topic + self.user: Optional[str] = user + self.password: Optional[str] = password + + def __enter__(self) -> 'Mqtt': + client = paho.Client() + client.on_connect = self.on_connect + client.on_publish = self.on_publish + + client.username_pw_set(self.user, self.password) + client.connect(self.host, self.port) + client.loop_start() + + self.client: paho.Client = client + + return self + + def __exit__( + self, + exc_type: type, + exc_val: Exception, + exc_tb: type + ) -> None: + self.client.loop_stop() + self.client.disconnect() + + def on_connect( + self, + client: paho.Client, + userdata: Optional[dict], + flags: int, + rc: int + ) -> None: + logger.debug('CONNACK received with code %d.' % (rc)) + + def on_publish( + self, + client: paho.Client, + userdata: Optional[dict], + mid: int + ): + logger.debug("mid: " + str(mid)) + + def publish(self, values: list[tuple[datetime, str, int | str]]) -> None: + payload = [ + { + "timestamp": timestamp.isoformat(), + "name": name, + "value": value + } + for inner_list in values + for timestamp, name, value in [inner_list] + ] + + self.client.publish(self.topic, json.dumps(payload), retain=True) diff --git a/sepicker/datastore/mysql.py b/sepicker/datasinks/mysql.py similarity index 64% rename from sepicker/datastore/mysql.py rename to sepicker/datasinks/mysql.py index 99c4c64..ac183a2 100644 --- a/sepicker/datastore/mysql.py +++ b/sepicker/datasinks/mysql.py @@ -1,17 +1,18 @@ +from datetime import datetime import logging import mysql.connector as mysql -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class Mysql: - def __init__(self, user, password, host, database): + def __init__(self, user: str, password: str, host: str, database: str): self.user = user self.password = password self.host = host self.database = database - def __enter__(self): + def __enter__(self) -> 'Mysql': self.connection = mysql.connect( user=self.user, password=self.password, @@ -21,12 +22,17 @@ def __enter__(self): self.cursor = self.connection.cursor() return self - def __exit__(self): + def __exit__( + self, + exc_type: type, + exc_val: Exception, + exc_tb: type + ): self.connection.commit() self.cursor.close() self.connection.close() - def save(self, values): + def save(self, values: list[tuple[datetime, str, int | str]]) -> None: self.cursor.executemany( 'INSERT INTO sepicker(timestamp, name, value) VALUES(%s, %s, %s)', values diff --git a/sepicker/elster/elster.py b/sepicker/elster/elster.py index 97d9587..ce1a847 100644 --- a/sepicker/elster/elster.py +++ b/sepicker/elster/elster.py @@ -1,35 +1,38 @@ import ctypes import logging from datetime import datetime +import can from .elster_frame import ElsterFrame -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class Elster: - RESPONSE = 0x2 + RESPONSE: int = 0x2 - def __init__(self, sender, items): - self.frames = [ElsterFrame(**item) for item in items] - self.values = [] + def __init__(self, sender: int, items: list) -> None: + self.frames: list[ElsterFrame] = [ + ElsterFrame(**item) for item in items + ] + self.values: list[tuple] = [] self.datetime = datetime.now() - self.sender = int(str(sender), 16) + self.sender: int = int(str(sender), 16) - def listener(self, msg): - data = msg.data + def listener(self, msg: 'can.Message') -> None: + data: bytearray = msg.data receiver = (data[0] & 0xf0) * 8 + (data[1] & 0x7f) - type = data[0] & 0x0f + msg_type = data[0] & 0x0f - if type != self.RESPONSE or \ + if msg_type != self.RESPONSE or \ receiver != self.sender or \ not self._exist_receiver(msg.arbitration_id): return if data[2] == 0xfa: - register = ((data[3] & 0xff) << 8) | (data[4] & 0xff) + register: int = ((data[3] & 0xff) << 8) | (data[4] & 0xff) if len(data) == 7: - value = ctypes.c_int16( + value: int = ctypes.c_int16( ((data[5] & 0xff) << 8) | (data[6] & 0xff) ).value else: @@ -45,14 +48,14 @@ def listener(self, msg): (self.datetime, entry.name, entry.formatter(value)) ) - def is_done(self): + def is_done(self) -> bool: return len(self.values) == len(self.frames) - def _exist_receiver(self, receiver): + def _exist_receiver(self, receiver: int) -> bool: frames = [frame for frame in self.frames if frame.receiver == receiver] return len(frames) > 0 - def _get_frame(self, receiver, register): + def _get_frame(self, receiver: int, register: int) -> 'ElsterFrame': for frame in self.frames: if frame.receiver == receiver and frame.register == register: return frame diff --git a/sepicker/elster/elster_frame.py b/sepicker/elster/elster_frame.py index fad3af9..fece338 100644 --- a/sepicker/elster/elster_frame.py +++ b/sepicker/elster/elster_frame.py @@ -1,20 +1,21 @@ import logging +from typing import Dict -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class ElsterFrame: - READ = 0x1 + READ: int = 0x1 - def __init__(self, name, index, format=None): - receiver, register = str(index).split('.') - self.name = name - self.receiver = int(receiver, 16) - self.register = int(register, 16) - self.format = format + def __init__(self, name: str, index: str, format: str | None = None): + receiver, register = index.split('.') + self.name: str = name + self.receiver: int = int(receiver, 16) + self.register: int = int(register, 16) + self.format: str | None = format - def message(self): - msg = [0] * 5 + def message(self) -> list[int]: + msg: list[int] = [0] * 5 msg[0] = (self.READ & 0xf) | ((self.receiver >> 3) & 0xf0) msg[1] = self.receiver & 0x7f msg[2] = 0xfa @@ -23,12 +24,15 @@ def message(self): return msg - def formatter(self, value): + def formatter(self, value: int) -> int | str: if not self.format: return value - return { - 'dec_val': '{0:.1f}'.format(value / 10.0), - 'mil_val': '{0:.3f}'.format(value / 1000.0), - 'little_endian': '{:d}'.format((value >> 8) + 256 * (value & 0xff)) - }[self.format] + formats: Dict[str, callable] = { + 'dec_val': lambda x: '{:.1f}'.format(x / 10.0), + 'mil_val': lambda x: '{:.3f}'.format(x / 1000.0), + 'little_endian': lambda x: '{:d}'.format( + (x >> 8) + 256 * (x & 0xff) + ) + } + return formats[self.format](value) diff --git a/sepicker/interface/can_bus.py b/sepicker/interface/can_bus.py index ca700b2..6ea6602 100644 --- a/sepicker/interface/can_bus.py +++ b/sepicker/interface/can_bus.py @@ -2,31 +2,44 @@ import sys import can -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class CanBus: - def __init__(self, interface, sender): + def __init__(self, interface: str, sender: int) -> None: self.interface = interface self.sender = int(str(sender), 16) + self.notifier = None + self.bus = None - def __enter__(self): + def __enter__(self) -> 'CanBus': try: self.bus = can.interface.Bus( channel=self.interface, - bustype='socketcan' + interface='socketcan' ) - return self - except OSError: - LOGGER.error('Can not connect to interface "%s".', self.interface) + except OSError as e: + logger.error( + 'Cannot connect to interface "%s": %s', + self.interface, + str(e) + ) sys.exit(1) - def __exit__(self): - self.notifier.stop() - self.bus.shutdown() + def __exit__( + self, + exc_type: type, + exc_val: Exception, + exc_tb: type + ) -> None: + if self.notifier: + self.notifier.stop() + + if self.bus: + self.bus.shutdown() - def send(self, data): + def send(self, data: bytes) -> None: msg = can.Message( arbitration_id=self.sender, is_extended_id=False, @@ -34,5 +47,5 @@ def send(self, data): ) self.bus.send(msg) - def notifier(self, listener): + def set_notifier(self, listener: 'can.Listener') -> None: self.notifier = can.Notifier(self.bus, [listener]) diff --git a/sepicker/resources/dashboard/grafana.json b/sepicker/resources/grafana/dashboard.json similarity index 100% rename from sepicker/resources/dashboard/grafana.json rename to sepicker/resources/grafana/dashboard.json diff --git a/sepicker/resources/datastore/seed.sql b/sepicker/resources/mysql/seed.sql similarity index 100% rename from sepicker/resources/datastore/seed.sql rename to sepicker/resources/mysql/seed.sql diff --git a/tests/test_can_bus.py b/tests/test_can_bus.py new file mode 100644 index 0000000..dbf52b4 --- /dev/null +++ b/tests/test_can_bus.py @@ -0,0 +1,74 @@ +import can +from freezegun import freeze_time +import pytest + +from sepicker.elster.elster import Elster +from sepicker.interface.can_bus import CanBus + + +class TestCanBus: + @pytest.fixture + def can_bus(self): + return CanBus(interface='can0', sender=680) + + @pytest.fixture + def mock_can_interface_bus(self, mocker): + return mocker.patch('can.interface.Bus') + + @pytest.fixture + @freeze_time("2024-11-10") + def elster(self): + return Elster( + sender=680, + items=[ + { + "name": 'OUTSIDE_TEMPERATURE', + "index": '180.000c', + "format": 'dec_val' + } + ] + ) + + def test_init(self, can_bus): + assert can_bus.interface == 'can0' + assert can_bus.sender == int('680', 16) + + def test_enter_success(self, mock_can_interface_bus, can_bus, elster): + with can_bus: + can_bus.set_notifier(elster.listener) + assert can_bus.bus is not None + mock_can_interface_bus.assert_called_once_with( + channel='can0', + interface='socketcan' + ) + + def test_enter_failure(self, mocker, can_bus): + mock_exit = mocker.patch('sys.exit', side_effect=SystemExit) + with pytest.raises(SystemExit): + with can_bus: + pass + mock_exit.assert_called_once_with(1) + + def test_exit(self, mock_can_interface_bus, mocker, can_bus): + can_bus.notifier = mocker.Mock() + with can_bus: + pass + can_bus.notifier.stop.assert_called_once() + mock_can_interface_bus.return_value.shutdown.assert_called_once() + + def test_send(self, mock_can_interface_bus, mocker, can_bus, elster): + with can_bus: + can_bus.set_notifier(elster.listener) + can_bus.send(bytearray([0xd2, 0x00, 0x0c, 0x01, 0x80])) + mock_can_interface_bus.return_value.send.assert_called_once() + sent_msg = mock_can_interface_bus.return_value.send.call_args[0][0] + assert sent_msg.arbitration_id == int('680', 16) + assert sent_msg.data == bytearray([0xd2, 0x00, 0x0c, 0x01, 0x80]) + assert sent_msg.is_extended_id is False + + def test_set_notifier(self, mock_can_interface_bus, mocker, can_bus): + listener = mocker.Mock() + with can_bus: + can_bus.set_notifier(listener) + assert isinstance(can_bus.notifier, can.Notifier) + assert listener in can_bus.notifier.listeners diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..86588b4 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,84 @@ +import logging +import pytest +import yaml +import os + +from sepicker.config import read_config_file, get_mqtt_config, get_mysql_config + + +def test_read_config_file_success(mocker): + mock_config_data = { + 'can': {'key1': 'value1'}, + 'data': {'key2': 'value2'} + } + mock_open = mocker.mock_open(read_data=yaml.dump(mock_config_data)) + mocker.patch('builtins.open', mock_open) + mocker.patch('os.path.realpath', return_value='/path/to/config.yml') + mocker.patch('os.path.join', return_value='/path/to/config.yml') + + config = read_config_file() + assert config == mock_config_data + + +def test_read_config_file_not_found(mocker, caplog): + mocker.patch('builtins.open', side_effect=FileNotFoundError) + mocker.patch('os.path.realpath', return_value='/path/to/config.yml') + mocker.patch('os.path.join', return_value='/path/to/config.yml') + + with pytest.raises(SystemExit): + read_config_file() + + with caplog.at_level(logging.INFO): + assert 'Configuration file "/path/to/config.yml" not found:' \ + in caplog.text + + +def test_read_config_file_yaml_error(mocker, caplog): + mock_open = mocker.mock_open() + mocker.patch('builtins.open', mock_open) + mocker.patch('os.path.realpath', return_value='/path/to/config.yml') + mocker.patch('os.path.join', return_value='/path/to/config.yml') + mocker.patch('yaml.full_load', side_effect=yaml.YAMLError('Invalid YAML')) + + with pytest.raises(SystemExit): + read_config_file() + + with caplog.at_level(logging.INFO): + assert 'Configuration file "/path/to/config.yml" not valid:' \ + in caplog.text + + +def test_mysql_config(mocker): + mocker.patch.dict(os.environ, { + 'MYSQL_USER': 'test_user', + 'MYSQL_PASSWORD': 'test_password', + 'MYSQL_HOST': 'test_host', + 'MYSQL_DATABASE': 'test_database' + }) + + mysql_config = get_mysql_config() + assert mysql_config == { + 'user': 'test_user', + 'password': 'test_password', + 'host': 'test_host', + 'database': 'test_database' + } + + +def test_mqtt_config(mocker): + mocker.patch.dict(os.environ, { + 'MQTT_HOST': 'test_mqtt_host', + 'MQTT_PORT': '1883', + 'MQTT_TOPIC': 'test_topic', + 'MQTT_USER': 'test_mqtt_user', + 'MQTT_PASSWORD': 'test_mqtt_password' + }) + + mqtt_config = get_mqtt_config() + assert mqtt_config == { + 'host': 'test_mqtt_host', + 'port': 1883, + 'topic': 'test_topic', + 'user': 'test_mqtt_user', + 'password': 'test_mqtt_password' + } diff --git a/tests/test_elster.py b/tests/test_elster.py new file mode 100644 index 0000000..ce21eb9 --- /dev/null +++ b/tests/test_elster.py @@ -0,0 +1,80 @@ +from datetime import datetime +from freezegun import freeze_time +import pytest + +from sepicker.elster.elster import Elster + + +class TestElster: + @pytest.fixture + @freeze_time("2024-11-10") + def elster(self): + return Elster( + sender=680, + items=[ + { + "name": 'OUTSIDE_TEMPERATURE', + "index": '180.000c', + "format": 'dec_val' + } + ] + ) + + def test_init(self, elster): + assert elster.frames[0].name == 'OUTSIDE_TEMPERATURE' + assert elster.frames[0].receiver == int('180', 16) + assert elster.frames[0].register == int('000c', 16) + assert elster.frames[0].format == 'dec_val' + assert elster.values == [] + assert elster.datetime == datetime(2024, 11, 10) + assert elster.sender == int(str(680), 16) + + @pytest.mark.parametrize("msg_data, arbitration_id", [ + ([0xd2, 0x00, 0x0c, 0x01, 0x80], int('180', 16)), + ([0xd2, 0x00, 0xfa, 0x00, 0x0c, 0x01, 0x80], int('180', 16)), + ]) + def test_listener(self, msg_data, arbitration_id, mocker, elster): + msg_mock = mocker.Mock( + spec="can.Message", + data=bytearray(msg_data), + arbitration_id=arbitration_id + ) + elster.listener(msg_mock) + + assert len(elster.values) == 1 + assert elster.values[0][0] == datetime(2024, 11, 10) + assert elster.values[0][1] == 'OUTSIDE_TEMPERATURE' + assert elster.values[0][2] == '38.4' + + def test_listener_invalid(self, mocker, elster): + msg_mock = mocker.Mock( + spec="can.Message", + data=bytearray([0xd2, 0x00, 0x0c, 0x01, 0x80]), + arbitration_id=int('601', 16) + ) + elster.listener(msg_mock) + + assert len(elster.values) == 0 + + def test_is_done(self, elster): + elster.values = [ + (datetime.now(), 'OUTSIDE_TEMPERATURE', int('180', 16)) + ] + assert elster.is_done() + + elster.values = [ + (datetime.now(), 'OUTSIDE_TEMPERATURE', int('180', 16)), + (datetime.now(), 'ISTTEMPERATUR_HK_2', int('601', 16)) + ] + assert not elster.is_done() + + def test__exist_receiver(self, elster): + assert elster._exist_receiver(int('180', 16)) + assert not elster._exist_receiver(int('601', 16)) + + def test__get_frame(self, elster): + frame = elster._get_frame(int('180', 16), 0x0c) + assert frame.name == 'OUTSIDE_TEMPERATURE' + + frame = elster._get_frame(int('601', 16), 0x0c) + assert frame is None diff --git a/tests/test_elster_frame.py b/tests/test_elster_frame.py index ce9a128..33de76e 100644 --- a/tests/test_elster_frame.py +++ b/tests/test_elster_frame.py @@ -3,11 +3,12 @@ class TestElsterFrame: - elster_frame = ElsterFrame( - 'OUTSIDE_TEMPERATURE', - '180.000c', - 'dec_val' - ) + def setup_method(self): + self.elster_frame = ElsterFrame( + 'OUTSIDE_TEMPERATURE', + '180.000c', + 'dec_val' + ) def test_message(self): assert self.elster_frame.message() == [49, 0, 250, 0, 12] diff --git a/tests/test_mqtt.py b/tests/test_mqtt.py new file mode 100644 index 0000000..deda972 --- /dev/null +++ b/tests/test_mqtt.py @@ -0,0 +1,83 @@ +import logging +from freezegun import freeze_time +import pytest +from datetime import datetime +import json + +from sepicker.datasinks.mqtt import Mqtt + + +class TestMqtt: + @pytest.fixture(autouse=True) + def mqtt_client_mock(self, mocker): + return mocker.patch('paho.mqtt.client.Client', autospec=True) + + @pytest.fixture + def mqtt_instance(self): + user = 'user' + password = 'password' # noqa: S105 + host = 'localhost' + port = 1883 + topic = 'test/topic' + + return Mqtt(host, port, topic, user, password) + + def test_mqtt_init(self, mqtt_instance): + assert mqtt_instance.host == 'localhost' + assert mqtt_instance.port == 1883 + assert mqtt_instance.topic == 'test/topic' + assert mqtt_instance.user == 'user' + assert mqtt_instance.password == 'password' # noqa: S105 + + def test_mqtt_enter(self, mqtt_instance, mqtt_client_mock): + client_instance = mqtt_client_mock.return_value + + with mqtt_instance as mqtt: + assert mqtt.client == client_instance + + mqtt_client_mock.assert_called_once() + client_instance.username_pw_set.assert_called_once_with( + 'user', + 'password' + ) + client_instance.connect.assert_called_once_with('localhost', 1883) + client_instance.loop_start.assert_called_once() + + def test_mqtt_exit(self, mqtt_instance, mqtt_client_mock): + with mqtt_instance: + pass + + client_instance = mqtt_client_mock.return_value + client_instance.loop_stop.assert_called_once() + client_instance.disconnect.assert_called_once() + + def test_on_connect(self, mqtt_instance, mocker, caplog): + client_mock = mocker.Mock() + with caplog.at_level(logging.DEBUG): + mqtt_instance.on_connect(client_mock, None, None, 0) + assert 'CONNACK received with code 0.' in caplog.text + + def test_on_publish(self, mqtt_instance, mocker, caplog): + client_mock = mocker.Mock() + with caplog.at_level(logging.DEBUG): + mqtt_instance.on_publish(client_mock, None, 1) + assert 'mid: 1' in caplog.text + + @freeze_time("2024-11-10") + def test_publish(self, mqtt_instance, mqtt_client_mock): + client_instance = mqtt_client_mock.return_value + values = [(datetime.now(), 'OUTSIDE_TEMPERATURE', '38.4')] + + with mqtt_instance as mqtt: + mqtt.publish(values) + client_instance.publish.assert_called_once_with( + 'test/topic', + json.dumps([ + { + "timestamp": "2024-11-10T00:00:00", + "name": 'OUTSIDE_TEMPERATURE', + "value": '38.4' + } + ]), + retain=True + ) diff --git a/tests/test_mysql.py b/tests/test_mysql.py new file mode 100644 index 0000000..77f2450 --- /dev/null +++ b/tests/test_mysql.py @@ -0,0 +1,65 @@ +from datetime import datetime +import pytest + +from sepicker.datasinks.mysql import Mysql + + +class TestMysql: + @pytest.fixture(autouse=True) + def mock_mysql_connect(self, mocker): + return mocker.patch('mysql.connector.connect', autospec=True) + + @pytest.fixture + def mysql_instance(self): + user = 'user' + password = 'password' # noqa: S105 + host = 'localhost' + database = 'database' + + return Mysql(user, password, host, database) + + def test_mysql_init(self, mysql_instance): + assert mysql_instance.user == 'user' + assert mysql_instance.password == 'password' # noqa: S105 + assert mysql_instance.host == 'localhost' + assert mysql_instance.database == 'database' + + def test_enter(self, mock_mysql_connect, mysql_instance): + client_instance = mock_mysql_connect.return_value + + with mysql_instance: + pass + + mock_mysql_connect.assert_called_once_with( # noqa: S106 + user='user', + password='password', + host='localhost', + database='database' + ) + assert client_instance.cursor.called + + def test_exit(self, mock_mysql_connect, mysql_instance): + client_instance = mock_mysql_connect.return_value + + with mysql_instance: + pass + + assert client_instance.commit.called + assert client_instance.cursor().close.called + assert client_instance.close.called + + def test_save(self, mock_mysql_connect, mysql_instance): + client_instance = mock_mysql_connect.return_value + values = [(datetime.now(), 'OUTSIDE_TEMPERATURE', '24.5')] + + with mysql_instance as mysql: + mysql.save(values) + + client_instance. \ + cursor() \ + .executemany \ + .assert_called_once_with( + 'INSERT INTO sepicker(timestamp, name, value) ' + 'VALUES(%s, %s, %s)', + values + ) diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..9151bdd --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,7 @@ +import re +from sepicker._version import __version__ + + +def test_version(): + pattern = r"^(\d+)\.(\d+)\.(\d+)$" + assert re.match(pattern, __version__) is not None From 03ea7a1878fef254c7a04639b83d1880e9a6ecb2 Mon Sep 17 00:00:00 2001 From: Daniel Bayerlein Date: Fri, 7 Mar 2025 15:18:05 +0100 Subject: [PATCH 3/3] ci: add publish workflow --- .dockerignore | 16 ++++++++++ .github/workflows/ci.yml | 6 ++-- .github/workflows/publish.yml | 58 +++++++++++++++++++++++++++++++++++ Dockerfile | 25 +++++++++++++++ crontab | 1 + 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/publish.yml create mode 100644 Dockerfile create mode 100644 crontab diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..61c3491 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.coverage +.dockerignore +.env +.env.example +.git +.github +.gitignore +.pytest_cache +.vscode +compose.yml +Dockerfile +htmlcov +LICENSE +README.md +setup.cfg +tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58b11c1..fd8dc99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,11 @@ name: CI on: push: - branches: [main] + branches: + - main pull_request: - branches: [main] + branches: + - main jobs: build: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..3ad950b --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,58 @@ +name: Publish + +on: + push: + branches: + - main + tags: + - v* + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: ${{ github.event_name != 'pull_request' }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e591269 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 + +ARG PYTHON_VERSION=3.13 +FROM python:${PYTHON_VERSION}-alpine + +# Prevents Python from writing pyc files. +ENV PYTHONDONTWRITEBYTECODE=1 + +# Keeps Python from buffering stdout and stderr to avoid situations where +# the application crashes without emitting any logs due to buffering. +ENV PYTHONUNBUFFERED=1 + +WORKDIR /usr/src/app + +RUN pip install --no-cache-dir pipenv + +COPY Pipfile Pipfile.lock . + +RUN pipenv install --system --deploy + +COPY . . + +RUN crontab crontab + +CMD ["crond", "-f", "-l", "2"] diff --git a/crontab b/crontab new file mode 100644 index 0000000..c3d76f5 --- /dev/null +++ b/crontab @@ -0,0 +1 @@ +*/2 * * * * /usr/src/app/bin/sepicker >> /var/log/cron.log 2>&1