diff --git a/Changelog b/Changelog index ef9c9ac..4f29621 100644 --- a/Changelog +++ b/Changelog @@ -1,8 +1,9 @@ -Version 0.6.10 unreleased +Version 0.7.0 unreleased * Migrate to Poetry v2 and project-managed Poetry plugins. * Move configuration into pyproject.toml for pytest, mypy & coverage. * Upgrade to gha-shared-workflows@v8 for Poetry v2 support. + * Migrate from pendulum to arrow for dates (interface change). * Update all dependencies and outdated constraints. Version 0.6.9 02 Jan 2025 diff --git a/poetry.lock b/poetry.lock index 2b90a99..a734b02 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,6 +13,26 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + [[package]] name = "astroid" version = "3.3.8" @@ -748,106 +768,6 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] -[[package]] -name = "pendulum" -version = "3.0.0" -description = "Python datetimes made easy" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, - {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, - {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, - {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, - {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, - {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, - {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, - {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, - {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, - {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, - {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, - {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, - {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, - {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, - {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, - {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, - {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, - {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, - {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, - {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, - {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, - {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, - {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, - {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, - {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, - {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, - {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, - {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, - {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, - {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, - {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, - {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, -] - -[package.dependencies] -python-dateutil = ">=2.6" -tzdata = ">=2020.1" - -[package.extras] -test = ["time-machine (>=2.6.0)"] - [[package]] name = "platformdirs" version = "4.3.6" @@ -1407,6 +1327,18 @@ files = [ {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20241230" @@ -1447,18 +1379,6 @@ files = [ ] markers = {main = "python_version < \"3.11\""} -[[package]] -name = "tzdata" -version = "2024.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, -] - [[package]] name = "urllib3" version = "2.3.0" @@ -1525,4 +1445,4 @@ docs = ["importlib-metadata", "sphinx", "sphinx-autoapi"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "5b1f3013b10f9a01cd8b80bdf101f3e2a1a7a0e856e6e8ddd17c96948e5aec46" +content-hash = "caa90bfe7c4a35224f3cf7644320c169a591694af96b1c3c855cf2788eac732a" diff --git a/pyproject.toml b/pyproject.toml index 5463d2e..c192b35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,13 +43,13 @@ license = "Apache-2.0" readme = "PyPI.md" dynamic = [ "classifiers", "version" ] dependencies = [ - "pendulum (>=3.0.0,<4.0.0)", "attrs (>=24.2.0,<25.0.0)", "cattrs (>=24.1.2,<25.0.0)", "PyYAML (>=6.0.1,<7.0.0)", "pycryptodomex (>=3.19.0,<4.0.0)", "requests (>=2.31.0,<3.0.0)", "tenacity (>=9.0.0,<10.0.0)", + "arrow (>=1.3.0,<2.0.0)", ] [project.urls] diff --git a/src/smartapp/converter.py b/src/smartapp/converter.py index 4f944a2..5f03e95 100644 --- a/src/smartapp/converter.py +++ b/src/smartapp/converter.py @@ -9,11 +9,12 @@ from typing import Any, Dict, Type, TypeVar import yaml +from arrow import Arrow +from arrow import get as arrow_get +from arrow import now as arrow_now from attrs import fields, has from cattrs import GenConverter from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override -from pendulum import from_format, now -from pendulum.datetime import DateTime from .interface import ( CONFIG_SETTING_BY_TYPE, @@ -29,36 +30,36 @@ DATETIME_ZONE = "UTC" -DATETIME_SEC_EPOCH = "1970-01-01T00:00:00Z" # date of the UNIX epoch, which sometimes seem to mean "no date" +DATETIME_SEC_EPOCH = "1970-01-01T00:00:00Z" # date of the UNIX epoch, which sometimes seems to mean "no date" DATETIME_SEC_LEN = len("YYYY-MM-DDTHH:MM:SSZ") # like "2017-09-13T04:18:12Z" DATETIME_SEC_FORMAT = "YYYY-MM-DD[T]HH:mm:ss[Z]" -DATETIME_MS_EPOCH = "1970-01-01T00:00:00.000Z" # date of the UNIX epoch, which sometimes seem to mean "no date" +DATETIME_MS_EPOCH = "1970-01-01T00:00:00.000Z" # date of the UNIX epoch, which sometimes seems to mean "no date" DATETIME_MS_LEN = len("YYYY-MM-DDTHH:MM:SS.SSSZ") # like "2017-09-13T04:18:12.992Z" DATETIME_MS_FORMAT = "YYYY-MM-DD[T]HH:mm:ss.SSS[Z]" T = TypeVar("T") # pylint: disable=invalid-name: -def serialize_datetime(datetime: DateTime) -> str: - """Serialize a DateTime to a string.""" +def serialize_datetime(datetime: Arrow) -> str: + """Serialize an Arrow datetime to a string.""" # Note that we always use the full millisecond timestamp here and always convert to UTC - return datetime.in_timezone(DATETIME_ZONE).format(DATETIME_MS_FORMAT) # type: ignore[no-untyped-call,no-any-return,unused-ignore] + return datetime.to(DATETIME_ZONE).format(DATETIME_MS_FORMAT) -def deserialize_datetime(datetime: str) -> DateTime: - """Deserialize a string into a DateTime.""" +def deserialize_datetime(datetime: str) -> Arrow: + """Deserialize a string into an Arrow datetime.""" # Dates from SmartThings are not as reliable as I had hoped. The samples show a # format including milliseconds. Actual data (at least sometimes) comes without # milliseconds. Further, some requests come with a UNIX epoch date (1970-01-01) which # I guess is probably what happens when no date was set by the device. I'm choosing # to interpret that as "now". if datetime in (DATETIME_MS_EPOCH, DATETIME_SEC_EPOCH): - return now() + return arrow_now() elif len(datetime) == DATETIME_MS_LEN: - return from_format(datetime, DATETIME_MS_FORMAT, tz=DATETIME_ZONE) + return arrow_get(datetime, DATETIME_MS_FORMAT, tzinfo=DATETIME_ZONE) elif len(datetime) == DATETIME_SEC_LEN: - return from_format(datetime, DATETIME_SEC_FORMAT, tz=DATETIME_ZONE) + return arrow_get(datetime, DATETIME_SEC_FORMAT, tzinfo=DATETIME_ZONE) else: raise ValueError("Unknown datetime format: %s" % datetime) @@ -118,18 +119,18 @@ class SmartAppConverter(StandardConverter): def __init__(self) -> None: super().__init__() - self.register_unstructure_hook(DateTime, self._unstructure_datetime) - self.register_structure_hook(DateTime, self._structure_datetime) + self.register_unstructure_hook(Arrow, self._unstructure_datetime) + self.register_structure_hook(Arrow, self._structure_datetime) self.register_structure_hook(ConfigValue, self._structure_config_value) self.register_structure_hook(ConfigSetting, self._structure_config_setting) self.register_structure_hook(LifecycleRequest, self._structure_request) - def _unstructure_datetime(self, datetime: DateTime) -> str: - """Serialize a DateTime to a string.""" + def _unstructure_datetime(self, datetime: Arrow) -> str: + """Serialize an Arrow datetime to a string.""" return serialize_datetime(datetime) - def _structure_datetime(self, datetime: str, _: Type[DateTime]) -> DateTime: - """Deserialize a string into a DateTime.""" + def _structure_datetime(self, datetime: str, _: Type[Arrow]) -> Arrow: + """Deserialize a string into an Arrow datetime.""" return deserialize_datetime(datetime) def _structure_config_value(self, data: Dict[str, Any], _: Type[ConfigValue]) -> ConfigValue: diff --git a/src/smartapp/interface.py b/src/smartapp/interface.py index 0d739d8..6745cac 100644 --- a/src/smartapp/interface.py +++ b/src/smartapp/interface.py @@ -28,8 +28,8 @@ from enum import Enum from typing import Any, Callable, Dict, List, Mapping, Optional, Union +from arrow import Arrow from attrs import field, frozen -from pendulum.datetime import DateTime AUTHORIZATION_HEADER = "authorization" CORRELATION_ID_HEADER = "x-st-correlation" @@ -362,7 +362,7 @@ def as_float(self, key: str) -> float: class Event: """Holds the triggered event, one of several different attributes depending on event type.""" - event_time: Optional[DateTime] = None + event_time: Optional[Arrow] = None event_type: EventType device_event: Optional[Dict[str, Any]] = None device_lifecycle_event: Optional[Dict[str, Any]] = None diff --git a/src/smartapp/signature.py b/src/smartapp/signature.py index c83397a..9b48787 100644 --- a/src/smartapp/signature.py +++ b/src/smartapp/signature.py @@ -41,12 +41,13 @@ from typing import List, Mapping, Optional import requests +from arrow import Arrow +from arrow import get as arrow_get +from arrow import now as arrow_now from attrs import field, frozen from Cryptodome.Hash import SHA256 from Cryptodome.PublicKey import RSA from Cryptodome.Signature import pkcs1_15 -from pendulum import from_format, now -from pendulum.datetime import DateTime from requests import ConnectionError as RequestsConnectionError from requests import HTTPError, RequestException from tenacity import retry @@ -72,7 +73,7 @@ def retrieve_public_key(key_server_url: str, key_id: str) -> str: return response.text -DATE_FORMAT = "DD MMM YYYY HH:mm:ss z" # like "05 Jan 2014 21:31:40 GMT"; we strip off the leading day of week +DATE_FORMAT = "DD MMM YYYY HH:mm:ss ZZZ" # like "05 Jan 2014 21:31:40 GMT"; we strip off the leading day of week # noinspection PyUnresolvedReferences @@ -89,7 +90,7 @@ class SignatureVerifier: method: str = field(init=False) path: str = field(init=False) request_target: str = field(init=False) - date: DateTime = field(init=False) + date: Arrow = field(init=False) authorization: str = field(init=False) signing_attributes: Mapping[str, str] = field(init=False) signing_headers: str = field(init=False) @@ -129,8 +130,8 @@ def _default_authorization(self) -> str: return self.header("authorization") @date.default - def _default_date(self) -> DateTime: - return from_format(self.header("date")[5:], DATE_FORMAT) # remove the day ("Thu, ") from front + def _default_date(self) -> Arrow: + return arrow_get(self.header("date")[5:], DATE_FORMAT) # remove the day ("Thu, ") from front @signing_attributes.default def _default_signing_attributes(self) -> Mapping[str, str]: @@ -206,7 +207,7 @@ def retrieve_public_key(self) -> str: def verify_date(self) -> None: """Verify the date, ensuring that it is current per skew configuration.""" if self.config.clock_skew_sec is not None: - skew = abs(now() - self.date) + skew = abs(arrow_now() - self.date) if skew.seconds > self.config.clock_skew_sec: raise SignatureError("Request date is not current, skew of %d seconds" % skew.seconds, self.correlation_id) diff --git a/tests/test_converter.py b/tests/test_converter.py index bf34867..bd95839 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -6,8 +6,8 @@ from json import JSONDecodeError from unittest.mock import patch -import pendulum import pytest +from arrow import arrow from smartapp.converter import CONVERTER, deserialize_datetime, serialize_datetime from smartapp.interface import * @@ -18,7 +18,7 @@ RESPONSE_DIR = os.path.join(FIXTURE_DIR, "response") SETTINGS_DIR = os.path.join(FIXTURE_DIR, "settings") -NOW = pendulum.datetime(2022, 6, 1, 2, 3, 4, microsecond=5, tz="UTC") +NOW = arrow.Arrow(2022, 6, 1, 2, 3, 4, microsecond=5, tzinfo="UTC") @pytest.fixture @@ -58,24 +58,25 @@ class TestDatetime: @pytest.mark.parametrize( "datetime,expected", [ - (pendulum.datetime(2017, 9, 13, 4, 18, 12, microsecond=469000, tz="UTC"), "2017-09-13T04:18:12.469Z"), - (pendulum.datetime(2017, 9, 13, 4, 18, 12, microsecond=0, tz="UTC"), "2017-09-13T04:18:12.000Z"), - (pendulum.datetime(1970, 1, 1, 0, 0, 0, microsecond=0, tz="UTC"), "1970-01-01T00:00:00.000Z"), + (arrow.Arrow(2017, 9, 13, 4, 18, 12, microsecond=469000, tzinfo="UTC"), "2017-09-13T04:18:12.469Z"), + (arrow.Arrow(2017, 9, 13, 4, 18, 12, microsecond=469123, tzinfo="UTC"), "2017-09-13T04:18:12.469Z"), + (arrow.Arrow(2017, 9, 13, 4, 18, 12, microsecond=0, tzinfo="UTC"), "2017-09-13T04:18:12.000Z"), + (arrow.Arrow(1970, 1, 1, 0, 0, 0, microsecond=0, tzinfo="UTC"), "1970-01-01T00:00:00.000Z"), ], ) def test_serialize_datetime(self, datetime, expected): assert serialize_datetime(datetime) == expected - @patch("smartapp.converter.now") + @patch("smartapp.converter.arrow_now") @pytest.mark.parametrize( "datetime,expected", [ - ("2017-09-13T04:18:12.469Z", pendulum.datetime(2017, 9, 13, 4, 18, 12, microsecond=469000, tz="UTC")), - ("2017-09-13T04:18:12.000Z", pendulum.datetime(2017, 9, 13, 4, 18, 12, microsecond=0, tz="UTC")), - ("2017-09-13T04:18:12Z", pendulum.datetime(2017, 9, 13, 4, 18, 12, microsecond=0, tz="UTC")), - ("2022-06-16T15:17:24.883Z", pendulum.datetime(2022, 6, 16, 10, 17, 24, microsecond=883000, tz="America/Chicago")), - ("2022-06-16T15:17:24.000Z", pendulum.datetime(2022, 6, 16, 10, 17, 24, microsecond=0, tz="America/Chicago")), - ("2022-06-16T15:16:24Z", pendulum.datetime(2022, 6, 16, 10, 16, 24, microsecond=0, tz="America/Chicago")), + ("2017-09-13T04:18:12.469Z", arrow.Arrow(2017, 9, 13, 4, 18, 12, microsecond=469000, tzinfo="UTC")), + ("2017-09-13T04:18:12.000Z", arrow.Arrow(2017, 9, 13, 4, 18, 12, microsecond=0, tzinfo="UTC")), + ("2017-09-13T04:18:12Z", arrow.Arrow(2017, 9, 13, 4, 18, 12, microsecond=0, tzinfo="UTC")), + ("2022-06-16T15:17:24.883Z", arrow.Arrow(2022, 6, 16, 10, 17, 24, microsecond=883000, tzinfo="America/Chicago")), + ("2022-06-16T15:17:24.000Z", arrow.Arrow(2022, 6, 16, 10, 17, 24, microsecond=0, tzinfo="America/Chicago")), + ("2022-06-16T15:16:24Z", arrow.Arrow(2022, 6, 16, 10, 16, 24, microsecond=0, tzinfo="America/Chicago")), ("1970-01-01T00:00:00.000Z", NOW), ("1970-01-01T00:00:00Z", NOW), ], diff --git a/tests/test_signature.py b/tests/test_signature.py index 930ac25..f8c4283 100644 --- a/tests/test_signature.py +++ b/tests/test_signature.py @@ -4,9 +4,9 @@ from typing import Dict, Optional from unittest.mock import patch -import pendulum import pytest import responses +from arrow import Arrow from requests import HTTPError from responses import matchers from responses.registries import OrderedRegistry @@ -76,7 +76,7 @@ PATH = "/foo?param=value&pet=dog" REQUEST_TARGET = "post /foo?param=value&pet=dog" DATE_STR = "Thu, 05 Jan 2014 21:31:40 GMT" -DATE_OBJ = pendulum.datetime(2014, 1, 5, 21, 31, 40, tz="GMT") +DATE_OBJ = Arrow(2014, 1, 5, 21, 31, 40, tzinfo="GMT") BODY = '{"hello": "world"}' CONTENT_TYPE = "application/json" CONTENT_LENGTH = str(len(BODY)) # should be 18 @@ -268,17 +268,17 @@ def test_header(self): with pytest.raises(SignatureError, match="Header not found: %s" % header): verifier.header(header) - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") @pytest.mark.parametrize( "date", [ - DATE_OBJ.subtract(seconds=CLOCK_SKEW), - DATE_OBJ.subtract(seconds=CLOCK_SKEW).add(seconds=1), - DATE_OBJ.subtract(seconds=CLOCK_SKEW).add(seconds=2), + DATE_OBJ.shift(seconds=-CLOCK_SKEW), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=1), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=2), DATE_OBJ, - DATE_OBJ.add(seconds=CLOCK_SKEW).subtract(seconds=2), - DATE_OBJ.add(seconds=CLOCK_SKEW).subtract(seconds=1), - DATE_OBJ.add(seconds=CLOCK_SKEW), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=-2), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=-1), + DATE_OBJ.shift(seconds=CLOCK_SKEW), ], ) def test_verify_date_valid(self, now, date): @@ -290,14 +290,14 @@ def test_verify_date_valid(self, now, date): verifier = SignatureVerifier(context=context, config=config, definition=definition) verifier.verify_date() - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") @pytest.mark.parametrize( "date", [ - DATE_OBJ.subtract(seconds=CLOCK_SKEW).subtract(seconds=2), - DATE_OBJ.subtract(seconds=CLOCK_SKEW).subtract(seconds=1), - DATE_OBJ.add(seconds=CLOCK_SKEW).add(seconds=1), - DATE_OBJ.add(seconds=CLOCK_SKEW).add(seconds=2), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=-2), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=-1), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=1), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=2), ], ) def test_verify_date_invalid(self, now, date): @@ -311,21 +311,21 @@ def test_verify_date_invalid(self, now, date): verifier.verify_date() assert e.value.correlation_id == context.correlation_id - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") @pytest.mark.parametrize( "date", [ - DATE_OBJ.subtract(seconds=CLOCK_SKEW).subtract(seconds=2), - DATE_OBJ.subtract(seconds=CLOCK_SKEW).subtract(seconds=1), - DATE_OBJ.subtract(seconds=CLOCK_SKEW), - DATE_OBJ.subtract(seconds=CLOCK_SKEW).add(seconds=1), - DATE_OBJ.subtract(seconds=CLOCK_SKEW).add(seconds=2), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=-2), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=-1), + DATE_OBJ.shift(seconds=-CLOCK_SKEW), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=1), + DATE_OBJ.shift(seconds=-CLOCK_SKEW).shift(seconds=2), DATE_OBJ, - DATE_OBJ.add(seconds=CLOCK_SKEW).subtract(seconds=2), - DATE_OBJ.add(seconds=CLOCK_SKEW).subtract(seconds=1), - DATE_OBJ.add(seconds=CLOCK_SKEW), - DATE_OBJ.add(seconds=CLOCK_SKEW).add(seconds=1), - DATE_OBJ.add(seconds=CLOCK_SKEW).add(seconds=2), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=-2), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=-1), + DATE_OBJ.shift(seconds=CLOCK_SKEW), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=1), + DATE_OBJ.shift(seconds=CLOCK_SKEW).shift(seconds=2), ], ) def test_verify_date_no_skew(self, now, date): @@ -338,7 +338,7 @@ def test_verify_date_no_skew(self, now, date): verifier.verify_date() @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_default_verify_valid(self, now, retrieve): # with a valid key from the Joyent sample data, everything should line up and we should have a valid signature context = build_context(headers=DEFAULT_ORIGINAL_HEADERS) @@ -351,7 +351,7 @@ def test_default_verify_valid(self, now, retrieve): retrieve.assert_called_once_with(KEYSERVER_URL, KEY_ID) @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_all_headers_verify_valid(self, now, retrieve): # with a valid key from the Joyent sample data, everything should line up and we should have a valid signature context = build_context(headers=DEFAULT_ORIGINAL_HEADERS) @@ -364,7 +364,7 @@ def test_all_headers_verify_valid(self, now, retrieve): retrieve.assert_called_once_with(KEYSERVER_URL, KEY_ID) @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_default_verify_key_download_failure(self, now, retrieve): # if we fail to download the key, the signature can never be valid context = build_context(headers=DEFAULT_ORIGINAL_HEADERS) @@ -378,7 +378,7 @@ def test_default_verify_key_download_failure(self, now, retrieve): assert e.value.correlation_id == context.correlation_id @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_all_headers_verify_download_failure(self, now, retrieve): # if we fail to download the key, the signature can never be valid context = build_context(headers=ALL_HEADERS_ORIGINAL_HEADERS) @@ -392,7 +392,7 @@ def test_all_headers_verify_download_failure(self, now, retrieve): assert e.value.correlation_id == context.correlation_id @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_default_verify_invalid_key(self, now, retrieve): # with an invalid key, the signature can never be valid context = build_context(headers=DEFAULT_ORIGINAL_HEADERS) @@ -406,7 +406,7 @@ def test_default_verify_invalid_key(self, now, retrieve): assert e.value.correlation_id == context.correlation_id @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_all_headers_verify_invalid_key(self, now, retrieve): # with an invalid key, the signature can never be valid context = build_context(headers=ALL_HEADERS_ORIGINAL_HEADERS) @@ -420,7 +420,7 @@ def test_all_headers_verify_invalid_key(self, now, retrieve): assert e.value.correlation_id == context.correlation_id @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_default_verify_wrong_key(self, now, retrieve): # with the wrong key, the signature can never be valid context = build_context(headers=DEFAULT_ORIGINAL_HEADERS) @@ -434,7 +434,7 @@ def test_default_verify_wrong_key(self, now, retrieve): assert e.value.correlation_id == context.correlation_id @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_all_headers_verify_wrong_key(self, now, retrieve): # with the wrong key, the signature can never be valid context = build_context(headers=ALL_HEADERS_ORIGINAL_HEADERS) @@ -448,7 +448,7 @@ def test_all_headers_verify_wrong_key(self, now, retrieve): assert e.value.correlation_id == context.correlation_id @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_default_verify_mismatch(self, now, retrieve): # this has a Date header that does not match the Joyent sample, causing an invalid signature headers = DEFAULT_ORIGINAL_HEADERS.copy() @@ -465,7 +465,7 @@ def test_default_verify_mismatch(self, now, retrieve): assert e.value.correlation_id == context.correlation_id @patch("smartapp.signature.retrieve_public_key") - @patch("smartapp.signature.now") + @patch("smartapp.signature.arrow_now") def test_all_headers_verify_mismatch(self, now, retrieve): # the bad_definition contains a URL that doesn't match the Joyent sample # we will generate a bad signing string and hence the signature is invalid