From e5eca85c4e2b3fb00277511b920747ae7969f33a Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 20 Jan 2025 09:31:56 +0100 Subject: [PATCH 1/9] chore(deps): install pandas --- poetry.lock | 308 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 305 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index b9320b8..c94e4c2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,8 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -17,6 +19,8 @@ version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, @@ -37,6 +41,8 @@ version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, @@ -48,6 +54,8 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -62,6 +70,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev", "examples"] +markers = "(platform_system == \"Windows\" or sys_platform == \"win32\") and (python_version == \"3.11\" or python_version >= \"3.12\")" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -73,6 +83,8 @@ version = "7.6.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, @@ -137,6 +149,8 @@ version = "0.20.0" description = "A command line utility to check for unused, missing and transitive dependencies in a Python project." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "deptry-0.20.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:41434d95124851b83cb05524d1a09ad6fea62006beafed2ef90a6b501c1b237f"}, {file = "deptry-0.20.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:b3b4b22d1406147de5d606a24042126cd74d52fdfdb0232b9c5fd0270d601610"}, @@ -162,6 +176,8 @@ version = "2.6.1" description = "DNS toolkit" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, @@ -182,6 +198,8 @@ version = "2.2.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, @@ -197,6 +215,8 @@ version = "0.115.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fastapi-0.115.2-py3-none-any.whl", hash = "sha256:61704c71286579cc5a598763905928f24ee98bfcc07aabe84cfefb98812bbc86"}, {file = "fastapi-0.115.2.tar.gz", hash = "sha256:3995739e0b09fa12f984bce8fa9ae197b35d433750d3d312422d846e283697ee"}, @@ -223,6 +243,8 @@ version = "0.0.5" description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46"}, {file = "fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f"}, @@ -241,6 +263,8 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -252,6 +276,8 @@ version = "1.0.5" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, @@ -273,6 +299,8 @@ version = "0.6.1" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, @@ -321,6 +349,8 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -345,6 +375,8 @@ version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, @@ -356,6 +388,8 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -367,6 +401,8 @@ version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, @@ -384,6 +420,8 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -408,6 +446,8 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -477,28 +517,187 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "numpy" +version = "2.2.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"}, + {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"}, + {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"}, + {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"}, + {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"}, + {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"}, + {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"}, + {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"}, + {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"}, + {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"}, + {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"}, + {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"}, +] + [[package]] name = "packaging" version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pandas" +version = "2.2.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -514,6 +713,8 @@ version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, @@ -523,8 +724,8 @@ files = [ annotated-types = ">=0.6.0" pydantic-core = "2.23.4" typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, ] [package.extras] @@ -537,6 +738,8 @@ version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, @@ -638,6 +841,8 @@ version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, @@ -652,6 +857,8 @@ version = "3.1.2" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, @@ -666,6 +873,8 @@ version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, @@ -686,6 +895,8 @@ version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -704,6 +915,8 @@ version = "3.15.0" description = "Pytest plugin to randomly order tests and control random.seed." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, @@ -712,12 +925,30 @@ files = [ [package.dependencies] pytest = "*" +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -732,6 +963,8 @@ version = "0.0.9" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, @@ -740,12 +973,27 @@ files = [ [package.extras] dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -808,6 +1056,8 @@ version = "7.1.1" description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." optional = false python-versions = "<4.0.0,>=3.8.1" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rdflib-7.1.1-py3-none-any.whl", hash = "sha256:e590fa9a2c34ba33a667818b5a84be3fb8a4d85868f8038f17912ec84f912a25"}, {file = "rdflib-7.1.1.tar.gz", hash = "sha256:164de86bd3564558802ca983d84f6616a4a1a420c7a17a8152f5016076b2913e"}, @@ -829,6 +1079,8 @@ version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, @@ -847,6 +1099,8 @@ version = "0.7.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"}, {file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"}, @@ -874,17 +1128,34 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -896,6 +1167,8 @@ version = "2.0.0" description = "SPARQL Endpoint interface to Python" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "SPARQLWrapper-2.0.0-py3-none-any.whl", hash = "sha256:c99a7204fff676ee28e6acef327dc1ff8451c6f7217dcd8d49e8872f324a8a20"}, {file = "SPARQLWrapper-2.0.0.tar.gz", hash = "sha256:3fed3ebcc77617a4a74d2644b86fd88e0f32e7f7003ac7b2b334c026201731f1"}, @@ -916,6 +1189,8 @@ version = "0.38.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff"}, {file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"}, @@ -933,6 +1208,8 @@ version = "0.12.4" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6"}, {file = "typer-0.12.4.tar.gz", hash = "sha256:c9c1613ed6a166162705b3347b8d10b661ccc5d95692654d0fb628118f2c34e6"}, @@ -950,17 +1227,34 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + [[package]] name = "uvicorn" version = "0.30.6" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, @@ -986,6 +1280,8 @@ version = "0.20.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" +groups = ["examples"] +markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and (python_version == \"3.11\" or python_version >= \"3.12\")" files = [ {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996"}, {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b"}, @@ -1030,6 +1326,8 @@ version = "0.23.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "watchfiles-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4"}, {file = "watchfiles-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b"}, @@ -1129,6 +1427,8 @@ version = "13.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.8" +groups = ["examples"] +markers = "python_version == \"3.11\" or python_version >= \"3.12\"" files = [ {file = "websockets-13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad4fa707ff9e2ffee019e946257b5300a45137a58f41fbd9a4db8e684ab61528"}, {file = "websockets-13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6fd757f313c13c34dae9f126d3ba4cf97175859c719e57c6a614b781c86b617e"}, @@ -1219,6 +1519,6 @@ files = [ ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" -content-hash = "0a2e465322bae2eaee949d9268fb74e79735a2dd33e1d7f7b390f2b21d797124" +content-hash = "2f968fcdf6791712bc19e82bd6eb133653caf9314d32b9c505416528a279c984" diff --git a/pyproject.toml b/pyproject.toml index cac608a..9b17d10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ pydantic = "^2.9.2" httpx = "^0.28.1" rdflib = "^7.1.1" +pandas = "^2.2.3" [tool.poetry.group.dev.dependencies] ruff = "^0.7.0" deptry = "^0.20.0" From e519f5ac90cda3f49129b173bb5f65b1325680be Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 20 Jan 2025 09:54:05 +0100 Subject: [PATCH 2/9] refactor: implement ModelBindingsMapper utilizing a DataFrame This change introduces a refactor of the important ModelBindingsMapper class using a pandas DataFrame for effecting grouping and aggregation and a CurryModel utility for partially instantiating a given Pydantic model with checks running for every partial application, allowing for fast validation failure. Closes #170. Aggregation behavior is now only triggered for actually aggregated fields, and not for top-level models as well. Therefore this also closes #181. --- rdfproxy/mapper.py | 192 +++++++++++++++++++++++++++------------- rdfproxy/utils/utils.py | 51 ++++++++++- 2 files changed, 180 insertions(+), 63 deletions(-) diff --git a/rdfproxy/mapper.py b/rdfproxy/mapper.py index 7c3d56d..c38e752 100644 --- a/rdfproxy/mapper.py +++ b/rdfproxy/mapper.py @@ -1,89 +1,157 @@ """ModelBindingsMapper: Functionality for mapping binding maps to a Pydantic model.""" from collections.abc import Iterator -from typing import Any, Generic, get_args +from itertools import chain +from typing import Generic, get_args +import pandas as pd +from pandas.api.typing import DataFrameGroupBy from pydantic import BaseModel from rdfproxy.utils._types import ModelBoolPredicate, _TModelInstance from rdfproxy.utils.mapper_utils import ( - _collect_values_from_bindings, - _get_group_by, - _get_key_from_metadata, _is_list_basemodel_type, _is_list_type, get_model_bool_predicate, ) +from rdfproxy.utils.utils import CurryModel, FieldsBindingsMap class ModelBindingsMapper(Generic[_TModelInstance]): - """Utility class for mapping flat bindings to a (potentially nested) Pydantic model.""" + """Utility class for mapping bindings to nested/grouped Pydantic models. + + RDFProxy utilizes Pydantic models also as a modelling grammar for grouping + and aggregation, mainly by treating the 'group_by' entry in ConfigDict in + combination with list-type annoted model fields as grouping + and aggregation indicators. _ModelBindingsMapper applies this grammar + for mapping flat bindings to potentially nested and grouped Pydantic models. + + Note: _ModelBindingsMapper is intended for use in rdfproxy.SPARQLModelAdapter and - + since no model sanity checking runs in the mapper itself - somewhat coupled to + SPARQLModelAdapter. The mapper can be useful in its own right though. + For standalone use, the initializer should be overwritten and model sanity checking + should be added to the _ModelBindingsMapper subclass. + """ def __init__(self, model: type[_TModelInstance], *bindings: dict): self.model = model self.bindings = bindings - self._contexts = [] + + self.df = pd.DataFrame(data=self.bindings) + self.df.replace(pd.NA, None, inplace=True) def get_models(self) -> list[_TModelInstance]: - """Generate a list of (potentially nested) Pydantic models based on (flat) bindings.""" - return self._get_unique_models(self.model, self.bindings) - - def _get_unique_models(self, model, bindings): - """Call the mapping logic and collect unique and non-empty models.""" - models = [] - model_bool_predicate: ModelBoolPredicate = get_model_bool_predicate(model) - - for _bindings in bindings: - _model = model(**dict(self._generate_binding_pairs(model, **_bindings))) - - if model_bool_predicate(_model) and (_model not in models): - models.append(_model) - - return models - - def _get_group_by(self, model) -> str: - """Get the group_by value from a model and register it in self._contexts.""" - group_by: str = _get_group_by(model) - - if group_by not in self._contexts: - self._contexts.append(group_by) - - return group_by - - def _generate_binding_pairs( - self, - model: type[BaseModel], - **kwargs, - ) -> Iterator[tuple[str, Any]]: - """Generate an Iterator[tuple] projection of the bindings needed for model instantation.""" - for k, v in model.model_fields.items(): - if _is_list_basemodel_type(v.annotation): - group_by: str = self._get_group_by(model) - group_model, *_ = get_args(v.annotation) - - applicable_bindings = filter( - lambda x: (x[group_by] == kwargs[group_by]) - and (x[self._contexts[0]] == kwargs[self._contexts[0]]), - self.bindings, + """Run the model mapping logic against bindings and collect a list of model instances.""" + return list(self._instantiate_models(self.df, self.model)) + + def _instantiate_models( + self, df: pd.DataFrame, model: type[_TModelInstance] + ) -> Iterator[_TModelInstance]: + """Generate potentially nested and grouped model instances from a dataframe. + + Note: The DataFrameGroupBy object must not be sorted, + else the result set order will not be maintained. + """ + alias_map = FieldsBindingsMap(model=model) + + if (_group_by := model.model_config.get("group_by")) is None: + for _, row in df.iterrows(): + yield self._instantiate_ungrouped_model_from_row(row, model) + else: + group_by = alias_map[_group_by] + group_by_object: DataFrameGroupBy = df.groupby(group_by, sort=False) + + for _, group_df in group_by_object: + yield self._instantiate_grouped_model_from_df(group_df, model) + + def _instantiate_ungrouped_model_from_row( + self, row: pd.Series, model: type[_TModelInstance] + ) -> _TModelInstance: + """Instantiate an ungrouped model from a pd.Series row. + + This handles the UNGROUPED code path in _ModelBindingsMapper._instantiate_models. + """ + alias_map = FieldsBindingsMap(model=model) + curried_model = CurryModel(model=model) + + for field_name, field_info in model.model_fields.items(): + if isinstance(nested_model := field_info.annotation, type(BaseModel)): + curried_model( + **{ + field_name: self._instantiate_ungrouped_model_from_row( + row, + nested_model, # type: ignore + ) + } ) - value = self._get_unique_models(group_model, applicable_bindings) - - elif _is_list_type(v.annotation): - group_by: str = self._get_group_by(model) - applicable_bindings = filter( - lambda x: x[group_by] == kwargs[group_by], - self.bindings, + else: + _sentinel = object() + field_value = ( + field_info.default + if (value := row.get(alias_map[field_name], _sentinel)) is _sentinel + else value ) + curried_model(**{field_name: field_value}) + + model_instance = curried_model() + assert isinstance(model_instance, model) # type narrow + return model_instance + + @staticmethod + def _get_unique_models(models: Iterator[_TModelInstance]) -> list[_TModelInstance]: + """Get a list of unique models from an iterable. + + Note: Unless frozen=True is specified in a model class, + Pydantic models instances are not hashable, i.e. dict.fromkeys + is not feasible for acquiring ordered unique models. - binding_key: str = _get_key_from_metadata(v, default=k) - value = _collect_values_from_bindings(binding_key, applicable_bindings) + Note: StopIteration in _get_unique_models should be unreachable, + because the result of _instantiate_models (the input of _get_unique_models + when called in _instantiate_grouped_model_from_df) gets called + on grouped dataframes and empty groups do not exist. + """ + unique_models = [] - elif isinstance(v.annotation, type(BaseModel)): - nested_model = v.annotation - value = nested_model( - **dict(self._generate_binding_pairs(nested_model, **kwargs)) + _model = next(models, None) + assert _model is not None, "StopIteration should be unreachable" + + model_bool_predicate: ModelBoolPredicate = get_model_bool_predicate(_model) + + for model in chain([_model], models): + if (model not in unique_models) and (model_bool_predicate(model)): + unique_models.append(model) + + return unique_models + + def _instantiate_grouped_model_from_df( + self, df: pd.DataFrame, model: type[_TModelInstance] + ) -> _TModelInstance: + """Instantiate a grouped model from a pd.DataFrame (a group dataframe). + + This handles the GROUPED code path in _ModelBindingsMapper._instantiate_models. + """ + alias_map = FieldsBindingsMap(model=model) + curried_model = CurryModel(model=model) + + for field_name, field_info in model.model_fields.items(): + if _is_list_basemodel_type(field_info.annotation): + nested_model, *_ = get_args(field_info.annotation) + value = self._get_unique_models( + self._instantiate_models(df, nested_model) + ) + elif _is_list_type(field_info.annotation): + value = list(dict.fromkeys(df[alias_map[field_name]].dropna())) + elif isinstance(nested_model := field_info.annotation, type(BaseModel)): + first_row = df.iloc[0] + value = self._instantiate_ungrouped_model_from_row( + first_row, + nested_model, # type: ignore ) else: - binding_key: str = _get_key_from_metadata(v, default=k) - value = kwargs.get(binding_key, v.default) + first_row = df.iloc[0] + value = first_row.get(alias_map[field_name]) or field_info.default + + curried_model(**{field_name: value}) - yield k, value + model_instance = curried_model() + assert isinstance(model_instance, model) # type narrow + return model_instance diff --git a/rdfproxy/utils/utils.py b/rdfproxy/utils/utils.py index 279d19c..6777bc0 100644 --- a/rdfproxy/utils/utils.py +++ b/rdfproxy/utils/utils.py @@ -3,7 +3,7 @@ from collections import UserDict from collections.abc import Callable from functools import partial -from typing import TypeVar +from typing import Any, Generic, Self, TypeVar from rdfproxy.utils._types import _TModelInstance from rdfproxy.utils._types import SPARQLBinding @@ -70,3 +70,52 @@ def __call__(self, query) -> str: if tkwargs := {k: v for k, v in self.kwargs.items() if v is not None}: return partial(self.f, **tkwargs)(query) return query + + +class CurryModel(Generic[_TModelInstance]): + """Constructor for currying a Pydantic Model. + + A CurryModel instance can be called with kwargs which are run against + the respective model field validators and kept in a kwargs cache. + Once the model can be instantiated, calling a CurryModel object will + instantiate the Pydantic model and return the model instance. + + If the eager flag is True (default), model field default values are + added to the cache automatically, which means that models can be instantiated + as soon possible, i.e. as soon as all /required/ field values are provided. + """ + + def __init__(self, model: type[_TModelInstance], eager: bool = True) -> None: + self.model = model + self.eager = eager + + self._kwargs_cache: dict = ( + {k: v.default for k, v in model.model_fields.items() if not v.is_required()} + if eager + else {} + ) + + def __repr__(self): # pragma: no cover + return f"CurryModel object {self._kwargs_cache}" + + @staticmethod + def _validate_field(model: type[_TModelInstance], field: str, value: Any) -> Any: + """Validate value for a single field given a model. + + Note: Using a TypeVar for value is not possible here, + because Pydantic might coerce values (if not not in Strict Mode). + """ + result = model.__pydantic_validator__.validate_assignment( + model.model_construct(), field, value + ) + return result + + def __call__(self, **kwargs: Any) -> Self | _TModelInstance: + for k, v in kwargs.items(): + self._validate_field(self.model, k, v) + + self._kwargs_cache.update(kwargs) + + if self.model.model_fields.keys() == self._kwargs_cache.keys(): + return self.model(**self._kwargs_cache) + return self From f1403d9096ccbe31533d788a7525a680f2b768f0 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 20 Jan 2025 10:29:56 +0100 Subject: [PATCH 3/9] test: mark model sanity tests xfail Some model sanity checking was implemented in the old ModelBindingsMapper, all checking should be done in a dedicated model sanity checking pipeline, see issue #108. So these tests - for now - are xfails. --- tests/tests_mapper/test_sad_path_mapper_grouped_models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/tests_mapper/test_sad_path_mapper_grouped_models.py b/tests/tests_mapper/test_sad_path_mapper_grouped_models.py index 2a5beb5..8b18a63 100644 --- a/tests/tests_mapper/test_sad_path_mapper_grouped_models.py +++ b/tests/tests_mapper/test_sad_path_mapper_grouped_models.py @@ -1,5 +1,6 @@ -from pydantic import BaseModel import pytest + +from pydantic import BaseModel from rdfproxy import ConfigDict, ModelBindingsMapper from rdfproxy.utils._exceptions import ( InvalidGroupingKeyException, @@ -17,11 +18,13 @@ class ModelMissingGroupByValue(BaseModel): x: list[int] +@pytest.mark.xfail(reason="Not yet implemented, checks will run in model checkers.") def test_sad_path_adapter_missing_grouping_config(): with pytest.raises(MissingModelConfigException): ModelBindingsMapper(ModelMissingGroupByConfig, {"x": 1}).get_models() +@pytest.mark.xfail(reason="Not yet implemented, checks will run in model checkers.") def test_sad_path_adapter_missing_grouping_value(): with pytest.raises(InvalidGroupingKeyException): ModelBindingsMapper(ModelMissingGroupByValue, {"x": 1}).get_models() From 0957ba0269ffed3dc34f1cd762847455f7d41d60 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 20 Jan 2025 10:40:16 +0100 Subject: [PATCH 4/9] fix(test): fix sketchy test for ModelBindingsMapper The expected data was actually wrong and passed a buggy behavior in the old ModelBindingsMapper. The change fixes the test to expect the correct result. Note that this test case is somewhat contrived, because the binding data is unordered and actual data from an RDFProxy-modified query for grouped models would be ordered. The refactored mapper uses a pd.DataFrame for grouping, so (unlike a possible low-level solution with itertools.groupby) the above case can be handled, because dataframes implement efficient grouping also across unordered row series. This is actually a quite powerful feature and makes the ModelBingingsMapper class generally useful, not just in the context of RDFProxy. --- tests/tests_mapper/params/model_bindings_mapper_parameters.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/tests_mapper/params/model_bindings_mapper_parameters.py b/tests/tests_mapper/params/model_bindings_mapper_parameters.py index 8f9b70b..3734182 100644 --- a/tests/tests_mapper/params/model_bindings_mapper_parameters.py +++ b/tests/tests_mapper/params/model_bindings_mapper_parameters.py @@ -219,10 +219,6 @@ }, {"p": "p value 2", "q": [{"a": "a value 2", "b": [{"x": 1, "y": 2}]}]}, {"p": "p value 3", "q": [{"a": "a value 3", "b": [{"x": 1, "y": 3}]}]}, - { - "p": "p value 4", - "q": [{"a": "a value 1", "b": [{"x": 1, "y": 2}, {"x": 2, "y": 2}]}], - }, ], ) ] From 6713b91b7cf3a91ed757ee744bb48c9dd10287b0 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 20 Jan 2025 13:13:16 +0100 Subject: [PATCH 5/9] test: introduce tests for grouped models with non-aggregated nesting --- .../model_bindings_mapper_parameters.py | 34 +++++++++++++++++++ .../params/models/nested_grouping_model.py | 15 ++++++++ .../test_model_bindings_mapper.py | 2 ++ 3 files changed, 51 insertions(+) diff --git a/tests/tests_mapper/params/model_bindings_mapper_parameters.py b/tests/tests_mapper/params/model_bindings_mapper_parameters.py index 3734182..9cf576a 100644 --- a/tests/tests_mapper/params/model_bindings_mapper_parameters.py +++ b/tests/tests_mapper/params/model_bindings_mapper_parameters.py @@ -9,6 +9,8 @@ ) from tests.tests_mapper.params.models.grouping_model import GroupingComplexModel from tests.tests_mapper.params.models.nested_grouping_model import ( + GroupingNestedComplexModel, + NestedComplexModel, NestedGroupingComplexModel, ) from tests.utils._types import ModelBindingsMapperParameter @@ -222,3 +224,35 @@ ], ) ] + + +grouping_nested_model_parameters = [ + ModelBindingsMapperParameter( + model=NestedComplexModel, + bindings=[ + {"x": 1, "y": 2, "a": "a value 1", "p": "p value 1"}, + {"x": 1, "y": 2, "a": "a value 2", "p": "p value 2"}, + {"x": 1, "y": 3, "a": "a value 3", "p": "p value 3"}, + {"x": 2, "y": 2, "a": "a value 1", "p": "p value 4"}, + ], + expected=[ + {"b": {"x": 1, "y": 2}}, + {"b": {"x": 1, "y": 2}}, + {"b": {"x": 1, "y": 3}}, + ], + ), + ModelBindingsMapperParameter( + model=GroupingNestedComplexModel, + bindings=[ + {"x": 1, "y": 2, "a": "a value 1", "p": "p value 1"}, + {"x": 1, "y": 2, "a": "a value 2", "p": "p value 2"}, + {"x": 1, "y": 3, "a": "a value 3", "p": "p value 3"}, + {"x": 2, "y": 2, "a": "a value 1", "p": "p value 4"}, + ], + expected=[ + {"b": {"x": 1, "y": 2}, "c": [{"x": 1, "y": 2}, {"x": 2, "y": 2}]}, + {"b": {"x": 1, "y": 2}, "c": [{"x": 1, "y": 2}]}, + {"b": {"x": 1, "y": 3}, "c": [{"x": 1, "y": 3}]}, + ], + ), +] diff --git a/tests/tests_mapper/params/models/nested_grouping_model.py b/tests/tests_mapper/params/models/nested_grouping_model.py index ed89d11..7c930fa 100644 --- a/tests/tests_mapper/params/models/nested_grouping_model.py +++ b/tests/tests_mapper/params/models/nested_grouping_model.py @@ -25,3 +25,18 @@ class NestedGroupingComplexModel(BaseModel): p: str q: list[NestedGroupingNestedModel] + + +class NestedComplexModel(BaseModel): + model_config = ConfigDict(group_by="a") + + a: str = Field(exclude=True) + b: NestedGroupingSimpleModel + + +class GroupingNestedComplexModel(BaseModel): + model_config = ConfigDict(group_by="a") + + a: str = Field(exclude=True) + b: NestedGroupingSimpleModel + c: list[NestedGroupingSimpleModel] diff --git a/tests/tests_mapper/test_model_bindings_mapper.py b/tests/tests_mapper/test_model_bindings_mapper.py index 52c47ed..4dea3c3 100644 --- a/tests/tests_mapper/test_model_bindings_mapper.py +++ b/tests/tests_mapper/test_model_bindings_mapper.py @@ -8,6 +8,7 @@ author_array_collection_parameters, author_work_title_parameters, basic_parameters, + grouping_nested_model_parameters, grouping_parameters, nested_grouping_parameters, ) @@ -21,6 +22,7 @@ *nested_grouping_parameters, *author_work_title_parameters, *author_array_collection_parameters, + *grouping_nested_model_parameters, ], ) def test_basic_model_bindings_mapper(model, bindings, expected): From 933da4a9236db3b14ba2bc72ee5fc61a87c85f36 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 20 Jan 2025 13:17:02 +0100 Subject: [PATCH 6/9] refactor: remove obsolete functions from mapper_utils --- rdfproxy/utils/mapper_utils.py | 67 ++-------------------------------- 1 file changed, 3 insertions(+), 64 deletions(-) diff --git a/rdfproxy/utils/mapper_utils.py b/rdfproxy/utils/mapper_utils.py index acce14e..ea3d37e 100644 --- a/rdfproxy/utils/mapper_utils.py +++ b/rdfproxy/utils/mapper_utils.py @@ -1,14 +1,8 @@ -from collections.abc import Callable, Iterable -from typing import Any, TypeGuard, get_args, get_origin +from collections.abc import Iterable +from typing import TypeGuard, get_args, get_origin from pydantic import BaseModel -from pydantic.fields import FieldInfo -from rdfproxy.utils._exceptions import ( - InvalidGroupingKeyException, - MissingModelConfigException, -) -from rdfproxy.utils._types import _TModelInstance -from rdfproxy.utils._types import ModelBoolPredicate, SPARQLBinding, _TModelBoolValue +from rdfproxy.utils._types import ModelBoolPredicate, _TModelBoolValue def _is_type(obj: type | None, _type: type) -> bool: @@ -28,61 +22,6 @@ def _is_list_basemodel_type(obj: type | None) -> bool: ) -def _collect_values_from_bindings( - binding_name: str, - bindings: Iterable[dict], - predicate: Callable[[Any], bool] = lambda x: x is not None, -) -> list: - """Scan bindings for a key binding_name and collect unique predicate-compliant values. - - Note that element order is important for testing, so a set cast won't do. - """ - values = dict.fromkeys( - value - for binding in bindings - if predicate(value := binding.get(binding_name, None)) - ) - return list(values) - - -def _get_key_from_metadata(v: FieldInfo, *, default: Any) -> str | Any: - """Try to get a SPARQLBinding object from a field's metadata attribute. - - Helper for _generate_binding_pairs. - """ - return next(filter(lambda x: isinstance(x, SPARQLBinding), v.metadata), default) - - -def _get_applicable_grouping_keys(model: type[_TModelInstance]) -> list[str]: - return [k for k, v in model.model_fields.items() if not _is_list_type(v.annotation)] - - -def _get_group_by(model: type[_TModelInstance]) -> str: - """Get the name of a grouping key from a model Config class.""" - try: - group_by = model.model_config["group_by"] # type: ignore - except KeyError as e: - raise MissingModelConfigException( - "Model config with 'group_by' value required " - "for field-based grouping behavior." - ) from e - else: - applicable_keys = _get_applicable_grouping_keys(model=model) - - if group_by not in applicable_keys: - raise InvalidGroupingKeyException( - f"Invalid grouping key '{group_by}'. " - f"Applicable grouping keys: {', '.join(applicable_keys)}." - ) - - if meta := model.model_fields[group_by].metadata: - if binding := next( - filter(lambda entry: isinstance(entry, SPARQLBinding), meta), None - ): - return binding - return group_by - - def default_model_bool_predicate(model: BaseModel) -> bool: """Default predicate for determining model truthiness. From c58a9597917e80355a0ab4603c20774d287b8be5 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Mon, 20 Jan 2025 16:57:34 +0100 Subject: [PATCH 7/9] refactor: make ModelBindingsMapper private As indicated in the docstring for ModelBindingsMapper, the class is somewhat coupled to SPARQLModelAdapter, because ModelBindinsMapper does not run model sanity by itself - sanity checking should happen in SPARQLModelAdapter, i.e. as early as possible. See issue #108. Once sanity checking is implemented, RDFProxy will make a public ModelBindingsMapper class available which will run model sanity checking itself. --- rdfproxy/__init__.py | 1 - rdfproxy/adapter.py | 4 ++-- rdfproxy/mapper.py | 2 +- tests/tests_mapper/test_model_bindings_mapper.py | 4 ++-- .../tests_mapper/test_model_bindings_mapper_model_bool.py | 4 ++-- tests/tests_mapper/test_sad_path_mapper_grouped_models.py | 7 ++++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rdfproxy/__init__.py b/rdfproxy/__init__.py index abb8cfd..5cde0d8 100644 --- a/rdfproxy/__init__.py +++ b/rdfproxy/__init__.py @@ -1,5 +1,4 @@ from rdfproxy.adapter import SPARQLModelAdapter # noqa: F401 -from rdfproxy.mapper import ModelBindingsMapper # noqa: F401 from rdfproxy.sparql_strategies import ( SPARQLStrategy, # noqa: F401 SPARQLWrapperStrategy, # noqa: F401 diff --git a/rdfproxy/adapter.py b/rdfproxy/adapter.py index ebb4902..1faeffc 100644 --- a/rdfproxy/adapter.py +++ b/rdfproxy/adapter.py @@ -6,7 +6,7 @@ from typing import Generic from rdfproxy.constructor import QueryConstructor -from rdfproxy.mapper import ModelBindingsMapper +from rdfproxy.mapper import _ModelBindingsMapper from rdfproxy.sparql_strategies import HttpxStrategy, SPARQLStrategy from rdfproxy.utils._types import _TModelInstance from rdfproxy.utils.models import Page, QueryParameters @@ -70,7 +70,7 @@ def query( logger.debug(f"Running items query: \n{items_query}") items_query_bindings: Iterator[dict] = self.sparql_strategy.query(items_query) - mapper = ModelBindingsMapper(self._model, *items_query_bindings) + mapper = _ModelBindingsMapper(self._model, *items_query_bindings) items: list[_TModelInstance] = mapper.get_models() logger.debug(f"Running count query: \n{count_query}") diff --git a/rdfproxy/mapper.py b/rdfproxy/mapper.py index c38e752..5ae7250 100644 --- a/rdfproxy/mapper.py +++ b/rdfproxy/mapper.py @@ -16,7 +16,7 @@ from rdfproxy.utils.utils import CurryModel, FieldsBindingsMap -class ModelBindingsMapper(Generic[_TModelInstance]): +class _ModelBindingsMapper(Generic[_TModelInstance]): """Utility class for mapping bindings to nested/grouped Pydantic models. RDFProxy utilizes Pydantic models also as a modelling grammar for grouping diff --git a/tests/tests_mapper/test_model_bindings_mapper.py b/tests/tests_mapper/test_model_bindings_mapper.py index 4dea3c3..cc5566b 100644 --- a/tests/tests_mapper/test_model_bindings_mapper.py +++ b/tests/tests_mapper/test_model_bindings_mapper.py @@ -3,7 +3,7 @@ import pytest from pydantic import BaseModel -from rdfproxy.mapper import ModelBindingsMapper +from rdfproxy.mapper import _ModelBindingsMapper from tests.tests_mapper.params.model_bindings_mapper_parameters import ( author_array_collection_parameters, author_work_title_parameters, @@ -31,6 +31,6 @@ def test_basic_model_bindings_mapper(model, bindings, expected): Given a model and a set of bindings, run the BindingsModelMapper logic and compare the result against the expected shape. """ - mapper: ModelBindingsMapper = ModelBindingsMapper(model, *bindings) + mapper: _ModelBindingsMapper = _ModelBindingsMapper(model, *bindings) models: list[BaseModel] = mapper.get_models() assert [model.model_dump() for model in models] == expected diff --git a/tests/tests_mapper/test_model_bindings_mapper_model_bool.py b/tests/tests_mapper/test_model_bindings_mapper_model_bool.py index a14077b..b9c6709 100644 --- a/tests/tests_mapper/test_model_bindings_mapper_model_bool.py +++ b/tests/tests_mapper/test_model_bindings_mapper_model_bool.py @@ -3,7 +3,7 @@ import pytest from pydantic import BaseModel -from rdfproxy.mapper import ModelBindingsMapper +from rdfproxy.mapper import _ModelBindingsMapper from tests.tests_mapper.params.model_bindings_mapper_model_bool_parameters import ( parent_child_parameters, ) @@ -19,6 +19,6 @@ def test_basic_model_bindings_mapper(model, bindings, expected): Given a model and a set of bindings, run the BindingsModelMapper logic and compare the result against the expected shape. """ - mapper: ModelBindingsMapper = ModelBindingsMapper(model, *bindings) + mapper: _ModelBindingsMapper = _ModelBindingsMapper(model, *bindings) models: list[BaseModel] = mapper.get_models() assert [model.model_dump() for model in models] == expected diff --git a/tests/tests_mapper/test_sad_path_mapper_grouped_models.py b/tests/tests_mapper/test_sad_path_mapper_grouped_models.py index 8b18a63..98ed00c 100644 --- a/tests/tests_mapper/test_sad_path_mapper_grouped_models.py +++ b/tests/tests_mapper/test_sad_path_mapper_grouped_models.py @@ -1,11 +1,12 @@ import pytest from pydantic import BaseModel -from rdfproxy import ConfigDict, ModelBindingsMapper +from rdfproxy.mapper import _ModelBindingsMapper from rdfproxy.utils._exceptions import ( InvalidGroupingKeyException, MissingModelConfigException, ) +from rdfproxy.utils._types import ConfigDict class ModelMissingGroupByConfig(BaseModel): @@ -21,10 +22,10 @@ class ModelMissingGroupByValue(BaseModel): @pytest.mark.xfail(reason="Not yet implemented, checks will run in model checkers.") def test_sad_path_adapter_missing_grouping_config(): with pytest.raises(MissingModelConfigException): - ModelBindingsMapper(ModelMissingGroupByConfig, {"x": 1}).get_models() + _ModelBindingsMapper(ModelMissingGroupByConfig, {"x": 1}).get_models() @pytest.mark.xfail(reason="Not yet implemented, checks will run in model checkers.") def test_sad_path_adapter_missing_grouping_value(): with pytest.raises(InvalidGroupingKeyException): - ModelBindingsMapper(ModelMissingGroupByValue, {"x": 1}).get_models() + _ModelBindingsMapper(ModelMissingGroupByValue, {"x": 1}).get_models() From bcbb4c0a624e420cbf947371a79cb8a7412b0196 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Tue, 21 Jan 2025 10:36:00 +0100 Subject: [PATCH 8/9] test: implement empty model and default-only model mapper tests Concerns #181. --- .../params/model_bindings_mapper_parameters.py | 14 ++++++++++++++ .../params/models/empty_default_only_model.py | 11 +++++++++++ tests/tests_mapper/test_model_bindings_mapper.py | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 tests/tests_mapper/params/models/empty_default_only_model.py diff --git a/tests/tests_mapper/params/model_bindings_mapper_parameters.py b/tests/tests_mapper/params/model_bindings_mapper_parameters.py index 9cf576a..3f10826 100644 --- a/tests/tests_mapper/params/model_bindings_mapper_parameters.py +++ b/tests/tests_mapper/params/model_bindings_mapper_parameters.py @@ -7,6 +7,7 @@ BasicNestedModel, BasicSimpleModel, ) +from tests.tests_mapper.params.models.empty_default_only_model import DefaultOnly, Empty from tests.tests_mapper.params.models.grouping_model import GroupingComplexModel from tests.tests_mapper.params.models.nested_grouping_model import ( GroupingNestedComplexModel, @@ -256,3 +257,16 @@ ], ), ] + +empty_default_only_model_parameters = [ + ModelBindingsMapperParameter( + model=Empty, + bindings=[{} for _ in range(100)], + expected=[{} for _ in range(100)], + ), + ModelBindingsMapperParameter( + model=DefaultOnly, + bindings=[{} for _ in range(100)], + expected=[{"x": 1} for _ in range(100)], + ), +] diff --git a/tests/tests_mapper/params/models/empty_default_only_model.py b/tests/tests_mapper/params/models/empty_default_only_model.py new file mode 100644 index 0000000..4a69566 --- /dev/null +++ b/tests/tests_mapper/params/models/empty_default_only_model.py @@ -0,0 +1,11 @@ +"""Models for testing empty/defaul-only model cases.""" + +from pydantic import BaseModel + + +class Empty(BaseModel): + pass + + +class DefaultOnly(BaseModel): + x: int = 1 diff --git a/tests/tests_mapper/test_model_bindings_mapper.py b/tests/tests_mapper/test_model_bindings_mapper.py index cc5566b..269bd16 100644 --- a/tests/tests_mapper/test_model_bindings_mapper.py +++ b/tests/tests_mapper/test_model_bindings_mapper.py @@ -8,6 +8,7 @@ author_array_collection_parameters, author_work_title_parameters, basic_parameters, + empty_default_only_model_parameters, grouping_nested_model_parameters, grouping_parameters, nested_grouping_parameters, @@ -23,6 +24,7 @@ *author_work_title_parameters, *author_array_collection_parameters, *grouping_nested_model_parameters, + *empty_default_only_model_parameters, ], ) def test_basic_model_bindings_mapper(model, bindings, expected): From 3b338b0db41a0437754088138b6a9a8214c3b69b Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Wed, 22 Jan 2025 09:57:22 +0100 Subject: [PATCH 9/9] test: add tests for empty/falsy string and None fields This test is related to a fixed bug in _ModelBindingsMapper._instantiate ungrouped_model_from_row, where default values were consulted on falsy field values, i.e. also on empty strings and None field values. --- .../model_bindings_mapper_parameters.py | 47 +++++++++++++++++++ .../tests_mapper/params/models/none_models.py | 12 +++++ .../test_model_bindings_mapper.py | 2 + 3 files changed, 61 insertions(+) create mode 100644 tests/tests_mapper/params/models/none_models.py diff --git a/tests/tests_mapper/params/model_bindings_mapper_parameters.py b/tests/tests_mapper/params/model_bindings_mapper_parameters.py index 3f10826..2ed7344 100644 --- a/tests/tests_mapper/params/model_bindings_mapper_parameters.py +++ b/tests/tests_mapper/params/model_bindings_mapper_parameters.py @@ -14,6 +14,10 @@ NestedComplexModel, NestedGroupingComplexModel, ) +from tests.tests_mapper.params.models.none_models import ( + SimpleNoneModel, + TwoFieldNoneModel, +) from tests.utils._types import ModelBindingsMapperParameter @@ -150,6 +154,20 @@ {"a": "a value", "b": {"x": 3, "y": 4}}, ], ), + # test for empty string/falsy fields + ModelBindingsMapperParameter( + model=BasicNestedModel, + bindings=[ + {"a": "a value", "x": 1, "y": 2}, + {"a": "a value", "x": 3, "y": 4}, + {"a": "", "x": 3, "y": 4}, + ], + expected=[ + {"a": "a value", "b": {"x": 1, "y": 2}}, + {"a": "a value", "b": {"x": 3, "y": 4}}, + {"a": "", "b": {"x": 3, "y": 4}}, + ], + ), ModelBindingsMapperParameter( model=BasicComplexModel, bindings=[{"a": "a value", "x": 1, "y": 2, "p": "p value"}], @@ -166,6 +184,17 @@ {"p": "p value", "q": {"a": "a value", "b": {"x": 3, "y": 4}}}, ], ), + # tests for empty string/falsy fields + ModelBindingsMapperParameter( + model=BasicComplexModel, + bindings=[{"a": "", "x": 1, "y": 2, "p": "p value"}], + expected=[{"p": "p value", "q": {"a": "", "b": {"x": 1, "y": 2}}}], + ), + ModelBindingsMapperParameter( + model=BasicComplexModel, + bindings=[{"a": "a value", "x": 1, "y": 2, "p": ""}], + expected=[{"p": "", "q": {"a": "a value", "b": {"x": 1, "y": 2}}}], + ), ] @@ -270,3 +299,21 @@ expected=[{"x": 1} for _ in range(100)], ), ] + +none_model_parameters = [ + ModelBindingsMapperParameter( + model=SimpleNoneModel, + bindings=[{"x": None}], + expected=[{"x": None}], + ), + ModelBindingsMapperParameter( + model=TwoFieldNoneModel, + bindings=[{"y": None}], + expected=[{"x": 1, "y": None}], + ), + ModelBindingsMapperParameter( + model=TwoFieldNoneModel, + bindings=[{"x": 2, "y": None}], + expected=[{"x": 2, "y": None}], + ), +] diff --git a/tests/tests_mapper/params/models/none_models.py b/tests/tests_mapper/params/models/none_models.py new file mode 100644 index 0000000..dfdebd8 --- /dev/null +++ b/tests/tests_mapper/params/models/none_models.py @@ -0,0 +1,12 @@ +"""Models for testing None fields.""" + +from pydantic import BaseModel + + +class SimpleNoneModel(BaseModel): + x: None + + +class TwoFieldNoneModel(BaseModel): + x: int = 1 + y: None diff --git a/tests/tests_mapper/test_model_bindings_mapper.py b/tests/tests_mapper/test_model_bindings_mapper.py index 269bd16..3ee4074 100644 --- a/tests/tests_mapper/test_model_bindings_mapper.py +++ b/tests/tests_mapper/test_model_bindings_mapper.py @@ -12,6 +12,7 @@ grouping_nested_model_parameters, grouping_parameters, nested_grouping_parameters, + none_model_parameters, ) @@ -25,6 +26,7 @@ *author_array_collection_parameters, *grouping_nested_model_parameters, *empty_default_only_model_parameters, + *none_model_parameters, ], ) def test_basic_model_bindings_mapper(model, bindings, expected):