diff --git a/poetry.lock b/poetry.lock index 66f4150a..3e154820 100644 --- a/poetry.lock +++ b/poetry.lock @@ -28,6 +28,32 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "altair" +version = "6.2.2" +description = "Vega-Altair: A declarative statistical visualization library for Python." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "altair-6.2.2-py3-none-any.whl", hash = "sha256:94014f8ad8617c3cb163d1137359cd6db5ba134b9b46d93cfd8b609fd245a583"}, + {file = "altair-6.2.2.tar.gz", hash = "sha256:a1ff9d9cfe81c75414641826312b9471780e19d39293ba0b012933f6b6cba0fe"}, +] + +[package.dependencies] +jinja2 = "*" +jsonschema = ">=3.0" +narwhals = ">=2.4.0" +packaging = "*" +typing-extensions = {version = ">=4.12.0", markers = "python_version < \"3.15\""} + +[package.extras] +all = ["altair-tiles (>=0.3.0)", "anywidget (>=0.9.0)", "numpy", "pandas (>=1.1.3)", "pyarrow (>=11)", "vegafusion (>=2.0.3)", "vl-convert-python (>=1.9.0)"] +dev = ["duckdb (>=1.0)", "geopandas (>=0.14.3)", "hatch (>=1.13.0)", "ipykernel", "ipython", "mistune", "mypy", "pandas (>=1.1.3)", "pandas-stubs (<2.3.3) ; python_version < \"3.11\"", "pandas-stubs (>=3.0.3.260530) ; python_version >= \"3.11\"", "polars (>=0.20.3)", "pyarrow-stubs", "pytest", "pytest-cov", "pytest-xdist[psutil] (>=3.5,<4.0)", "ruff (>=0.9.5)", "taskipy (>=1.14.1)", "tomli (>=2.2.1)", "ty", "types-jsonschema", "types-setuptools"] +doc = ["docutils", "jinja2", "myst-parser", "numpydoc", "pillow", "pydata-sphinx-theme (>=0.14.1)", "scipy", "scipy-stubs ; python_version >= \"3.10\"", "sphinx", "sphinx-autobuild", "sphinx-copybutton", "sphinx-design", "sphinxext-altair"] +save = ["vl-convert-python (>=1.9.0)"] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -207,11 +233,12 @@ version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] +markers = {main = "extra == \"web\""} [[package]] name = "babel" @@ -321,6 +348,19 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0)"] +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + [[package]] name = "boto3" version = "1.43.14" @@ -361,6 +401,19 @@ urllib3 = ">=1.25.4,<2.2.0 || >2.2.0,<3" [package.extras] crt = ["awscrt (==0.32.2)"] +[[package]] +name = "cachetools" +version = "7.1.4" +description = "Extensible memoizing collections and decorators" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54"}, + {file = "cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6"}, +] + [[package]] name = "certifi" version = "2026.5.20" @@ -535,7 +588,7 @@ version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, @@ -667,6 +720,7 @@ files = [ {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, ] +markers = {main = "extra == \"web\""} [[package]] name = "click" @@ -694,7 +748,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", docs = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", docs = "sys_platform == \"win32\""} [[package]] name = "comm" @@ -1577,6 +1631,42 @@ files = [ {file = "genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37"}, ] +[[package]] +name = "gitdb" +version = "4.0.12" +description = "Git Object Database" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, + {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.51" +description = "GitPython is a Python library used to interact with Git repositories" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "gitpython-3.1.51-py3-none-any.whl", hash = "sha256:d5b29685708f3c80a56db040b665bfd69492c8e150b5848532af8013b686c5a4"}, + {file = "gitpython-3.1.51.tar.gz", hash = "sha256:22c9c94bb6b0b9f3c7157c684fece45a414cea204586b600beae6cd4570dcd6d"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] + [[package]] name = "greenlet" version = "3.5.1" @@ -1705,6 +1795,67 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<1.0)"] +[[package]] +name = "httptools" +version = "0.8.0" +description = "A collection of framework independent HTTP protocol utils." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826"}, + {file = "httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77"}, + {file = "httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4"}, + {file = "httptools-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb"}, + {file = "httptools-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813"}, + {file = "httptools-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba"}, + {file = "httptools-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557"}, + {file = "httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168"}, + {file = "httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d"}, + {file = "httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376"}, + {file = "httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d"}, + {file = "httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085"}, + {file = "httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124"}, + {file = "httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07"}, + {file = "httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d"}, + {file = "httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5"}, + {file = "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2"}, + {file = "httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09"}, + {file = "httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a"}, + {file = "httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745"}, + {file = "httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150"}, + {file = "httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8"}, + {file = "httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c"}, + {file = "httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7"}, + {file = "httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d"}, + {file = "httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681"}, + {file = "httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683"}, + {file = "httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1"}, + {file = "httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6"}, + {file = "httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b"}, + {file = "httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0"}, + {file = "httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e"}, + {file = "httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b"}, + {file = "httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0"}, + {file = "httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527"}, + {file = "httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568"}, + {file = "httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b"}, + {file = "httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca"}, + {file = "httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f"}, + {file = "httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d"}, + {file = "httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081"}, + {file = "httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77"}, + {file = "httptools-0.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8"}, + {file = "httptools-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5"}, + {file = "httptools-0.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b"}, + {file = "httptools-0.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72"}, + {file = "httptools-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005"}, + {file = "httptools-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247"}, + {file = "httptools-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62"}, + {file = "httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999"}, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1936,6 +2087,19 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + [[package]] name = "jedi" version = "0.20.0" @@ -1961,11 +2125,12 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] +markers = {main = "extra == \"web\""} [package.dependencies] MarkupSafe = ">=2.0" @@ -2015,11 +2180,12 @@ version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.10" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, ] +markers = {main = "extra == \"web\""} [package.dependencies] attrs = ">=22.2.0" @@ -2046,11 +2212,12 @@ version = "2025.9.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, ] +markers = {main = "extra == \"web\""} [package.dependencies] referencing = ">=0.31.0" @@ -2464,7 +2631,7 @@ version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -2556,6 +2723,7 @@ files = [ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] +markers = {main = "extra == \"web\""} [[package]] name = "matplotlib" @@ -2714,6 +2882,33 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "narwhals" +version = "2.24.0" +description = "Extremely lightweight compatibility layer between dataframe libraries" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489"}, + {file = "narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d"}, +] + +[package.extras] +cudf = ["cudf-cu12 (>=24.10.0) ; sys_platform == \"linux\""] +dask = ["dask[dataframe] (>=2024.8)"] +duckdb = ["duckdb (>=1.1)"] +ibis = ["ibis-framework (>=6.0.0)", "packaging (>=21.3)", "pyarrow-hotfix (>=0.7)"] +modin = ["modin (>=0.22.0)"] +pandas = ["pandas (>=1.3.4)"] +polars = ["polars (>=0.20.4)"] +pyarrow = ["pyarrow (>=13.0.0)"] +pyspark = ["pyspark (>=3.5.0)"] +pyspark-connect = ["pyspark[connect] (>=3.5.0)"] +sql = ["narwhals[duckdb]", "sqlparse (>=0.5.5)"] +sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"] + [[package]] name = "nbclient" version = "0.10.4" @@ -3150,7 +3345,7 @@ version = "12.2.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" -groups = ["docs"] +groups = ["main", "docs"] files = [ {file = "pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f"}, {file = "pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97"}, @@ -3244,6 +3439,7 @@ files = [ {file = "pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e"}, {file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"}, ] +markers = {main = "extra == \"web\""} [package.extras] docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] @@ -3332,6 +3528,25 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "protobuf" +version = "7.35.1" +description = "" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4"}, + {file = "protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30"}, + {file = "protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87"}, + {file = "protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9"}, + {file = "protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a"}, +] + [[package]] name = "psutil" version = "7.2.2" @@ -3635,6 +3850,27 @@ files = [ [package.dependencies] typing-extensions = ">=4.14.1" +[[package]] +name = "pydeck" +version = "0.9.3" +description = "Widget for deck.gl maps" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "pydeck-0.9.3-py2.py3-none-any.whl", hash = "sha256:d8a47c11c81fb12d51b1feb42427ff4f0e13cb599e48931021b2cba98b6849a6"}, + {file = "pydeck-0.9.3.tar.gz", hash = "sha256:695775cbfe51f5fdffbd9735ba469987fdc5efc96bc40a0ee4808170509c78b2"}, +] + +[package.dependencies] +jinja2 = ">=2.10.1" +numpy = ">=1.16.4" + +[package.extras] +carto = ["pydeck-carto"] +jupyter = ["ipykernel (>=5.1.2)", "ipywidgets (>=7,<8)", "traitlets (>=4.3.2)"] + [[package]] name = "pyflakes" version = "2.5.0" @@ -3872,30 +4108,16 @@ files = [ dev = ["black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec ; implementation_name != \"pypy\"", "mypy", "orjson ; implementation_name != \"pypy\"", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] [[package]] -name = "python-magic" -version = "0.4.27" -description = "File type identification using libmagic" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["main"] -markers = "sys_platform != \"win32\"" -files = [ - {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, - {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, -] - -[[package]] -name = "python-magic-bin" -version = "0.4.14" -description = "File type identification using libmagic binary package" -optional = false -python-versions = "*" +name = "python-multipart" +version = "0.0.32" +description = "A streaming multipart parser for Python" +optional = true +python-versions = ">=3.10" groups = ["main"] -markers = "sys_platform == \"win32\"" +markers = "extra == \"web\"" files = [ - {file = "python_magic_bin-0.4.14-py2.py3-none-macosx_10_6_intel.whl", hash = "sha256:7b1743b3dbf16601d6eedf4e7c2c9a637901b0faaf24ad4df4d4527e7d8f66a4"}, - {file = "python_magic_bin-0.4.14-py2.py3-none-win32.whl", hash = "sha256:34a788c03adde7608028203e2dbb208f1f62225ad91518787ae26d603ae68892"}, - {file = "python_magic_bin-0.4.14-py2.py3-none-win_amd64.whl", hash = "sha256:90be6206ad31071a36065a2fc169c5afb5e0355cbe6030e87641c6c62edc2b69"}, + {file = "python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23"}, + {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, ] [[package]] @@ -4131,11 +4353,12 @@ version = "0.37.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.10" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, ] +markers = {main = "extra == \"web\""} [package.dependencies] attrs = ">=22.2.0" @@ -4272,11 +4495,12 @@ version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] +markers = {main = "extra == \"web\""} [package.dependencies] certifi = ">=2023.5.7" @@ -4358,7 +4582,7 @@ version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.10" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, @@ -4476,6 +4700,7 @@ files = [ {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] +markers = {main = "extra == \"web\""} [[package]] name = "s3transfer" @@ -4578,6 +4803,19 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "smmap" +version = "5.0.3" +description = "A pure Python implementation of a sliding window memory map manager" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f"}, + {file = "smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c"}, +] + [[package]] name = "snowballstemmer" version = "3.1.0" @@ -4901,6 +5139,91 @@ files = [ {file = "standard_imghdr-3.13.0.tar.gz", hash = "sha256:8d9c68058d882f6fc3542a8d39ef9ff94d2187dc90bd0c851e0902776b7b7a42"}, ] +[[package]] +name = "starlette" +version = "1.3.1" +description = "The little ASGI library that shines." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6"}, + {file = "starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "streamlit" +version = "1.59.2" +description = "A faster way to build and share data apps" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "streamlit-1.59.2-py3-none-any.whl", hash = "sha256:5014cb4f0064bd16c58fe334cb0d3c4b7e2be3d1ce4c02202c07114a33576dab"}, + {file = "streamlit-1.59.2.tar.gz", hash = "sha256:8fcb35df152868d33eec7ad2d89e276499a5351fb861eb1752b76476be7eb582"}, +] + +[package.dependencies] +altair = ">=4.0,<5.4.0 || >5.4.0,<5.4.1 || >5.4.1,<7" +anyio = ">=4.0.0" +blinker = ">=1.5.0,<2" +cachetools = ">=5.5,<8" +click = ">=7.0,<9" +gitpython = ">=3.0.7,<3.1.19 || >3.1.19,<4" +httptools = ">=0.6.3" +itsdangerous = ">=2.1.2" +numpy = ">=1.23,<3" +packaging = ">=20" +pandas = ">=1.4.0,<4" +pillow = ">=7.1.0,<13" +protobuf = ">=3.20,<8" +pyarrow = ">=7.0,<25" +pydeck = ">=0.8.0b4,<1" +python-multipart = ">=0.0.10" +requests = ">=2.27,<3" +starlette = ">=0.40.0" +tenacity = ">=8.1.0,<10" +toml = ">=0.10.1,<2" +typing-extensions = ">=4.10.0,<5" +uvicorn = ">=0.30.0" +watchdog = {version = ">=2.1.5,<7", markers = "platform_system != \"Darwin\""} +websockets = ">=12.0.0" + +[package.extras] +all = ["rich (>=11.0.0)", "streamlit[auth,charts,pdf,performance,snowflake,sql]"] +auth = ["Authlib (>=1.3.2)", "httpx (>=0.24.1)"] +charts = ["graphviz (>=0.19.0)", "matplotlib (>=3.0.0)", "orjson (>=3.5.0)", "plotly (>=4.0.0)"] +pdf = ["streamlit-pdf (>=1.0.0)"] +performance = ["orjson (>=3.5.0)", "uvloop (>=0.15.2) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""] +snowflake = ["snowflake-connector-python (>=3.3.0)", "snowflake-snowpark-python[modin] (>=1.17.0)"] +sql = ["SQLAlchemy (>=2.0.0)"] + +[[package]] +name = "tenacity" +version = "9.1.4" +description = "Retry code until it succeeds" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55"}, + {file = "tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + [[package]] name = "terminado" version = "0.18.1" @@ -4942,6 +5265,19 @@ webencodings = ">=0.4" doc = ["sphinx", "sphinx_rtd_theme"] test = ["pytest", "ruff"] +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = true +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + [[package]] name = "tomli" version = "2.4.1" @@ -5193,6 +5529,26 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +[[package]] +name = "uvicorn" +version = "0.51.0" +description = "The lightning-fast ASGI server." +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b"}, + {file = "uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=13.0)"] + [[package]] name = "virtualenv" version = "21.3.3" @@ -5211,6 +5567,50 @@ filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" python-discovery = ">=1.3.1" +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"web\" and platform_system != \"Darwin\"" +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "wcwidth" version = "0.7.0" @@ -5264,6 +5664,126 @@ docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["pytest", "websockets"] +[[package]] +name = "websockets" +version = "16.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"web\"" +files = [ + {file = "websockets-16.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de72a9c611178b15557d98eabd3101c9663c4d68938510478a6d162f99afd213"}, + {file = "websockets-16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:37b0e4d726ffea3776670092d3d13e1cb605076f036a695fd1259de0d9b9fe02"}, + {file = "websockets-16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:00d50c0a27098fcb7ab47b3d99a1b1159b534dbcd959fbf05113ebc37e5f927b"}, + {file = "websockets-16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1acb698bff1da1782b31aebd8d7a24d7d05453964abcd7d03dbf6e25893908e8"}, + {file = "websockets-16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc2c453f3b5f99c56b16e233aad5299860558487d26adb2ed27a00c14ca24b8c"}, + {file = "websockets-16.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1a9f08a0728b0835f1c6abe1d9b746ab3de49b7336a0e1919cf96be1e76273eb"}, + {file = "websockets-16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a089979d6173b27af18026c8d8b0077f83669a9169174482c4651e9f5739a5b6"}, + {file = "websockets-16.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3c18dba232ec2b92a68579c9fed8ff5a18f853d1e09fc0b6ca3159e94f689fe"}, + {file = "websockets-16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c1eb7df4170d5068892a8834fb5c07b9552353deb0dbeb0bff3820481ae4792"}, + {file = "websockets-16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c522bd48e625b6d557aa228967258d6d3da031c4cc21d3352fb302479aa9ba0a"}, + {file = "websockets-16.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d106396927a7f00b0f3a69215c3357f87bf0bca6844247121f7e8291e826a3b1"}, + {file = "websockets-16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d71bed12909b8039955536e192867d02d76cd3797cedfd0facf822e7668636c3"}, + {file = "websockets-16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9c1cf6f9a936b030b5bed0e800c5ee32069338129084546baf5ff5014dc62fa9"}, + {file = "websockets-16.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3fd3e6a7af2c8fcdcf4ffbeaf7f54a567b91a83267204187797f31faaa2a4efa"}, + {file = "websockets-16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dddd27175bf640acae5561fa79b77e8ec71fc445816200523e5c19b6a556fb72"}, + {file = "websockets-16.1-cp310-cp310-win32.whl", hash = "sha256:cce36c80b3f2fede7942f1756d3d885fa6fa086766c8c1bcf00695ab80f0d51a"}, + {file = "websockets-16.1-cp310-cp310-win_amd64.whl", hash = "sha256:115fc4695b94bb855995b23fb1abcb66099a5995575d3d5bc5605a616c58d0eb"}, + {file = "websockets-16.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a9b1d7a63cba8e6b9b77e499a81eab29d31100298d090ad4507d1048c0b9cae0"}, + {file = "websockets-16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bedbc5efeb96621aa2921d2d92608246691399418cac22acba427eb11877ea1f"}, + {file = "websockets-16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd847ab82133015afe65d778e7966ab42dba16bd7ad2e5b8a7918db6539f3f94"}, + {file = "websockets-16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2fb33ccb16ee40a95cc676d7b0ff451a9a2632f11a0dbc2e666326892b2e1de"}, + {file = "websockets-16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f15b6d9ea9c2eaf6ccab964a082b09bfa6634a495bb0c2e9e7ee6943f58976"}, + {file = "websockets-16.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:638cf57c48b4ad8ac1ff1e453f4f97db2426b690ddc111e6da96b27b4a340bc3"}, + {file = "websockets-16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c1c85f61bc9d5eac57ce705d848dc2d2ce3680638300bf4e1da7d749e2cf4ce"}, + {file = "websockets-16.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eeab6d27f51c7e579023c971f5e6dff200deadf01faf6831beaecd32052dfaef"}, + {file = "websockets-16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ed64e5a97b0b97a0b66e18bfe281317a75fbbd5afe692f939ea8d14a4292f2c"}, + {file = "websockets-16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b3b021d0ed4bc16eea9775f62c9fa71acdacba0fc790b38581754dedf29ca60"}, + {file = "websockets-16.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6eb604a4167f0a0d53c2243dfc667a29f0b43c3436057184e070bb82a1000fa2"}, + {file = "websockets-16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9a3f125e44c3e34d61d111652e608e0f5b85ce08c225c8d56ad0eb822fa40030"}, + {file = "websockets-16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8fdf0b00d0d1f30d1f06a92cab46fe542eec3eb302a7aee7163f142d0780f216"}, + {file = "websockets-16.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67b56828712f5fa7852de4c0265c28827311a657a4d275b7312ed0d1a918bee4"}, + {file = "websockets-16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39c7e7730be33b8f0cd6f0aa8e8c82f9cdd1813f159765e073b2ece65f4824b5"}, + {file = "websockets-16.1-cp311-cp311-win32.whl", hash = "sha256:c54fe94fb2f11e11b48920c5f971e298cec73ac35db56efe57a49db63dfc95d4"}, + {file = "websockets-16.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9f4fb9ae8b802e55609685db98382d48fd3feb1397804e1e774968dea0f28c7"}, + {file = "websockets-16.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b6aa3f7ad345cf3862c21f4fbf2ef5e14d911348476c2845e137c091fe3a3f0b"}, + {file = "websockets-16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b43fcfb521ac2f34ba80b7b8ea16303e4ad82dd8af667bf40839ad3a5d37b164"}, + {file = "websockets-16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bd3e12cd9afbe2baedae0b1eeade8ba64329b60fe2f9abdc966bd10fd2c2ef5"}, + {file = "websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a"}, + {file = "websockets-16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a24d1f35aef07d794a16c853c688e74956c50239bec37b4f2de080056046419b"}, + {file = "websockets-16.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c64c024ddf7a35331b21fcddb562a039c275d2c82e8c2d12939e7da23997270"}, + {file = "websockets-16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e99757f5baafe20fc598e202ea6f5b0b265186ad38d0a17bd8beca16296955"}, + {file = "websockets-16.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:353f3bc6e058ac1ccab4b3588e8598837a8c04cfc8351233e6d523be675d844c"}, + {file = "websockets-16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0352f5b38b40e857b6428d468fa21dbb4dd4a567d933c26d9831b4efe1b92f43"}, + {file = "websockets-16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70bd789afab579602968c39f21cb925466505f3edff22f0ae852bca54978a4f9"}, + {file = "websockets-16.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d0fb4b46f121eccd539353baebd1083a8767a9a351109453d1d1caecd1ba40c2"}, + {file = "websockets-16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c14b6634af01541e4efe2954fd8f263386f7aa6d37c01e55dd8109fd17661452"}, + {file = "websockets-16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a58532c49a851bcb481e58c1be23b315c17fe2fbbed509d75aeea12f543d2c15"}, + {file = "websockets-16.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4e969170c3b08e1d8dabd990fef1fa702c4233aeaabec33f871806e444f6a0e4"}, + {file = "websockets-16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff9b000064b88787ba9f7a3cb2af2b68a658ca5aad76458a46469e7124b678a0"}, + {file = "websockets-16.1-cp312-cp312-win32.whl", hash = "sha256:b9f5d83f80f4d7c4bba6d97f3755ac05850c784dce0fd2ab371c4e41172f53ff"}, + {file = "websockets-16.1-cp312-cp312-win_amd64.whl", hash = "sha256:6852c9f653966c16109d3b6f31181fd734f7914927e3f0fa1117af7a18c9aa21"}, + {file = "websockets-16.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b0232ed141cec3df2af5a3959a071c51f40036336b0d37e17faf9ef52fc73e47"}, + {file = "websockets-16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a71b73d143991714144e159f767b698f03c4a70b8a65ae1733b650cff488045b"}, + {file = "websockets-16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:187323204c3b2fc465e8fc2609e60437c521790cb9c1acb49c4c452a33e57f37"}, + {file = "websockets-16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dba74233c8c3ce368850818c98354dad2570f57231b3fd3bd00d7aa57628881"}, + {file = "websockets-16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63339bc8c63c86a463177775cb7c677691f5bcfac7b3b2f01b286d42acd41600"}, + {file = "websockets-16.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e545ea8ae4263e37cdfd4e22a217f519e48e432728bc461185bbf585f38a83"}, + {file = "websockets-16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2237081454846fb40403a80ba86d82e2038b9c45865ab96af0abe7d002a91045"}, + {file = "websockets-16.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f5218de1ed047385ca53744caba9435d65f75d008364970a3fae95a05812cf9"}, + {file = "websockets-16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75c98e3920039d0edff03b74478ada504b7ce3a1bc406db2cabfca84320f7baf"}, + {file = "websockets-16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1facd189d8190af30487a55b4c3688484dd50801628a3b5b2ccd26db08e67057"}, + {file = "websockets-16.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cc0c6a6eef613c7da32d4fb068f82ef834b58134f6a16b54e6c1e5bf9529ab3d"}, + {file = "websockets-16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ad9411eded8988b879be6038206698bf7106c85a78f642c004485bcb95be17eb"}, + {file = "websockets-16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cd68f0914f3b64694895bc5e9b14e8b447e41d7bf5ffaf989bb8dcb5e2dfdce7"}, + {file = "websockets-16.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fef2debfe7f7ebdda12176f26166f95b7af17af05ba06150fcf889032e0213e9"}, + {file = "websockets-16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3cd6c9b798218798f4bb7b2e71c38f0e744bb94ca537b13376f88019d46384d"}, + {file = "websockets-16.1-cp313-cp313-win32.whl", hash = "sha256:84c170c6869633536921e4474b1cce7254c0c9b0053ef5725f966cee47e718e4"}, + {file = "websockets-16.1-cp313-cp313-win_amd64.whl", hash = "sha256:bef52d327d70fa75dad93ee61ea2cb1d1489aca9f35c188833563f5a3b4df0a5"}, + {file = "websockets-16.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f881fca0a45dd6789939bd6637cd98169b92f1c3fdc78262f2cb9ec2cb1f324e"}, + {file = "websockets-16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c379d5b207d3a7f0ba4c2e4602a895b0bcc63fb5f5371a4ae7fbddb03b672b"}, + {file = "websockets-16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98ab58a4faa72b46da0127ccc1931dcbfc0985b0778892300a092185910c4cbe"}, + {file = "websockets-16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e9c4e369fc181b2d41a99e01477215cecdc8546a39f7d41a59cc0a7065a0b09"}, + {file = "websockets-16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0704df094b2d5fa7f6f410925a594c2a5c9a09167731a76292e5410934208209"}, + {file = "websockets-16.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b22b1f4950f6ab7126623329c3b47b3b90a14c05db517f2db2a026ad6c928352"}, + {file = "websockets-16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1ae4a686a662964a6671069f84f7f908cc3475e782227726b0c622c715962105"}, + {file = "websockets-16.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:856bdd638f8277f86465057bfdd4da097c73058fb0f9d2bd5baea29e2bf2d367"}, + {file = "websockets-16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9003a1fde1c21a322a3ca3fa0c4bda8c639da81dbc925162766086643b05ba87"}, + {file = "websockets-16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e947b1f5fdab045174306e3916785bf3ed537648acc1549827c08c33b10953"}, + {file = "websockets-16.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5dd0e666b5931c0509cf65714686a1c5126771e663a79ac5d40da4f58b1f9502"}, + {file = "websockets-16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a0285df7925657ad65a65fb8dc330808bce082827538fd50ef45fa12d1fc5bca"}, + {file = "websockets-16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:82d1c2cab3c133e9d059b3a5420bed9376bd30e21c185c63dda4ddadf6ddda47"}, + {file = "websockets-16.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c39907f1eaf11f6277def65aa02d68f30576b693d0c1ca332aafa3caa723ac6d"}, + {file = "websockets-16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45c5ea55446171949eb99fd34b771ceddd511ca21958d40d0197ced33159e5ee"}, + {file = "websockets-16.1-cp314-cp314-win32.whl", hash = "sha256:b8ef8b1c8d6bd029a475ac432e730fba2dfd456715d26c473e2a82291024b99c"}, + {file = "websockets-16.1-cp314-cp314-win_amd64.whl", hash = "sha256:7358ff21632b5d062707f73e859c824f1c3807e73d8ca25e71caca7c4cdcf145"}, + {file = "websockets-16.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d0f38f4c3e9b359e257c339c2cc1967ccaeedb102e57c1c986bdce4bf4f32268"}, + {file = "websockets-16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3c3d2cbd1602593bad49bd86fa3fbb25407d87a3b4bf8857c0ac5ac4914e1901"}, + {file = "websockets-16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:36069b74671e7e667f48a7484249f84c45a825a134c8b1bdc01875d0daa10d79"}, + {file = "websockets-16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:587f83c2ce8a5d628e166384d77fa7f0ac69b9007d515ab442123e6615aa8da3"}, + {file = "websockets-16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6db7972d52bc1b66cefe2246902e256cbaebc9ba8a45eac09343d7eb6671b2"}, + {file = "websockets-16.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e7d6014888a0632e1ed7a4095248bb3095232999447f2d83bfb1900987dd9ed9"}, + {file = "websockets-16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cb074d150e4ad2a77aa8a332c2be85f3f64f2681519d2570c1225c12c9821ff"}, + {file = "websockets-16.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d19c9067e1fe9490f974bffbc0e443b80a7674c5efb4980c429cc00771f07c5a"}, + {file = "websockets-16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d440ff0c6c7469ad59c0a412c383c235935b43635e89425e3f6a0c36de90c31b"}, + {file = "websockets-16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8613129a2533f08de24505e69a3e403cedaadae49abdb043c4d170ca71b7e4bd"}, + {file = "websockets-16.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a5bf9c23f197b4ec88290fd5463f33db67362a1bb10f85fc2e8e7627f0ddab97"}, + {file = "websockets-16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:520b0fd0395f075febb283c76755af724ab9fd19dffa4f3bfd18cb4e622790a3"}, + {file = "websockets-16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7143aa09a67e1c013be44e81a88dfe90fc6244198ab86c7edd064152cf619805"}, + {file = "websockets-16.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7acb811fad08e611755800d1560e395c67e11a6bd563598ea6abb319afb86938"}, + {file = "websockets-16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c5cf88e3faa2f7931bc6baeee7599c97656a3f6ac7f831f4fccba233e141783a"}, + {file = "websockets-16.1-cp314-cp314t-win32.whl", hash = "sha256:589f8842521c8307684ce0b40ce4ad70c5e0aa46484c6f1225a94ef4b8970341"}, + {file = "websockets-16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c0e0857c30bbbc2bb5c30687508f0b7ec19aa026cd9f2ff8424d0fee42dcc07"}, + {file = "websockets-16.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7289d899c79e763e6221c8dcb8959361cb43274418538d7c7ad16a43b01d12f9"}, + {file = "websockets-16.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e22e9e3719f5131bd62da4db63c8da63eb8c91cc99e16c1cbd122f130e1ae07a"}, + {file = "websockets-16.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:83bdabafef431247e6b11a9aab8a0893fd8e82e1ed95b32e0373625b03ffce4a"}, + {file = "websockets-16.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b8d13ceabc5c60995f201b5211d76876e17e68706ebf5d3bc666b32eefff1a6"}, + {file = "websockets-16.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81495f9c0085361c582efbc3207fb877174cfe03370f17d9cd70624404aa526f"}, + {file = "websockets-16.1-py3-none-any.whl", hash = "sha256:c5149dfe490ec7e5ee5dbf624c642fb725f93a5575c7f00ab594ca9eddb8dd81"}, + {file = "websockets-16.1.tar.gz", hash = "sha256:299468cbe42e2b9981134c7c51d99387d8a7bf562b00183b3eec53f882846dad"}, +] + [[package]] name = "win32-setctime" version = "1.2.0" @@ -5281,9 +5801,9 @@ files = [ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [extras] -web = [] +web = ["streamlit"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.14" -content-hash = "1036baf1ddcc1e53fab8f168ad5ce713f85370f19d46462af0b36e360be37733" +content-hash = "9bf808065543441f012b9c6c05e4911888c1e89102b6b2a0d0a9653e783b449b" diff --git a/pyproject.toml b/pyproject.toml index c355ba28..219c1271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,6 @@ pydantic = "^2.12.5" duckdb = "^1.4.4" duckdb-engine = "^0.17.0" sqlalchemy = "^2.0.48" -python-magic = { version = "*", platform = "!=win32" } -python-magic-bin = { version = "*", platform = "win32" } chardet = "^7.4.0.post2" anyio = "^4.13.0" httpx = ">=0.28.0" @@ -43,6 +41,7 @@ dotenv = "^0.9.9" boto3 = "^1.42.89" typer = "^0.24.1" humanize = "^4.8.0" +streamlit = {version = ">=1.30.0", optional = true} [tool.poetry.extras] web = ["streamlit"] diff --git a/pysus/api/extensions.py b/pysus/api/extensions.py index ed82ab33..b561b0e8 100644 --- a/pysus/api/extensions.py +++ b/pysus/api/extensions.py @@ -3,8 +3,8 @@ import asyncio import csv import gzip +import json import shutil -import sys import tarfile import zipfile from collections.abc import AsyncGenerator, Callable @@ -129,11 +129,15 @@ class CSV(BaseTabularFile): type: FileType = Field("CSV") _encoding: str | None = PrivateAttr(default=None) _sep: str | None = PrivateAttr(default=None) + _columns_cache: list["Column"] | None = PrivateAttr(default=None) + _rows_cache: int | None = PrivateAttr(default=None) @property def columns(self) -> list["Column"]: """Return the column metadata from the CSV header row.""" + if self._columns_cache is not None: + return self._columns_cache if self._encoding is not None: enc = self._encoding else: @@ -146,20 +150,39 @@ def columns(self) -> list["Column"]: else raw_enc ) self._encoding = enc - df = pd.read_csv(self.path, sep=",", nrows=0, encoding=enc) - return [ + if self._sep is not None: + sep = self._sep + else: + try: + with open(self.path, encoding=enc) as f: + sample = f.read(1024 * 10) + dialect = csv.Sniffer().sniff(sample) + sep = dialect.delimiter + except ValueError: + sep = "," + self._sep = sep + df = pd.read_csv(self.path, sep=sep, nrows=0, encoding=enc) + result = [ Column.from_schema(name=col, dtype=_map_dtype(str(dt))) for col, dt in zip(df.columns, df.dtypes) ] + self._columns_cache = result + return result @property def rows(self) -> int: """Return the number of data rows in the file.""" + if self._rows_cache is not None: + return self._rows_cache count = 0 with open(self.path, "rb") as f: - for _ in f: + reader = csv.reader( + line.decode("latin-1", errors="replace") for line in f + ) + for _ in reader: count += 1 - return max(0, count - 1) + self._rows_cache = max(0, count - 1) + return self._rows_cache async def _get_encoding(self) -> str: """Detect and cache the file's character encoding.""" @@ -238,28 +261,40 @@ class Parquet(BaseTabularFile): type: FileType = Field("PARQUET") add_dv: bool = True + _schema_cache: pa.Schema | None = PrivateAttr(default=None) + _metadata_cache: object | None = PrivateAttr(default=None) + _columns_cache: list["Column"] | None = PrivateAttr(default=None) + _rows_cache: int | None = PrivateAttr(default=None) @property def schema(self) -> pa.Schema: """Return the Parquet schema as a PyArrow Schema object.""" - return pq.read_schema(self.path) + if self._schema_cache is None: + self._schema_cache = pq.read_schema(self.path) + return self._schema_cache @property def columns(self) -> list["Column"]: """Return the column metadata from the Parquet schema.""" - schema = pq.read_schema(self.path) - return [ + if self._columns_cache is not None: + return self._columns_cache + result = [ Column.from_schema( name=field.name, dtype=_map_dtype(str(field.type)) ) - for field in schema + for field in self.schema ] + self._columns_cache = result + return result @property def rows(self) -> int: """Return the number of rows from the Parquet metadata.""" - return pq.read_metadata(self.path).num_rows + if self._rows_cache is not None: + return self._rows_cache + self._rows_cache = pq.read_metadata(self.path).num_rows + return self._rows_cache @staticmethod def _apply_add_dv(df: pd.DataFrame) -> pd.DataFrame: @@ -341,51 +376,46 @@ class DBF(BaseTabularFile): """Represents a dBASE (DBF) file.""" type: FileType = Field("DBF") + _columns_cache: list["Column"] | None = PrivateAttr(default=None) + _rows_cache: int | None = PrivateAttr(default=None) @property def columns(self) -> list["Column"]: """Return the column metadata from the DBF file.""" + if self._columns_cache is not None: + return self._columns_cache try: schema = read_dbf_schema(self.path) - except Exception: # noqa: B902 — fallback to dbfread + except Exception: # noqa: BLE001 reader = DBFReader(self.path, load=False) - _DBF_DTYPE = { - "C": "VARCHAR", - "N": "INTEGER", - "F": "FLOAT", - "D": "DATE", - "L": "BOOLEAN", - "M": "VARCHAR", - } - return [ + result = [ Column.from_schema( name=f.name, dtype=_DBF_DTYPE.get(f.type, "VARCHAR") ) for f in reader.fields ] - _DBF_DTYPE = { - "C": "VARCHAR", - "N": "INTEGER", - "F": "FLOAT", - "D": "DATE", - "L": "BOOLEAN", - "M": "VARCHAR", - } - return [ - Column.from_schema( - name=f.name, dtype=_DBF_DTYPE.get(f.type_, "VARCHAR") - ) - for f in schema.fields - ] + else: + result = [ + Column.from_schema( + name=f.name, + dtype=_DBF_DTYPE.get(f.type_, "VARCHAR"), + ) + for f in schema.fields + ] + self._columns_cache = result + return result @property def rows(self) -> int: """Return the number of records in the DBF file.""" + if self._rows_cache is not None: + return self._rows_cache try: - return read_dbf_schema(self.path).num_records - except Exception: # noqa: B902 — fallback to dbfread - return len(DBFReader(self.path, load=False)) + self._rows_cache = read_dbf_schema(self.path).num_records + except Exception: # noqa: BLE001 + self._rows_cache = len(DBFReader(self.path, load=False)) + return self._rows_cache def decode_column(self, value): """Decode a raw DBF value, handling byte strings and null bytes. @@ -596,7 +626,9 @@ async def _to_parquet_dbfread( callback(total_rows, total_rows) if writer is None: - df_empty = pd.DataFrame(columns=pd.Index(self.columns)) + df_empty = pd.DataFrame( + columns=pd.Index([c.name for c in self.columns]) + ) table_empty = pa.Table.from_pandas(df_empty) writer = pq.ParquetWriter(str(out), table_empty.schema) @@ -701,11 +733,21 @@ class JSON(BaseTabularFile): def columns(self) -> list["Column"]: """Return the column metadata from the JSON file.""" - df = ( - pd.read_json(self.path, nrows=0) - if self.path.stat().st_size > 0 - else pd.DataFrame() - ) + if self.path.stat().st_size == 0: + return [] + with open(self.path, encoding="utf-8") as f: + data = json.load(f) + if ( + isinstance(data, list) + and len(data) > 0 + and isinstance(data[0], dict) + ): + sample = data[0] + elif isinstance(data, dict): + sample = data + else: + return [] + df = pd.DataFrame([sample]) return [ Column.from_schema(name=col, dtype=_map_dtype(str(dt))) for col, dt in zip(df.columns, df.dtypes) @@ -797,7 +839,19 @@ async def extract( def _extract_sync(): """Extract ZIP contents synchronously in a thread.""" + root = target_dir.resolve() with zipfile.ZipFile(self.path) as z: + for info in z.infolist(): + unix_mode = info.external_attr >> 16 + if unix_mode and unix_mode & 0o170000 == 0o120000: + raise ValueError(f"Symlink blocked: {info.filename}") + member_path = (target_dir / info.filename).resolve() + try: + member_path.relative_to(root) + except ValueError: + raise ValueError( + f"Path traversal blocked: {info.filename}" + ) z.extractall(target_dir) await to_thread.run_sync(_extract_sync) @@ -844,22 +898,19 @@ async def to_parquet( async def _safe_cleanup(self, directory: Path): """Remove a temporary directory and its contents.""" - def _cleanup(): - """Remove directory contents synchronously in a thread.""" - if not directory.exists(): - return - - for item in directory.iterdir(): - if item.is_file(): - item.unlink() - elif item.is_dir(): - for subitem in item.iterdir(): - if subitem.is_file(): - subitem.unlink() - item.rmdir() + def _rmdir_recursive(d: Path): + """Recursively remove a directory tree.""" + for item in d.iterdir(): + if item.is_dir(): + _rmdir_recursive(item) + else: + item.unlink(missing_ok=True) + d.rmdir() + def _cleanup(): + """Remove directory synchronously in a thread.""" if directory.exists(): - directory.rmdir() + _rmdir_recursive(directory) await to_thread.run_sync(_cleanup) @@ -867,7 +918,7 @@ def _cleanup(): class GZip(BaseCompressedFile): """Represents a GZip-compressed file.""" - type: FileType = Field("ZIP") + type: FileType = Field("GZIP") async def load(self) -> bytes: """Decompress and read the entire file contents into memory.""" @@ -916,7 +967,7 @@ def _decompress(): class Tar(BaseCompressedFile): """Represents a Tar archive file.""" - type: FileType = Field("ZIP") + type: FileType = Field("TAR") async def load(self) -> tarfile.TarFile: """Open and return the tar archive.""" @@ -955,7 +1006,20 @@ async def extract( def _extract(): """Extract Tar contents synchronously in a thread.""" + root = target_dir.resolve() with tarfile.open(self.path) as t: + for member in t.getmembers(): + if member.issym() or member.islnk(): + raise ValueError( + f"Symlink/hardlink blocked: {member.name}" + ) + member_path = (target_dir / member.name).resolve() + try: + member_path.relative_to(root) + except ValueError: + raise ValueError( + f"Path traversal blocked: {member.name}" + ) t.extractall(target_dir) await to_thread.run_sync(_extract) @@ -963,17 +1027,118 @@ def _extract(): return list(await asyncio.gather(*tasks)) +_DBF_SIGNATURES = frozenset( + {0x02, 0x03, 0x04, 0x05, 0x30, 0x31, 0x43, 0x63, 0x83, 0x8B, 0x8E, 0xF5} +) + +_DBF_DTYPE = { + "C": "VARCHAR", + "N": "INTEGER", + "F": "FLOAT", + "D": "DATE", + "L": "BOOLEAN", + "M": "VARCHAR", +} + + +def _detect_parquet(path: Path, header: bytes) -> type[Parquet] | None: + if header[:4] != b"PAR1": + return None + try: + pq.read_metadata(str(path)) + return Parquet + except Exception: # noqa: BLE001 + return None + + +def _detect_pdf(path: Path, header: bytes) -> type[PDF] | None: + if header[:5] == b"%PDF-": + return PDF + return None + + +def _detect_zip(path: Path, header: bytes) -> type[Zip] | None: + if header[:4] in ( + b"PK\x03\x04", + b"PK\x05\x06", + b"PK\x07\x08", + ): + if zipfile.is_zipfile(path): + return Zip + return None + + +def _detect_gzip(path: Path, header: bytes) -> type[GZip] | None: + if header[:2] != b"\x1f\x8b": + return None + try: + with gzip.open(path) as f: + f.read(1) + return GZip + except OSError: + return None + + +def _detect_tar(path: Path, header: bytes) -> type[Tar] | None: + if len(header) >= 262 and header[257:262] == b"ustar": + if tarfile.is_tarfile(path): + return Tar + return None + + +def _detect_dbf(path: Path, header: bytes) -> type[DBF] | None: + if not header or header[0] not in _DBF_SIGNATURES: + return None + try: + schema = read_dbf_schema(path) + expected_size = ( + schema.header_len + schema.num_records * schema.record_len + ) + actual_size = path.stat().st_size + if expected_size <= actual_size + schema.record_len: + return DBF + except Exception: # noqa: BLE001 + pass + return None + + +def _detect_json(path: Path, header: bytes) -> type[JSON] | None: + prefix = header.lstrip() + if prefix[:1] not in (b"{", b"["): + return None + try: + json.loads(prefix) + return JSON + except (UnicodeDecodeError, json.JSONDecodeError): + return None + + +_DETECTORS = [ + _detect_parquet, + _detect_pdf, + _detect_zip, + _detect_gzip, + _detect_tar, + _detect_dbf, + _detect_json, +] + + +def _detect_type(path: Path) -> type[BaseLocalFile] | None: + try: + with open(path, "rb") as f: + header = f.read(4096) + except OSError: + return None + for detector in _DETECTORS: + result = detector(path, header) + if result is not None: + return result + return None + + class ExtensionFactory: - """Factory that maps file extensions and MIME types to handler classes.""" - - _mime: dict[str, type[BaseLocalFile]] = { - "application/zip": Zip, - "application/x-gzip": GZip, - "application/x-tar": Tar, - "text/csv": CSV, - "application/pdf": PDF, - "application/json": JSON, - } + """Factory that maps file content and extensions to handler classes.""" _extensions: dict[str, type[BaseLocalFile]] = { ".zip": Zip, @@ -989,33 +1154,34 @@ class ExtensionFactory: ".json": JSON, } - _magic_available: bool = sys.platform != "win32" + _detection_cache: dict[str, type[BaseLocalFile] | None] = {} + + @classmethod + def _cache_key(cls, path: Path) -> str: + stat = path.stat() + return f"{path.resolve()}:{stat.st_size}:{stat.st_mtime_ns}" @classmethod async def _identify(cls, path: Path) -> type[BaseLocalFile] | None: - """Identify the file class by its MIME type.""" - if not cls._magic_available: - return None + """Identify the file type by attempting to parse it.""" try: - import magic - except (ImportError, OSError): - cls._magic_available = False + key = cls._cache_key(path) + except OSError: return None + if key in cls._detection_cache: + return cls._detection_cache[key] try: - mime = await to_thread.run_sync( - magic.from_file, - str(path), - True, - ) - return cls._mime.get(mime) - except (magic.MagicException, OSError): - return None + result = await to_thread.run_sync(_detect_type, path) + except OSError: + result = None + cls._detection_cache[key] = result + return result @classmethod async def get_file_class(cls, path: Path) -> type[BaseLocalFile]: - """Return the file handler class for a given path. + """Return the handler class for a given path. - First attempts MIME-type identification; falls back to extension + First attempts content-based identification; falls back to extension matching. Parameters diff --git a/pysus/api/types.py b/pysus/api/types.py index 14d37015..ce940d73 100644 --- a/pysus/api/types.py +++ b/pysus/api/types.py @@ -49,6 +49,8 @@ def _validate_file_type(v: str) -> str: "DBC", "DBF", "ZIP", + "TAR", + "GZIP", ) assert v in valid, f"Invalid file type: {v!r}" return v diff --git a/pysus/tests/api/test_extensions.py b/pysus/tests/api/test_extensions.py index 3879eac2..02378bc7 100644 --- a/pysus/tests/api/test_extensions.py +++ b/pysus/tests/api/test_extensions.py @@ -1,4 +1,3 @@ -import builtins import gzip import json import struct @@ -691,8 +690,9 @@ async def test_dbf_to_parquet_non_parquet_extension(tmp_dir): obj = DBF(path=dbf_path) out = tmp_dir / "out.custom" - with pytest.raises(ConversionError, match="Could not parse"): - await obj.to_parquet(output_path=out) + result = await obj.to_parquet(output_path=out) + assert isinstance(result, Parquet) + assert out.exists() # --------------------------------------------------------------------------- @@ -772,8 +772,10 @@ def test_json_columns(tmp_dir): path = tmp_dir / "data.json" path.write_text('[{"a": 1, "b": "x"}]') obj = JSON(path=path) - with pytest.raises(ValueError, match="nrows can only be passed"): - _ = obj.columns + cols = obj.columns + assert len(cols) == 2 + assert cols[0].name == "a" + assert cols[1].name == "b" def test_json_columns_empty(tmp_dir): @@ -924,63 +926,130 @@ async def test_tar_load(tmp_dir): @pytest.mark.asyncio -async def test_extension_factory_identify_magic_not_available(tmp_dir): - orig = ExtensionFactory._magic_available - ExtensionFactory._magic_available = False - try: - path = tmp_dir / "test.csv" - path.write_text("a,b\n1,2") - result = await ExtensionFactory._identify(path) - assert result is None - finally: - ExtensionFactory._magic_available = orig +async def test_extension_factory_identify_oserror(tmp_dir): + path = tmp_dir / "nonexistent.csv" + result = await ExtensionFactory._identify(path) + assert result is None @pytest.mark.asyncio -async def test_extension_factory_identify_magic_import_error( - monkeypatch, tmp_dir -): - orig_available = ExtensionFactory._magic_available - ExtensionFactory._magic_available = True - try: - path = tmp_dir / "test.csv" - path.write_text("a,b\n1,2") +async def test_extension_factory_identify_csv_falls_back(tmp_dir): + path = tmp_dir / "test.csv" + path.write_text("a,b\n1,2") + result = await ExtensionFactory._identify(path) + assert result is None + cls = await ExtensionFactory.get_file_class(path) + assert cls is CSV - original_import = builtins.__import__ - def mock_import(name, globals=None, locals=None, fromlist=(), level=0): - if name == "magic": - raise ImportError("Mock error") - return original_import(name, globals, locals, fromlist, level) +@pytest.mark.asyncio +async def test_extension_factory_identify_zip(tmp_dir): + path = tmp_dir / "test.zip" + with zipfile.ZipFile(path, "w") as zf: + zf.writestr("file.txt", "hello") + result = await ExtensionFactory._identify(path) + assert result is Zip - monkeypatch.setattr(builtins, "__import__", mock_import) - result = await ExtensionFactory._identify(path) - assert result is None - assert not ExtensionFactory._magic_available - finally: - ExtensionFactory._magic_available = orig_available +@pytest.mark.asyncio +async def test_extension_factory_identify_gzip(tmp_dir): + path = tmp_dir / "test.gz" + with gzip.open(path, "wb") as f: + f.write(b"hello") + result = await ExtensionFactory._identify(path) + assert result is GZip @pytest.mark.asyncio -async def test_extension_factory_identify_magic_exception(monkeypatch, tmp_dir): - magic = pytest.importorskip("magic") - orig_available = ExtensionFactory._magic_available - ExtensionFactory._magic_available = True - try: - path = tmp_dir / "test.csv" - path.write_text("a,b\n1,2") +async def test_extension_factory_identify_tar(tmp_dir): + path = tmp_dir / "test.tar" + with tarfile.open(path, "w") as tf: + info = tarfile.TarInfo(name="file.txt") + data = b"hello" + info.size = len(data) + import io - def mock_from_file(*args, **kwargs): - raise magic.MagicException("Mock error") + tf.addfile(info, io.BytesIO(data)) + result = await ExtensionFactory._identify(path) + assert result is Tar - monkeypatch.setattr(magic, "from_file", mock_from_file) - result = await ExtensionFactory._identify(path) - assert result is None - assert ExtensionFactory._magic_available - finally: - ExtensionFactory._magic_available = orig_available +@pytest.mark.asyncio +async def test_extension_factory_identify_pdf(tmp_dir): + path = tmp_dir / "test.pdf" + path.write_bytes(b"%PDF-1.4" + b"\x00" * 10) + result = await ExtensionFactory._identify(path) + assert result is PDF + + +@pytest.mark.asyncio +async def test_extension_factory_identify_json(tmp_dir): + path = tmp_dir / "test.json" + path.write_text('{"key": "value"}') + result = await ExtensionFactory._identify(path) + assert result is JSON + + +@pytest.mark.asyncio +async def test_extension_factory_identify_json_array(tmp_dir): + path = tmp_dir / "test.json" + path.write_text("[1, 2, 3]") + result = await ExtensionFactory._identify(path) + assert result is JSON + + +@pytest.mark.asyncio +async def test_extension_factory_identify_json_false_positive(tmp_dir): + path = tmp_dir / "test.bin" + path.write_text("{not valid json {{{") + result = await ExtensionFactory._identify(path) + assert result is None + cls = await ExtensionFactory.get_file_class(path) + assert cls is File + + +@pytest.mark.asyncio +async def test_extension_factory_identify_parquet(tmp_dir): + path = tmp_dir / "test.parquet" + import pyarrow as pa + import pyarrow.parquet as pq + + table = pa.table({"a": [1, 2, 3]}) + pq.write_table(table, path) + result = await ExtensionFactory._identify(path) + assert result is Parquet + + +@pytest.mark.asyncio +async def test_extension_factory_identify_unknown(tmp_dir): + path = tmp_dir / "test.bin" + path.write_bytes(b"\x00\x01\x02\x03") + result = await ExtensionFactory._identify(path) + assert result is None + + +@pytest.mark.asyncio +async def test_extension_factory_identify_dbf(tmp_dir): + pytest.importorskip("dbfread") + path = tmp_dir / "test.dbf" + _create_dbf(path, [("NAME", "C", 10, 0)], [("Alice",)]) + result = await ExtensionFactory._identify(path) + assert result is DBF + + +@pytest.mark.asyncio +async def test_extension_factory_identify_wrong_extension(tmp_dir): + """Parquet content with .txt extension is detected by content.""" + path = tmp_dir / "data.txt" + import pyarrow as pa + import pyarrow.parquet as pq + + table = pa.table({"a": [1, 2, 3]}) + pq.write_table(table, path) + result = await ExtensionFactory._identify(path) + assert result is Parquet + cls = await ExtensionFactory.get_file_class(path) + assert cls is Parquet # --------------------------------------------------------------------------- @@ -1363,3 +1432,201 @@ async def test_dbf_to_parquet_fast_empty(tmp_dir): result = await obj.to_parquet(output_path=out, fast=True) assert isinstance(result, Parquet) assert out.exists() + + +# --------------------------------------------------------------------------- +# Regression tests for bugs found during review +# --------------------------------------------------------------------------- + + +def test_json_columns_reads_keys_correctly(tmp_dir): + """JSON.columns must parse first record's keys, not use nrows=0.""" + path = tmp_dir / "data.json" + path.write_text( + '[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]' + ) + obj = JSON(path=path) + cols = obj.columns + assert len(cols) == 2 + names = {c.name for c in cols} + assert names == {"name", "age"} + + +def test_json_columns_dict_object(tmp_dir): + """JSON.columns must handle top-level dict objects.""" + path = tmp_dir / "data.json" + path.write_text('{"name": "Alice", "age": 30}') + obj = JSON(path=path) + cols = obj.columns + assert len(cols) == 2 + names = {c.name for c in cols} + assert names == {"name", "age"} + + +def test_json_columns_nested_list(tmp_dir): + """JSON.columns must return empty for nested structures with no keys.""" + path = tmp_dir / "data.json" + path.write_text("[1, 2, 3]") + obj = JSON(path=path) + cols = obj.columns + assert cols == [] + + +@pytest.mark.asyncio +async def test_tar_path_traversal_blocked(tmp_dir): + """Tar.extract() must reject members with path traversal.""" + path = tmp_dir / "evil.tar" + with tarfile.open(path, "w") as t: + info = tarfile.TarInfo(name="../../etc/passwd") + data = b"evil" + info.size = len(data) + import io + + t.addfile(info, io.BytesIO(data)) + obj = Tar(path=path) + with pytest.raises(ValueError, match="Path traversal"): + await obj.extract(target_dir=tmp_dir / "out") + + +@pytest.mark.asyncio +async def test_zip_path_traversal_blocked(tmp_dir): + """Zip.extract() must reject members with path traversal.""" + path = tmp_dir / "evil.zip" + with zipfile.ZipFile(path, "w") as z: + z.writestr("../../etc/passwd", "evil") + obj = Zip(path=path) + with pytest.raises(ValueError, match="Path traversal"): + await obj.extract(target_dir=tmp_dir / "out") + + +@pytest.mark.asyncio +async def test_csv_columns_semicolon_delimiter(tmp_dir): + """CSV.columns must use detected delimiter, not hardcode comma.""" + path = tmp_dir / "data.csv" + path.write_text("name;age\nAlice;30\n") + obj = CSV(path=path) + await obj._get_sep() + cols = obj.columns + assert len(cols) == 2 + names = {c.name for c in cols} + assert names == {"name", "age"} + + +def test_csv_rows_quoted_newlines(tmp_dir): + """CSV.rows must not count newlines inside quoted fields.""" + path = tmp_dir / "data.csv" + path.write_text('name,bio\nAlice,"line1\nline2"\nBob,x\n') + obj = CSV(path=path) + assert obj.rows == 2 + + +@pytest.mark.asyncio +async def test_tar_type_is_tar(tmp_dir): + """Tar.type must be 'TAR', not 'ZIP'.""" + path = tmp_dir / "test.tar" + with tarfile.open(path, "w") as t: + info = tarfile.TarInfo(name="file.txt") + data = b"hello" + info.size = len(data) + import io + + t.addfile(info, io.BytesIO(data)) + obj = Tar(path=path) + assert obj.type == "TAR" + + +@pytest.mark.asyncio +async def test_gzip_type_is_gzip(tmp_dir): + """GZip.type must be 'GZIP', not 'ZIP'.""" + path = tmp_dir / "test.gz" + with gzip.open(path, "wb") as f: + f.write(b"hello") + obj = GZip(path=path) + assert obj.type == "GZIP" + + +@pytest.mark.asyncio +async def test_safe_cleanup_nested_directories(tmp_dir): + """Zip._safe_cleanup must handle nested directory structures.""" + nested = tmp_dir / "a" / "b" / "c" + nested.mkdir(parents=True) + (nested / "file.txt").write_text("data") + (tmp_dir / "a" / "file2.txt").write_text("data2") + + zip_path = tmp_dir / "test.zip" + with zipfile.ZipFile(zip_path, "w") as z: + z.writestr("a/b/c/file.txt", "data") + z.writestr("a/file2.txt", "data2") + + obj = Zip(path=zip_path) + result = await obj.extract(target_dir=tmp_dir / "out") + assert len(result) > 0 + + await obj._safe_cleanup(tmp_dir / "out") + assert not (tmp_dir / "out").exists() + + +@pytest.mark.asyncio +async def test_tar_symlink_blocked(tmp_dir): + """Tar.extract() must reject symlinks.""" + path = tmp_dir / "evil.tar" + with tarfile.open(path, "w") as t: + link = tarfile.TarInfo(name="link") + link.type = tarfile.SYMTYPE + link.linkname = "/etc/passwd" + t.addfile(link) + obj = Tar(path=path) + with pytest.raises(ValueError, match="Symlink"): + await obj.extract(target_dir=tmp_dir / "out") + + +@pytest.mark.asyncio +async def test_json_detect_valid(tmp_dir): + """JSON detection must succeed for valid small JSON.""" + path = tmp_dir / "data.json" + path.write_text('{"a": 1, "b": 2}') + result = await ExtensionFactory._identify(path) + assert result is JSON + + +@pytest.mark.asyncio +async def test_json_detect_rejects_binary_starting_with_brace(tmp_dir): + """JSON detection must reject binary files starting with {.""" + path = tmp_dir / "data.bin" + path.write_bytes(b"{\x00\x01\x02\x03}") + result = await ExtensionFactory._identify(path) + assert result is None + + +@pytest.mark.asyncio +async def test_parquet_columns_cached(tmp_dir): + """Parquet.columns must return the same list on repeated access.""" + path = tmp_dir / "test.parquet" + table = pa.table({"x": [1, 2], "y": ["a", "b"]}) + pq.write_table(table, path) + obj = Parquet(path=path) + first = obj.columns + second = obj.columns + assert first is second + + +@pytest.mark.asyncio +async def test_csv_columns_cached(tmp_dir): + """CSV.columns must return the same list on repeated access.""" + path = tmp_dir / "test.csv" + path.write_text("a,b\n1,2\n") + obj = CSV(path=path) + first = obj.columns + second = obj.columns + assert first is second + + +@pytest.mark.asyncio +async def test_csv_rows_cached(tmp_dir): + """CSV.rows must return the same int on repeated access.""" + path = tmp_dir / "test.csv" + path.write_text("a,b\n1,2\n3,4\n") + obj = CSV(path=path) + first = obj.rows + second = obj.rows + assert first == second