diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c0612e5d..93d0f26b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,21 +15,21 @@ repos: hooks: - id: black args: [--line-length=80] - exclude: ^docs/ + exclude: ^docs/|^pysus/http/ - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort args: [--profile=black, --line-length=80] - exclude: ^.*/js/.*$ + exclude: ^.*/js/.*$|^pysus/http/ - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 args: [--max-line-length=80, --extend-ignore=E203] - exclude: ^docs/ + exclude: ^docs/|^pysus/http/ additional_dependencies: [ 'flake8-blind-except', 'flake8-bugbear', @@ -50,7 +50,7 @@ repos: 'pydantic>=2.0.0', ] args: [--ignore-missing-imports, --explicit-package-bases] - exclude: ^docs/ + exclude: ^docs/|^pysus/http/ - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 diff --git a/README.md b/README.md index a34c1d27..cc675833 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ PySUS is a Python package for accessing and analyzing Brazil's public health dat ## What's New in PySUS 2.0 - **Simplified API**: New high-level functions for direct DataFrame access -- **CLI & TUI**: Launch the text-based user interface from command line +- **Streamlit Web UI**: Launch a local web interface for browsing and downloading datasets - **Flexible Schema Modes**: Read multiple parquet files with union, intersection, or strict modes - **SQL Query**: Filter catalog queries by dataset, group, state, year, and month @@ -20,9 +20,9 @@ PySUS is a Python package for accessing and analyzing Brazil's public health dat pip install pysus ``` -For the terminal user interface (TUI): +For the local Streamlit web interface: ```bash -pip install pysus[tui] +pip install pysus[http] ``` ### Docker @@ -123,22 +123,34 @@ async def main(): df = pysus.read_parquet(paths, mode="union").df() ``` -### Using the TUI (unstable/under testing) +### Using the Streamlit Web UI (experimental feature) -Launch the interactive text-based interface: +Launch the local web interface: ```bash -pysus tui -l pt +pysus http ``` -Or from Python: +Or with a custom port: -```python -from pysus.tui.app import PySUS -app = PySUS(lang="pt") -app.run() +```bash +pysus http -p 8080 ``` +Or run directly with Streamlit: + +```bash +streamlit run pysus/http/app.py +``` + +The web interface provides three data sources: + +- **Default (DuckLake)**: Queries the PySUS S3 catalog — the primary data source. Select a dataset and filter by group, state, year, and month. +- **FTP DataSUS**: Browses legacy DATASUS FTP directories. Auto-connects on tab selection. +- **API DataSUS (DadosGov)**: Queries the dados.gov.br open data API. Requires an API token. + +Use the interactive filters to find files, add them to the download queue, and download with a single click. After a query, an expandable Python snippet shows the equivalent code to reproduce the same operation in a script or notebook. + ## Features - **Automatic Downloads**: Fetch data from FTP, DuckLake (S3), and dados.gov.br API @@ -146,7 +158,7 @@ app.run() - **DuckLake Integration**: S3-compatible cloud storage for parquet catalogs - **Local Catalog**: SQLite-based tracking of download history to avoid re-downloads - **Type Inference**: Automatic data type conversion from legacy formats (DBF, DBC) -- **CLI with TUI**: Command-line interface with interactive text-based UI +- **CLI with Streamlit UI**: Command-line interface with local web-based UI ## Architecture diff --git a/poetry.lock b/poetry.lock index a568981f..bf59fe7f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1734,10 +1734,9 @@ zstd = ["zstandard (>=0.18.0)"] name = "humanize" version = "4.15.0" description = "Python humanize utilities" -optional = true +optional = false python-versions = ">=3.10" groups = ["main"] -markers = "extra == \"tui\"" files = [ {file = "humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769"}, {file = "humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10"}, @@ -2416,28 +2415,6 @@ interegular = ["interegular (>=0.3.1,<0.4.0)"] nearley = ["js2py"] regex = ["regex"] -[[package]] -name = "linkify-it-py" -version = "2.1.0" -description = "Links recognition library with FULL unicode support." -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"}, -] - -[package.dependencies] -uc-micro-py = "*" - -[package.extras] -benchmark = ["pytest", "pytest-benchmark"] -dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] -doc = ["myst-parser", "sphinx", "sphinx_book_theme"] -test = ["coverage", "pytest", "pytest-cov"] - [[package]] name = "loguru" version = "0.6.0" @@ -2470,7 +2447,6 @@ files = [ ] [package.dependencies] -linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} mdurl = ">=0.1,<1.0" [package.extras] @@ -2690,27 +2666,6 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] -[[package]] -name = "mdit-py-plugins" -version = "0.6.1" -description = "Collection of plugins for markdown-it-py" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"tui\"" -files = [ - {file = "mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d"}, - {file = "mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0"}, -] - -[package.dependencies] -markdown-it-py = ">=2.0.0,<5.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["myst-parser", "sphinx-book-theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout"] - [[package]] name = "mdurl" version = "0.1.2" @@ -3360,12 +3315,11 @@ version = "4.9.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" -groups = ["main", "dev", "docs"] +groups = ["dev", "docs"] files = [ {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, ] -markers = {main = "extra == \"tui\""} [[package]] name = "pluggy" @@ -5025,46 +4979,6 @@ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] -[[package]] -name = "textual" -version = "8.2.7" -description = "Modern Text User Interface framework" -optional = true -python-versions = "<4.0,>=3.9" -groups = ["main"] -markers = "extra == \"tui\"" -files = [ - {file = "textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73"}, - {file = "textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105"}, -] - -[package.dependencies] -markdown-it-py = {version = ">=2.1.0", extras = ["linkify"]} -mdit-py-plugins = "*" -platformdirs = ">=3.6.0,<5" -pygments = ">=2.19.2,<3.0.0" -rich = ">=14.2.0" -tree-sitter = {version = ">=0.25.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-bash = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-css = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-go = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-html = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-java = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-javascript = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-json = {version = ">=0.24.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-markdown = {version = ">=0.3.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-python = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-regex = {version = ">=0.24.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-rust = {version = ">=0.23.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-sql = {version = ">=0.3.11", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-toml = {version = ">=0.6.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-xml = {version = ">=0.7.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -tree-sitter-yaml = {version = ">=0.6.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"syntax\""} -typing-extensions = ">=4.4.0,<5.0.0" - -[package.extras] -syntax = ["tree-sitter (>=0.25.0) ; python_version >= \"3.10\"", "tree-sitter-bash (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-css (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-go (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-html (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-java (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-javascript (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-json (>=0.24.0) ; python_version >= \"3.10\"", "tree-sitter-markdown (>=0.3.0) ; python_version >= \"3.10\"", "tree-sitter-python (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-regex (>=0.24.0) ; python_version >= \"3.10\"", "tree-sitter-rust (>=0.23.0) ; python_version >= \"3.10\"", "tree-sitter-sql (>=0.3.11) ; python_version >= \"3.10\"", "tree-sitter-toml (>=0.6.0) ; python_version >= \"3.10\"", "tree-sitter-xml (>=0.7.0) ; python_version >= \"3.10\"", "tree-sitter-yaml (>=0.6.0) ; python_version >= \"3.10\""] - [[package]] name = "tinycss2" version = "1.4.0" @@ -5200,397 +5114,6 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "mypy (>=1.7.0,<1.19) ; platform_python_implementation == \"PyPy\"", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] -[[package]] -name = "tree-sitter" -version = "0.25.2" -description = "Python bindings to the Tree-sitter parsing library" -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"}, - {file = "tree_sitter-0.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44488e0e78146f87baaa009736886516779253d6d6bac3ef636ede72bc6a8234"}, - {file = "tree_sitter-0.25.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2f8e7d6b2f8489d4a9885e3adcaef4bc5ff0a275acd990f120e29c4ab3395c5"}, - {file = "tree_sitter-0.25.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b570690f87f1da424cd690e51cc56728d21d63f4abd4b326d382a30353acc7"}, - {file = "tree_sitter-0.25.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0ec41b895da717bc218a42a3a7a0bfcfe9a213d7afaa4255353901e0e21f696"}, - {file = "tree_sitter-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:7712335855b2307a21ae86efe949c76be36c6068d76df34faa27ce9ee40ff444"}, - {file = "tree_sitter-0.25.2-cp310-cp310-win_arm64.whl", hash = "sha256:a925364eb7fbb9cdce55a9868f7525a1905af512a559303bd54ef468fd88cb37"}, - {file = "tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b"}, - {file = "tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26"}, - {file = "tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266"}, - {file = "tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c"}, - {file = "tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f"}, - {file = "tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc"}, - {file = "tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5"}, - {file = "tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960"}, - {file = "tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c"}, - {file = "tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99"}, - {file = "tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9"}, - {file = "tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac"}, - {file = "tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897"}, - {file = "tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5"}, - {file = "tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd"}, - {file = "tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601"}, - {file = "tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053"}, - {file = "tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614"}, - {file = "tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae"}, - {file = "tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b"}, - {file = "tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8"}, - {file = "tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0"}, - {file = "tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87"}, - {file = "tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab"}, - {file = "tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358"}, - {file = "tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0"}, - {file = "tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721"}, - {file = "tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f"}, -] - -[package.extras] -docs = ["sphinx (>=8.1,<9.0)", "sphinx-book-theme"] -tests = ["tree-sitter-html (>=0.23.2)", "tree-sitter-javascript (>=0.23.1)", "tree-sitter-json (>=0.24.8)", "tree-sitter-python (>=0.23.6)", "tree-sitter-rust (>=0.23.2)"] - -[[package]] -name = "tree-sitter-bash" -version = "0.25.1" -description = "Bash grammar for tree-sitter" -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"}, - {file = "tree_sitter_bash-0.25.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e76c4cfb20b076552406782b7f8c2a3946835993df0a44df006de54b7030c7dc"}, - {file = "tree_sitter_bash-0.25.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f484c4bb8796cde7a87ca351e6116f09653edac0eb3c6d238566359dd28b117"}, - {file = "tree_sitter_bash-0.25.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5e76af6df46d958c7f5b6d5884c9743218e3902a00ccb493ec92728b1084430b"}, - {file = "tree_sitter_bash-0.25.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3332d71c7b7d5f78259b19d02d0ea111fcb82b72712ee4a93aaa5b226d3f0a8"}, - {file = "tree_sitter_bash-0.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:52a6802d9218f86278aa3e8b459c3abdad67eed0fde1f9f13aca5b6c634217a6"}, - {file = "tree_sitter_bash-0.25.1-cp310-abi3-win_arm64.whl", hash = "sha256:59115057ec2bae319e8082ff29559861045002964c3431ccb0fc92aa4bc9bccb"}, - {file = "tree_sitter_bash-0.25.1.tar.gz", hash = "sha256:bfc0bdaa77bc1e86e3c6652e5a6e140c40c0a16b84185c2b63ad7cd809b88f14"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "tree-sitter-css" -version = "0.25.0" -description = "CSS grammar for tree-sitter" -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"}, - {file = "tree_sitter_css-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4f5e1135bfd01bce24e2fc7bca1381f52bdd6c6282ee28f7aa77185340bcd135"}, - {file = "tree_sitter_css-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b6d0084536828c733a66524a43c9df89f335971d5b1b973e9d1c42ba9dd426b"}, - {file = "tree_sitter_css-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8a83825daf538656cb88f4f7a0dd9963e3f204e83e7f8d92131f17e5bd712a77"}, - {file = "tree_sitter_css-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b486c097d250a598fba5f1f46f62697c7f4428252c8bdaad696a907ee913421d"}, - {file = "tree_sitter_css-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:fe319e4ad1b8327afbd9758b3ae22b09226d6c28dc9b022bcadabdaf6ea3716c"}, - {file = "tree_sitter_css-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:4fc2c82645cd593f1c695b4d6b678d71e633212ca030f26dedee4f92434bfe21"}, - {file = "tree_sitter_css-0.25.0.tar.gz", hash = "sha256:2fc996bf05b04e06061e88ee4c60837783dc4e62a695205acbc262ee30454138"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "tree-sitter-go" -version = "0.25.0" -description = "Go grammar for tree-sitter" -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"}, - {file = "tree_sitter_go-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04b3b3cb4aff18e74e28d49b716c6f24cb71ddfdd66768987e26e4d0fa812f74"}, - {file = "tree_sitter_go-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:148255aca2f54b90d48c48a9dbb4c7faad6cad310a980b2c5a5a9822057ed145"}, - {file = "tree_sitter_go-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4d338116cdf8a6c6ff990d2441929b41323ef17c710407abe0993c13417d6aad"}, - {file = "tree_sitter_go-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5608e089d2a29fa8d2b327abeb2ad1cdb8e223c440a6b0ceab0d3fa80bdeebae"}, - {file = "tree_sitter_go-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:30d4ada57a223dfc2c32d942f44d284d40f3d1215ddcf108f96807fd36d53022"}, - {file = "tree_sitter_go-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:d5d62362059bf79997340773d47cc7e7e002883b527a05cca829c46e40b70ded"}, - {file = "tree_sitter_go-0.25.0.tar.gz", hash = "sha256:a7466e9b8d94dda94cae8d91629f26edb2d26166fd454d4831c3bf6dfa2e8d68"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "tree-sitter-html" -version = "0.23.2" -description = "HTML grammar for tree-sitter" -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"}, - {file = "tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b3775732fffc0abd275a419ef018fd4c1ad4044b2a2e422f3378d93c30eded"}, - {file = "tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bdaa7ac5030d416aea0c512d4810ef847bbbd62d61e3d213f370b64ce147293"}, - {file = "tree_sitter_html-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2e9631b66041a4fd792d7f79a0c4128adb3bfc71f3dcb7e1a3eab5dbee77d67"}, - {file = "tree_sitter_html-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:85095f49f9e57f0ac9087a3e830783352c8447fdda55b1c1139aa47e5eaa0e21"}, - {file = "tree_sitter_html-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:0f65ed9e877144d0f04ade5644e5b0e88bf98a9e60bce65235c99905623e2f1a"}, - {file = "tree_sitter_html-0.23.2.tar.gz", hash = "sha256:bc9922defe23144d9146bc1509fcd00d361bf6b3303f9effee6532c6a0296961"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-java" -version = "0.23.5" -description = "Java grammar for tree-sitter" -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"}, - {file = "tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9401e7271f0b333df39fc8a8336a0caf1b891d9a2b89ddee99fae66b794fc5b7"}, - {file = "tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370b204b9500b847f6d0c5ad584045831cee69e9a3e4d878535d39e4a7e4c4f1"}, - {file = "tree_sitter_java-0.23.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aae84449e330363b55b14a2af0585e4e0dae75eb64ea509b7e5b0e1de536846a"}, - {file = "tree_sitter_java-0.23.5-cp39-abi3-win_amd64.whl", hash = "sha256:1ee45e790f8d31d416bc84a09dac2e2c6bc343e89b8a2e1d550513498eedfde7"}, - {file = "tree_sitter_java-0.23.5-cp39-abi3-win_arm64.whl", hash = "sha256:402efe136104c5603b429dc26c7e75ae14faaca54cfd319ecc41c8f2534750f4"}, - {file = "tree_sitter_java-0.23.5.tar.gz", hash = "sha256:f5cd57b8f1270a7f0438878750d02ccc79421d45cca65ff284f1527e9ef02e38"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-javascript" -version = "0.25.0" -description = "JavaScript grammar for tree-sitter" -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"}, - {file = "tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75"}, - {file = "tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b"}, - {file = "tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc"}, - {file = "tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54"}, - {file = "tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c"}, - {file = "tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b"}, - {file = "tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "tree-sitter-json" -version = "0.24.8" -description = "JSON grammar for tree-sitter" -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"}, - {file = "tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8627f7d375fda9fc193ebee368c453f374f65c2f25c58b6fea4e6b49a7fccbc"}, - {file = "tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85cca779872f7278f3a74eb38533d34b9c4de4fd548615e3361fa64fe350ad0a"}, - {file = "tree_sitter_json-0.24.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:deeb45850dcc52990fbb52c80196492a099e3fa3512d928a390a91cf061068cc"}, - {file = "tree_sitter_json-0.24.8-cp39-abi3-win_amd64.whl", hash = "sha256:e4849a03cd7197267b2688a4506a90a13568a8e0e8588080bd0212fcb38974e3"}, - {file = "tree_sitter_json-0.24.8-cp39-abi3-win_arm64.whl", hash = "sha256:591e0096c882d12668b88f30d3ca6f85b9db3406910eaaab6afb6b17d65367dd"}, - {file = "tree_sitter_json-0.24.8.tar.gz", hash = "sha256:ca8486e52e2d261819311d35cf98656123d59008c3b7dcf91e61d2c0c6f3120e"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-markdown" -version = "0.5.1" -description = "Markdown grammar for tree-sitter" -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"}, - {file = "tree_sitter_markdown-0.5.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727242a70c46222092eba86c102301646f21ba32aee221f4b1f70e2020755e81"}, - {file = "tree_sitter_markdown-0.5.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b2fde19e692bb90e300d9788887528c624b659c794de6337f8193396de4399"}, - {file = "tree_sitter_markdown-0.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:13da82db04cec7910b6afd4a67d02da9ef402df8d56fc6ed85e00584af1730ee"}, - {file = "tree_sitter_markdown-0.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8a8a04a5d942c177cc590ec40074fcf3658f3a7c0a3388a8575990003665d8c"}, - {file = "tree_sitter_markdown-0.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:b1b0e4cbcf5a7b85005f1e9266fc2ed9b649b41a6048f3b1abae3612368d97a6"}, - {file = "tree_sitter_markdown-0.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:2296ef53a757d8f5b848616706d0518e04d487bc7748bd05755d4a3a65711542"}, - {file = "tree_sitter_markdown-0.5.1.tar.gz", hash = "sha256:6c69d7270a7e09be8988ced44584c09a6a4f541cea0dc394dd1c1a5ac3b5601d"}, -] - -[package.extras] -core = ["tree-sitter (>=0.23,<1.0)"] - -[[package]] -name = "tree-sitter-python" -version = "0.25.0" -description = "Python grammar for tree-sitter" -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"}, - {file = "tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5"}, - {file = "tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b"}, - {file = "tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d"}, - {file = "tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683"}, - {file = "tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76"}, - {file = "tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb"}, - {file = "tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "tree-sitter-regex" -version = "0.25.0" -description = "Regex grammar for tree-sitter" -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"}, - {file = "tree_sitter_regex-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cdd92400fd9d8229e584c55e12410251561f0d47eea49db17805e2f64a8b2490"}, - {file = "tree_sitter_regex-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cceab1c14deeec9c5899babcb2b7942f0607b4355e66eab4083514f644f1bd52"}, - {file = "tree_sitter_regex-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:253436be178150ca4a0603720e0c246e08b5bdd2dc6df313667d97e6c0fce846"}, - {file = "tree_sitter_regex-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:883eacc46fd7eaffc328efd5865f1fe8825711892d3a89fccc2c414b061e806d"}, - {file = "tree_sitter_regex-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:f0f2ebf9a6bb5d0d0da2a8ac51d7e5a985b87cdb24d86db5ddc6a58baf115d5d"}, - {file = "tree_sitter_regex-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:d5a36150daa452f8aec1c2d6d1f2d26255dc05d1490f9618b14c12a6a648cda4"}, - {file = "tree_sitter_regex-0.25.0.tar.gz", hash = "sha256:5d29111b3f27d4afb31496476d392d1f562fe0bfe954e8968f1d8683424fc331"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "tree-sitter-rust" -version = "0.24.2" -description = "Rust grammar for tree-sitter" -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"}, - {file = "tree_sitter_rust-0.24.2-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e033c5a93b57c88e0a835880de39fc802909ff69f57aaff6000211c196ea5190"}, - {file = "tree_sitter_rust-0.24.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d76d1208c3638b871236090759dfc13d478921320653a6c9da5336e7c58f65a"}, - {file = "tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87930163a462408c49ab62c667e74029bc26b4cc7123dd1bdc7352215786c64a"}, - {file = "tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:da2b86099028fd42c6cd32878b7b16b01f8aac0f7b0e98742b7fa6bc3cf09b89"}, - {file = "tree_sitter_rust-0.24.2-cp39-abi3-win_amd64.whl", hash = "sha256:4529c125d928882ddfb879fdc6bc0704913261ecc078b6fa7902559e0daf200d"}, - {file = "tree_sitter_rust-0.24.2-cp39-abi3-win_arm64.whl", hash = "sha256:66ba90f61bd54f4c4f5d30434957daf64507c16b0313df76becb37d63f70a227"}, - {file = "tree_sitter_rust-0.24.2.tar.gz", hash = "sha256:54fb02a5911e345308b405174465112479f56dc39e3f1e7744d7568595f00db9"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-sql" -version = "0.3.11" -description = "Tree-sitter Grammar for SQL" -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"}, - {file = "tree_sitter_sql-0.3.11-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:344e99b59c8c8d72f7154041e9d054400f4a3fccc16c2c96ac106dde0e7f8d0c"}, - {file = "tree_sitter_sql-0.3.11-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5128b12f71ac0f5ebcc607f67a62cdc56a187c1a5ba7553feeb9c5f6f9bc3c72"}, - {file = "tree_sitter_sql-0.3.11-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:03cc164fcf7b1f711e7d939aeb4d1f62c76f4162e081c70b860b4fcd91806a38"}, - {file = "tree_sitter_sql-0.3.11-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0e22ea8de690dd9960d8c0c36c4cd25417b084e1e29c91ac0235fbdb3abb4664"}, - {file = "tree_sitter_sql-0.3.11-cp310-abi3-win_amd64.whl", hash = "sha256:c57b877702d218c0856592d33320c02b2dc8411d8820b3bf7b81be86c54fa0bb"}, - {file = "tree_sitter_sql-0.3.11-cp310-abi3-win_arm64.whl", hash = "sha256:8a1e42f0a2c9b01b23074708ecf5b8d21b9a0440e3dff279d8cf466cdf1a877e"}, - {file = "tree_sitter_sql-0.3.11.tar.gz", hash = "sha256:700b93be2174c3c83d174ec3e10b682f72a4fb451f0076c7ce5012f1d5a76cbc"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "tree-sitter-toml" -version = "0.7.0" -description = "TOML grammar for tree-sitter" -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"}, - {file = "tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a045e0acfcf91b7065066f7e51ea038ed7385c1e35e7e8fae18f252d3f8adb8c"}, - {file = "tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2f8cf9d73f07b6628093b35e5c5fbac039247e32cb075eaa5289a5914e73af"}, - {file = "tree_sitter_toml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:860ffa4513b2dc3083d8e412bd815a350b0a9490624b37e7c8f6ed5c6f9ce63c"}, - {file = "tree_sitter_toml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:2760a04f06937b01b1562a2135cd7e8207e399e73ef75bbebc77e37b1ad3b15d"}, - {file = "tree_sitter_toml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fd00fd8a51c65aa19c40539431cb1773d87c30af5757b4041fa6c229058420b4"}, - {file = "tree_sitter_toml-0.7.0.tar.gz", hash = "sha256:29e257612fa8f0c1fcbc4e7e08ddc561169f1725265302e64d81086354144a70"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-xml" -version = "0.7.0" -description = "XML & DTD grammars for tree-sitter" -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"}, - {file = "tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c0fe5f2d6cc09974c8375c8ea9b24909f493b5bf04aacdc4c694b5d2ae6b040"}, - {file = "tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd3209516a4d84dff90bc91d2ad2ce246de8504cede4358849687fa8e71536e7"}, - {file = "tree_sitter_xml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87578e15fa55f44ecd9f331233b6f8a2cbde3546b354c830ecb862a632379455"}, - {file = "tree_sitter_xml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:9ba2dafc6ce9feaf4ccc617d3aeea57f8e0ca05edad34953e788001ebff79133"}, - {file = "tree_sitter_xml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fc759f710a8fd7a01c23e2d7cb013679199045bea3dc0e5151650a11322aaf40"}, - {file = "tree_sitter_xml-0.7.0.tar.gz", hash = "sha256:ab0ff396f20230ad8483d968151ce0c35abe193eb023b20fbd8b8ce4cf9e9f61"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-yaml" -version = "0.7.2" -description = "YAML grammar for tree-sitter" -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"}, - {file = "tree_sitter_yaml-0.7.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1a5c60c98b6c4c037aae023569f020d0c489fad8dc26fdfd5510363c9c29a41"}, - {file = "tree_sitter_yaml-0.7.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88636d19d0654fd24f4f242eaaafa90f6f5ebdba8a62e4b32d251ed156c51a2a"}, - {file = "tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d2e8f0bb14aa4537320952d0f9607eef3021d5aada8383c34ebeece17db1e06"}, - {file = "tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:74ca712c50fc9d7dbc68cb36b4a7811d6e67a5466b5a789f19bf8dd6084ef752"}, - {file = "tree_sitter_yaml-0.7.2-cp310-abi3-win_amd64.whl", hash = "sha256:7587b5ca00fc4f9a548eff649697a3b395370b2304b399ceefa2087d8a6c9186"}, - {file = "tree_sitter_yaml-0.7.2-cp310-abi3-win_arm64.whl", hash = "sha256:f63c227b18e7ce7587bce124578f0bbf1f890ac63d3e3cd027417574273642c4"}, - {file = "tree_sitter_yaml-0.7.2.tar.gz", hash = "sha256:756db4c09c9d9e97c81699e8f941cb8ce4e51104927f6090eefe638ee567d32c"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - [[package]] name = "typeguard" version = "4.5.2" @@ -5681,22 +5204,6 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] -[[package]] -name = "uc-micro-py" -version = "2.0.0" -description = "Micro subset of unicode data files for linkify-it-py projects." -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"}, -] - -[package.extras] -test = ["coverage", "pytest", "pytest-cov"] - [[package]] name = "unidecode" version = "1.4.0" @@ -5841,9 +5348,9 @@ files = [ dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [extras] -tui = ["humanize", "textual"] +http = [] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.14" -content-hash = "d1f063bac8e016d4ccc3dae4ddca6d126d654f598d8ced0ead4541d36b5fc287" +content-hash = "2fdd6705ff86f8c17e9ab4a869440f57910087935a3c6a434d9b7ff9e2398208" diff --git a/pyproject.toml b/pyproject.toml index 8167e588..6bdc8048 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,8 @@ exclude = [ "pysus/tests", "pysus/tests/**", "pysus/management", - "pysus/management/**" + "pysus/management/**", + "pysus/http/**", ] [tool.poetry.dependencies] @@ -43,12 +44,10 @@ pyreaddbc = ">=2.0.4" dotenv = "^0.9.9" boto3 = "^1.42.89" typer = "^0.24.1" - -humanize = { version = "^4.8.0", optional = true } -textual = { extras = ["syntax"], version = "^8.2.1", optional = true } +humanize = "^4.8.0" [tool.poetry.extras] -tui = ["textual", "humanize"] +http = ["streamlit"] [tool.poetry.group.dev.dependencies] pytest = ">=6.1.0" @@ -107,6 +106,7 @@ exclude = ["*.git", "docs/"] omit = [ "pysus/management/client.py", "pysus/tui/*", + "pysus/http/*", ] [[tool.mypy.overrides]] diff --git a/pysus/cli/__init__.py b/pysus/cli/__init__.py index 69b2381d..b998853d 100644 --- a/pysus/cli/__init__.py +++ b/pysus/cli/__init__.py @@ -5,28 +5,52 @@ @app.command() -def tui( - lang: str = typer.Option( # noqa - "en", - "-l", - "--lang", - help="Language (en, pt)", +def version(): + print(__version__) + + +@app.command() +def http( + port: int = typer.Option( # noqa: B008 + 8501, + "-p", + "--port", + help="Port to bind the server to", ), ): + """Launch the local Streamlit visual interface.""" try: - from pysus.tui.app import PySUS + import streamlit.web.bootstrap as bootstrap # noqa + from streamlit.runtime.scriptrunner import get_script_run_ctx # noqa except ImportError: raise ImportError( - "The TUI requires extra dependencies. " - "Install them with: pip install pysus[tui]" + "The HTTP UI requires extra dependencies. " + "Install them with: pip install pysus[http]" ) - app = PySUS(lang=lang) - app.run() + import os + import sys + import webbrowser + app_path = os.path.join(os.path.dirname(__file__), "..", "http", "app.py") + app_path = os.path.abspath(app_path) -@app.command() -def version(): - print(__version__) + from streamlit.web import cli as stcli + + sys.argv = [ + "streamlit", + "run", + app_path, + "--server.port", + str(port), + "--server.headless", + "true", + "--server.address", + "localhost", + ] + + webbrowser.open(f"http://localhost:{port}") + + stcli.main() if __name__ == "__main__": diff --git a/pysus/http/__init__.py b/pysus/http/__init__.py new file mode 100644 index 00000000..c16bcf3f --- /dev/null +++ b/pysus/http/__init__.py @@ -0,0 +1,3 @@ +"""PySUS HTTP — Streamlit-based visual interface for localhost use.""" + +from pysus.http.app import home # noqa diff --git a/pysus/http/app.py b/pysus/http/app.py new file mode 100644 index 00000000..b79a3049 --- /dev/null +++ b/pysus/http/app.py @@ -0,0 +1,90 @@ +"""PySUS Streamlit App — localhost visual interface for the PySUS package. + +Run with: + streamlit run pysus/http/app.py + or + pysus http +""" + +import asyncio + +import streamlit as st + +from pysus import __version__ +from pysus.http.translations import t +from pysus.api.client import PySUS + +LANGUAGES = {"English": "en", "Português": "pt"} +LANG_LABELS = {v: k for k, v in LANGUAGES.items()} + +st.set_page_config( + page_title="PySUS", + page_icon=":hospital:", + layout="wide", +) + +st.markdown( + """ + + """, + unsafe_allow_html=True, +) + + +@st.cache_data(show_spinner="loading datasets...") +def _load_catalog() -> None: + async def _fetch(): + async with PySUS(): + return + + return asyncio.run(_fetch()) + + +def _init_lang() -> None: + if "lang" not in st.session_state: + st.session_state.lang = "en" + + +def _on_lang_change() -> None: + label = st.session_state.get("_lang_select", "English") + st.session_state.lang = LANGUAGES[label] + + +def _lang_selector() -> None: + _init_lang() + current_label = LANG_LABELS.get(st.session_state.lang, "English") + st.sidebar.selectbox( + t("lang_label", st.session_state.lang), + list(LANGUAGES.keys()), + index=list(LANGUAGES.keys()).index(current_label), + key="_lang_select", + on_change=_on_lang_change, + ) + + +def home() -> None: + _lang_selector() + lang: str = st.session_state.lang + + st.title("Datasets") + + +if __name__ == "__main__": + _init_lang() + _load_catalog() + lang = st.session_state.lang + + home_page = st.Page(home, title=f"🏠️ {t('home_page', lang)}", default=True) + client_page = st.Page("pages/1_client.py", title="📥️ Downloads") + + examples_page = st.Page("pages/2_examples.py", title="Examples") + + st.logo( + "https://raw.githubusercontent.com/luabida/PySUS/1001b6bf8c294ab20a7432e66434755b6d6250d5/pysus/http/assets/logo_large.svg", + icon_image="https://raw.githubusercontent.com/luabida/PySUS/1001b6bf8c294ab20a7432e66434755b6d6250d5/pysus/http/assets/logo.svg", + ) + + pg = st.navigation({"": [home_page, client_page], "docs": [examples_page]}) + pg.run() diff --git a/pysus/http/assets/logo.svg b/pysus/http/assets/logo.svg new file mode 100644 index 00000000..44c09e79 --- /dev/null +++ b/pysus/http/assets/logo.svg @@ -0,0 +1,7 @@ + + + Layer 1 + PySUS + + + diff --git a/pysus/http/assets/logo_large.svg b/pysus/http/assets/logo_large.svg new file mode 100644 index 00000000..5c11a76b --- /dev/null +++ b/pysus/http/assets/logo_large.svg @@ -0,0 +1,9 @@ + + + Layer 1 + PySUS + Web + Server + + + diff --git a/pysus/http/pages/1_client.py b/pysus/http/pages/1_client.py new file mode 100644 index 00000000..8617de12 --- /dev/null +++ b/pysus/http/pages/1_client.py @@ -0,0 +1,766 @@ +import asyncio +from collections.abc import Coroutine +from typing import Any + +import streamlit as st +from humanize import naturalsize + +from pysus import CACHEPATH +from pysus.api.client import PySUS +from pysus.api.models import BaseRemoteFile +from pysus.http.translations import t + +STATES = [ + "AC", + "AL", + "AP", + "AM", + "BA", + "CE", + "ES", + "GO", + "MA", + "MT", + "MS", + "MG", + "PA", + "PB", + "PR", + "PE", + "PI", + "RJ", + "RN", + "RS", + "RO", + "RR", + "SC", + "SP", + "SE", + "TO", + "DF", +] + +CLIENTS = { + "ducklake": "ducklake_label", + "ftp": "ftp_label", + "dadosgov": "dadosgov_label", +} + + +def _lang() -> str: + return st.session_state.get("lang", "en") + + +@st.cache_resource +def _get_orchestrator() -> PySUS: + return PySUS() + + +def _run_async(coro: Coroutine[Any, Any, Any]) -> Any: + return asyncio.run(coro) + + +def _clear_options_cache(client: str) -> None: + for key in list(st.session_state.keys()): + key = str(key) + if key.startswith(f"_opts_{client}_"): + del st.session_state[key] + + +def _cached_datasets(pysus: PySUS, client: str) -> list[Any]: + cache_key = f"_datasets_{client}" + if cache_key not in st.session_state: + + async def _load(): + if client == "ducklake": + if pysus._ducklake is None: + return [] + return await pysus._ducklake.datasets() + elif client == "ftp": + ftp = await pysus.get_ftp() + return await ftp.datasets() + elif client == "dadosgov": + if pysus._dadosgov is not None: + return await pysus._dadosgov.datasets() + return [] + return [] + + result = _run_async(_load()) + if not result: + return [] + st.session_state[cache_key] = result + return st.session_state[cache_key] + + +def _cached_options( + client: str, datasets: list[Any], ds_name: str, opt_type: str +) -> list[Any]: + cache_key = f"_opts_{client}_{ds_name}_{opt_type}" + if cache_key not in st.session_state: + if opt_type == "group": + st.session_state[cache_key] = _get_group_options(client, datasets, ds_name) + elif opt_type == "year": + st.session_state[cache_key] = _get_year_options(client, datasets, ds_name) + elif opt_type == "month": + st.session_state[cache_key] = _get_month_options(client, datasets, ds_name) + return st.session_state[cache_key] + + +def _get_group_options(client: str, datasets: list[Any], ds_name: str) -> list[str]: + if not ds_name: + return [] + target = next((d for d in datasets if d.name.upper() == ds_name.upper()), None) + if target is None: + return [] + + if client == "ducklake": + from sqlalchemy import select + from pysus.api.ducklake.catalog.orm.dataset import Group as OrmGroup + + async def _fetch() -> list[str]: + await target.adapter.connect() + with target.adapter.get_session() as session: + stmt = select(OrmGroup).filter(OrmGroup.dataset_id == target.id) + orm_groups = session.scalars(stmt).all() + return sorted(g.name for g in orm_groups) + + return _run_async(_fetch()) + + if client == "ftp": + return sorted(target.group_definitions.keys()) + + if client == "dadosgov": + return sorted(target.group_aliases.values()) + + return [] + + +def _get_year_options(client: str, datasets: list[Any], ds_name: str) -> list[int]: + if not ds_name: + return [] + if client != "ducklake": + return [] + target = next((d for d in datasets if d.name.upper() == ds_name.upper()), None) + if target is None: + return [] + + from sqlalchemy import distinct, select + from pysus.api.ducklake.catalog.orm.dataset import File as OrmFile + + async def _fetch() -> list[int]: + await target.adapter.connect() + with target.adapter.get_session() as session: + stmt = ( + select(distinct(OrmFile.year)) + .filter(OrmFile.dataset_id == target.id, OrmFile.year.isnot(None)) + .order_by(OrmFile.year) + ) + return sorted(y for y in session.scalars(stmt).all()) + + return _run_async(_fetch()) + + +def _get_month_options(client: str, datasets: list[Any], ds_name: str) -> list[int]: + if not ds_name: + return [] + if client != "ducklake": + return [] + target = next((d for d in datasets if d.name.upper() == ds_name.upper()), None) + if target is None: + return [] + + from sqlalchemy import distinct, select + from pysus.api.ducklake.catalog.orm.dataset import File as OrmFile + + async def _fetch() -> list[int]: + await target.adapter.connect() + with target.adapter.get_session() as session: + stmt = ( + select(distinct(OrmFile.month)) + .filter(OrmFile.dataset_id == target.id, OrmFile.month.isnot(None)) + .order_by(OrmFile.month) + ) + return sorted(m for m in session.scalars(stmt).all()) + + return _run_async(_fetch()) + + +def _render_year_filter(client: str, year_options: list[int]) -> list[int] | None: + if year_options: + return ( + st.multiselect( + t("year", _lang()), + year_options, + placeholder=t("select_years", _lang()), + ) + or None + ) + if client != "ducklake": + raw = st.text_input(t("year", _lang()), placeholder="2020, 2021, ...") + if raw.strip(): + try: + return [int(y.strip()) for y in raw.split(",") if y.strip()] + except ValueError: + st.error(t("invalid_year", _lang())) + return None + return None + + +def _render_month_filter(client: str, month_options: list[int]) -> list[int] | None: + if month_options: + return ( + st.multiselect( + t("month", _lang()), + month_options, + placeholder=t("select_months", _lang()), + ) + or None + ) + if client != "ducklake": + raw = st.text_input(t("month", _lang()), placeholder="1, 2, 3, ...") + if raw.strip(): + try: + return [int(m.strip()) for m in raw.split(",") if m.strip()] + except ValueError: + st.error(t("invalid_month", _lang())) + return None + return None + + +def _render_ducklake_filters(pysus: PySUS) -> None: + if pysus._ducklake is None: + with st.spinner(t("loading_catalog", _lang())): + try: + _run_async(pysus.get_ducklake()) + except Exception: + st.warning(t("catalog_failed", _lang())) + return + datasets = _cached_datasets(pysus, "ducklake") + if not datasets: + st.warning(t("catalog_failed", _lang())) + return + ds_names = sorted(d.name.upper() for d in datasets) + + selected_ds = st.selectbox( + t("dataset", _lang()), + ds_names, + index=None, + placeholder=t("browser_choose", _lang()), + key=f"_ds_ducklake", + ) + ds_key = selected_ds or "" + + _prev = st.session_state.get("_prev_ds_ducklake") + if selected_ds and selected_ds != _prev: + st.session_state.pop("_query_results_ducklake", None) + st.session_state.pop("_query_dataset_ducklake", None) + st.session_state.pop("_query_params_ducklake", None) + st.session_state["_prev_ds_ducklake"] = selected_ds + + if not selected_ds: + return + + group_options = _cached_options("ducklake", datasets, ds_key, "group") + year_options = _cached_options("ducklake", datasets, ds_key, "year") + month_options = _cached_options("ducklake", datasets, ds_key, "month") + + col1, col2, col3 = st.columns(3) + with col1: + group = ( + st.multiselect( + t("group", _lang()), + group_options, + placeholder=t("select_groups", _lang()), + ) + or None + ) + with col2: + state = ( + st.multiselect( + t("state", _lang()), STATES, placeholder=t("select_states", _lang()) + ) + or None + ) + with col3: + year = _render_year_filter("ducklake", year_options) + + month = _render_month_filter("ducklake", month_options) + + if st.button(t("fetch", _lang()), width="stretch"): + _clear_options_cache("ducklake") + _parse_and_query(pysus, "ducklake", selected_ds, group, state, year, month) + + +def _render_ftp_filters(pysus: PySUS) -> None: + try: + datasets = _cached_datasets(pysus, "ftp") + except Exception as exc: + st.warning(f"{t('ftp_failed', _lang())} {exc}") + return + if not datasets: + return + ds_names = [d.name for d in datasets] + selected_ds = st.selectbox( + t("dataset", _lang()), + ds_names, + index=None, + placeholder=t("browser_choose", _lang()), + key=f"_ds_ftp", + ) + ds_key = selected_ds or "" + + _prev = st.session_state.get("_prev_ds_ftp") + if selected_ds and selected_ds != _prev: + st.session_state.pop("_query_results_ftp", None) + st.session_state.pop("_query_dataset_ftp", None) + st.session_state.pop("_query_params_ftp", None) + st.session_state["_prev_ds_ftp"] = selected_ds + + if not selected_ds: + return + + group_options = _cached_options("ftp", datasets, ds_key, "group") + + col1, col2, col3 = st.columns(3) + with col1: + group = ( + st.multiselect( + t("group", _lang()), + group_options, + placeholder=t("select_groups", _lang()), + ) + or None + ) + with col2: + state = ( + st.multiselect( + t("state", _lang()), STATES, placeholder=t("select_states", _lang()) + ) + or None + ) + with col3: + year = _render_year_filter("ftp", []) + + month = _render_month_filter("ftp", []) + + if st.button(t("fetch", _lang()), width="stretch"): + _parse_and_query(pysus, "ftp", selected_ds, group, state, year, month) + + +def _render_dadosgov_filters(pysus: PySUS) -> None: + token = st.text_input( + t("api_token", _lang()), + type="password", + placeholder=t("token_placeholder", _lang()), + ) + + if not token: + st.info(t("token_required", _lang())) + return + + if st.button(t("connect", _lang()), width="stretch"): + with st.spinner(t("connecting_dadosgov", _lang())): + _run_async(pysus.get_dadosgov(token)) + if "_datasets_dadosgov" in st.session_state: + del st.session_state["_datasets_dadosgov"] + + if pysus._dadosgov is None: + return + + datasets = _cached_datasets(pysus, "dadosgov") + ds_names = [d.name for d in datasets] + selected_ds = st.selectbox( + t("dataset", _lang()), + ds_names, + index=None, + placeholder=t("browser_choose", _lang()), + key=f"_ds_dadosgov", + ) + ds_key = selected_ds or "" + + _prev = st.session_state.get("_prev_ds_dadosgov") + if selected_ds and selected_ds != _prev: + st.session_state.pop("_query_results_dadosgov", None) + st.session_state.pop("_query_dataset_dadosgov", None) + st.session_state.pop("_query_params_dadosgov", None) + st.session_state["_prev_ds_dadosgov"] = selected_ds + + if not selected_ds: + return + + group_options = _cached_options("dadosgov", datasets, ds_key, "group") + + col1, col2, col3 = st.columns(3) + with col1: + group = ( + st.multiselect( + t("group", _lang()), + group_options, + placeholder=t("select_groups", _lang()), + ) + or None + ) + with col2: + state = ( + st.multiselect( + t("state", _lang()), STATES, placeholder=t("select_states", _lang()) + ) + or None + ) + with col3: + year = _render_year_filter("dadosgov", []) + + month = _render_month_filter("dadosgov", []) + + if st.button(t("fetch", _lang()), width="stretch"): + _parse_and_query(pysus, "dadosgov", selected_ds, group, state, year, month) + + +def _parse_and_query( + pysus: PySUS, + client: str, + ds_name: str, + group: list[str] | None, + state: list[str] | None, + year: list[int] | None, + month: list[int] | None, +) -> None: + if not ds_name: + st.warning(t("select_dataset", _lang())) + return + + groups = group or None + states = state or None + year_vals = year or None + month_vals = month or None + + with st.spinner(t("querying", _lang(), client=client)): + files = _run_async( + _query_client( + pysus, + client, + ds_name, + groups, + states, + year_vals, + month_vals, + ) + ) + + if not files: + st.info(t("no_files", _lang())) + return + + st.session_state[f"_query_results_{client}"] = files + st.session_state[f"_query_dataset_{client}"] = ds_name + st.session_state[f"_query_params_{client}"] = { + "dataset": ds_name, + "group": groups, + "state": states, + "year": year_vals, + "month": month_vals, + } + st.success(t("files_found", _lang(), count=str(len(files)))) + + +async def _ftp_dadosgov_search( + target, groups, states, years, months +) -> list[BaseRemoteFile]: + all_files = await target.search() + if groups: + all_files = [f for f in all_files if getattr(f.group, "name", None) in groups] + if states: + all_files = [f for f in all_files if getattr(f, "state", None) in states] + if years: + all_files = [f for f in all_files if getattr(f, "year", None) in years] + if months: + all_files = [f for f in all_files if getattr(f, "month", None) in months] + return all_files + + +async def _query_client( + pysus: PySUS, + client: str, + ds_name: str, + groups: list[str] | None, + states: list[str] | None, + years: list[int] | None, + months: list[int] | None, +) -> list[BaseRemoteFile]: + if client == "ducklake": + return await pysus.query( + dataset=ds_name, + group=groups, + state=states, + year=years, + month=months, + ) + + if client == "ftp": + ftp_client = await pysus.get_ftp() + datasets = await ftp_client.datasets() + target = next((d for d in datasets if d.name.upper() == ds_name.upper()), None) + if target is None: + return [] + try: + return await _ftp_dadosgov_search(target, groups, states, years, months) + except (ConnectionResetError, BrokenPipeError, OSError): + await ftp_client.close() + pysus._ftp = None + ftp_client = await pysus.get_ftp() + datasets = await ftp_client.datasets() + target = next( + (d for d in datasets if d.name.upper() == ds_name.upper()), None + ) + if target is None: + return [] + return await _ftp_dadosgov_search(target, groups, states, years, months) + else: + dadosgov_client = await pysus.get_dadosgov(None) + datasets = await dadosgov_client.datasets() # type: ignore[assignment] + target = next((d for d in datasets if d.name.upper() == ds_name.upper()), None) + if target is None: + return [] + return await _ftp_dadosgov_search(target, groups, states, years, months) + + +def _build_file_row(f: BaseRemoteFile, queued: bool = False) -> dict[str, Any]: + record = getattr(f, "record", None) + year = record.year if record is not None else getattr(f, "year", None) + month = record.month if record is not None else getattr(f, "month", None) + state = record.state if record is not None else getattr(f, "state", None) + if record is not None and record.group is not None: + group_name = record.group.name + elif f.group is not None: + group_name = getattr(f.group, "name", "") + else: + group_name = "" + + return { + "File": f.basename, + "Size": naturalsize(f.size), + "Year": year, + "Month": month, + "State": state, + "Group": group_name, + "Queued": "✅" if queued else "", + } + + +def _show_results(pysus: PySUS, client: str) -> None: + query_key = f"_query_results_{client}" + queue_key = f"_download_queue_{client}" + files = st.session_state.get(query_key, []) + + if not files: + return + + if queue_key not in st.session_state: + st.session_state[queue_key] = [] + + download_queue = st.session_state[queue_key] + queued_paths = {str(f.path) for f in download_queue} + + # --- Box 1: Query Results --- + st.subheader(t("results_title", _lang(), count=str(len(files)))) + + import pandas as pd + + df = pd.DataFrame([_build_file_row(f, str(f.path) in queued_paths) for f in files]) + + event = st.dataframe( + df, + width="stretch", + hide_index=True, + on_select="rerun", + selection_mode="multi-row", + ) + + selected_indices = event.selection.get("rows", []) # type: ignore[attr-defined] + + col1, col2 = st.columns([3, 1]) + with col2: + if selected_indices and st.button(t("add_to_queue", _lang()), width="stretch"): + added = 0 + for idx in selected_indices: + f = files[idx] + if str(f.path) not in queued_paths: + st.session_state[queue_key].append(f) + added += 1 + if added: + st.rerun() + + # --- Box 2: Download Queue --- + st.divider() + st.subheader(t("queue_title", _lang(), count=str(len(download_queue)))) + + if not download_queue: + st.caption(t("queue_empty", _lang())) + return + + queue_df = pd.DataFrame( + [ + {"": idx, "File": f.basename, "Size": naturalsize(f.size)} + for idx, f in enumerate(download_queue) + ] + ).set_index("") + + selection = st.dataframe( + queue_df, + width="stretch", + height=min(35 * len(download_queue) + 38, 250), + on_select="rerun", + selection_mode="multi-row", + ) + + remove_indices = selection.selection.get("rows", []) # type: ignore[attr-defined] + + dataset_name = st.session_state.get(f"_query_dataset_{client}", "") + default_dir = str(CACHEPATH / "downloads" / client / (dataset_name or "data")) + + col1, col2, col3, col4 = st.columns([3, 1, 1, 2]) + with col1: + download_dir = st.text_input( + t("save_to", _lang()), + value=default_dir, + key=f"_dl_dir_{client}", + label_visibility="collapsed", + placeholder=t("download_dir_placeholder", _lang()), + ) + with col2: + if remove_indices and st.button(t("remove", _lang()), width="stretch"): + for idx in sorted(remove_indices, reverse=True): + st.session_state[queue_key].pop(idx) + st.rerun() + with col3: + if st.button(t("clear", _lang()), width="stretch"): + st.session_state[queue_key] = [] + st.rerun() + with col4: + if st.button(t("download", _lang()), width="stretch", type="primary"): + _download_selected(pysus, client, download_queue, download_dir) + + +def _download_selected( + pysus: PySUS, + client: str, + files: list[BaseRemoteFile], + output_dir: str, +) -> None: + queue_key = f"_download_queue_{client}" + total = len(files) + progress = st.progress(0, text=t("download_start", _lang())) + + async def _download(): + for i, f in enumerate(files): + progress.progress( + (i + 1) / total, + text=t( + "downloading", + _lang(), + name=f.basename, + i=str(i + 1), + total=str(total), + ), + ) + try: + await pysus.download(file=f) + except Exception as exc: + st.error(t("download_failed", _lang(), name=f.basename, error=str(exc))) + + _run_async(_download()) + progress.progress(1.0, text=t("download_done", _lang())) + st.session_state[queue_key] = [] + st.success(t("download_success", _lang(), count=str(total), dir=output_dir)) + + +def _build_query_code(client: str) -> str | None: + params = st.session_state.get(f"_query_params_{client}") + files = st.session_state.get(f"_query_results_{client}") + if not params or not files: + return None + + def _fmt_args(exclude_dataset: bool = False) -> str: + lines = [] + for k in ("dataset", "group", "state", "year", "month"): + if exclude_dataset and k == "dataset": + continue + v = params.get(k) + if v: + lines.append(f" {k}={v!r},") + return "\n".join(lines) + + if client == "ducklake": + return ( + "from pysus.api.client import PySUS\n" + "\n" + "async with PySUS() as pysus:\n" + " files = await pysus.query(\n" + _fmt_args() + "\n )\n" + " for f in files:\n" + " print(f.basename)\n" + " await pysus.download(file=f)\n" + ) + + if client == "ftp": + return ( + "from pysus.api.client import PySUS\n" + "\n" + "async with PySUS() as pysus:\n" + " ftp = await pysus.get_ftp()\n" + " datasets = await ftp.datasets()\n" + " ds = next(d for d in datasets if d.name == " + + repr(params["dataset"]) + + ")\n" + " files = await ds.search(\n" + + _fmt_args(exclude_dataset=True) + + "\n )\n" + " for f in files:\n" + " print(f.basename)\n" + " await pysus.download(file=f)\n" + ) + + return ( + "from pysus.api.client import PySUS\n" + "\n" + "async with PySUS() as pysus:\n" + " dg = await pysus.get_dadosgov('YOUR_TOKEN')\n" + " datasets = await dg.datasets()\n" + " ds = next(d for d in datasets if d.name == " + + repr(params["dataset"]) + + ")\n" + " files = await ds.search(\n" + _fmt_args(exclude_dataset=True) + "\n )\n" + " for f in files:\n" + " print(f.basename)\n" + " await pysus.download(file=f)\n" + ) + + +# --- Page Layout --- + +client_choice = st.segmented_control( + t("source_label", _lang()), + options=list(CLIENTS.keys()), + format_func=lambda x: t(CLIENTS[x], _lang()), + default="ducklake", +) + +if client_choice is None: + client_choice = "ducklake" + +pysus = _get_orchestrator() + +if client_choice == "ducklake": + _render_ducklake_filters(pysus) +elif client_choice == "ftp": + _render_ftp_filters(pysus) +elif client_choice == "dadosgov": + _render_dadosgov_filters(pysus) + +if st.session_state.get(f"_query_results_{client_choice}"): + st.divider() + _show_results(pysus, client_choice) + +query_code = _build_query_code(client_choice) +if query_code: + with st.expander(t("python_snippet", _lang())): + st.code(query_code, language="python") diff --git a/pysus/http/pages/2_examples.py b/pysus/http/pages/2_examples.py new file mode 100644 index 00000000..2dc68d8e --- /dev/null +++ b/pysus/http/pages/2_examples.py @@ -0,0 +1,3 @@ +import streamlit as st + +st.title("Examples") diff --git a/pysus/http/translations.py b/pysus/http/translations.py new file mode 100644 index 00000000..3df41b63 --- /dev/null +++ b/pysus/http/translations.py @@ -0,0 +1,159 @@ +"""Translation dictionaries for the PySUS Streamlit UI.""" + +from typing import Final + +EN: Final[dict[str, str]] = { + "lang_label": "Language", + "home_page": "Home", + "sidebar_title": "Datasets", + "sidebar_select": "Select a dataset", + "home_title": "PySUS", + "home_subtitle": "Tools for dealing with Brazil's Public health data (SUS — Sistema Único de Saúde).", + "coming_soon": "coming soon", + "datasets": "Datasets", + "data_sources": "Data sources", + "about_title": "About PySUS", + "about_intro": "PySUS v{version} — Tools for dealing with Brazil's Public health data (SUS — Sistema Único de Saúde).", + "sinan_desc": "Notifiable Diseases Information System", + "sinasc_desc": "Live Births Information System", + "sim_desc": "Mortality Information System", + "sih_desc": "Hospital Information System", + "sia_desc": "Ambulatory Information System", + "pni_desc": "National Immunization Program", + "ibge_desc": "Brazilian Institute of Geography and Statistics", + "cnes_desc": "National Registry of Health Facilities", + "ciha_desc": "Hospital Admission Communication", + "ducklake_desc": "Modern data lake backend (default).", + "ftp_desc": "Legacy FTP downloads from DATASUS.", + "dadosgov_desc": "Brazilian open-data portal (dados.gov.br).", + "ducklake_label": "PySUS s3", + "ftp_label": "FTP DataSUS", + "dadosgov_label": "API DataSUS", + "source_label": "Data source", + "loading_catalog": "Loading DuckLake catalog...", + "catalog_failed": "Could not connect to DuckLake catalog.", + "ftp_failed": "Could not connect to DATASUS FTP.", + "select_groups": "Select groups...", + "select_states": "SP, RJ, ...", + "select_years": "Select years...", + "select_months": "Select months...", + "invalid_year": "Invalid year format.", + "invalid_month": "Invalid month format.", + "api_token": "API Token", + "token_placeholder": "Chave de API do dados.gov.br", + "token_required": "Enter your dados.gov.br API token to browse datasets.", + "connect": "Connect", + "connecting_dadosgov": "Connecting to DadosGov...", + "select_dataset": "Please select a dataset.", + "querying": "Querying {client}...", + "no_files": "No files matching the criteria.", + "files_found": "Found {count} file(s).", + "browser_title": "Datasets", + "browser_choose": "Choose a dataset", + "browser_placeholder": "{dataset} browser — coming soon.", + "state": "State", + "year": "Year", + "month": "Month", + "group": "Group", + "dataset": "Dataset", + "fetch": "Fetch files", + "add_to_queue": "➕ Add to queue", + "results_title": "📊 Results ({count} files)", + "queue_title": "📥 Download Queue ({count} files)", + "queue_empty": "No files in the download queue.", + "save_to": "Save to", + "download_dir_placeholder": "Download directory...", + "remove": "✕ Remove", + "clear": "🗑 Clear", + "download": "⬇ Download", + "downloading": "Downloading {name} ({i}/{total})", + "download_start": "Starting downloads...", + "download_done": "Done!", + "download_failed": "Failed: {name} — {error}", + "download_success": "Downloaded {count} file(s) to {dir}", + "python_snippet": "📋 Python snippet", +} + +PT: Final[dict[str, str]] = { + "lang_label": "Idioma", + "home_page": "Início", + "sidebar_title": "Bases de dados", + "sidebar_select": "Selecione uma base", + "home_title": "PySUS", + "home_subtitle": "Ferramentas para dados públicos de saúde do Brasil (SUS — Sistema Único de Saúde).", + "coming_soon": "em breve", + "datasets": "Bases de dados", + "data_sources": "Fontes de dados", + "about_title": "Sobre o PySUS", + "about_intro": "PySUS v{version} — Ferramentas para dados públicos de saúde do Brasil (SUS — Sistema Único de Saúde).", + "sinan_desc": "Sistema de Informação de Agravos de Notificação", + "sinasc_desc": "Sistema de Informações sobre Nascidos Vivos", + "sim_desc": "Sistema de Informação sobre Mortalidade", + "sih_desc": "Sistema de Informações Hospitalares", + "sia_desc": "Sistema de Informações Ambulatoriais", + "pni_desc": "Programa Nacional de Imunizações", + "ibge_desc": "Instituto Brasileiro de Geografia e Estatística", + "cnes_desc": "Cadastro Nacional de Estabelecimentos de Saúde", + "ciha_desc": "Comunicação de Internação Hospitalar e Ambulatorial", + "ducklake_desc": "Backend moderno de data lake (padrão).", + "ftp_desc": "Downloads legados via FTP do DATASUS.", + "dadosgov_desc": "Portal de dados abertos do governo (dados.gov.br).", + "ducklake_label": "PySUS s3", + "ftp_label": "FTP DataSUS", + "dadosgov_label": "API DataSUS", + "source_label": "Fonte de dados", + "loading_catalog": "Carregando catálogo DuckLake...", + "catalog_failed": "Não foi possível conectar ao catálogo DuckLake.", + "ftp_failed": "Não foi possível conectar ao FTP do DATASUS.", + "select_groups": "Selecionar grupos...", + "select_states": "SP, RJ, ...", + "select_years": "Selecionar anos...", + "select_months": "Selecionar meses...", + "invalid_year": "Formato de ano inválido.", + "invalid_month": "Formato de mês inválido.", + "api_token": "Token da API", + "token_placeholder": "Chave de API do dados.gov.br", + "token_required": "Insira seu token da API do dados.gov.br para navegar.", + "connect": "Conectar", + "connecting_dadosgov": "Conectando ao DadosGov...", + "select_dataset": "Selecione uma base de dados.", + "querying": "Consultando {client}...", + "no_files": "Nenhum arquivo corresponde aos critérios.", + "files_found": "{count} arquivo(s) encontrado(s).", + "browser_title": "Bases de dados", + "browser_choose": "Escolha uma base", + "browser_placeholder": "Navegador {dataset} — em breve.", + "state": "Estado", + "year": "Ano", + "month": "Mês", + "group": "Grupo", + "dataset": "Base de dados", + "fetch": "Buscar arquivos", + "add_to_queue": "➕ Adicionar à fila", + "results_title": "📊 Resultados ({count} arquivos)", + "queue_title": "📥 Fila de download ({count} arquivos)", + "queue_empty": "Nenhum arquivo na fila de download.", + "save_to": "Salvar em", + "download_dir_placeholder": "Diretório de download...", + "remove": "✕ Remover", + "clear": "🗑 Limpar", + "download": "⬇ Baixar", + "downloading": "Baixando {name} ({i}/{total})", + "download_start": "Iniciando downloads...", + "download_done": "Concluído!", + "download_failed": "Falha: {name} — {error}", + "download_success": "{count} arquivo(s) baixado(s) para {dir}", + "python_snippet": "📋 Código Python", +} + +TRANSLATIONS: Final[dict[str, dict[str, str]]] = { + "en": EN, + "pt": PT, +} + + +def t(key: str, lang: str = "en", **kwargs: str) -> str: + text = TRANSLATIONS.get(lang, EN).get(key, key) + if kwargs: + text = text.format(**kwargs) + return text diff --git a/pysus/tui/__init__.py b/pysus/tui/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/pysus/tui/app.py b/pysus/tui/app.py deleted file mode 100644 index dacaa12a..00000000 --- a/pysus/tui/app.py +++ /dev/null @@ -1,251 +0,0 @@ -from __future__ import annotations - -import asyncio - -from pysus import __version__ -from pysus.api import PySUSClient -from pysus.api.client import DownloadStatus -from pysus.tui.i18n import TRANSLATIONS, t -from pysus.tui.screens import ( - ConfigScreen, - InfoModal, - LoadingScreen, - MainScreen, - SearchModal, -) -from textual import work -from textual.app import App -from textual.binding import Binding -from textual.widgets import ( - ContentSwitcher, - DataTable, - ProgressBar, - Static, - Tree, -) - - -class PySUS(App): - TITLE = "PySUS" - SUB_TITLE = f"v{__version__}" - CSS_PATH = "style.tcss" - - lang: str - pysus: PySUSClient - - BINDINGS = [ - Binding("escape", "back", "Back"), - Binding("q", "quit", "Quit"), - Binding("f10", "push_screen('config')", "Config", priority=True), - Binding("i", "show_info", "Info"), - Binding("d", "download", "Download"), - Binding("/", "search", "Search"), - Binding("h", "focus_previous", "Focus Prev", show=False), - Binding("l", "focus_next", "Focus Next", show=False), - Binding("j", "cursor_down", "Down", show=False), - Binding("k", "cursor_up", "Up", show=False), - ] - - SCREENS = { - "main": MainScreen, - "config": ConfigScreen, - } - - def __init__(self, lang="en", **kwargs): - self.lang = lang if lang in TRANSLATIONS else "en" - super().__init__(**kwargs) - - async def on_mount(self) -> None: - self.pysus = PySUSClient() - await self.push_screen(LoadingScreen()) - self.init() - - @work - async def init(self) -> None: - try: - await self.pysus.__aenter__() - await asyncio.sleep(2) - self.switch_screen("main") - except Exception as e: # noqa - err_msg = t("loading_err", lang=self.lang) - self.notify(f"{err_msg}: {e}", severity="error") - - @work - async def action_download(self) -> None: - screen = self.screen - if not isinstance(screen, MainScreen): - return - - switcher = screen.query_one("#client-switcher", ContentSwitcher) - current_client = switcher.current - - if current_client == "ducklake": - table = screen.query_one("#ducklake", DataTable) - manager = screen.ducklake_manager - elif current_client == "ftp": - table = screen.query_one("#ftp", DataTable) - manager = screen.ftp_manager - else: - table = screen.query_one("#dadosgov", DataTable) - manager = screen.dadosgov_manager - - if table.cursor_row is None: - return - - saved_row_index = table.cursor_row - selected_wrapper = manager.filtered[saved_row_index] - file_item = selected_wrapper.raw - - progress_bar = screen.query_one( - "#download-progress", - ProgressBar, - ) - progress_bar.add_class("visible") - - download_text = screen.query_one("#download-text", Static) - download_text.update(f"Downloading: {file_item.name}") - download_text.add_class("visible") - - def run_download(): - import anyio - - async def do_download(): - return await self.pysus.download_to_parquet(file_item) - - return anyio.run(do_download) - - await asyncio.get_event_loop().run_in_executor(None, run_download) - - progress_bar.remove_class("visible") - download_text.remove_class("visible") - download_text.update("") - - completed_paths = self.pysus.get_completed_remote_paths() - manager.set_items( - [w.raw for w in manager.items], - downloaded_paths=completed_paths, - ) - - if hasattr(manager, "search_text") and manager.search_text: - manager.apply_filter(manager.search_text) - - manager.populate(table) - self.populate_local_tree() - - @work - async def refresh_local_tree(self) -> None: - await asyncio.sleep(0.5) - self.populate_local_tree() - - def action_back(self) -> None: - if isinstance(self.screen, ConfigScreen): - self.pop_screen() - - def action_search(self) -> None: - screen = self.screen - if not isinstance(screen, MainScreen): - return - - def perform_search(val: str | None) -> None: - switcher = screen.query_one("#client-switcher", ContentSwitcher) - current_client = switcher.current - if current_client == "ducklake": - screen.ducklake_manager.apply_filter(val) - screen.ducklake_manager.populate( - screen.query_one("#ducklake", DataTable) - ) - elif current_client == "ftp": - screen.ftp_manager.apply_filter(val) - screen.ftp_manager.populate(screen.query_one("#ftp", DataTable)) - else: - screen.dadosgov_manager.apply_filter(val) - screen.dadosgov_manager.populate( - screen.query_one("#dadosgov", DataTable) - ) - - self.push_screen(SearchModal(), perform_search) - - def action_show_info(self) -> None: - screen = self.screen - if not isinstance(screen, MainScreen): - return - - switcher = screen.query_one("#client-switcher", ContentSwitcher) - current_client = switcher.current - - if current_client == "ducklake": - table = screen.query_one("#ducklake", DataTable) - manager = screen.ducklake_manager - elif current_client == "ftp": - table = screen.query_one("#ftp", DataTable) - manager = screen.ftp_manager - else: - table = screen.query_one("#dadosgov", DataTable) - manager = screen.dadosgov_manager - - try: - if table.cursor_row is not None: - selected_wrapper = manager.filtered[table.cursor_row] - self.push_screen(InfoModal(selected_wrapper.raw)) - except Exception as e: # noqa - self.notify(f"Metadata error: {e}", severity="error") - - def action_cursor_down(self) -> None: - if isinstance(self.focused, (DataTable, Tree)): - self.focused.action_cursor_down() - - def action_cursor_up(self) -> None: - if isinstance(self.focused, (DataTable, Tree)): - self.focused.action_cursor_up() - - async def action_quit(self) -> None: - await self.pysus.__aexit__(None, None, None) - self.exit() - - def on_screen_activated(self) -> None: - if isinstance(self.screen, MainScreen): - self.populate_local_tree() - - def populate_local_tree(self) -> None: - screen = self.screen - if not isinstance(screen, MainScreen): - return - try: - tree = screen.query_one("#local-tree", Tree) - except Exception: # noqa - return - - tree.clear() - tree.root.expand_all() - hierarchy = self.pysus.get_local_hierarchy() - - status_icons = { - DownloadStatus.COMPLETED: "ok", - DownloadStatus.DOWNLOADING: "⏳", - DownloadStatus.FAILED: "❌", - DownloadStatus.PENDING: "💤", - DownloadStatus.MISSING: "❓", - } - - for client, datasets in hierarchy.items(): - client_node = tree.root.add(f"📂 {client}", expand=True) - for dataset, groups in datasets.items(): - ds_node = client_node.add(f"📦 {dataset}", expand=True) - for group, files in groups.items(): - parent = ds_node.add(f"📁 {group}") if group else ds_node - for f in files: - status = status_icons.get(f["status"], None) - - if not status: - icon = "📄 " - elif status == "ok": - icon = "" - else: - icon = f"{status} " - - parent.add_leaf(f"{icon}{f['name']}", data=f["record"]) - - -if __name__ == "__main__": - app = PySUS(lang="pt") - app.run() diff --git a/pysus/tui/i18n.py b/pysus/tui/i18n.py deleted file mode 100644 index 91067d75..00000000 --- a/pysus/tui/i18n.py +++ /dev/null @@ -1,105 +0,0 @@ -TRANSLATIONS: dict[str, dict[str, str | dict[str, str]]] = { - "en": { - "welcome": "Welcome to PySUS Client", - "clients": "Clients", - "local": "Local", - "remote": "Remote", - "search": "Search or leave empty to list all", - "loading_err": "Failed to load", - "loading": "Loading", - "settings": "Settings", - "quit": "Quit", - "files": "Files", - "ftp_browser": "FTP", - "ducklake_browser": "DuckLake", - "fetching": "Fetching datasets...", - "name": "Name", - "type": "Type", - "info": "Info", - "path": "Path", - "size": "Size", - "year": "Year", - "month": "Month", - "modified": "Modified", - "state": "State", - "description": "Description", - "group": "Group", - "months": { - "1": "Jan", - "2": "Feb", - "3": "Mar", - "4": "Apr", - "5": "May", - "6": "Jun", - "7": "Jul", - "8": "Aug", - "9": "Sep", - "10": "Oct", - "11": "Nov", - "12": "Dec", - }, - "esc": "Press ESC to close", - }, - "pt": { - "welcome": "Bem-vindo ao Cliente PySUS", - "clients": "Clientes", - "local": "Local", - "remote": "Remoto", - "search": "Busque ou deixe em branco para listar tudo", - "loading_err": "Erro ao carregar", - "loading": "Carregando", - "settings": "Configurações", - "quit": "Sair", - "files": "Arquivos", - "ftp_browser": "FTP", - "ducklake_browser": "DuckLake", - "fetching": "Carregando datasets...", - "name": "Nome", - "type": "Tipo", - "info": "Info", - "path": "Path", - "size": "Tamanho", - "year": "Ano", - "month": "Mês", - "modified": "Modificado", - "state": "Estado", - "description": "Descrição", - "group": "Grupo", - "months": { - "1": "Jan", - "2": "Fev", - "3": "Mar", - "4": "Abr", - "5": "Mai", - "6": "Jun", - "7": "Jul", - "8": "Ago", - "9": "Set", - "10": "Out", - "11": "Nov", - "12": "Dez", - }, - "esc": "ESC para fechar", - }, -} - -SUPPORTED_LANGUAGES = tuple(TRANSLATIONS.keys()) - - -def t(field: str, default: str = "", lang: str = "en") -> str: - if lang not in TRANSLATIONS: - lang = "en" - - data: dict = TRANSLATIONS[lang] - keys = field.split(".") - - for key in keys: - value = data.get(key) - if isinstance(value, str): - return value - if isinstance(value, dict): - data = value - else: - return default - - return default diff --git a/pysus/tui/models.py b/pysus/tui/models.py deleted file mode 100644 index 3be866f0..00000000 --- a/pysus/tui/models.py +++ /dev/null @@ -1,258 +0,0 @@ -from dataclasses import dataclass -from typing import Any - -import humanize -from textual.widgets import DataTable - - -@dataclass -class SourceRef: - source: str # "ducklake", "ftp", "local" - path: str | None = None - is_downloaded: bool = False - remote_modified: str | None = None - - -class BaseTUIItem: - def __init__(self, raw): - self.raw = raw - self.name = getattr(raw, "name", str(raw)) - self.type = raw.__class__.__name__ - self._links: list[SourceRef] = [] - self.is_downloading: bool = False - - @property - def source_key(self) -> str: - parts = [self.name] - for attr in ("year", "month", "state"): - val = getattr(self.raw, attr, None) - if val: - parts.append(str(val)) - return ":".join(parts) - - def add_link( - self, - source: str, - path: str | None = None, - is_downloaded: bool = False, - remote_modified: str | None = None, - ): - self._links.append( - SourceRef(source, path, is_downloaded, remote_modified) - ) - - @property - def links(self) -> list[SourceRef]: - return self._links - - def get_columns(self) -> list[str]: - return [self.name, self.type, "", ""] - - -class File(BaseTUIItem): - def __init__( - self, - raw, - is_downloaded: bool = False, - is_downloading: bool = False, - source: str = "unknown", - path: str | None = None, - remote_modified: str | None = None, - ): - super().__init__(raw) - self.is_downloaded = is_downloaded - self.is_downloading = is_downloading - self._source = source - if path: - self.add_link(source, path, is_downloaded, remote_modified) - - @property - def size(self) -> str: - raw_size = getattr(self.raw, "size", None) - if raw_size is not None and isinstance(raw_size, (int, float)): - return humanize.naturalsize(raw_size, binary=True) - return "-" - - @property - def modified(self) -> str: - raw_mod = getattr(self.raw, "modify", None) - if raw_mod: - if hasattr(raw_mod, "strftime"): - return raw_mod.strftime("%Y-%m-%d") - elif hasattr(raw_mod, "modified"): - mod = raw_mod.modified - if hasattr(mod, "strftime"): - return mod.strftime("%Y-%m-%d") - raw_dt = getattr(self.raw, "modify_date", None) - if raw_dt and hasattr(raw_dt, "strftime"): - return raw_dt.strftime("%Y-%m-%d") - return "-" - - def get_columns(self) -> list[str]: - display_name = self.name - link_indicators = [] - sources_seen = set() - item_type = self.type - downloaded = self.is_downloaded - - if self.is_downloading: - link_indicators.append("[yellow]◐[/yellow]") - - for link in self.links: - sources_seen.add(link.source) - if link.is_downloaded: - downloaded = True - link_indicators.append("[green]✓[/green]") - - if hasattr(self, "_source") and self._source not in sources_seen: - if downloaded or self.is_downloaded: - link_indicators = ["[green]✓[/green]"] - - if not link_indicators: - if downloaded or self.is_downloaded: - link_indicators = ["[green]✓[/green]"] - elif item_type in ( - "Dataset", - "BaseRemoteDataset", - "ConjuntoDados", - ): - link_indicators = ["[yellow]📦[/yellow]"] - item_type = "Dataset" - elif item_type in ("File", "CatalogFile"): - link_indicators = ["[yellow] [/yellow]"] - elif item_type in ("Group", "DatasetGroup"): - link_indicators = ["[yellow]📁[/yellow]"] - item_type = "Group" - - if link_indicators: - display_name = f"{display_name} {''.join(link_indicators)}" - - long_name = getattr(self.raw, "long_name", None) or "" - return [display_name, item_type, self.modified, self.size, long_name] - - -class Group(BaseTUIItem): - def get_columns(self) -> list[str]: - desc = getattr(self.raw, "long_name", "Directory") - modified = "-" - if hasattr(self.raw, "modify") and self.raw.modify: - if hasattr(self.raw.modify, "strftime"): - modified = self.raw.modify.strftime("%Y-%m-%d") - return [self.name, "Group", desc, modified, ""] - - -class Dataset(BaseTUIItem): - def get_columns(self) -> list[str]: - long_name = getattr(self.raw, "long_name", self.name) - modified = "-" - if hasattr(self.raw, "record") and hasattr(self.raw.record, "modified"): - mod = self.raw.record.modified - if hasattr(mod, "strftime"): - modified = mod.strftime("%Y-%m-%d") - return [self.name, "File", long_name, modified, ""] - - -class ContentManager: - def __init__(self): - self.items: list[BaseTUIItem] = [] - self.filtered: list[BaseTUIItem] = [] - self._item_index: dict[str, BaseTUIItem] = {} - self._search_text: str | None = None - - @property - def search_text(self) -> str | None: - return self._search_text - - def _normalize_key(self, name: str) -> str: - return name.replace(".parquet", "").replace(".dbc", "").upper() - - def _get_key(self, item: Any) -> str: - base = self._normalize_key(getattr(item, "name", "")) - year = getattr(item, "year", None) - month = getattr(item, "month", None) - if year: - base += f":{year}" - if month: - base += f":{month:02d}" - return base - - def set_items( - self, - raw_items: list, - downloaded_paths: set[str] | None = None, - downloading_paths: set[str] | None = None, - source: str = "unknown", - clear: bool = True, - ) -> None: - if clear: - self.items = [] - self._item_index = {} - - downloaded_paths = downloaded_paths or set() - downloading_paths = downloading_paths or set() - - new_items = [] - for item in raw_items: - key = self._get_key(item) - is_done = str(getattr(item, "path", None)) in downloaded_paths - is_downloading = ( - str(getattr(item, "path", None)) in downloading_paths - ) - remote_modified = None - - if hasattr(item, "remote_modified"): - remote_modified = str(item.remote_modified) - elif hasattr(item, "modify_date"): - remote_modified = str(item.modify_date) - - file_obj = File( - item, - is_downloaded=is_done, - is_downloading=is_downloading, - source=source, - path=getattr(item, "path", None), - remote_modified=remote_modified, - ) - - if key in self._item_index: - existing = self._item_index[key] - for link in file_obj.links: - existing.add_link( - link.source, - link.path, - link.is_downloaded, - link.remote_modified, - ) - else: - self._item_index[key] = file_obj - new_items.append(file_obj) - - self.items.extend(new_items) - self.filtered = list(self.items) - - def set_downloading(self, path: str, is_downloading: bool) -> None: - for item in self.items: - if str(getattr(item.raw, "path", None)) == path: - item.is_downloading = is_downloading - break - - def apply_filter(self, search_text: str | None) -> None: - self._search_text = search_text - if not search_text: - self.filtered = list(self.items) - else: - search_text = search_text.lower() - self.filtered = [ - item for item in self.items if search_text in item.name.lower() - ] - - def populate(self, table: DataTable, reset_cursor: bool = False) -> None: - if not reset_cursor: - cursor_row = table.cursor_row - else: - cursor_row = None - table.clear() - for item in self.filtered: - table.add_row(*item.get_columns()) - if cursor_row is not None and cursor_row < table.row_count: - table.move_cursor(row=cursor_row) diff --git a/pysus/tui/screens.py b/pysus/tui/screens.py deleted file mode 100644 index a4d2a6f0..00000000 --- a/pysus/tui/screens.py +++ /dev/null @@ -1,410 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import humanize -from pysus.tui.i18n import TRANSLATIONS, t -from pysus.tui.models import ContentManager -from textual import work -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Center, Grid, Horizontal, Middle, Vertical -from textual.screen import ModalScreen, Screen -from textual.widgets import ( - Button, - ContentSwitcher, - DataTable, - Footer, - Header, - Input, - Label, - LoadingIndicator, - ProgressBar, - Select, - Static, - Switch, - Tree, -) - -if TYPE_CHECKING: - from pysus.tui.types import PySUSApp # type: ignore - - -class PySUSScreen(Screen): - app: PySUSApp - - -def _get_app(screen: Screen) -> PySUSApp: - return screen.app # type: ignore[return-value] - - -class LoadingScreen(PySUSScreen): - def compose(self) -> ComposeResult: - app = _get_app(self) - lang = app.lang - yield Header() - with Vertical(id="loading-container"): - with Middle(): - yield Static(t("welcome", lang=lang), id="welcome-text") - yield Static(t("fetching", lang=lang), id="loading-status") - with Center(): - yield LoadingIndicator(id="loader") - yield Footer() - - def on_key(self, event) -> None: - if event.key == "q": - return - event.stop() - event.prevent_default() - - -class MainScreen(Screen): - BINDINGS = [ - Binding("f1", "switch_client('ducklake')", "DuckLake", priority=True), - Binding("f2", "switch_client('ftp')", "FTP", priority=True), - # Binding("f3", "switch_client('dadosgov')", "DadosGov", priority=True), - Binding("f10", "push_screen('config')", "Config", priority=True), - Binding("i", "show_info", "Info"), - Binding("d", "download", "Download"), - Binding("/", "search", "Search"), - Binding("escape", "back", "Back"), - Binding("q", "quit", "Quit"), - ] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.ducklake_manager = ContentManager() - self.ftp_manager = ContentManager() - self.dadosgov_manager = ContentManager() - self._nav_stack: list[tuple[ContentManager, list]] = [] - - def compose(self) -> ComposeResult: - app = _get_app(self) - lang = app.lang - yield Header() - with Horizontal(): - with Vertical(id="main-container"): - yield Static(t("remote", lang=lang), id="panel-label") - with ContentSwitcher(id="client-switcher", initial="ducklake"): - yield DataTable(id="ducklake") - yield DataTable(id="ftp") - yield DataTable(id="dadosgov") - yield Static("", id="download-text") - yield ProgressBar(id="download-progress", show_percentage=True) - with Vertical(id="local-sidebar"): - yield Static(t("local", lang=lang), id="sidebar-label") - yield Tree(t("files", lang=lang), id="local-tree") - yield Footer() - - async def on_mount(self) -> None: - app: PySUSApp = self.app - app.populate_local_tree() - self.fetch_ducklake() - - def action_switch_client(self, client_id: str) -> None: - switcher = self.query_one("#client-switcher", ContentSwitcher) - switcher.current = client_id - self._update_panel_label() - - if client_id == "ducklake": - self.fetch_ducklake() - elif client_id == "ftp": - self.fetch_ftp() - elif client_id == "dadosgov": - self.fetch_dadosgov() - - self.call_later(self._focus_current_table) - - def _focus_current_table(self) -> None: - switcher = self.query_one("#client-switcher", ContentSwitcher) - table = switcher.query_one(f"#{switcher.current}", DataTable) - table.focus() - - def _update_panel_label(self) -> None: - try: - switcher = self.query_one("#client-switcher", ContentSwitcher) - label = self.query_one("#panel-label", Static) - client = switcher.current - if not client: - client = "ducklake" - client = client.upper() - if self._nav_stack: - label.update(f"{t('remote', lang=self.app.lang)} - {client} ⬅") - else: - label.update(f"{t('remote', lang=self.app.lang)} - {client}") - except Exception: # noqa - pass - - def action_back(self) -> None: - if not self._nav_stack: - return - - switcher = self.query_one("#client-switcher", ContentSwitcher) - current_client = switcher.current - manager = getattr(self, f"{current_client}_manager") - table = self.query_one(f"#{current_client}", DataTable) - - previous = self._nav_stack.pop() - manager.set_items(previous[1], clear=True) - manager.populate(table) - self._update_panel_label() - - @work - async def on_data_table_row_selected( - self, event: DataTable.RowSelected - ) -> None: - switcher = self.query_one("#client-switcher", ContentSwitcher) - current_client = switcher.current - table = event.data_table - - manager = getattr(self, f"{current_client}_manager") - - if event.cursor_row >= len(manager.filtered): - return - - selected_wrapper = manager.filtered[event.cursor_row] - selected_item = selected_wrapper.raw - - table.loading = True - label = self.query_one("#panel-label", Static) - label.update(f"{t('loading', lang=self.app.lang)}...") - self._nav_stack.append((manager, [item.raw for item in manager.items])) - - try: - new_raw_data = [] - - if hasattr(selected_item, "_fetch_content"): - new_raw_data = await selected_item._fetch_content() - elif hasattr(selected_item, "_fetch_files"): - new_raw_data = await selected_item._fetch_files() - elif hasattr(selected_item, "groups") and selected_item.groups: - new_raw_data = [ - g for g in selected_item.groups if hasattr(g, "record") - ] - elif hasattr(selected_item, "files") and selected_item.files: - new_raw_data = list(selected_item.files) - else: - if self._nav_stack: - self._nav_stack.pop() - table.loading = False - return - - completed_paths = self.app.pysus.get_completed_remote_paths() - - manager.set_items( - new_raw_data, - downloaded_paths=completed_paths, - source=current_client, - ) - - manager.populate(table) - self._update_panel_label() - - except Exception as e: # noqa - if self._nav_stack: - self._nav_stack.pop() - self.app.notify(f"Navigation Error: {e}", severity="error") - finally: - table.loading = False - - @work - async def fetch_ducklake(self) -> None: - table = self.query_one("#ducklake", DataTable) - if table.row_count > 0: - return - - table.cursor_type = "row" - app = _get_app(self) - lang = app.lang - - table.clear(columns=True) - table.add_columns( - t("name", lang=lang), - t("type", lang=lang), - t("modified", lang=lang), - t("size", lang=lang), - t("info", lang=lang), - ) - table.loading = True - try: - ducklake = await app.pysus.get_ducklake() - datasets = await ducklake.datasets() - completed_paths = app.pysus.get_completed_remote_paths() - self.ducklake_manager.set_items( - datasets, downloaded_paths=completed_paths, source="ducklake" - ) - self.ducklake_manager.populate(table) - except Exception as e: # noqa - app.notify(f"DuckLake Error: {e}", severity="error") - finally: - table.loading = False - - @work - async def fetch_ftp(self) -> None: - table = self.query_one("#ftp", DataTable) - - if table.row_count > 0: - return - - table.cursor_type = "row" - app = _get_app(self) - lang = app.lang - - table.clear(columns=True) - table.add_columns( - t("name", lang=lang), - t("type", lang=lang), - t("modified", lang=lang), - t("size", lang=lang), - t("info", lang=lang), - ) - - table.loading = True - try: - ftp = await app.pysus.get_ftp() - files = await ftp.datasets() - completed_paths = app.pysus.get_completed_remote_paths() - - self.ftp_manager.set_items( - files, downloaded_paths=completed_paths, source="ftp" - ) - self.ftp_manager.populate(table) - except Exception as e: # noqa - app.notify(f"FTP Error: {e}", severity="error") - finally: - table.loading = False - - @work - async def fetch_dadosgov(self) -> None: - app: PySUSApp = self.app - table = self.query_one("#dadosgov", DataTable) - if table.row_count > 0: - return - - table.cursor_type = "row" - lang = app.lang - table.clear(columns=True) - table.add_columns( - t("name", lang=lang), - t("type", lang=lang), - t("modified", lang=lang), - t("size", lang=lang), - t("info", lang=lang), - ) - table.loading = True - try: - dadosgov = await app.pysus.get_dadosgov() - datasets = await dadosgov.datasets() - completed_paths = app.pysus.get_completed_remote_paths() - self.dadosgov_manager.set_items( - datasets, downloaded_paths=completed_paths, source="dadosgov" - ) - self.dadosgov_manager.populate(table) - except Exception as e: # noqa - self.app.notify(f"DadosGov Error: {e}", severity="error") - finally: - table.loading = False - - -class ConfigScreen(Screen): - def compose(self) -> ComposeResult: - app: PySUSApp = self.app - lang = app.lang - yield Header() - with Center(): - with Vertical(id="config-container"): - yield Static(t("settings", lang=lang), id="config-title") - - with Grid(id="config-grid"): - yield Label("Language / Idioma") - yield Select( - [(lang.upper(), lang) for lang in TRANSLATIONS.keys()], - value=lang, - id="cfg-lang", - ) - - yield Label("Dark Mode") - yield Switch(value=True, id="cfg-dark") - - yield Button("Save & Apply", variant="success", id="cfg-save") - yield Footer() - - def on_button_pressed(self, event: Button.Pressed) -> None: - app: PySUSApp = self.app - if event.button.id == "cfg-save": - new_lang = self.query_one("#cfg-lang", Select).value - if new_lang: - app.lang = new_lang - self.app.pop_screen() - - -class InfoModal(ModalScreen): - BINDINGS = [("escape", "dismiss", "Close")] - - def __init__(self, item, **kwargs): - super().__init__(**kwargs) - self.item = item - - def compose(self) -> ComposeResult: - app: PySUSApp = self.app - name = getattr(self.item, "name", "Unknown") - long_name = getattr(self.item, "long_name", None) - title = f"{name} ({long_name})" if long_name else name - lang = app.lang - - with Vertical(id="modal-content-wrapper"): - yield Static(title, id="modal-title") - - info_text = [] - attrs = [ - "description", - "path", - "size", - "year", - "month", - "state", - ] - for attr in attrs: - if hasattr(self.item, attr): - val = getattr(self.item, attr) - if val: - label = t( - attr, - default=attr.replace("_", " ").title(), - lang=lang, - ) - - if attr == "size": - val = humanize.naturalsize(val, binary=True) - elif attr == "month": - val = t( - f"months.{val}", - default=str(val), - lang=lang, - ) - - info_text.append(f"[b]{label}:[/b] {val}") - - yield Static( - "\n".join(info_text) if info_text else "No metadata", - id="modal-content", - ) - yield Static(t("esc", lang=lang), id="modal-footer") - - def action_dismiss(self) -> None: - self.dismiss() - - -class SearchModal(ModalScreen): - def compose(self) -> ComposeResult: - with Center(): - yield Input( - placeholder=t("search", default="Search..."), - id="search-input", - ) - - def on_mount(self) -> None: - self.query_one(Input).focus() - - def on_input_submitted(self, event: Input.Submitted) -> None: - self.dismiss(event.value) diff --git a/pysus/tui/style.tcss b/pysus/tui/style.tcss deleted file mode 100644 index 7cd731f0..00000000 --- a/pysus/tui/style.tcss +++ /dev/null @@ -1,327 +0,0 @@ -/* ========================= - Color Scheme (High Contrast Dark) - ========================= */ - -$primary: #4a5568; -$secondary: #718096; -$accent: #9ae6b4; -$surface: #0d1117; -$text: #f0f6fc; -$text-muted: #8b949e; - -/* ========================= - Layout - ========================= */ - -#main-layout { - width: 100%; - height: 100%; -} - -#screen-container { - width: 100%; - height: 100%; -} - -#main-container { - width: 65fr; - height: 100%; - border: solid $primary; -} - -#local-sidebar { - width: 35fr; - height: 100%; - border: solid $primary; - background: $surface; -} - -DataTable { - height: 1fr; - border: none; - padding: 1; - scrollbar-size: 1 1; -} - -#client-switcher { - height: 1fr; -} - -#loading-container { - width: 100%; - height: 100%; - align: center middle; -} - -#welcome-text { - text-align: center; - text-style: bold; - color: $accent; - margin-top: 1; -} - -#loading-status { - text-align: center; - color: $text-muted; - margin-top: 1; -} - -#loader { - align: center middle; -} - -#screen-container { - width: 100%; - height: 100%; -} - -.sidebar { - width: 30; - min-width: 30; - height: 100%; - dock: right; - border: solid $primary; - background: $surface; - layer: sidebar; -} - -Screen { - background: transparent; -} - -LoadingScreen Middle { - width: 100%; - height: 90%; - align: center middle; -} - -#main-container { - width: 65%; - margin-right: 1; - border: solid $primary; - background: transparent; -} - -#sidebar { - width: 35%; - border: solid $primary; - background: $surface; -} - - -/* ========================= - Typography - ========================= */ - -#welcome-text { - width: 100%; - text-align: center; - text-style: bold; - color: $accent; -} - -#panel-label, -#sidebar-label { - padding: 1 2; - text-style: bold; - color: $text; - background: $primary; -} - -#config-title { - text-align: center; - text-style: bold; - margin-bottom: 1; - color: $accent; -} - -#modal-title { - width: 100%; - text-align: center; - text-style: bold; - color: $accent; - border-bottom: solid $primary; - padding-bottom: 1; - margin-bottom: 1; -} - -#modal-footer { - text-align: center; - color: $text-muted; - margin-top: 1; -} - -Label { - height: 3; - content-align: left middle; -} - - -/* ========================= - Containers - ========================= */ - -#config-container { - width: 60; - height: auto; - padding: 2 3; - background: $surface; - border: solid $primary; -} - -#modal-content-wrapper { - width: 70%; - height: auto; - padding: 2 3; - background: $surface; - border: solid $primary; -} - -#modal-content { - margin: 1 0; - color: $text; -} - - -/* ========================= - Grid - ========================= */ - -#config-grid { - grid-size: 2; - grid-columns: 1fr 2fr; - grid-gutter: 1 2; - - height: auto; - margin-bottom: 2; -} - - -/* ========================= - Components - ========================= */ - -#cfg-save { - width: 100%; -} - -#loader { - width: auto; - height: auto; -} - -DataTable > .datatable--cursor { - background: $primary; - color: $text; -} - -DataTable > .datatable--header { - background: $primary; - color: $text; -} - -#local-tree { - height: 95%; - padding: 1; - border: none; - scrollbar-size: 1 1; -} - -#local-tree Tree > .tree--selected { - background: $primary; -} - - -/* ========================= - Scrollbar - ========================= */ - -ScrollBar { - width: 1; -} - -ScrollBar > .scrollbar--button { - display: none; -} - - -/* ========================= - Progress - ========================= */ - -ProgressBar, -#download-progress { - width: 100%; - display: none; -} - -ProgressBar { - margin: 1 0; -} - -#download-progress { - margin-top: 1; -} - -ProgressBar.visible, -#download-progress.visible { - display: block; -} - - -/* ========================= - Modal - ========================= */ - -ModalScreen { - align: center middle; - background: rgba(0, 0, 0, 0.85); -} - - -/* ========================= - Buttons & Inputs - ========================= */ - -Button { - border: none; -} - -Button:hover { - background: $primary; -} - -Button:focus { - border: solid $accent; -} - -Input { - border: solid $primary; -} - -Input:focus { - border: solid $accent; -} - -Select { - border: solid $primary; -} - -Switch { - color: $accent; -} - - -/* ========================= - Headers & Footer - ========================= */ - -Header { - background: $primary; - color: $text; -} - -Footer { - background: $primary; - color: $text; -} diff --git a/pysus/tui/types.py b/pysus/tui/types.py deleted file mode 100644 index 4f9dcb89..00000000 --- a/pysus/tui/types.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Protocol - - -class PySUSApp(Protocol): - lang: str - - def populate_local_tree(self) -> None: ... # noqa - def notify(self, message: str, severity: str = "info") -> None: ... # noqa - def push_screen(self, screen, callback=None): ... # noqa - def pop_screen(self): ... # noqa - def switch_screen(self, name: str): ... # noqa - - class _pysus: - async def datasets(self): ... # noqa - def get_completed_remote_paths(self): ... # noqa - @property - async def get_ducklake(self): ... # noqa - @property - async def get_ftp(self): ... # noqa - @property - async def get_dadosgov(self): ... # noqa - - @property - def pysus(self) -> _pysus: ... # noqa diff --git a/setup.cfg b/setup.cfg index a9bca493..926ceac1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ release = 0.6.3 source-dir = ./docs/source [flake8] -exclude = tests,build,dist,docs,.git,__pycache__,.tox,.eggs,*.egg,.asv +exclude = tests,build,dist,docs,.git,__pycache__,.tox,.eggs,*.egg,.asv,pysus/http max-line-length = 79 ignore = D202,D203,W503,E203,E231