diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 50a2aa05..5a0ee038 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -4,8 +4,8 @@ on: [push, pull_request] jobs: tests: - runs-on: ubuntu-latest - timeout-minutes: 15 + runs-on: ${{ matrix.os }} + timeout-minutes: ${{ (matrix.os == 'windows-latest' && 30) || 15 }} defaults: run: @@ -13,10 +13,11 @@ jobs: strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + os: [ubuntu-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12"] concurrency: - group: ci-tests-${{ matrix.python-version }}-${{ github.ref }} + group: ci-tests-${{ matrix.os }}-${{ matrix.python-version }}-${{ github.ref }} cancel-in-progress: true steps: @@ -31,22 +32,28 @@ jobs: auto-update-conda: true conda-solver: libmamba - - name: Linting & Tests + - name: Install dependencies run: | pip install poetry poetry-plugin-export - poetry config virtualenvs.create false - - poetry export --with dev --extras dbc --format requirements.txt --output reqs.txt --without-hashes - - pip install -r reqs.txt - pip install -e ".[dbc]" - - pre-commit run --files pysus/**/* - - make test-pysus-with-coverage + if [ "${{ runner.os }}" = "Linux" ]; then + poetry install --without dev --extras dbc + pip install pre-commit + else + poetry install --without dev + fi + pip install pytest pytest-timeout pytest-retry pytest-asyncio pytest-cov + + - name: Linting + if: matrix.os == 'ubuntu-latest' + run: pre-commit run --files pysus/**/* + + - name: Tests + run: | + poetry run pytest -vv pysus/tests/ --retries 3 --retry-delay 15 --cov=pysus --cov-report=xml:coverage.xml --cov-report=term-missing - name: Upload coverage to Codecov + if: matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v5 with: files: ./coverage.xml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a48c0331..92158300 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -84,9 +84,11 @@ jobs: make html - name: Configure GitHub Pages + if: github.ref == 'refs/heads/main' uses: actions/configure-pages@v5 - name: Upload artifact + if: github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v3 with: path: docs/build/html diff --git a/README.md b/README.md index dd403dfc..11a5ba38 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,11 @@ sudo apt install libffi-dev pip install pysus[dbc] ``` +For the terminal user interface (TUI): +```bash +pip install pysus[tui] +``` + ## Quick Start ### Simplified Database Functions (New in 2.0) diff --git a/conda/dev.yaml b/conda/dev.yaml index 55d60c2c..79be6f06 100644 --- a/conda/dev.yaml +++ b/conda/dev.yaml @@ -4,7 +4,7 @@ channels: - defaults dependencies: - docker-compose - - python>=3.10,<3.14 + - python>=3.10,<3.13 - jupyter - make - pip diff --git a/poetry.lock b/poetry.lock index f538b09d..e430014c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -399,7 +399,7 @@ version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, @@ -1601,7 +1601,7 @@ 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 = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -1613,7 +1613,7 @@ version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -1631,14 +1631,14 @@ trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" -groups = ["dev", "docs"] +groups = ["main", "dev", "docs"] files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -1646,7 +1646,6 @@ anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] @@ -1659,9 +1658,10 @@ zstd = ["zstandard (>=0.18.0)"] name = "humanize" version = "4.11.0" description = "Python humanize utilities" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "humanize-4.11.0-py3-none-any.whl", hash = "sha256:b53caaec8532bcb2fff70c8826f904c35943f8cecaca29d272d9df38092736c0"}, {file = "humanize-4.11.0.tar.gz", hash = "sha256:e66f36020a2d5a974c504bd2555cf770621dbdbb6d82f94a6857c0b1ea2608be"}, @@ -2316,9 +2316,10 @@ files = [ name = "linkify-it-py" version = "2.1.0" description = "Links recognition library with FULL unicode support." -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e"}, {file = "linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b"}, @@ -2543,9 +2544,10 @@ files = [ name = "mdit-py-plugins" version = "0.5.0" description = "Collection of plugins for markdown-it-py" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}, {file = "mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}, @@ -2772,130 +2774,48 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numpy" -version = "2.2.6" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" groups = ["main", "docs"] -markers = "python_version == \"3.10\"" files = [ - {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, - {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, - {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, - {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, - {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, - {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, - {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, - {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, - {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, - {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, - {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, - {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, - {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, - {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, - {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, - {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, - {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, - {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, - {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, - {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, - {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, - {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, - {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, - {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, - {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, - {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, - {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, - {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, -] - -[[package]] -name = "numpy" -version = "2.3.0" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -groups = ["main", "docs"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9"}, - {file = "numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b"}, - {file = "numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3"}, - {file = "numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4"}, - {file = "numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96"}, - {file = "numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779"}, - {file = "numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58"}, - {file = "numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8"}, - {file = "numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f"}, - {file = "numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd"}, - {file = "numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8"}, - {file = "numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba"}, - {file = "numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e"}, - {file = "numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2"}, - {file = "numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459"}, - {file = "numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a"}, - {file = "numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a"}, - {file = "numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67"}, - {file = "numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc"}, - {file = "numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570"}, - {file = "numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd"}, - {file = "numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea"}, - {file = "numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a"}, - {file = "numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959"}, - {file = "numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe"}, - {file = "numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb"}, - {file = "numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0"}, - {file = "numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f"}, - {file = "numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8"}, - {file = "numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270"}, - {file = "numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f"}, - {file = "numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5"}, - {file = "numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e"}, - {file = "numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8"}, - {file = "numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3"}, - {file = "numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f"}, - {file = "numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808"}, - {file = "numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8"}, - {file = "numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad"}, - {file = "numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b"}, - {file = "numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555"}, - {file = "numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61"}, - {file = "numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb"}, - {file = "numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944"}, - {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b"}, - {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5"}, - {file = "numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2"}, - {file = "numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12"}, - {file = "numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97"}, - {file = "numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d"}, - {file = "numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] @@ -3169,6 +3089,7 @@ files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] +markers = {main = "extra == \"tui\""} [package.extras] docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] @@ -4381,18 +4302,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[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 = ["dev", "docs"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "snowballstemmer" version = "2.2.0" @@ -4709,6 +4618,18 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] +[[package]] +name = "standard-imghdr" +version = "3.13.0" +description = "Standard library imghdr redistribution. \"dead battery\"." +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "standard_imghdr-3.13.0-py3-none-any.whl", hash = "sha256:30a1bff5465605bb496f842a6ac3cc1f2131bf3025b0da28d4877d6d4b7cc8e9"}, + {file = "standard_imghdr-3.13.0.tar.gz", hash = "sha256:8d9c68058d882f6fc3542a8d39ef9ff94d2187dc90bd0c851e0902776b7b7a42"}, +] + [[package]] name = "terminado" version = "0.18.1" @@ -4735,9 +4656,10 @@ typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] name = "textual" version = "8.2.1" description = "Modern Text User Interface framework" -optional = false +optional = true python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "textual-8.2.1-py3-none-any.whl", hash = "sha256:746cbf947a8ca875afc09779ef38cadbc7b9f15ac886a5090f7099fef5ade990"}, {file = "textual-8.2.1.tar.gz", hash = "sha256:4176890e9cd5c95dcdd206541b2956b0808e74c8c36381c88db53dcb45237451"}, @@ -4910,9 +4832,10 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, name = "tree-sitter" version = "0.25.2" description = "Python bindings to the Tree-sitter parsing library" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20"}, {file = "tree_sitter-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72a510931c3c25f134aac2daf4eb4feca99ffe37a35896d7150e50ac3eee06c7"}, @@ -4960,9 +4883,10 @@ tests = ["tree-sitter-html (>=0.23.2)", "tree-sitter-javascript (>=0.23.1)", "tr name = "tree-sitter-bash" version = "0.25.1" description = "Bash grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_bash-0.25.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0e6235f59e366d220dde7d830196bed597d01e853e44d8ccd1a82c5dd2500acf"}, {file = "tree_sitter_bash-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f4a34a6504c7c5b2a9b8c5c4065531dea19ca2c35026e706cf2eeeebe2c92512"}, @@ -4982,9 +4906,10 @@ core = ["tree-sitter (>=0.24,<1.0)"] name = "tree-sitter-css" version = "0.25.0" description = "CSS grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_css-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ddce6f84eeb0bb2877b4587b07bffb0753040c44d811ed9ab2af978c313beda8"}, {file = "tree_sitter_css-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5a2a9c875037ef5f9da57697fb8075086476d42a49d25a88dcca60dfc09bd092"}, @@ -5004,9 +4929,10 @@ core = ["tree-sitter (>=0.24,<1.0)"] name = "tree-sitter-go" version = "0.25.0" description = "Go grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_go-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b852993063a3429a443e7bd0aa376dd7dd329d595819fabf56ac4cf9d7257b54"}, {file = "tree_sitter_go-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:503b81a2b4c31e302869a1de3a352ad0912ccab3df9ac9950197b0a9ceeabd8f"}, @@ -5026,9 +4952,10 @@ core = ["tree-sitter (>=0.24,<1.0)"] name = "tree-sitter-html" version = "0.23.2" description = "HTML grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_html-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e1641d5edf5568a246c6c47b947ed524b5bf944664e6473b21d4ae568e28ee9"}, {file = "tree_sitter_html-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3d0a83dd6cd1c7d4bcf6287b5145c92140f0194f8516f329ae8b9e952fbfa8ff"}, @@ -5047,9 +4974,10 @@ core = ["tree-sitter (>=0.22,<1.0)"] name = "tree-sitter-java" version = "0.23.5" description = "Java grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_java-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:355ce0308672d6f7013ec913dee4a0613666f4cda9044a7824240d17f38209df"}, {file = "tree_sitter_java-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:24acd59c4720dedad80d548fe4237e43ef2b7a4e94c8549b0ca6e4c4d7bf6e69"}, @@ -5068,9 +4996,10 @@ core = ["tree-sitter (>=0.22,<1.0)"] name = "tree-sitter-javascript" version = "0.25.0" description = "JavaScript grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc"}, {file = "tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1"}, @@ -5090,9 +5019,10 @@ core = ["tree-sitter (>=0.24,<1.0)"] name = "tree-sitter-json" version = "0.24.8" description = "JSON grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59ac06c6db1877d0e2076bce54a5fddcdd2fc38ca778905662e80fa9ffcea2ab"}, {file = "tree_sitter_json-0.24.8-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:62b4c45b561db31436a81a3f037f71ec29049f4fc9bf5269b6ec3ebaaa35a1cd"}, @@ -5111,9 +5041,10 @@ core = ["tree-sitter (>=0.22,<1.0)"] name = "tree-sitter-markdown" version = "0.5.1" description = "Markdown grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_markdown-0.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f00ce3f48f127377983859fcb93caf0693cbc7970f8c41f1e2bd21e4d56bdfd8"}, {file = "tree_sitter_markdown-0.5.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1ec4cc5d7b0d188bad22247501ab13663bb1bf1a60c2c020a22877fabce8daa9"}, @@ -5133,9 +5064,10 @@ core = ["tree-sitter (>=0.23,<1.0)"] name = "tree-sitter-python" version = "0.25.0" description = "Python grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361"}, {file = "tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762"}, @@ -5155,9 +5087,10 @@ core = ["tree-sitter (>=0.24,<1.0)"] name = "tree-sitter-regex" version = "0.25.0" description = "Regex grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_regex-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3fa11bbd76b29ac8ca2dbf85ad082f9b18ae6352251d805eb2d4191e1706a9d5"}, {file = "tree_sitter_regex-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:df5713649b89c5758649398053c306c41565f22a6f267cb5ec25596504bcf012"}, @@ -5177,9 +5110,10 @@ core = ["tree-sitter (>=0.24,<1.0)"] name = "tree-sitter-rust" version = "0.24.2" description = "Rust grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_rust-0.24.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3620cfd12340efa43082d45df76349ff511893a9c361da2f8d6d51e307020a59"}, {file = "tree_sitter_rust-0.24.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:01a46622735498493f29f3e628a90de95c96a07bfbeb88996243eb986b1cee36"}, @@ -5199,9 +5133,10 @@ core = ["tree-sitter (>=0.22,<1.0)"] name = "tree-sitter-sql" version = "0.3.11" description = "Tree-sitter Grammar for SQL" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_sql-0.3.11-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cf1b0c401756940bf47544ad7c4cc97373fc0dac118f821820953e7015a115e3"}, {file = "tree_sitter_sql-0.3.11-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a33cd6880ab2debef036f80365c32becb740ec79946805598488732b6c515fff"}, @@ -5221,9 +5156,10 @@ core = ["tree-sitter (>=0.24,<1.0)"] name = "tree-sitter-toml" version = "0.7.0" description = "TOML grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_toml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b9ae5c3e7c5b6bb05299dd73452ceafa7fa0687d5af3012332afa7757653b676"}, {file = "tree_sitter_toml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:18be09538e9775cddc0290392c4e2739de2201260af361473ca60b5c21f7bd22"}, @@ -5242,9 +5178,10 @@ core = ["tree-sitter (>=0.22,<1.0)"] name = "tree-sitter-xml" version = "0.7.0" description = "XML & DTD grammars for tree-sitter" -optional = false +optional = true python-versions = ">=3.9" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_xml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cc3e516d4c1e0860fb22172c172148debb825ba638971bc48bad15b22e5b0bae"}, {file = "tree_sitter_xml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0674fdf4cc386e4d323cb287d3b072663de0f20a9e9af5d5e09821aae56a9e5c"}, @@ -5263,9 +5200,10 @@ core = ["tree-sitter (>=0.22,<1.0)"] name = "tree-sitter-yaml" version = "0.7.2" description = "YAML grammar for tree-sitter" -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "tree_sitter_yaml-0.7.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7e269ddcfcab8edb14fbb1f1d34eed1e1e26888f78f94eedfe7cc98c60f8bc9f"}, {file = "tree_sitter_yaml-0.7.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0807b7966e23ddf7dddc4545216e28b5a58cdadedcecca86b8d8c74271a07870"}, @@ -5387,9 +5325,10 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) name = "uc-micro-py" version = "2.0.0" description = "Micro subset of unicode data files for linkify-it-py projects." -optional = false +optional = true python-versions = ">=3.10" groups = ["main"] +markers = "extra == \"tui\"" files = [ {file = "uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c"}, {file = "uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811"}, @@ -5545,8 +5484,9 @@ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [extras] dbc = ["pycparser", "pyreaddbc"] +tui = ["humanize", "textual"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.14" -content-hash = "ddaccd63d174d6604723e5b309cdeff958118868fc7b76a9c7fa6e514cdb8214" +content-hash = "c5bb87c75524bd9021f501a4662d090deaafda0974abf48dcf542a23221b8b66" diff --git a/pyproject.toml b/pyproject.toml index 922de920..3939dcb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ python = ">=3.10,<3.14" python-dateutil = "2.8.2" fastparquet = ">=2023.10.1,<=2024.11.0" pyarrow = ">=11.0.0" -numpy = ">1,<3" +numpy = ">=1.22,<2" tqdm = ">=4.67.0" wget = "^3.2" loguru = "^0.6.0" @@ -31,23 +31,25 @@ pydantic = "^2.12.5" duckdb = "^1.4.4" duckdb-engine = "^0.17.0" sqlalchemy = "^2.0.48" -textual = {extras = ["syntax"], version = "^8.2.1"} python-magic = "^0.4.27" chardet = "^7.4.0.post2" anyio = "^4.13.0" -humanize = "^4.8.0" +httpx = ">=0.28.0" aioftp = "^0.21.4" dbfread = "2.0.7" bigtree = "^0.12.2" pyreaddbc = { version = ">=1.1.0", optional = true } pycparser = { version = "2.21", optional = true } +textual = { extras = ["syntax"], version = "^8.2.1", optional = true } +humanize = { version = "^4.8.0", optional = true } dotenv = "^0.9.9" boto3 = "^1.42.89" typer = "^0.24.1" [tool.poetry.extras] dbc = ["pyreaddbc", "pycparser"] +tui = ["textual", "humanize"] [tool.poetry.group.dev.dependencies] pytest = ">=6.1.0" @@ -64,6 +66,7 @@ pytest-cov = "^7.1.0" [tool.poetry.group.docs.dependencies] sphinx = "^5.1.1" +standard-imghdr = "*" nbmake = "^1.4.1" matplotlib = "^3.7.1" jupyterlab = "^4.0.5" @@ -101,6 +104,12 @@ testpaths = [ exclude = ["*.git", "docs/"] +[tool.coverage.run] +omit = [ + "pysus/management/client.py", + "pysus/tui/*", +] + [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false diff --git a/pysus/api/client.py b/pysus/api/client.py index 99763a72..f2cf0913 100644 --- a/pysus/api/client.py +++ b/pysus/api/client.py @@ -10,7 +10,9 @@ from pathlib import Path from typing import TYPE_CHECKING, Literal +import anyio import duckdb +import pandas as pd from pysus import CACHEPATH from sqlalchemy import DateTime, Enum, Integer, String, create_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker @@ -235,14 +237,26 @@ async def download( file: BaseRemoteFile, token: str | None = None, callback: Callable | None = None, + timeout: float | None = None, ) -> BaseLocalFile: - """Download a remote file and return a local file handle.""" + """Download a remote file and return a local file handle. + + Parameters + ---------- + timeout : float | None + Maximum seconds to wait for the download. ``None`` (default) means + no timeout – use this when the socket-level timeout on the + underlying client is sufficient. + """ from pysus.api.extensions import ExtensionFactory existing_local = await self.get_local_file(file) if existing_local and existing_local.path.exists(): - return existing_local + if existing_local.size == file.size: + return existing_local + await self._delete_record(str(existing_local.path)) + existing_local.path.unlink(missing_ok=True) client_name = file.client.name.lower() remote_path = file.path @@ -271,7 +285,11 @@ async def download( f"No download logic for client: {client_name}", ) - await client._download_file(file, local_path, callback) + if timeout is not None: + with anyio.fail_after(timeout): + await client._download_file(file, local_path, callback) + else: + await client._download_file(file, local_path, callback) await self._update_state( local_path=local_path, @@ -311,6 +329,8 @@ async def download_to_parquet( file: BaseRemoteFile, token: str | None = None, callback: Callable[[int, int], None] | None = None, + timeout: float | None = None, + add_dv: bool = True, ) -> Parquet: """Download a file and convert it to Parquet format.""" @@ -318,11 +338,13 @@ async def download_to_parquet( file=file, token=token, callback=callback, + timeout=timeout, ) if hasattr(local_file, "to_parquet"): original_path = local_file.path parquet_file = await local_file.to_parquet(callback=callback) + parquet_file.add_dv = add_dv await self._update_state( local_path=parquet_file.path, @@ -346,7 +368,9 @@ async def download_to_parquet( ) def get_local_hierarchy(self): - """Build a nested dict of cached files grouped by client and dataset.""" + """ + Build a nested dict of cached files grouped by client and dataset. + """ with self.Session() as session: records = session.query(LocalFileState).all() @@ -414,8 +438,20 @@ def read_parquet( paths: list[Path], sql: str | None = None, mode: Literal["union", "intersection", "strict"] = "union", - ) -> "DuckDBPyConnection": - """Read Parquet files with optional schema handling and SQL filter.""" + add_dv: bool = True, + ) -> "DuckDBPyConnection | pd.DataFrame": + """Read Parquet files with optional schema handling and SQL filter. + + Parameters + ---------- + add_dv : bool + When True, automatically applies the IBGE verification digit to + municipality code columns. If there are matching columns, a + DataFrame is returned instead of a DuckDBPyConnection. + """ + + from pysus.api.utils import add_dv as _add_dv_fn + from pysus.api.utils import is_geocode_column if not paths: raise ValueError("No paths provided") @@ -452,8 +488,7 @@ def get_columns(path: Path) -> set[tuple[str, str]]: else: paths_str = ", ".join(f"'{p}'" for p in paths) query = ( - f"SELECT * FROM read_parquet([{paths_str}], " - "union_by_name=True)" + f"SELECT * FROM read_parquet([{paths_str}], union_by_name=True)" ) if sql: @@ -462,4 +497,29 @@ def get_columns(path: Path) -> set[tuple[str, str]]: else: query = f"SELECT {sql} FROM ({query}) AS t" + base = duckdb.execute(query) + + if not add_dv: + return base + + geocode_cols = [ + col[0] for col in base.description if is_geocode_column(col[0]) + ] + if not geocode_cols: + return base + + duckdb.create_function( + "__pysus_add_dv", + _add_dv_fn, + null_handling="special", + ) + selects = [ + ( + f'__pysus_add_dv("{c[0]}") AS "{c[0]}"' + if c[0] in geocode_cols + else f'"{c[0]}"' + ) + for c in base.description + ] + query = f"SELECT {', '.join(selects)} FROM ({query}) AS _t" return duckdb.execute(query) diff --git a/pysus/api/extensions.py b/pysus/api/extensions.py index d1029049..27746abd 100644 --- a/pysus/api/extensions.py +++ b/pysus/api/extensions.py @@ -14,7 +14,12 @@ from typing import ClassVar import chardet -import magic + +try: + import magic +except (ImportError, OSError): + magic = None # type: ignore[assignment] + import pandas as pd import pyarrow as pa import pyarrow.parquet as pq @@ -188,6 +193,7 @@ class Parquet(BaseTabularFile): """Represents a Parquet file with optional date and integer type parsing.""" type: FileType = Field("PARQUET") + add_dv: bool = True @property def schema(self) -> pa.Schema: @@ -204,12 +210,26 @@ def rows(self) -> int: """Return the number of rows from the Parquet metadata.""" return pq.read_metadata(self.path).num_rows + @staticmethod + def _apply_add_dv(df: pd.DataFrame) -> pd.DataFrame: + """Apply the IBGE verification digit to geocode columns in-place.""" + from pysus.api.utils import add_dv, is_geocode_column + + geocode_cols = [c for c in df.columns if is_geocode_column(c)] + for col in geocode_cols: + df[col] = df[col].astype(str).apply(add_dv) + return df + async def load(self, parse: bool = True) -> pd.DataFrame: """Read the entire Parquet file into a DataFrame.""" def _load(): df = pd.read_parquet(self.path, engine="pyarrow") - return self.parse_dftypes(df) if parse else df + if parse: + df = self.parse_dftypes(df) + if self.add_dv: + df = self._apply_add_dv(df) + return df return await to_thread.run_sync(_load) @@ -226,6 +246,8 @@ async def stream( df = batch.to_pandas() if parse: df = self.parse_dftypes(df) + if self.add_dv: + df = self._apply_add_dv(df) yield df await asyncio.sleep(0) @@ -815,6 +837,8 @@ class ExtensionFactory: @classmethod async def _identify(cls, path: Path) -> type[BaseLocalFile] | None: """Identify the file class by its MIME type.""" + if magic is None: + return None try: mime = await to_thread.run_sync( magic.from_file, diff --git a/pysus/api/ftp/client.py b/pysus/api/ftp/client.py index 0329038b..f06a8e7f 100644 --- a/pysus/api/ftp/client.py +++ b/pysus/api/ftp/client.py @@ -43,6 +43,7 @@ class FTP(BaseRemoteClient): """Async FTP client for navigating and downloading DATASUS data.""" host: str = "ftp.datasus.gov.br" + timeout: int = 60 _ftp: FTPLib | None = PrivateAttr(default=None) @@ -77,7 +78,7 @@ async def connect(self) -> None: def _connect(): if self.ftp is None: - self._ftp = FTPLib(self.host) + self._ftp = FTPLib(self.host, timeout=self.timeout) self.ftp.login() await to_thread.run_sync(_connect) diff --git a/pysus/api/utils.py b/pysus/api/utils.py new file mode 100644 index 00000000..1e827352 --- /dev/null +++ b/pysus/api/utils.py @@ -0,0 +1,51 @@ +GEOCODE_PREFIXES = ( + "ID_MUNICIP", + "ID_MN_RESI", + "ID_MUNI_RE", + "MUN_", + "COD_MUN_", + "CO_MUN_", + "ID_MUNI_AT", + "ID_MUNIC_", +) + + +def is_geocode_column(name: str) -> bool: + """Check if a column name corresponds to an IBGE municipality code.""" + upper = name.upper() + return any(upper.startswith(p) for p in GEOCODE_PREFIXES) + + +def add_dv(geocode: str) -> str: + if not geocode or not str(geocode).isdigit(): + return geocode + + miscalculated = { + "2201911": "2201919", + "2201986": "2201988", + "2202257": "2202251", + "2611531": "2611533", + "3117835": "3117836", + "3152139": "3152131", + "4305876": "4305871", + "5203963": "5203962", + "5203930": "5203939", + } + + if len(str(geocode)) == 7: + return miscalculated.get(str(geocode), geocode) + + if len(str(geocode)) == 6: + weight = [1, 2, 1, 2, 1, 2] + total = sum( + sum(divmod(int(d) * w, 10)) + for d, w in zip( + str(geocode), + weight, + ) + ) + dv = 0 if total % 10 == 0 else 10 - (total % 10) + code = str(geocode) + str(dv) + return miscalculated.get(code, code) + + return geocode diff --git a/pysus/cli/__init__.py b/pysus/cli/__init__.py index cab3892e..69b2381d 100644 --- a/pysus/cli/__init__.py +++ b/pysus/cli/__init__.py @@ -1,6 +1,5 @@ import typer from pysus import __version__ -from pysus.tui.app import PySUS app = typer.Typer(help="PySUS CLI") @@ -14,6 +13,13 @@ def tui( help="Language (en, pt)", ), ): + try: + from pysus.tui.app import PySUS + except ImportError: + raise ImportError( + "The TUI requires extra dependencies. " + "Install them with: pip install pysus[tui]" + ) app = PySUS(lang=lang) app.run() diff --git a/pysus/tests/api/ftp/test_client.py b/pysus/tests/api/ftp/test_client.py index a0029356..e3d6b999 100644 --- a/pysus/tests/api/ftp/test_client.py +++ b/pysus/tests/api/ftp/test_client.py @@ -48,7 +48,9 @@ async def test_connect_and_login(ftp_client): mock_instance = mock_ftplib.return_value await ftp_client.login() - mock_ftplib.assert_called_once_with(ftp_client.host) + mock_ftplib.assert_called_once_with( + ftp_client.host, timeout=ftp_client.timeout + ) mock_instance.login.assert_called_once() diff --git a/pysus/tests/api/test_client.py b/pysus/tests/api/test_client.py index 2c8064ad..3546cdfa 100644 --- a/pysus/tests/api/test_client.py +++ b/pysus/tests/api/test_client.py @@ -294,6 +294,123 @@ async def test_query_initializes_ducklake(self, test_db_path): await client.__aexit__(None, None, None) +class TestDownload: + @pytest.mark.asyncio + async def test_download_returns_existing_when_size_matches( + self, test_db_path + ): + from unittest.mock import AsyncMock, MagicMock, patch + + client = PySUS(db_path=test_db_path) + mock_local = MagicMock() + mock_local.path.exists.return_value = True + mock_local.size = 1000 + mock_file = MagicMock() + mock_file.size = 1000 + mock_file.client.name = "ftp" + + with patch.object( + client, "get_local_file", new=AsyncMock(return_value=mock_local) + ): + result = await client.download(mock_file) + + assert result == mock_local + + @pytest.mark.asyncio + async def test_download_re_fetches_when_size_differs(self, test_db_path): + import pathlib + from unittest.mock import AsyncMock, MagicMock, patch + + from pysus.api.extensions import ExtensionFactory + + mock_local = MagicMock() + mock_local.path.exists.return_value = True + mock_local.size = 500 + mock_file = MagicMock() + mock_file.size = 1000 + mock_file.client.name = "ftp" + mock_file.path = pathlib.Path("/remote/test.dbc") + mock_file.basename = "test.dbc" + + client = PySUS(db_path=test_db_path) + get_local_file_patch = patch.object( + client, "get_local_file", new=AsyncMock(return_value=mock_local) + ) + delete_record_patch = patch.object( + client, "_delete_record", new=AsyncMock() + ) + get_dest_patch = patch.object( + client, + "_get_dest_path", + return_value=test_db_path.parent / "test.dbc", + ) + update_state_patch = patch.object( + client, "_update_state", new=AsyncMock() + ) + get_ftp_patch = patch.object(client, "get_ftp", new=AsyncMock()) + + with ( + get_local_file_patch, + delete_record_patch as mock_delete, + get_dest_patch, + update_state_patch, + get_ftp_patch, + ): + with patch.object( + ExtensionFactory, "instantiate", return_value=mock_local + ): + mock_client = AsyncMock() + mock_client._download_file = AsyncMock() + client._ftp = mock_client + await client.download(mock_file) + + mock_delete.assert_awaited_once() + assert mock_local.path.unlink.called + + @pytest.mark.asyncio + async def test_download_passes_timeout(self, test_db_path): + from unittest.mock import AsyncMock, MagicMock, patch + + import anyio + from pysus.api.extensions import ExtensionFactory + + mock_local = MagicMock() + mock_local.path.exists.return_value = False + mock_file = MagicMock() + mock_file.size = 1000 + mock_file.client.name = "ftp" + mock_file.path = test_db_path.parent / "remote.dbc" + mock_file.basename = "remote.dbc" + + client = PySUS(db_path=test_db_path) + + async def _slow_download(*args, **kwargs): + await anyio.sleep(10) + + with ( + patch.object( + client, "get_local_file", new=AsyncMock(return_value=mock_local) + ), + patch.object( + client, + "_get_dest_path", + return_value=test_db_path.parent / "test.dbc", + ), + patch.object(client, "_update_state", new=AsyncMock()), + patch.object( + ExtensionFactory, "instantiate", return_value=mock_local + ), + ): + mock_client = AsyncMock() + mock_client._download_file = _slow_download + client._ftp = mock_client + + with pytest.raises( + RuntimeError, match="Unexpected error downloading" + ): + await client.download(mock_file, timeout=0.001) + + class TestReadParquet: def test_read_parquet_single_path(self, tmp_path): import pandas as pd @@ -407,6 +524,55 @@ def test_read_parquet_no_paths_raises(self, tmp_path): with pytest.raises(ValueError, match="No paths provided"): client.read_parquet([]) + def test_read_parquet_add_dv_applies_verification_digit(self, tmp_path): + import pandas as pd + + parquet_file = tmp_path / "test.parquet" + df = pd.DataFrame({"ID_MUNICIP": ["261160", "530010"], "value": [1, 2]}) + df.to_parquet(parquet_file) + + from pysus.api.client import PySUS + + client = PySUS(db_path=tmp_path / "config.db") + result = client.read_parquet([parquet_file], add_dv=True) + out = result.df() + + assert out["ID_MUNICIP"].iloc[0] == "2611606" + assert out["ID_MUNICIP"].iloc[1] == "5300108" + + def test_read_parquet_add_dv_skips_no_geocode_columns(self, tmp_path): + import pandas as pd + + parquet_file = tmp_path / "test.parquet" + df = pd.DataFrame({"DT_NOTIFIC": ["20230101"], "value": [1]}) + df.to_parquet(parquet_file) + + from pysus.api.client import PySUS + + client = PySUS(db_path=tmp_path / "config.db") + result = client.read_parquet([parquet_file], add_dv=True) + out = result.df() + + assert list(out.columns) == ["DT_NOTIFIC", "value"] + + def test_read_parquet_add_dv_false_returns_raw(self, tmp_path): + import pandas as pd + + parquet_file = tmp_path / "test.parquet" + df = pd.DataFrame({"ID_MUNICIP": ["261160"], "value": [1]}) + df.to_parquet(parquet_file) + + from pysus.api.client import PySUS + + client = PySUS(db_path=tmp_path / "config.db") + result = client.read_parquet([parquet_file], add_dv=False) + + from duckdb import DuckDBPyConnection + + assert isinstance(result, DuckDBPyConnection) + out = result.df() + assert out["ID_MUNICIP"].iloc[0] == "261160" + class TestPySUSGetMethods: @pytest.mark.asyncio diff --git a/pysus/tests/api/test_extensions.py b/pysus/tests/api/test_extensions.py index 936c4a1f..f7fc9559 100644 --- a/pysus/tests/api/test_extensions.py +++ b/pysus/tests/api/test_extensions.py @@ -124,6 +124,37 @@ async def test_parquet_parse_and_stream(tmp_dir): assert len(chunks) >= 1 +@pytest.mark.asyncio +async def test_parquet_load_applies_add_dv_to_geocode_columns(tmp_dir): + df = pd.DataFrame( + { + "ID_MUNICIP": ["261160", "530010"], + "DT_NOTIFIC": ["20230101", "20230102"], + } + ) + path = tmp_dir / "test.parquet" + df.to_parquet(path) + + pq_obj = Parquet(path=path, add_dv=True) + parsed = await pq_obj.load(parse=True) + + assert parsed["ID_MUNICIP"].iloc[0] == "2611606" + assert parsed["ID_MUNICIP"].iloc[1] == "5300108" + assert str(parsed["DT_NOTIFIC"].iloc[0]) == "2023-01-01" + + +@pytest.mark.asyncio +async def test_parquet_load_skips_add_dv_when_disabled(tmp_dir): + df = pd.DataFrame({"ID_MUNICIP": ["261160"]}) + path = tmp_dir / "test.parquet" + df.to_parquet(path) + + pq_obj = Parquet(path=path, add_dv=False) + parsed = await pq_obj.load(parse=True) + + assert parsed["ID_MUNICIP"].iloc[0] == "261160" + + @pytest.mark.asyncio async def test_dbf_decode_and_failure(tmp_dir): pytest.importorskip("dbfread") diff --git a/pysus/tests/api/test_utils.py b/pysus/tests/api/test_utils.py new file mode 100644 index 00000000..93ebbdd6 --- /dev/null +++ b/pysus/tests/api/test_utils.py @@ -0,0 +1,44 @@ +from pysus.api.utils import add_dv, is_geocode_column + + +def test_is_geocode_column_true(): + assert is_geocode_column("ID_MUNICIP") is True + assert is_geocode_column("ID_MN_RESI") is True + assert is_geocode_column("MUN_ACID") is True + assert is_geocode_column("COD_MUN_HO") is True + assert is_geocode_column("CO_MUN_EXP") is True + assert is_geocode_column("ID_MUNI_AT") is True + assert is_geocode_column("ID_MUNIC_A") is True + assert is_geocode_column("ID_MUNI_RE") is True + + +def test_is_geocode_column_false(): + assert is_geocode_column("DT_NOTIFIC") is False + assert is_geocode_column("SG_UF") is False + assert is_geocode_column("NM_PACIENT") is False + assert is_geocode_column("CS_SEXO") is False + assert is_geocode_column("") is False + + +def test_add_dv_6digit(): + assert add_dv("261160") == "2611606" + + +def test_add_dv_7digit_already_has_dv(): + assert add_dv("2611606") == "2611606" + + +def test_add_dv_miscalculated(): + assert add_dv("2201911") == "2201919" + + +def test_add_dv_none(): + assert add_dv(None) is None + + +def test_add_dv_empty(): + assert add_dv("") == "" + + +def test_add_dv_non_digit(): + assert add_dv("abc") == "abc"