From 6e5fd427f7bc59900d047570bb907f46615dded3 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Tue, 15 Sep 2026 00:16:23 +0100 Subject: [PATCH] first commit --- .gitattributes | 2 + .gitignore | 441 +++++ .pre-commit-config.yaml | 8 + .vscode/settings.json | 4 + README.md | 65 + img/cli.png | Bin 0 -> 33176 bytes img/tui.png | Bin 0 -> 31689 bytes pixi.lock | 1604 +++++++++++++++++ pixi.toml | 37 + pyproject.toml | 21 + ruff.toml | 77 + scripts/sync_manifests.py | 30 + src/bible/__init__.py | 0 src/bible/api.py | 43 + src/bible/cli/cli.py | 59 + src/bible/cli/commands/__init__.py | 4 + src/bible/cli/commands/book.py | 49 + .../cli/commands/book_commands/__init__.py | 5 + .../cli/commands/book_commands/chapter.py | 268 +++ src/bible/cli/commands/book_commands/list.py | 50 + src/bible/cli/commands/book_commands/verse.py | 246 +++ src/bible/cli/commands/list.py | 48 + src/bible/console.py | 3 + src/bible/render.py | 127 ++ src/bible/settings.py | 20 + src/bible/sqlite.py | 68 + src/bible/tui/exceptions.py | 6 + src/bible/tui/fetch.py | 134 ++ src/bible/tui/settings_modal.py | 85 + src/bible/tui/tui.py | 314 ++++ src/bible/tui/tui.tcss | 107 ++ src/bible/tui/util.py | 79 + src/bible/util.py | 22 + 33 files changed, 4026 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100755 img/cli.png create mode 100755 img/tui.png create mode 100644 pixi.lock create mode 100644 pixi.toml create mode 100644 pyproject.toml create mode 100644 ruff.toml create mode 100755 scripts/sync_manifests.py create mode 100644 src/bible/__init__.py create mode 100644 src/bible/api.py create mode 100644 src/bible/cli/cli.py create mode 100644 src/bible/cli/commands/__init__.py create mode 100644 src/bible/cli/commands/book.py create mode 100644 src/bible/cli/commands/book_commands/__init__.py create mode 100644 src/bible/cli/commands/book_commands/chapter.py create mode 100644 src/bible/cli/commands/book_commands/list.py create mode 100644 src/bible/cli/commands/book_commands/verse.py create mode 100644 src/bible/cli/commands/list.py create mode 100644 src/bible/console.py create mode 100644 src/bible/render.py create mode 100644 src/bible/settings.py create mode 100644 src/bible/sqlite.py create mode 100644 src/bible/tui/exceptions.py create mode 100644 src/bible/tui/fetch.py create mode 100644 src/bible/tui/settings_modal.py create mode 100644 src/bible/tui/tui.py create mode 100644 src/bible/tui/tui.tcss create mode 100644 src/bible/tui/util.py create mode 100644 src/bible/util.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..997504b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..292fcea --- /dev/null +++ b/.gitignore @@ -0,0 +1,441 @@ +# pixi environments +.pixi/* +!.pixi/config.toml +# Generated by ignr: github.com/onyx-and-iris/ignr + +## Python ## +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.pixi/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# End of ignr +# Generated by ignr: github.com/onyx-and-iris/ignr + +## Python ## +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +# End of ignr + +bible_cache.db \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cce5cc6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: + - repo: local + hooks: + - id: sync-pixi-pyproject + name: Sync pixi.toml and pyproject.toml + entry: pixi run sync + language: system + stages: [pre-commit] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5fead40 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "renan-r-santos.pixi-code:pixi", + "python-envs.defaultPackageManager": "renan-r-santos.pixi-code:pixi" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a8bd0c --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Bible CLI/TUI - *Powered by API.Bible* + +[![Pixi](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json&style=flat-square)](https://pixi.sh) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + +--- + +### Install + +#### Clone the repository, and then + +*with uv* + +```console +uv tool install . +``` + +*with pipx* + +```console +pipx install . +``` + +--- + +### Run as a CLI + +```console +bible-cli book chapter Genesis 1 +``` + +![cli](./img/cli.png) + +### Run as a TUI + +```console +bible-tui +``` + +![tui](./img/tui.png) + +--- + +### Requirements + +- An API key from [API Bible][api-bible] + +### Configure + +Example .env: + +```env +BIBLE_API_KEY="" +BIBLE_BIBLE_NAME="New King James Version" +BIBLE_BOOK_NAME='Genesis' +BIBLE_ENDPOINT='https://rest.api.bible' +BIBLE_THEME="onyx-light" +``` + +> Note, although cache expiry days is configurable it should NOT be set above 30 days, as per the terms and conditions of the API. See [Acceptable Use][acceptable-use] + +--- + +[api-bible]: https://api.bible +[acceptable-use]: https://api.bible/terms-and-conditions#acceptable_use \ No newline at end of file diff --git a/img/cli.png b/img/cli.png new file mode 100755 index 0000000000000000000000000000000000000000..30888cd5676e55f646ed4a7829ca68cbab33a953 GIT binary patch literal 33176 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 literal 0 HcmV?d00001 diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..e2b1df6 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,1604 @@ +version: 6 +environments: + default: + channels: + - url: https://prefix.dev/conda-forge/ + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://prefix.dev/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda + - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda + - conda: https://prefix.dev/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/clypi-1.9.1-pyhc364b38_0.conda + - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda + - conda: https://prefix.dev/conda-forge/linux-64/jh2-5.0.14-py314h7e8cd81_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + - conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/libffi-3.7.0-h81df57d_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/libgcc-16.2.0-ha9f2e26_4.conda + - conda: https://prefix.dev/conda-forge/linux-64/libgomp-16.2.0-he0feb66_4.conda + - conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_2.conda + - conda: https://prefix.dev/conda-forge/linux-64/libpython-3.14.7-hdc7f604_106_cp314.conda + - conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.53.4-h13e7031_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_4.conda + - conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.42.3-hcfc3c73_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda + - conda: https://prefix.dev/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/loguru-0.7.3-pyh707e725_0.conda + - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda + - conda: https://prefix.dev/conda-forge/noarch/niquests-3.21.1-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda + - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/pydantic-core-2.46.5-py314h7e8cd81_1.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/python-3.14.7-hcd007b5_106_cp314.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.14-9_cp314.conda + - conda: https://prefix.dev/conda-forge/linux-64/qh3-1.9.4-py314haf003cf_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda + - conda: https://prefix.dev/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/ruff-0.16.7-hbcb56a7_0.conda + - conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://prefix.dev/conda-forge/noarch/soupsieve-2.9.2-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda + - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-inspection-0.4.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://prefix.dev/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/urllib3-future-2.24.908-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/noarch/wassima-2.1.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda + - conda: . + osx-64: + - conda: https://prefix.dev/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/bzip2-1.0.8-h374f1ed_10.conda + - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda + - conda: https://prefix.dev/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/clypi-1.9.1-pyhc364b38_0.conda + - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://prefix.dev/conda-forge/osx-64/jh2-5.0.14-py314h3cfb53e_1.conda + - conda: https://prefix.dev/conda-forge/osx-64/libcxx-23.1.1-h19cb2f5_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://prefix.dev/conda-forge/osx-64/libffi-3.7.0-h6b3a05b_1.conda + - conda: https://prefix.dev/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_1.conda + - conda: https://prefix.dev/conda-forge/osx-64/libmpdec-4.0.0-ha1e9b39_2.conda + - conda: https://prefix.dev/conda-forge/osx-64/libpython-3.14.7-hfe304d0_106_cp314.conda + - conda: https://prefix.dev/conda-forge/osx-64/libsqlite-3.53.4-ha5db789_1.conda + - conda: https://prefix.dev/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_3.conda + - conda: https://prefix.dev/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/loguru-0.7.3-pyh707e725_0.conda + - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://prefix.dev/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_1.conda + - conda: https://prefix.dev/conda-forge/noarch/niquests-3.21.1-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/openssl-3.6.4-h332eb6d_0.conda + - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/pydantic-core-2.46.5-py314h3cfb53e_1.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/python-3.14.7-hafa0b4b_106_cp314.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.14-9_cp314.conda + - conda: https://prefix.dev/conda-forge/osx-64/qh3-1.9.4-py314h7d911d6_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/readline-8.3-h000c2f7_1.conda + - conda: https://prefix.dev/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/ruff-0.16.7-haa00350_0.conda + - conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://prefix.dev/conda-forge/noarch/soupsieve-2.9.2-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda + - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-inspection-0.4.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://prefix.dev/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/urllib3-future-2.24.908-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/noarch/wassima-2.1.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda + - conda: . + osx-arm64: + - conda: https://prefix.dev/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda + - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda + - conda: https://prefix.dev/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/clypi-1.9.1-pyhc364b38_0.conda + - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/jh2-5.0.14-py314hc05cd11_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-23.1.1-h55c6f16_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_2.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libpython-3.14.7-h4311e70_106_cp314.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.53.4-hca69786_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda + - conda: https://prefix.dev/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/loguru-0.7.3-pyh707e725_0.conda + - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda + - conda: https://prefix.dev/conda-forge/noarch/niquests-3.21.1-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.4-h55eecbc_0.conda + - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/pydantic-core-2.46.5-py314hc05cd11_1.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.14.7-h0ae1c2c_106_cp314.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.14-9_cp314.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/qh3-1.9.4-py314h548cc06_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda + - conda: https://prefix.dev/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/ruff-0.16.7-ha7a9cbd_0.conda + - conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://prefix.dev/conda-forge/noarch/soupsieve-2.9.2-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda + - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-inspection-0.4.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://prefix.dev/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/urllib3-future-2.24.908-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/noarch/wassima-2.1.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda + - conda: . + win-64: + - conda: https://prefix.dev/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_10.conda + - conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.7.22-h4c7d964_0.conda + - conda: https://prefix.dev/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/clypi-1.9.1-pyhc364b38_0.conda + - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://prefix.dev/conda-forge/win-64/jh2-5.0.14-py314h9f07db2_1.conda + - conda: https://prefix.dev/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + - conda: https://prefix.dev/conda-forge/win-64/libffi-3.7.0-h3d046cb_1.conda + - conda: https://prefix.dev/conda-forge/win-64/liblzma-5.8.3-hfd05255_1.conda + - conda: https://prefix.dev/conda-forge/win-64/libmpdec-4.0.0-hfd05255_2.conda + - conda: https://prefix.dev/conda-forge/win-64/libpython-3.14.7-h4f90d01_106_cp314.conda + - conda: https://prefix.dev/conda-forge/win-64/libsqlite-3.53.4-hf5d6505_1.conda + - conda: https://prefix.dev/conda-forge/win-64/libzlib-1.3.2-hfd05255_3.conda + - conda: https://prefix.dev/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/loguru-0.7.3-pyh7428d3b_0.conda + - conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://prefix.dev/conda-forge/noarch/niquests-3.21.1-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/win-64/openssl-3.6.4-hf411b9b_0.conda + - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/win-64/pydantic-core-2.46.5-py314h9f07db2_1.conda + - conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/win-64/python-3.14.7-h53f6dd8_106_cp314.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.14-9_cp314.conda + - conda: https://prefix.dev/conda-forge/win-64/qh3-1.9.4-py314h8a07082_0.conda + - conda: https://prefix.dev/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/win-64/ruff-0.16.7-hcc30601_0.conda + - conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://prefix.dev/conda-forge/noarch/soupsieve-2.9.2-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/win-64/tk-8.6.13-h967ab96_4.conda + - conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing-inspection-0.4.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://prefix.dev/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/urllib3-future-2.24.908-pyh5ded981_0.conda + - conda: https://prefix.dev/conda-forge/win-64/vc-14.5-ha367084_41.conda + - conda: https://prefix.dev/conda-forge/win-64/vc14_runtime-14.51.36247-habf1de7_41.conda + - conda: https://prefix.dev/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda + - conda: https://prefix.dev/conda-forge/noarch/wassima-2.1.4-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/win32_setctime-1.2.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda + - conda: . +packages: +- conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + size: 28948 + timestamp: 1770939786096 +- conda: https://prefix.dev/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda + sha256: b8fcb994134d3918d1c64a9d62f4168ff85d5bfb89e698102fe4ed679fe0df24 + md5: 108c928d2a8551832dbc762b535e90bb + depends: + - python >=3.10 + - typing-extensions >=4.0.0 + license: MIT + license_family: MIT + size: 19461 + timestamp: 1784935220549 +- conda: https://prefix.dev/conda-forge/noarch/beautifulsoup4-4.15.0-pyha770c72_0.conda + sha256: aed4b9dcf68ec2a75e5645fed14d77fd884d38d2e52bfa6ef4b278d90cd88781 + md5: 3b261da3fe9b4168738712832410b022 + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + size: 92704 + timestamp: 1780853175566 +- conda: . + name: bible + version: 0.1.0 + build: pyh4616a5c_0 + subdir: noarch + variants: + target_platform: noarch + depends: + - textual >=8.0.0 + - niquests >=3.21.1,<4 + - clypi >=1.9.1,<2 + - pydantic-settings >=2.15.0,<3 + - rich >=15.0.0,<16 + - loguru >=0.7.3,<0.8 + - beautifulsoup4 >=4.15.0,<5 + - python >=3.11 + - python * +- conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda + sha256: 1a0d382c515ebf55f8ee1f38c8b81bc95af5c2acc42ad53b66bc5df932032f96 + md5: e675fabcf81499adc7edf58124fb1e01 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + size: 257808 + timestamp: 1785906269155 +- conda: https://prefix.dev/conda-forge/osx-64/bzip2-1.0.8-h374f1ed_10.conda + sha256: 4ed83961876dc8844a6f0df49c07b408efbaea275ffb0b37133e24c006990b3a + md5: 9d9a39212a876e4bb751c1cc3927b678 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + size: 133271 + timestamp: 1785906721507 +- conda: https://prefix.dev/conda-forge/osx-arm64/bzip2-1.0.8-h4e30115_10.conda + sha256: 8ec22f0ba25cbfc2e64d70cf29459eccd7ffdf6436f6a6ff15bbfef799f7d4f6 + md5: b50612e7d190b8061ab4e7dc119cf4d5 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + size: 124965 + timestamp: 1785906749812 +- conda: https://prefix.dev/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_10.conda + sha256: 04767466ee9227c9c57ab2c6503e0149177d34111c7418d2f420297acb1eb229 + md5: c3301c058362f340100d91cd8be0393f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + size: 55919 + timestamp: 1785906343696 +- conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.7.22-h4c7d964_0.conda + sha256: 95e8e74062a5fe5f870ac8c90302b6e89945165fdaed7810606e84ddee6aac12 + md5: e27d2ac27b096dc51fedfcf775a53f9b + depends: + - __win + license: ISC + size: 132136 + timestamp: 1784754918886 +- conda: https://prefix.dev/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda + sha256: 0a0544cf95f64394fe4959286f5c71f5444ad58feb0602e53becb27448d24da6 + md5: 0f51e2391ade309db462a55611263e9c + depends: + - __unix + license: ISC + size: 131780 + timestamp: 1784754889428 +- conda: https://prefix.dev/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda + sha256: cb60ef3e0631c8bacb4f7057196dee4496091a22baa3bb4b9bccb12c7e1c921b + md5: e0ac3accc64e23e40969d660e5f58ac8 + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 64487 + timestamp: 1786835648298 +- conda: https://prefix.dev/conda-forge/noarch/clypi-1.9.1-pyhc364b38_0.conda + sha256: df8e4bafe4857b22b624219aae1919159db522ecb65cdb627ce80e84d5bb6db1 + md5: 59f642370ff72904cf0563b196269190 + depends: + - python >=3.11 + - python-dateutil >=2.9.0.post0 + - typing_extensions >=4.4.0 + - python + license: MIT + license_family: MIT + size: 45334 + timestamp: 1785180688623 +- conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + size: 27011 + timestamp: 1733218222191 +- conda: https://prefix.dev/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb + md5: b8993c19b0c32a2f7b66cbb58ca27069 + depends: + - python >=3.10 + - typing_extensions + - python + license: MIT + license_family: MIT + size: 39069 + timestamp: 1767729720872 +- conda: https://prefix.dev/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda + sha256: 9f07834f0c546ab14d885ce0366285f61f44e326c0edd1fc63b8294e113ae432 + md5: 72a381cbad04f24b1c2a43ef707f45b4 + depends: + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + license: MIT + license_family: MIT + size: 14459115 + timestamp: 1786545741408 +- conda: https://prefix.dev/conda-forge/osx-arm64/icu-78.3-py310h579977c_2.conda + sha256: 6cdb5dee54c72e56ab189fb3ad33cb28533553d42590e7e831160248f4416a43 + md5: a5efc0b42bb8b42e97d0a29ae3e3c187 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 14070242 + timestamp: 1786545847761 +- conda: https://prefix.dev/conda-forge/linux-64/jh2-5.0.14-py314h7e8cd81_1.conda + sha256: 855f1cb5e2e55e90813a95a0c77f1278036c994f4360a3f73256bdecebbf8fdb + md5: ec76ed6893070535d43dc96995fc5b16 + depends: + - python + - libgcc >=15 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 486147 + timestamp: 1788598668529 +- conda: https://prefix.dev/conda-forge/osx-64/jh2-5.0.14-py314h3cfb53e_1.conda + sha256: b6ebf78e10253bd18c5196ff2bdf15b22cb4b797d540acc5c77feb13705a3584 + md5: 442b3b6e257a0b3fe068297eb96d7718 + depends: + - python + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 460855 + timestamp: 1788598829718 +- conda: https://prefix.dev/conda-forge/osx-arm64/jh2-5.0.14-py314hc05cd11_1.conda + sha256: 010a43c9327438e00d4eafe0998c2a5f6cebe5981f5a12067ae41ca82fd2037d + md5: 01830f8e2a0bdb8218f975b061ec945d + depends: + - python + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 452119 + timestamp: 1788598721432 +- conda: https://prefix.dev/conda-forge/win-64/jh2-5.0.14-py314h9f07db2_1.conda + sha256: baf4bc5c06de999aba79547d384b5bb6f8b261611203edbc2c82b3a84cbdd4b3 + md5: d1e60085a46c8082a2f3ace59ebf8a5a + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 355077 + timestamp: 1788598687273 +- conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + sha256: 27d83f1188cd19bcb7754a078b3fa7f4cfb8527f8eb2fde54dd01fc529d1adec + md5: 449500f2c089da11c40f5c21312e3e07 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.46.1 + license: GPL-3.0-only + license_family: GPL + size: 745303 + timestamp: 1784214507189 +- conda: https://prefix.dev/conda-forge/osx-64/libcxx-23.1.1-h19cb2f5_0.conda + sha256: b702c638bb8f0c6f0a5dd6941e77ca2309b73cd4e575d34b4c5b8e4d01996ccc + md5: 23a540fb2961933d47ad411c9a679385 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 572932 + timestamp: 1788869719775 +- conda: https://prefix.dev/conda-forge/osx-arm64/libcxx-23.1.1-h55c6f16_0.conda + sha256: 3f902803f0fc2643a4f82ff15a06811d974ff5fb6fa8f957720a7ef84e13ad98 + md5: b61106a0b5ac7cfe874f8b2e4f067dfa + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + size: 578565 + timestamp: 1788869180613 +- conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + sha256: 16feffd9ddbbe5b718515d38ee376c685ba95491cd901244e24671d20b952a77 + md5: b24d3c612f71e7aa74158d92106318b2 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + size: 77856 + timestamp: 1781203599810 +- conda: https://prefix.dev/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + sha256: 9c96cc05e056e1bba5b545cbbd57b6e01db622dc2c82934caaaa25cfb22fe666 + md5: dcfdea7b7013beef0a4d744d776ea38f + depends: + - __osx >=11.0 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + size: 76020 + timestamp: 1781204303305 +- conda: https://prefix.dev/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + sha256: 5af74261101e3c777399c6294b2b5d290e508153268eb2e9ff99c4d69834612f + md5: a915151d5d3c5bf039f5ccc8402a436f + depends: + - __osx >=11.0 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + size: 69362 + timestamp: 1781203631990 +- conda: https://prefix.dev/conda-forge/win-64/libexpat-2.8.1-hac47afa_1.conda + sha256: 1a54d874addda73b6f7164d5f3905821277a1831bcc05edd74b3085391688571 + md5: ccc490c81ffe14181861beac0e8f3169 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + size: 71631 + timestamp: 1781203724164 +- conda: https://prefix.dev/conda-forge/linux-64/libffi-3.7.0-h81df57d_1.conda + sha256: c8c7583ef063bc3c430f1d48298e5ad24f796a35c22ed5b14183325825a61106 + md5: 6525a0b06aa4fd390795f0740636a9dd + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + license: MIT + license_family: MIT + size: 68004 + timestamp: 1787753412298 +- conda: https://prefix.dev/conda-forge/osx-64/libffi-3.7.0-h6b3a05b_1.conda + sha256: c4793041226c87f987b9903e6503b76f02f52855cc193be2729ac127e600529b + md5: e077b71ae65754eda067e4125364b905 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 60786 + timestamp: 1787754056669 +- conda: https://prefix.dev/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda + sha256: 2783389a7b9dda04c62cc54c6bbeb03dd3cef24b3fc642715d02a3fa19464a9a + md5: 216bbcc23c11e9695b3db4a8771d76eb + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 43637 + timestamp: 1787753403688 +- conda: https://prefix.dev/conda-forge/win-64/libffi-3.7.0-h3d046cb_1.conda + sha256: 613e8077c5cca5df7fe84ade7d5050301bee3c6c4c450ffe61d34a349ab4f11c + md5: ceb38b0ba900847d8da4289c3c8e5708 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + size: 49992 + timestamp: 1787753517346 +- conda: https://prefix.dev/conda-forge/linux-64/libgcc-16.2.0-ha9f2e26_4.conda + sha256: 24090e675d34403b4ee1cd4372d8f6c0937da7ecfd66a19a57cac2ed0f4ea793 + md5: cba14d01083fc62ffd32c24d7d390633 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==16.2.0=*_4 + - libgomp 16.2.0 he0feb66_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 1058083 + timestamp: 1787618680111 +- conda: https://prefix.dev/conda-forge/linux-64/libgomp-16.2.0-he0feb66_4.conda + sha256: 0fe5cb8e0752241ab55e11656ed1b9726248b522d23b929fe7c95b83eb55b9bb + md5: 89d2c1231f47bd818f5d624b9411459d + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 639968 + timestamp: 1787618616266 +- conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda + sha256: 9787df8c22a59c9a70d3e5a10db9ad663485e75e9ccc3f09bd092cb7b95e0dab + md5: 1390b7c5ac0b1d8e447bc5efa6d3c8c2 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.3.* + license: 0BSD + size: 112995 + timestamp: 1786348617826 +- conda: https://prefix.dev/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_1.conda + sha256: 7915dac7c71c208e40e716e2f6d3eff41a8d5584e0e7c2d46f9bf9bd5f9aa739 + md5: daf49067580fbfeae3ac7f9535400494 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + size: 104919 + timestamp: 1786349094608 +- conda: https://prefix.dev/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_1.conda + sha256: 23d0630046a3e8b164d8f80f2b74ed2605af2e7050ab9913018056402fae4311 + md5: 8ab10323068b107661a4b9a4af84f3b5 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + size: 91720 + timestamp: 1786348695846 +- conda: https://prefix.dev/conda-forge/win-64/liblzma-5.8.3-hfd05255_1.conda + sha256: d36c4a1e1f80fd08e18a407e03622ff2f34dfdd022da6488ad19603dea19e6d5 + md5: 880a0c8549479b198af21ba5dc49b109 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.3.* + license: 0BSD + size: 105809 + timestamp: 1786348717883 +- conda: https://prefix.dev/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_2.conda + sha256: 46820b4a835e175940ae20bec00fdddaff804cda27a1408ab1b95e77a2196437 + md5: fcfed1dc5053eb1901b66e7b1fc32588 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + size: 92759 + timestamp: 1786650399772 +- conda: https://prefix.dev/conda-forge/osx-64/libmpdec-4.0.0-ha1e9b39_2.conda + sha256: 6438d0ef76e81f2b75ad7599685a525407c1d9fb2d6a7c18f360614ae6807970 + md5: b10a43915a31193e06b2781b9d11aec3 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + size: 79639 + timestamp: 1786651070313 +- conda: https://prefix.dev/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_2.conda + sha256: 04cc136c5a956a73aa14e0a160b5822b0b29714e67b299fa1b1fd16dbcba5366 + md5: ff33a4dbd93abc8a798cc4e0e7c8136d + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + size: 73289 + timestamp: 1786651074391 +- conda: https://prefix.dev/conda-forge/win-64/libmpdec-4.0.0-hfd05255_2.conda + sha256: f07e451de3db1836b87f7aedf95c8e65cdb06c0e6105329ba24bb5f7b5c75e2a + md5: 5ae92fd6614edd024576e14069d7ad4c + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + size: 89109 + timestamp: 1786650384519 +- conda: https://prefix.dev/conda-forge/linux-64/libpython-3.14.7-hdc7f604_106_cp314.conda + build_number: 106 + sha256: 5f879892bd439c3d94f4a97b06c214b9a19c4b92f74d84f0305c6ca1b2b239b2 + md5: 28af2158bb8dbb62e55b0af3b44f9d4a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + - libstdcxx >=15 + license: Python-2.0 + size: 10520530 + timestamp: 1788383918299 +- conda: https://prefix.dev/conda-forge/osx-64/libpython-3.14.7-hfe304d0_106_cp314.conda + build_number: 106 + sha256: ddd0f95c3a5da64b9b37b925309f74d9078376f7f0e52f1f32b8b9209d020d6d + md5: 186a99fb514373907206c453031c3938 + depends: + - __osx >=11.0 + - libcxx >=20 + license: Python-2.0 + size: 2112028 + timestamp: 1788385157401 +- conda: https://prefix.dev/conda-forge/osx-arm64/libpython-3.14.7-h4311e70_106_cp314.conda + build_number: 106 + sha256: b239028180e1ba2f7daff4053c89e41f7c0f1a3c21bb82f6dcb09384d123ec1f + md5: 4d7a303343eff82e8b92d52c5fd3991a + depends: + - __osx >=11.0 + - libcxx >=20 + license: Python-2.0 + size: 1875368 + timestamp: 1788383432953 +- conda: https://prefix.dev/conda-forge/win-64/libpython-3.14.7-h4f90d01_106_cp314.conda + build_number: 106 + sha256: 95b7bd6fec10031b44b20276e57c11d71cd1095f966aa84c1bba5ece0eea64d2 + md5: 2d48db2a15a7b768bb563d2675c99b2c + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Python-2.0 + size: 51250 + timestamp: 1788384361391 +- conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.53.4-h13e7031_1.conda + sha256: f20d70da54e5b31dd4a51fb1efeaafafd5ab6dd8d7ac9d1438eaa2526ac4ed3d + md5: e72bbec309c2b0f37823ee7d4fabfcd3 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=15 + - libzlib >=1.3.2,<2.0a0 + license: blessing + size: 974348 + timestamp: 1787051145557 +- conda: https://prefix.dev/conda-forge/osx-64/libsqlite-3.53.4-ha5db789_1.conda + sha256: 78afe0bf88da66d7df62d39632aa69cb73eb32d923cd1175230ce2978618d9c8 + md5: 254c302876eacc77a4682b3fa6f72385 + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + size: 1008994 + timestamp: 1787051932723 +- conda: https://prefix.dev/conda-forge/osx-arm64/libsqlite-3.53.4-hca69786_1.conda + sha256: 839b31d4830e896b4d315b551d27f2bb08026bc946df04bcc360d8627c3ba2cd + md5: fde96d40ebe9a9cb34e4122380189ddb + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + size: 942754 + timestamp: 1787051243846 +- conda: https://prefix.dev/conda-forge/win-64/libsqlite-3.53.4-hf5d6505_1.conda + sha256: 0a45d7c0f20146fff787a106f8fa187872e309c70975ff8f0936e188914c26ad + md5: a72d495965b144bb7da033642d389047 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + size: 1314919 + timestamp: 1787051140039 +- conda: https://prefix.dev/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_4.conda + sha256: 40b792b0186c1e8859280a1f6f19a54fc50a11b32724fc7b637009c1a9bd302b + md5: 2f2ef0d96de5bdd8c1270ff22fdf9352 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 16.2.0 ha9f2e26_4 + constrains: + - libstdcxx-ng ==16.2.0=*_4 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 6613148 + timestamp: 1787618704262 +- conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.42.3-hcfc3c73_0.conda + sha256: aa58bbba56644ffd062a4a9b358782c5eb7560ccead2ab8f7c5c6ede0a7d33a6 + md5: 74a0a409d9f4561265d36b789d3f398a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + license: BSD-3-Clause + license_family: BSD + size: 39998 + timestamp: 1788347719520 +- conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda + sha256: eb8a0db0aa570124f7d2a93d7c7f596e3390df5e047818d873baad32985fc736 + md5: 0de0122d9570a8ab637c6b73db268389 + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_3 + license: Zlib + license_family: Other + size: 63713 + timestamp: 1785362952714 +- conda: https://prefix.dev/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_3.conda + sha256: b2dba286dd6632292b12296e761193b5ef9fb0eaeecaa481f5ba9af72c0c18e1 + md5: 7d3fa28263bb7f8ea32db11f570a5bb6 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_3 + license: Zlib + license_family: Other + size: 58993 + timestamp: 1785276808631 +- conda: https://prefix.dev/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_3.conda + sha256: a18fa5d5bac452401459f966cf0d872224e8080c4ff93c77e168d43ab42ef9d7 + md5: f39288f0ea63ae962e1a2e4f355a0d75 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_3 + license: Zlib + license_family: Other + size: 47822 + timestamp: 1785277049190 +- conda: https://prefix.dev/conda-forge/win-64/libzlib-1.3.2-hfd05255_3.conda + sha256: 0629c2cc0404d3bb29d6baa7b4ba62da80797015e86de050db81ea5a07050527 + md5: 5d2ff29d465097458cc3ff6569151991 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - zlib 1.3.2 *_3 + license: Zlib + license_family: Other + size: 58529 + timestamp: 1785276664143 +- conda: https://prefix.dev/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda + sha256: ffbbb6eace53e7edee1e2e9948e4c8774eb4b382dea185d827865afb7b694c7f + md5: 786c0fa25c80049e9d4b5cd960a5a0b2 + depends: + - python >=3.10 + - uc-micro-py + - python + license: MIT + license_family: MIT + size: 27991 + timestamp: 1788173299262 +- conda: https://prefix.dev/conda-forge/noarch/loguru-0.7.3-pyh707e725_0.conda + sha256: e4a07f357a4cf195a2345dabd98deab80f4d53574abe712a9cc7f22d3f2cc2c3 + md5: 49647ac1de4d1e4b49124aedf3934e02 + depends: + - __unix + - python >=3.9 + license: MIT + license_family: MIT + size: 59696 + timestamp: 1746634858826 +- conda: https://prefix.dev/conda-forge/noarch/loguru-0.7.3-pyh7428d3b_0.conda + sha256: 647ce9601c1a63af4710a2d718a74fa521da28262c41bbf86189f6c0906b23f6 + md5: a6c25c54d8d524735db2e5785aec0a4e + depends: + - __win + - colorama >=0.3.4 + - python >=3.9 + - win32_setctime >=1.0.0 + license: MIT + license_family: MIT + size: 60119 + timestamp: 1746634872414 +- conda: https://prefix.dev/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + sha256: 0c4c35376fe920714390d46e4b8d31c876d65f18e1655899e0763ec25f2a902f + md5: 6d03368f2b2b0a5fb6839df53b2eb5e0 + depends: + - mdurl >=0.1,<1 + - python >=3.10 + license: MIT + license_family: MIT + size: 69017 + timestamp: 1778169663339 +- conda: https://prefix.dev/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda + sha256: 49db23cbfb1c1d414a14d7540195208b994ebd747beba0f15c903f3a0a2dc446 + md5: ad6821df7a98510117db06e9a833281f + depends: + - markdown-it-py >=2.0.0,<5.0.0 + - python >=3.10 + license: MIT + license_family: MIT + size: 50460 + timestamp: 1778692223625 +- conda: https://prefix.dev/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 + md5: 592132998493b3ff25fd7479396e8351 + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 14465 + timestamp: 1733255681319 +- conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda + sha256: 5d46557214ed184381dafe835b7c94a474a1c3b307a08a250b1ea4779b44ffb3 + md5: ee6c0cd80a60961a1f48aa3e0b91f986 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: X11 AND BSD-3-Clause + size: 911196 + timestamp: 1786355078102 +- conda: https://prefix.dev/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_1.conda + sha256: 12c1d676b9a0e8109b576672519312c5428308f3f3d7706f8717e2f536d5d9e7 + md5: 88a79390f87c8f3334b9999a892e78d4 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + size: 834865 + timestamp: 1786356957789 +- conda: https://prefix.dev/conda-forge/osx-arm64/ncurses-6.6-he64c551_1.conda + sha256: 7024a48c8c0d0114ed4ab53c76bf9275d50e91ba7cea367a9aead638d3c29c68 + md5: 3dfa0d0316dc246cd44937a557de4501 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + size: 804298 + timestamp: 1786355189145 +- conda: https://prefix.dev/conda-forge/noarch/niquests-3.21.1-pyh5ded981_0.conda + sha256: 07b53b3eb1dbf3e2f274ae8613380ff91e2ec7493223494faa8494e030476f89 + md5: 5c8a86b56d3281cba8e2364a5c73e50f + depends: + - python >=3.11 + - charset-normalizer >=2,<4 + - wassima >=1.0.1,<3 + - urllib3-future <3,>=2.13.903 + - python + license: Apache-2.0 + license_family: APACHE + size: 158538 + timestamp: 1788759550025 +- conda: https://prefix.dev/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda + sha256: 4747b2d6a8336f52343bceb8a1ebf41a9e6e665b9d2e3f989972de53a310599e + md5: 16e3034a330cc625f2c685edec60cf4e + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=15 + license: Apache-2.0 + license_family: Apache + size: 3202955 + timestamp: 1787698780103 +- conda: https://prefix.dev/conda-forge/osx-64/openssl-3.6.4-h332eb6d_0.conda + sha256: 73d648d24f0994fd8641972848d74bd57b8d80888a64600643e3fe88a1b50545 + md5: 7a1b628e987d25df3ac6986d45bb0979 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 2780873 + timestamp: 1787700098779 +- conda: https://prefix.dev/conda-forge/osx-arm64/openssl-3.6.4-h55eecbc_0.conda + sha256: f23239eacd75c4c50705e68fae1aa3292da473e6a3a4abe2330f1e6afa680704 + md5: ae71ab40048c19a389a7dcccb86c2481 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 3110142 + timestamp: 1787698648639 +- conda: https://prefix.dev/conda-forge/win-64/openssl-3.6.4-hf411b9b_0.conda + sha256: 9dddb559ba49744d5d94092d8d13cb0567f5c3b3f439f3acf28433d1f4256acc + md5: 73cb1783f47c452be4c309430fbef89f + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + size: 9474879 + timestamp: 1787699876495 +- conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda + sha256: 8cbae1fed8437e63c9bcc691927402edb3809fccb26b15eb095df7dd2eb73e5f + md5: 48e9ecf08b0578a4313f13b2e2b0fd72 + depends: + - python >=3.11 + - python + license: MIT + size: 27718 + timestamp: 1788959137036 +- conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda + sha256: 701ed8122021f62e1c9a46bdf4932ada911c10afa56ba0459f56da940bcbe5a7 + md5: 2d0f9304fc1cb1e3e94e7b37c14cd70a + depends: + - typing-inspection >=0.4.2 + - typing_extensions >=4.14.1 + - python >=3.10 + - annotated-types >=0.6.0 + - pydantic-core ==2.46.5 + - python + license: MIT + license_family: MIT + size: 346633 + timestamp: 1787941273167 +- conda: https://prefix.dev/conda-forge/linux-64/pydantic-core-2.46.5-py314h7e8cd81_1.conda + sha256: 85154e3129bb93421f9307d45bc37f775a492978dfec5a260623b52cb5ed983c + md5: 2b163c163407eaaa862002a9ceb8ff11 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - libgcc >=15 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 1876160 + timestamp: 1787928980353 +- conda: https://prefix.dev/conda-forge/osx-64/pydantic-core-2.46.5-py314h3cfb53e_1.conda + sha256: 5ef14829b26ab72b0de67def73abdd40c6c1d9ed3dfb314e09b4949742abb04f + md5: 933dda543db0bebd40814c19463a5455 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 1862427 + timestamp: 1787929087521 +- conda: https://prefix.dev/conda-forge/osx-arm64/pydantic-core-2.46.5-py314hc05cd11_1.conda + sha256: 60a639a7bd3cde997574e0f41af74c76fe37056c42b94fe1bf61a4e1b376942b + md5: 586050b385a407a449fd7e5e4a5c8fef + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __osx >=11.0 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 1691220 + timestamp: 1787928985509 +- conda: https://prefix.dev/conda-forge/win-64/pydantic-core-2.46.5-py314h9f07db2_1.conda + sha256: 5f9b6cebac7129d445193e609e99bd6e66193098bc1fd1d2f784576a67ae5720 + md5: a2e1f7dbbdec163ff40ee75a65bc64dc + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: MIT + license_family: MIT + size: 1861822 + timestamp: 1787928979492 +- conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda + sha256: 674e1ba12010a88441f817a318d3cec7e7a7682813ebd3f427fbd1c6766a16af + md5: 687adac0b3eb2dd73762e9f912b4549e + depends: + - typing-inspection >=0.4.0 + - python >=3.10 + - pydantic >=2.7.0 + - python-dotenv >=0.21.0 + - python + license: MIT + license_family: MIT + size: 60984 + timestamp: 1786146776076 +- conda: https://prefix.dev/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + sha256: f5f015ff1bc3e1b7fc08eee096b1865234189ae0b82bebdb86a5369116e42fa0 + md5: 2882dee445dfa45b0c5afce3ffb7730a + depends: + - python >=3.10 + - python + license: BSD-2-Clause + license_family: BSD + size: 959376 + timestamp: 1786995678795 +- conda: https://prefix.dev/conda-forge/linux-64/python-3.14.7-hcd007b5_106_cp314.conda + build_number: 106 + sha256: 4cd05407d6b07d00fd5b6cc78bd2f60ae5e21f777eb26ead82be2e3751c610a1 + md5: 56ee91e118243e46bfc0c41e4030d354 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.7.0,<3.8.0a0 + - libgcc >=15 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libpython 3.14.7 hdc7f604_106_cp314 + - libsqlite >=3.53.4,<4.0a0 + - libuuid >=2.42.3,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.8,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 26484459 + timestamp: 1788383955577 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://prefix.dev/conda-forge/osx-64/python-3.14.7-hafa0b4b_106_cp314.conda + build_number: 106 + sha256: 74e72b101d4985b9919fcf01fec55ff9c82e862377d6be23b99bfe5562ffc207 + md5: 148a7a307869b0f1e4560ec5400d29bb + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.7.0,<3.8.0a0 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libpython 3.14.7 hfe304d0_106_cp314 + - libsqlite >=3.53.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.8,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 12612709 + timestamp: 1788385311014 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://prefix.dev/conda-forge/osx-arm64/python-3.14.7-h0ae1c2c_106_cp314.conda + build_number: 106 + sha256: 5ffc8349ca089ec5ac39ad36a095323ad177ebe05cbfc690c1e135560cfd68b7 + md5: 5fd37c78250b4f57c2bcf9ad2e54beb1 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.7.0,<3.8.0a0 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libpython 3.14.7 h4311e70_106_cp314 + - libsqlite >=3.53.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.8,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 12223441 + timestamp: 1788383470498 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://prefix.dev/conda-forge/win-64/python-3.14.7-h53f6dd8_106_cp314.conda + build_number: 106 + sha256: 9fdcb8a5018d91a22484e67ac6c5ae9116f082f953328f43ed4421418740dfb1 + md5: df0b471269a379db0262256e332c1e7c + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.7.0,<3.8.0a0 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libpython 3.14.7 h4f90d01_106_cp314 + - libsqlite >=3.53.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.8,<4.0a0 + - python_abi 3.14.* *_cp314 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 18377601 + timestamp: 1788384478011 + python_site_packages_path: Lib/site-packages +- conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + size: 233310 + timestamp: 1751104122689 +- conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda + sha256: ad7b2dd059c1b693a09f799cbaf4b9d06ec7677c02c9447be98e0b33a49bc04c + md5: b55edb60d527ce56226a1026abcb3e2c + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + size: 28328 + timestamp: 1787003234261 +- conda: https://prefix.dev/conda-forge/noarch/python_abi-3.14-9_cp314.conda + build_number: 9 + sha256: e7f8e965bbfb98e08feb2365cacdad8bb218fa5b5a0a2752f747287f425f213c + md5: 350d89b50d7de805abf2e63e29dee451 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 6791 + timestamp: 1788302824440 +- conda: https://prefix.dev/conda-forge/linux-64/qh3-1.9.4-py314haf003cf_0.conda + sha256: cc10452bfcd768a8df5193c85af501d2c4698c54c6cb2569d39a22ab7b1d1d47 + md5: c44cf26b641fae8dde45a8644510b6dc + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.14.* *_cp314 + constrains: + - __glibc >=2.17 + license: BSD-3-Clause + license_family: BSD + size: 2194668 + timestamp: 1784169677703 +- conda: https://prefix.dev/conda-forge/osx-64/qh3-1.9.4-py314h7d911d6_0.conda + sha256: 7631628935d25131418721ea4bca526224f7630278c1cd6c5cd4f25f89764658 + md5: 9e368ac7060cf38dd78c2bd72df39d1c + depends: + - python + - __osx >=11.0 + - libcxx >=19 + - python_abi 3.14.* *_cp314 + - openssl >=3.5.7,<4.0a0 + constrains: + - __osx >=11.0 + license: BSD-3-Clause + license_family: BSD + size: 2158169 + timestamp: 1784169859958 +- conda: https://prefix.dev/conda-forge/osx-arm64/qh3-1.9.4-py314h548cc06_0.conda + sha256: a33da386d8371723f25ffbdcee101467fffb4027346f195a6924aeceefe6dd26 + md5: 230357a1fc625bc15c3b3e25083ad482 + depends: + - python + - libcxx >=19 + - __osx >=11.0 + - openssl >=3.5.7,<4.0a0 + - python_abi 3.14.* *_cp314 + constrains: + - __osx >=11.0 + license: BSD-3-Clause + license_family: BSD + size: 2023587 + timestamp: 1784169760018 +- conda: https://prefix.dev/conda-forge/win-64/qh3-1.9.4-py314h8a07082_0.conda + sha256: 8f47619fc8cf22daf02040e9b2cf695c8b6f40f851e7145982f58720eab80770 + md5: 5f0e4f76903ea368e51520fa48ae453d + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 2033457 + timestamp: 1784169740544 +- conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda + sha256: 01b3fe073a66e321970d09e04b388708f8cbdca5cdfbfcb7c9eeb470ad10383d + md5: 69c01c781e7c8190bbdaf79e3848c5ca + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + - ncurses >=6.6,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 348899 + timestamp: 1787033801506 +- conda: https://prefix.dev/conda-forge/osx-64/readline-8.3-h000c2f7_1.conda + sha256: feb9248a062b7a0738e56339d8c9e87bc9adf8e8c76549fb55b0b2456bec3a57 + md5: 434eb49340f57827ef7ca3bb21f77c32 + depends: + - __osx >=11.0 + - ncurses >=6.6,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 319279 + timestamp: 1787034454836 +- conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h8b90a29_1.conda + sha256: d6782b430e6dce4fe8c7ac118cd988d5a193eb4a7f60741edfaede58016b6110 + md5: 9347fd56a002e6b58946831d48fe62f6 + depends: + - __osx >=11.0 + - ncurses >=6.6,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 314395 + timestamp: 1787033878899 +- conda: https://prefix.dev/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + sha256: 3d6ba2c0fcdac3196ba2f0615b4104e532525ffa1335b50a2878be5ff488814a + md5: 0242025a3c804966bf71aa04eee82f66 + depends: + - markdown-it-py >=2.2.0 + - pygments >=2.13.0,<3.0.0 + - python >=3.10 + - typing_extensions >=4.0.0,<5.0.0 + - python + license: MIT + license_family: MIT + size: 208577 + timestamp: 1775991661559 +- conda: https://prefix.dev/conda-forge/linux-64/ruff-0.16.7-hbcb56a7_0.conda + noarch: python + sha256: 5ed4c55297861e06c25fadf4b93c7ace90093368a8a8e71a0cc690d3ce806ca3 + md5: 23da3766254f778b56fe168882b84058 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 8979648 + timestamp: 1789115793914 +- conda: https://prefix.dev/conda-forge/osx-64/ruff-0.16.7-haa00350_0.conda + noarch: python + sha256: 27ecb48d8829ef7a5e74665bcf739f7d610804998024f316db35b1391f03ce5f + md5: d1ba04295dfdc65bf47eb4cab76bf7a9 + depends: + - python + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 8862602 + timestamp: 1789115953083 +- conda: https://prefix.dev/conda-forge/osx-arm64/ruff-0.16.7-ha7a9cbd_0.conda + noarch: python + sha256: a8fdd5b1906b4265d61bde50646bc5f8a4605d1644198501f384973d08966874 + md5: 8287f02a7a4c8babf7d05b7966b3bfe3 + depends: + - python + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 8210669 + timestamp: 1789115818497 +- conda: https://prefix.dev/conda-forge/win-64/ruff-0.16.7-hcc30601_0.conda + noarch: python + sha256: 61fa582f421a6edd66874338c149437337e4b19ac1cb3d6a802bebbf04e3b846 + md5: 9bce6eb98ca3a26401f82ce7d486cbe5 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + size: 9332759 + timestamp: 1789115768076 +- conda: https://prefix.dev/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + size: 18455 + timestamp: 1753199211006 +- conda: https://prefix.dev/conda-forge/noarch/soupsieve-2.9.2-pyhd8ed1ab_0.conda + sha256: 056d7a2e91e303a9ee37e580f6dde0511fc3fb72476581cc337aacf2cc747613 + md5: ba33e6c8a46ee373fdf6dd8665212778 + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 39439 + timestamp: 1786202135509 +- conda: https://prefix.dev/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda + sha256: 88997e493c8b74b05d8f9bc48133d297d4b5412809c2a2a041e5b40204fdff02 + md5: 89f2b015d7ee36ceaaf03fe687d1bd61 + depends: + - pygments >=2.19.2,<3.0.0 + - typing_extensions >=4.4.0,<5.0.0 + - platformdirs >=3.6.0,<5 + - python >=3.10,<4.0.0 + - markdown-it-py >=2.1.0 + - linkify-it-py >=1,<3 + - mdit-py-plugins + - rich >=14.2.0 + - python + constrains: + - tree_sitter >=0.25.0 + - tree_sitter_languages 1.10.2.* + license: MIT + license_family: MIT + size: 538351 + timestamp: 1782808451993 +- conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda + build_number: 104 + sha256: a1a241d172c1ccab067ba245206dd048bc3c2d1b84504b53c9468e99adfc16a1 + md5: 676a2f9b1c4fcf57b70aa5c65f6c60d0 + depends: + - libgcc >=15 + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - xorg-libx11 >=1.8.13,<2.0a0 + license: TCL + size: 3566806 + timestamp: 1787272857910 +- conda: https://prefix.dev/conda-forge/osx-64/tk-8.6.13-ha6c374e_4.conda + sha256: 74e5fabcaf6ca0db052c645646ea04ded98a5eb7540483217f8101a819eb3107 + md5: 71c9f1f0267f2639523e898c6b50a4dc + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: TCL + size: 3512384 + timestamp: 1787272935349 +- conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-hbeba79b_4.conda + sha256: 857d89087ae7f2c328cf256728affcb7343031a148b4af847334d248a9cf564c + md5: 90a01bf394d1c594e0eb9258f967d828 + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: TCL + size: 3342183 + timestamp: 1787272852357 +- conda: https://prefix.dev/conda-forge/win-64/tk-8.6.13-h967ab96_4.conda + sha256: f19618a3a82cc483dacf1c70a30367ee7b0c7e71b90f1f80e14feff44fbd3688 + md5: dd9eb33c99d352b6356f86964d6040f7 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: TCL + size: 3782376 + timestamp: 1787272860855 +- conda: https://prefix.dev/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda + sha256: 5cf833b4d199688b7652fb322ecc134de9555e9444d9249750de9a153421ad0a + md5: e4942e2de7436ff28be7f5645cf3c8d6 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 49054 + timestamp: 1784287707171 +- conda: https://prefix.dev/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + sha256: b141933ece3518f6d7b75dfb59451e2f26b405a44c18e2518a83e9a02e09315c + md5: c680b5747e8c4c8f23dca0bb7042a8fc + depends: + - typing_extensions ==4.16.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + size: 94080 + timestamp: 1783002732887 +- conda: https://prefix.dev/conda-forge/noarch/typing-inspection-0.4.4-pyhcf101f3_0.conda + sha256: 293c66b208468d186a94ff486c2ffbf67859a2bde8c88e965fc9d06c517191b2 + md5: 880e91eac5d926568ef278e20554148e + depends: + - python >=3.10 + - typing_extensions >=4.15.0 + - python + license: MIT + license_family: MIT + size: 21070 + timestamp: 1786818918217 +- conda: https://prefix.dev/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + sha256: 2d888f90af0686044882c74193ec80a90ec1943145d94a7b1b048958acda1848 + md5: c70ad746c22219b9700931707482992c + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + size: 52631 + timestamp: 1783002732887 +- conda: https://prefix.dev/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + sha256: b928c30ddcb0e3f544c6eade8352737e6e610e263276b90232db6a578ef899d8 + md5: fcb489df604d100968b737f2cb6076c6 + license: LicenseRef-Public-Domain + size: 118849 + timestamp: 1784250406640 +- conda: https://prefix.dev/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda + sha256: 9e5a0e70e876fca50de075b1cc6641cdc009a16c18f4d52ab03edb777729a1bc + md5: 4fdd327852ffefc83173a7c029690d0c + depends: + - python >=3.10 + license: MIT + license_family: MIT + size: 13378 + timestamp: 1772479008776 +- conda: https://prefix.dev/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + size: 694692 + timestamp: 1756385147981 +- conda: https://prefix.dev/conda-forge/noarch/urllib3-future-2.24.908-pyh5ded981_0.conda + sha256: aafb1f0241b3b68ed14d076339a09872bcbd0752f8ee8bffae202f6933975f75 + md5: 10643e49d2d1277d84b82fc16cd7a25b + depends: + - python >=3.11 + - qh3 >=1.5.4,<2.0.0 + - h11 >=0.11.0,<1.0.0 + - jh2 >=5.0.3,<6.0.0 + - python + license: MIT + license_family: MIT + size: 249368 + timestamp: 1788878396093 +- conda: https://prefix.dev/conda-forge/win-64/vc-14.5-ha367084_41.conda + sha256: 35444c55a92e2f7f7ba26bc70f81e56e52344f7d064c0fd4b40a46a58517b79c + md5: aa805b5522c2a98fa286e551a1f48546 + depends: + - vc14_runtime >=14.51.36247 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + size: 21383 + timestamp: 1785359368566 +- conda: https://prefix.dev/conda-forge/win-64/vc14_runtime-14.51.36247-habf1de7_41.conda + sha256: 4e4cb599cdc41bf2109d1464c127b5bcbddf548ce3e322e612afb691338b48f8 + md5: ac5333bb3d429361f23adf704cc49a78 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.51.36247 habf1de7_41 + constrains: + - vs2015_runtime 14.51.36247.* *_41 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + size: 767955 + timestamp: 1785359364369 +- conda: https://prefix.dev/conda-forge/win-64/vcomp14-14.51.36247-habf1de7_41.conda + sha256: 731e043390c9457299484d39e427221fc868a9249540a498a5a4f6456c7744d1 + md5: 350bb67a5c8e5f1c53347ac544ab6600 + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.51.36247.* *_41 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + size: 155910 + timestamp: 1785359349999 +- conda: https://prefix.dev/conda-forge/noarch/wassima-2.1.4-pyhcf101f3_0.conda + sha256: effa446b287ab00b4d0a03df334558c78478746930a40842e0dd05b6157dd674 + md5: c360ddb001e67d7c564dd3668f103ee9 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + size: 137275 + timestamp: 1788549669686 +- conda: https://prefix.dev/conda-forge/noarch/win32_setctime-1.2.0-pyhd8ed1ab_0.conda + sha256: d7b3128166949d462133d7a86fd8a8d80224dd2ce49cfbdcde9e4b3f8b67bbf2 + md5: e79f83003ee3dba79bf795fcd1bfcc89 + depends: + - python >=3.9 + license: MIT + license_family: MIT + size: 9751 + timestamp: 1733752552137 +- conda: https://prefix.dev/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda + sha256: 47d682b9f6d6ec9eb1a6e6c3e75ea6273e899e78fb7fc59f81d39745009fbc60 + md5: aa459086047c0e5e27023ab19f8cb86a + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 601301 + timestamp: 1786599621503 +- conda: https://prefix.dev/conda-forge/osx-64/zstd-1.5.7-hbc1a06c_7.conda + sha256: 5277886d9704a624dc9b79ac985861e1e431bdb39a57c082507ab577a138ec6c + md5: c1d11f04327e40537ffe6cad5b131019 + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 528228 + timestamp: 1786599702092 +- conda: https://prefix.dev/conda-forge/osx-arm64/zstd-1.5.7-hf451053_7.conda + sha256: da867f5092eb0cb746d353694f0098031fd9817a4ce7d5743121209ae0f406ca + md5: 4ec2684c73812cc2c3d78379384a39cc + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 433687 + timestamp: 1786599629846 +- conda: https://prefix.dev/conda-forge/win-64/zstd-1.5.7-h534d264_7.conda + sha256: ca7daae4f218a11fab82cc2857f0ea518ec3f46acec60490485347a4c22c6b3e + md5: e4ac308c39d6d0e131154976da67cf3b + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 387535 + timestamp: 1786599623274 diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000..a82333e --- /dev/null +++ b/pixi.toml @@ -0,0 +1,37 @@ +[workspace] +channels = ["https://prefix.dev/conda-forge"] +platforms = ["win-64", "linux-64", "osx-arm64", "osx-64"] +preview = ["pixi-build"] + +[tasks] +cli = "bible-cli" +tui = "bible-tui" +sync = "python scripts/sync_manifests.py" +fmt = "ruff format" + +[dependencies] +tomlkit = ">=0.15.1,<0.16" +ruff = ">=0.16.7,<0.17" + +[dependencies.bible] +path = "." + +[package] +name = "bible" +version = "0.1.0" + +[package.build.backend] +name = "pixi-build-python" +version = "0.4.*" + +[package.host-dependencies] +hatchling = "==1.26.3" + +[package.run-dependencies] +textual = ">=8.0.0" +niquests = ">=3.21.1,<4" +clypi = ">=1.9.1,<2" +pydantic-settings = ">=2.15.0,<3" +rich = ">=15.0.0,<16" +loguru = ">=0.7.3,<0.8" +beautifulsoup4 = ">=4.15.0,<5" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6f8a930 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "bible" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "textual>=8.0.0", + "niquests>=3.21.1,<4", + "clypi>=1.9.1,<2", + "pydantic-settings>=2.15.0,<3", + "rich>=15.0.0,<16", + "loguru>=0.7.3,<0.8", + "beautifulsoup4>=4.15.0,<5", +] + +[project.scripts] +bible-tui = "bible.tui.tui:main" +bible-cli = "bible.cli.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..ff96593 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,77 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.12 +target-version = "py312" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Unlike Black, use single quotes for strings. +quote-style = "single" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" \ No newline at end of file diff --git a/scripts/sync_manifests.py b/scripts/sync_manifests.py new file mode 100755 index 0000000..6d12f33 --- /dev/null +++ b/scripts/sync_manifests.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import pathlib +import tomllib + +import tomlkit + +root = pathlib.Path(__file__).resolve().parents[1] +pixi_path = root / 'pixi.toml' +pyproject_path = root / 'pyproject.toml' + +pixi = tomllib.loads(pixi_path.read_text()) +pyproject = tomllib.loads(pyproject_path.read_text()) + +# --- Sync version --- +pixi_version = pixi['package']['version'] +pyproject['project']['version'] = pixi_version + +# --- Sync only [package.run-dependencies] --- +run_deps = pixi.get('package', {}).get('run-dependencies', {}) +runtime_deps = [f'{k}{v}' if isinstance(v, str) else k for k, v in run_deps.items()] + +pyproject['project']['dependencies'] = runtime_deps + +# --- Write back --- +pixi_path.write_text(tomlkit.dumps(pixi)) +pyproject_path.write_text(tomlkit.dumps(pyproject)) + +print( + f'Synced version {pixi_version} and {len(runtime_deps)} runtime dependencies from [package.run-dependencies].' +) diff --git a/src/bible/__init__.py b/src/bible/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bible/api.py b/src/bible/api.py new file mode 100644 index 0000000..db4ba03 --- /dev/null +++ b/src/bible/api.py @@ -0,0 +1,43 @@ +from niquests import AsyncSession + +from .settings import settings + + +class BibleAPI: + async def __aenter__(self): + self.session = AsyncSession( + base_url=settings.ENDPOINT, headers={'api-key': settings.API_KEY} + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.session.close() + + async def get(self, path: str, params: dict | None = None): + response = await self.session.get(path, params=params) + response.raise_for_status() + return response.json() + + async def get_bibles(self): + return await self.get('/v1/bibles') + + async def get_books(self, bible_id: str): + return await self.get(f'/v1/bibles/{bible_id}/books') + + async def get_chapters(self, bible_id: str, book_id: str): + return await self.get(f'/v1/bibles/{bible_id}/books/{book_id}/chapters') + + async def get_verses(self, bible_id: str, chapter_id: str): + return await self.get(f'/v1/bibles/{bible_id}/chapters/{chapter_id}/verses') + + async def get_verse(self, bible_id: str, verse_id: str): + return await self.get(f'/v1/bibles/{bible_id}/verses/{verse_id}') + + async def get_chapter(self, bible_id: str, chapter_id: str): + return await self.get(f'/v1/bibles/{bible_id}/chapters/{chapter_id}') + + async def get_passage(self, bible_id: str, passage_id: str): + return await self.get(f'/v1/bibles/{bible_id}/passages/{passage_id}') + + async def get_section(self, bible_id: str, section_id: str): + return await self.get(f'/v1/bibles/{bible_id}/sections/{section_id}') diff --git a/src/bible/cli/cli.py b/src/bible/cli/cli.py new file mode 100644 index 0000000..f2c5670 --- /dev/null +++ b/src/bible/cli/cli.py @@ -0,0 +1,59 @@ +from typing import final + +from clypi import Command, arg +from loguru import logger +from typing_extensions import override + +from bible.api import BibleAPI +from bible.settings import settings +from bible.sqlite import cache_json, get_cached, init_db + +from .commands import Book, List + + +class BibleCli(Command): + """Bible CLI — interact with Bible.API""" + + subcommand: Book | List | None + api_key: str = arg( + settings.API_KEY, + help='The API key for the Bible CLI', + group='Connection', + ) + bible_name: str = arg( + settings.BIBLE_NAME, + help='The name of the Bible to retrieve books from', + group='Connection', + ) + + @final + @classmethod + def epilog(cls): + return 'Attribution:\n Data provided by Bible.API — https://api.bible.com' + + @override + async def pre_run_hook(self): + """Hook to run before the main command execution.""" + init_db() + + key = 'bibles:list' + if get_cached(key): + logger.debug('Using cached list of Bibles.') + return + + logger.debug('Fetching list of Bibles from the API.') + async with BibleAPI() as api: + response = await api.get_bibles() + data = response.get('data', []) + if not data: + raise ValueError('Unable to fetch list of Bibles from the API.') + cache_json(key, data) + + @override + async def run(self): + """Run the appropriate subcommand based on user input.""" + + +def main(): + cli = BibleCli() + cli.parse().start() diff --git a/src/bible/cli/commands/__init__.py b/src/bible/cli/commands/__init__.py new file mode 100644 index 0000000..cd21512 --- /dev/null +++ b/src/bible/cli/commands/__init__.py @@ -0,0 +1,4 @@ +from .book import Book +from .list import List + +__all__ = ['Book', 'List'] diff --git a/src/bible/cli/commands/book.py b/src/bible/cli/commands/book.py new file mode 100644 index 0000000..a0a5fcf --- /dev/null +++ b/src/bible/cli/commands/book.py @@ -0,0 +1,49 @@ +from typing import final + +from clypi import Command, arg +from loguru import logger +from typing_extensions import override + +from bible.api import BibleAPI +from bible.sqlite import cache_json, load_json + +from .book_commands import Chapter, Chapters, List, Verse, Verses + + +class Book(Command): + """Manage Bible books, including listing and retrieving chapters.""" + + subcommand: List | Chapter | Chapters | Verse | Verses + bible_name: str = arg(inherited=True) + + @final + @classmethod + def epilog(cls): + return 'Attribution:\n Data provided by Bible.API — https://api.bible.com' + + @override + async def pre_run_hook(self): + """Hook to run before the main command execution.""" + if not self.bible_name: + raise ValueError("The 'bible_name' flag is required.") + + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name', '') == self.bible_name: + break + else: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + key = f'books:list:{self.bible_name}' + books = load_json(key) + if books: + logger.debug(f"Using cached list of Bible books for '{self.bible_name}'.") + return + + logger.debug(f"Fetching list of Bible books for '{self.bible_name}'.") + async with BibleAPI() as api: + response = await api.get_books(bible.get('id', '')) + data = response.get('data', []) + if not data: + raise ValueError('Unable to fetch list of Bible books from the API.') + cache_json(key, data) diff --git a/src/bible/cli/commands/book_commands/__init__.py b/src/bible/cli/commands/book_commands/__init__.py new file mode 100644 index 0000000..dccd5f6 --- /dev/null +++ b/src/bible/cli/commands/book_commands/__init__.py @@ -0,0 +1,5 @@ +from .chapter import Chapter, Chapters +from .list import List +from .verse import Verse, Verses + +__all__ = ['Chapter', 'Chapters', 'List', 'Verse', 'Verses'] diff --git a/src/bible/cli/commands/book_commands/chapter.py b/src/bible/cli/commands/book_commands/chapter.py new file mode 100644 index 0000000..9fd2d57 --- /dev/null +++ b/src/bible/cli/commands/book_commands/chapter.py @@ -0,0 +1,268 @@ +import asyncio +from typing import final + +from clypi import Command, Positional, arg +from loguru import logger +from typing_extensions import override + +from bible import console +from bible.api import BibleAPI +from bible.render import generate_summary, render_chapter +from bible.sqlite import cache_json, load_json + + +class Chapter(Command): + """Retrieve a Bible chapter.""" + + bible_name: str = arg(inherited=True) + book_name: Positional[str] = arg( + help='The name of the Bible book to retrieve', + ) + chapter_number: Positional[int | str] = arg( + help='The number of the chapter to retrieve', + ) + + @final + @classmethod + def epilog(cls): + return 'Attribution:\n Data provided by Bible.API — https://api.bible.com' + + @override + async def pre_run_hook(self): + """Hook to run before the main command execution.""" + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name', '') == self.bible_name: + break + else: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + books = load_json(f'books:list:{self.bible_name}') + for book in books: + if book.get('name', '') == self.book_name: + break + else: + raise ValueError(f"Book with name '{self.book_name}' not found.") + + key = f'chapters:list:{self.bible_name}:{self.book_name}' + chapters = load_json(key) + if chapters: + logger.debug( + f"Using cached list of chapters for '{self.bible_name}:{self.book_name}'." + ) + else: + async with BibleAPI() as api: + response = await api.get_chapters(bible['id'], book['id']) + chapters = response.get('data', []) + if not chapters: + raise ValueError(f"No chapters found for '{self.book_name}'.") + + cache_json(key, chapters) + + @override + async def run(self): + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name', '') == self.bible_name: + break + else: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + chapters = load_json(f'chapters:list:{self.bible_name}:{self.book_name}') + for chapter in chapters: + number = chapter.get('number', None) + + if number in ('intro',): + if self.chapter_number == 'intro': + break + continue + + try: + if self.chapter_number == int(number): + break + except ValueError: + logger.warning( + f"Invalid chapter number '{number}' for '{self.bible_name}:{self.book_name}'. Skipping." + ) + continue + else: + raise ValueError( + f"Chapter number '{self.chapter_number}' not found in book '{self.book_name}'." + ) + + key = ( + f'chapters:content:{self.bible_name}:{self.book_name}:{self.chapter_number}' + ) + chapter_cache = load_json(key) + if chapter_cache: + logger.debug( + f"Using cached content for chapter '{self.bible_name}:{self.book_name}:{self.chapter_number}'." + ) + else: + async with BibleAPI() as api: + response = await api.get_chapter(bible['id'], chapter['id']) + chapter_cache = response.get('data', []) + if not chapter_cache: + raise ValueError( + f"No content found for chapter '{self.bible_name}:{self.book_name}:{self.chapter_number}'." + ) + cache_json(key, chapter_cache) + + text = render_chapter( + html=chapter_cache.get('content', ''), + reference=chapter_cache.get('reference', ''), + summary=generate_summary(chapter_cache.get('content', '')), + metadata={ + 'Book': chapter_cache.get('reference', '').split()[0], + 'Chapter': chapter_cache.get('number'), + 'Verses': chapter_cache.get('verseCount'), + 'Copyright': chapter_cache.get('copyright', '').replace('©', '(c)'), + }, + ) + console.out.print(text) + + +class Chapters(Command): + """Retrieve multiple chapters of a Bible book.""" + + bible_name: str = arg(inherited=True) + book_name: Positional[str] = arg( + help='The name of the Bible book to retrieve', + ) + chapter_numbers: Positional[list[int | str]] = arg( + None, + help="The list of chapter numbers or ranges. For example, '1 2 3', '2-4'.", + ) + + @override + async def pre_run_hook(self): + """Hook to run before the main command execution.""" + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name', '') == self.bible_name: + break + else: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + books = load_json(f'books:list:{self.bible_name}') + for book in books: + if book.get('name', '') == self.book_name: + break + else: + raise ValueError(f"Book with name '{self.book_name}' not found.") + + key = f'chapters:list:{self.bible_name}:{self.book_name}' + chapters = load_json(key) + if chapters: + logger.debug( + f"Using cached list of chapters for '{self.bible_name}:{self.book_name}'." + ) + else: + async with BibleAPI() as api: + response = await api.get_chapters(bible['id'], book['id']) + chapters = response.get('data', []) + if not chapters: + raise ValueError(f"No chapters found for '{self.book_name}'.") + + cache_json(key, chapters) + + @override + async def run(self): + bibles = load_json('bibles:list') + bible = next((b for b in bibles if b.get('name') == self.bible_name), None) + if not bible: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + chapters = load_json(f'chapters:list:{self.bible_name}:{self.book_name}') + allowed = expand_chapter_numbers(self.chapter_numbers) + + async def fetch_chapter(chapter): + number = chapter.get('number') + if number is None: + logger.warning( + f"Missing chapter number for '{self.book_name}'. Skipping." + ) + return None + + if number == 'intro' and allowed and 'intro' not in allowed: + return None + if number != 'intro' and allowed and int(number) not in allowed: + return None + + key = f'chapters:content:{self.bible_name}:{self.book_name}:{number}' + cached = load_json(key) + if cached: + logger.debug( + f"Using cached content for chapter '{self.book_name} {number}'." + ) + return cached + + async with BibleAPI() as api: + response = await api.get_chapter(bible['id'], chapter['id']) + data = response.get('data', {}) + if not data: + logger.warning( + f"No content found for chapter '{self.book_name} {number}'." + ) + return None + cache_json(key, data) + return data + + # ✅ Run all chapter fetches concurrently + tasks = [fetch_chapter(ch) for ch in chapters] + results = await asyncio.gather(*tasks, return_exceptions=True) + results = [r for r in results if isinstance(r, dict)] + + if not results: + raise ValueError('No chapters retrieved.') + + for chapter in results: + text = render_chapter( + html=chapter.get('content', ''), + reference=chapter.get('reference', ''), + summary=generate_summary(chapter.get('content', '')), + metadata={ + 'Book': chapter.get('reference', '').split()[0], + 'Chapter': chapter.get('number'), + 'Verses': chapter.get('verseCount'), + 'Copyright': chapter.get('copyright', '').replace('©', '(c)'), + }, + ) + console.out.print(text) + + +def expand_chapter_numbers(chapter_numbers): + if not chapter_numbers: + return None + + allowed = set() + + for item in chapter_numbers: + # intro + if item == 'intro': + allowed.add('intro') + continue + + # numeric int + if isinstance(item, int): + allowed.add(item) + continue + + # numeric string + if isinstance(item, str) and item.isdigit(): + allowed.add(int(item)) + continue + + # range string "2-4" + if isinstance(item, str) and '-' in item: + try: + start, end = item.split('-', maxsplit=1) + allowed.update(range(int(start), int(end) + 1)) + except ValueError: + logger.warning(f"Invalid chapter range '{item}'. Skipping.") + continue + + # anything else is invalid + logger.warning(f"Invalid chapter number '{item}'. Skipping.") + + return allowed diff --git a/src/bible/cli/commands/book_commands/list.py b/src/bible/cli/commands/book_commands/list.py new file mode 100644 index 0000000..c05d7b5 --- /dev/null +++ b/src/bible/cli/commands/book_commands/list.py @@ -0,0 +1,50 @@ +from typing import final + +from clypi import Command, arg +from loguru import logger +from typing_extensions import override + +from bible import console +from bible.sqlite import load_json + + +class List(Command): + """List all available Bible books.""" + + contains: str = arg( + None, + help='Filter Bible books by name containing this string', + group='Filter', + ) + bible_name: str = arg(inherited=True) + sort: bool = arg( + False, + help='Sort the Bible books alphabetically', + group='Filter', + ) + + @final + @classmethod + def epilog(cls): + return 'Attribution:\n Data provided by Bible.API — https://api.bible.com' + + @override + async def run(self): + """List all Bible books, optionally filtered by the 'contains' argument.""" + books = load_json(f'books:list:{self.bible_name}') + if self.sort: + books = sorted(books, key=lambda x: x.get('name', '').lower()) + seen = set() + for book in books: + if ( + self.contains + and self.contains.lower() not in book.get('name', '').lower() + ): + continue + + if book.get('name', '') in seen: + logger.debug(f'Skipping duplicate Bible book: {book.get("name", "")}') + continue + + console.out.print(f'{book.get("name", "")}') + seen.add(book.get('name', '')) diff --git a/src/bible/cli/commands/book_commands/verse.py b/src/bible/cli/commands/book_commands/verse.py new file mode 100644 index 0000000..18f671c --- /dev/null +++ b/src/bible/cli/commands/book_commands/verse.py @@ -0,0 +1,246 @@ +import asyncio +from typing import final + +from clypi import Command, Positional, arg +from loguru import logger +from typing_extensions import override + +from bible import console +from bible.api import BibleAPI +from bible.render import render_verse +from bible.sqlite import cache_json, load_json + + +class Verse(Command): + """Retrieve a single Bible verse.""" + + bible_name: str = arg(inherited=True) + book_name: Positional[str] = arg(help='The name of the Bible book to retrieve') + chapter_number: Positional[int] = arg(help='The number of the chapter to retrieve') + verse_number: Positional[int] = arg(help='The number of the verse to retrieve') + + @final + @classmethod + def epilog(cls): + return 'Attribution:\n Data provided by Bible.API — https://api.bible.com' + + @override + async def pre_run_hook(self): + """Ensure chapter list is cached.""" + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name', '') == self.bible_name: + break + else: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + books = load_json(f'books:list:{self.bible_name}') + for book in books: + if book.get('name', '') == self.book_name: + break + else: + raise ValueError(f"Book with name '{self.book_name}' not found.") + + key = f'chapters:list:{self.bible_name}:{self.book_name}' + chapters = load_json(key) + if chapters: + logger.debug( + f"Using cached list of chapters for '{self.bible_name}:{self.book_name}'." + ) + else: + async with BibleAPI() as api: + response = await api.get_chapters(bible['id'], book['id']) + chapters = response.get('data', []) + if not chapters: + raise ValueError(f"No chapters found for '{self.book_name}'.") + cache_json(key, chapters) + + @override + async def run(self): + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name', '') == self.bible_name: + break + else: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + chapters = load_json(f'chapters:list:{self.bible_name}:{self.book_name}') + for chapter in chapters: + if self.chapter_number == int(chapter.get('number', None)): + break + else: + raise ValueError( + f"Chapter number '{self.chapter_number}' not found in book '{self.book_name}'." + ) + + key = f'verses:list:{self.bible_name}:{self.book_name}:{self.chapter_number}' + verses = load_json(key) + if verses: + logger.debug( + f"Using cached list of verses for '{self.bible_name}:{self.book_name}:{self.chapter_number}'." + ) + else: + async with BibleAPI() as api: + response = await api.get_verses(bible['id'], chapter['id']) + verses = response.get('data', []) + if not verses: + raise ValueError( + f'No verses found for chapter {self.chapter_number}.' + ) + cache_json(key, verses) + + # Match verse by reference string (e.g., "Ruth 1:1") + target_ref = f'{self.book_name} {self.chapter_number}:{self.verse_number}' + verse_meta = next((v for v in verses if v.get('reference') == target_ref), None) + if not verse_meta: + raise ValueError(f'Verse {target_ref} not found.') + + verse_id = verse_meta['id'] + cache_key = f'verses:content:{self.bible_name}:{self.book_name}:{self.chapter_number}:{self.verse_number}' + + verse_data = load_json(cache_key) + if verse_data: + logger.debug(f'Using cached verse {target_ref}.') + else: + async with BibleAPI() as api: + response = await api.get_verse(bible['id'], verse_id) + verse_data = response.get('data', {}) + if not verse_data: + raise ValueError(f'No content found for verse {target_ref}.') + cache_json(cache_key, verse_data) + + text = render_verse(verse_data.get('content', '')) + console.out.print(text) + + +class Verses(Command): + """Retrieve multiple Bible verses.""" + + bible_name: str = arg(inherited=True) + book_name: Positional[str] = arg(help='The name of the Bible book to retrieve') + chapter_number: Positional[int] = arg(help='The number of the chapter to retrieve') + verse_numbers: Positional[list[int | str]] = arg( + None, help="List of verse numbers or ranges. For example, '1 2 3', '5-9'." + ) + + @override + async def pre_run_hook(self): + """Ensure chapter list is cached.""" + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name', '') == self.bible_name: + break + else: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + books = load_json(f'books:list:{self.bible_name}') + for book in books: + if book.get('name', '') == self.book_name: + break + else: + raise ValueError(f"Book with name '{self.book_name}' not found.") + + key = f'chapters:list:{self.bible_name}:{self.book_name}' + chapters = load_json(key) + if not chapters: + async with BibleAPI() as api: + response = await api.get_chapters(bible['id'], book['id']) + chapters = response.get('data', []) + if not chapters: + raise ValueError(f"No chapters found for '{self.book_name}'.") + cache_json(key, chapters) + + @override + async def run(self): + bibles = load_json('bibles:list') + bible = next((b for b in bibles if b.get('name') == self.bible_name), None) + if not bible: + raise ValueError(f"Bible with name '{self.bible_name}' not found.") + + chapters = load_json(f'chapters:list:{self.bible_name}:{self.book_name}') + chapter = next( + (ch for ch in chapters if int(ch.get('number', 0)) == self.chapter_number), + None, + ) + if not chapter: + raise ValueError( + f"Chapter {self.chapter_number} not found in '{self.book_name}'." + ) + + key = f'verses:list:{self.bible_name}:{self.book_name}:{self.chapter_number}' + verses = load_json(key) + if not verses: + async with BibleAPI() as api: + response = await api.get_verses(bible['id'], chapter['id']) + verses = response.get('data', []) + if not verses: + raise ValueError( + f'No verses found for chapter {self.chapter_number}.' + ) + cache_json(key, verses) + + allowed = expand_verse_numbers(self.verse_numbers) + + async def fetch_verse(index, verse_meta): + target_ref = f'{self.book_name} {self.chapter_number}:{index}' + if allowed and index not in allowed: + return None + + verse_id = verse_meta['id'] + cache_key = f'verses:content:{self.bible_name}:{self.book_name}:{self.chapter_number}:{index}' + cached = load_json(cache_key) + if cached: + logger.debug(f'Using cached verse {target_ref}.') + return cached + + async with BibleAPI() as api: + response = await api.get_verse(bible['id'], verse_id) + data = response.get('data', {}) + if not data: + logger.warning(f'No content found for verse {target_ref}.') + return None + cache_json(cache_key, data) + return data + + # ✅ Run all verse fetches concurrently + tasks = [fetch_verse(i, v) for i, v in enumerate(verses, start=1)] + results = await asyncio.gather(*tasks, return_exceptions=True) + results = [r for r in results if isinstance(r, dict)] + + if not results: + raise ValueError('No verses retrieved.') + + for verse in results: + text = render_verse(verse.get('content', '')) + console.out.print(text) + + +def expand_verse_numbers(verse_numbers): + if not verse_numbers: + return None + + allowed = set() + + for item in verse_numbers: + # numeric int + if isinstance(item, int): + allowed.add(item) + continue + + # numeric string + if isinstance(item, str) and item.isdigit(): + allowed.add(int(item)) + continue + + # range string "5-9" + if isinstance(item, str) and '-' in item: + try: + start, end = item.split('-', maxsplit=1) + allowed.update(range(int(start), int(end) + 1)) + except ValueError: + logger.warning(f"Invalid verse range '{item}'. Skipping.") + continue + + logger.warning(f"Invalid verse number '{item}'. Skipping.") + + return allowed diff --git a/src/bible/cli/commands/list.py b/src/bible/cli/commands/list.py new file mode 100644 index 0000000..7205e5d --- /dev/null +++ b/src/bible/cli/commands/list.py @@ -0,0 +1,48 @@ +from typing import final + +from clypi import Command, arg +from loguru import logger +from typing_extensions import override + +from bible import console +from bible.sqlite import load_json + + +class List(Command): + """List all available Bibles.""" + + contains: str = arg( + None, + help='Filter Bibles by name containing this string', + group='Filter', + ) + sort: bool = arg( + False, + help='Sort the Bibles alphabetically', + group='Filter', + ) + + @final + @classmethod + def epilog(cls): + return 'Attribution:\n Data provided by Bible.API — https://api.bible.com' + + @override + async def run(self): + bibles = load_json('bibles:list') + if self.sort: + bibles = sorted(bibles, key=lambda x: x.get('name', '').lower()) + seen = set() + for bible in bibles: + if ( + self.contains + and self.contains.lower() not in bible.get('name', '').lower() + ): + continue + + if bible.get('name', '') in seen: + logger.debug(f'Skipping duplicate Bible: {bible.get("name", "")}') + continue + + console.out.print(f'{bible.get("name", "")}') + seen.add(bible.get('name', '')) diff --git a/src/bible/console.py b/src/bible/console.py new file mode 100644 index 0000000..1ef6a41 --- /dev/null +++ b/src/bible/console.py @@ -0,0 +1,3 @@ +from rich.console import Console + +out = Console() diff --git a/src/bible/render.py b/src/bible/render.py new file mode 100644 index 0000000..5758908 --- /dev/null +++ b/src/bible/render.py @@ -0,0 +1,127 @@ +from bs4 import BeautifulSoup +from rich.markup import escape +from rich.text import Text + +from . import util + + +def generate_summary(html: str) -> str: + soup = BeautifulSoup(html, 'html.parser') + soup = util.normalize_paragraph_classes(soup) + + # Prefer section title if available + section = soup.find('p', class_='s') + if section: + title = section.get_text(strip=True) + return f'{title} — a summary of key events and teachings.' + + # Otherwise, use the first sentence of the first paragraph + first_para = soup.find('p', class_='p') + if first_para: + text = first_para.get_text(' ', strip=True) + first_sentence = text.split('.')[0] + return f'{first_sentence.strip()}.' + + return 'Summary unavailable.' + + +def render_chapter( + html: str, + reference: str, + summary: str | None = None, + metadata: dict | None = None, +) -> Text: + """ + Render a full Bible chapter with rich formatting, summaries, and metadata. + - summary: short overview of the chapter's theme + - metadata: dictionary with contextual info (e.g., {"book": "Genesis", "chapter": 1, "verses": 31}) + """ + soup = BeautifulSoup(html, 'html.parser') + soup = util.normalize_paragraph_classes(soup) + text = Text() + + # Header block + text.append(f'\n{reference}\n', style='bold gold3') + text.append('─' * len(reference) + '\n', style='grey50') + + # Metadata block + if metadata: + meta_line = ' • '.join(f'{k.capitalize()}: {v}' for k, v in metadata.items()) + text.append(f'{meta_line}\n', style='dim white') + + # Chapter summary + if summary: + text.append(f'\n{summary}\n', style='italic grey70') + text.append('─' * len(summary) + '\n\n', style='grey50') + + # Section titles + for s in soup.find_all('p', class_='s'): + title = s.get_text(strip=True) + text.append(f'{title}\n\n', style='bold gold3') + + # Paragraphs with verses + for p in soup.find_all('p', class_='p'): + paragraph_text = Text() + + for span in p.find_all('span', class_='v'): + verse_num = span.get_text(strip=True) + verse_content_parts = [] + + # Collect siblings until next verse span + for sibling in span.next_siblings: + if getattr(sibling, 'name', None) == 'span' and 'v' in sibling.get( + 'class', [] + ): + break + if getattr(sibling, 'name', None) == 'span' and 'it' in sibling.get( + 'class', [] + ): + verse_content_parts.append( + (' ' + sibling.get_text(strip=True) + ' ', 'italic white') + ) + elif isinstance(sibling, str): + verse_content_parts.append((sibling, 'white')) + elif getattr(sibling, 'name', None): + verse_content_parts.append( + (sibling.get_text(' ', strip=True), 'white') + ) + + # Build verse line + paragraph_text.append(f' {verse_num} ', style='bold bright_cyan') + for part, style in verse_content_parts: + if paragraph_text and not paragraph_text.plain.endswith(' '): + paragraph_text.append(' ') + paragraph_text.append(part.strip(), style=style) + paragraph_text.append('\n') + + text.append(paragraph_text) + text.append('\n') + + return text + + +def render_verse(html: str) -> Text: + """Render a single verse with rich formatting.""" + soup = BeautifulSoup(html, 'html.parser') + soup = util.normalize_paragraph_classes(soup) + text = Text() + + # Section titles + for s in soup.find_all('p', class_='s'): + title = s.get_text(strip=True) + text.append(f'\n{title}\n', style='bold gold3') + + # Verse paragraphs + for p in soup.find_all('p', class_='p'): + verse_num = p.find('span', class_='v') + verse_text = p.get_text(' ', strip=True) + + if verse_num: + num = verse_num.get_text(strip=True) + # Remove the verse number from the paragraph text + verse_text = verse_text.replace(num, '', 1).strip() + text.append(f'{num} ', style='bold cyan') + + text.append(escape(verse_text) + '\n', style='white') + + return text diff --git a/src/bible/settings.py b/src/bible/settings.py new file mode 100644 index 0000000..5903456 --- /dev/null +++ b/src/bible/settings.py @@ -0,0 +1,20 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + BIBLE_NAME: str + BOOK_NAME: str = 'Genesis' + ENDPOINT: str = 'https://rest.api.bible' + API_KEY: str + CACHE_EXPIRY_DAYS: int = 30 + THEME: str = 'dark' + + model_config = SettingsConfigDict( + env_file='.env', + env_file_encoding='utf-8', + env_prefix='BIBLE_', + validate_assignment=True, + ) + + +settings = Settings() diff --git a/src/bible/sqlite.py b/src/bible/sqlite.py new file mode 100644 index 0000000..cdaa108 --- /dev/null +++ b/src/bible/sqlite.py @@ -0,0 +1,68 @@ +import json +import sqlite3 +from datetime import datetime, timedelta + +from loguru import logger + +from .settings import settings + +DB_PATH = 'bible_cache.db' + + +def init_db(): + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.execute(""" + SELECT name FROM sqlite_master + WHERE type='table' AND name='cache' + """) + existed = cursor.fetchone() is not None + + conn.execute(""" + CREATE TABLE IF NOT EXISTS cache ( + key TEXT PRIMARY KEY, + value TEXT, + last_updated TEXT + ) + """) + conn.commit() + if existed: + logger.debug('Database already existed.') + else: + logger.debug('Database initialised.') + + +def get_cached(key: str): + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute('SELECT value, last_updated FROM cache WHERE key = ?', (key,)) + row = cursor.fetchone() + + if not row: + return None + + value, last_updated = row + if datetime.fromisoformat(last_updated) < datetime.now() - timedelta( # noqa: DTZ005 + days=settings.CACHE_EXPIRY_DAYS + ): + logger.debug(f'Cache expired for key: {key}') + return None + + return value + + +def set_cached(key: str, value: str): + with sqlite3.connect(DB_PATH) as conn: + conn.execute( + 'REPLACE INTO cache (key, value, last_updated) VALUES (?, ?, ?)', + (key, value, datetime.now().isoformat()), # noqa: DTZ005 + ) + conn.commit() + + +def cache_json(key: str, data: dict | list): + set_cached(key, json.dumps(data)) + + +def load_json(key: str): + cached = get_cached(key) + return json.loads(cached) if cached else None diff --git a/src/bible/tui/exceptions.py b/src/bible/tui/exceptions.py new file mode 100644 index 0000000..3bfd1c5 --- /dev/null +++ b/src/bible/tui/exceptions.py @@ -0,0 +1,6 @@ +class BibleTUIError(Exception): + """Base exception class for Bible TUI errors.""" + + +class BibleTUIFetchError(BibleTUIError): + """Exception raised for errors during fetching chapters or verses.""" diff --git a/src/bible/tui/fetch.py b/src/bible/tui/fetch.py new file mode 100644 index 0000000..a283697 --- /dev/null +++ b/src/bible/tui/fetch.py @@ -0,0 +1,134 @@ +import asyncio + +from bible.api import BibleAPI +from bible.settings import settings +from bible.sqlite import cache_json, load_json + +from .exceptions import BibleTUIFetchError + + +async def fetch_multiple(app, book: str, chapters: list[int], verses: list[int] | None): + """Fetch multiple chapters or verses concurrently.""" + tasks = [] + + for chapter in chapters: + if verses: + for verse in verses: + # Each task resolves IDs and fetches independently + tasks.append(fetch_chapter_or_verse(app, str(chapter), str(verse))) + else: + tasks.append(fetch_chapter_or_verse(app, str(chapter))) + + # Run all tasks concurrently + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Pair results with their reference + output = [] + idx = 0 + for chapter in chapters: + if verses: + for verse in verses: + output.append(('verse', chapter, verse, results[idx])) + idx += 1 + else: + output.append(('chapter', chapter, None, results[idx])) + idx += 1 + + return output + + +async def fetch_chapter_or_verse( + app, + chapter_number: str, + verse_number: str | None = None, +): + """Fetch chapter or verse from cache or API.""" + if verse_number: + key = f'verses:content:{settings.BIBLE_NAME}:{settings.BOOK_NAME}:{chapter_number}:{verse_number}' + else: + key = f'chapters:content:{settings.BIBLE_NAME}:{settings.BOOK_NAME}:{chapter_number}' + + cached = load_json(key) + if cached: + return cached + + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name') == settings.BIBLE_NAME: + bible_id = bible.get('id') + break + else: + raise BibleTUIFetchError(f"Bible '{settings.BIBLE_NAME}' not found in cache.") + + chapter_list = load_json( + f'chapters:list:{settings.BIBLE_NAME}:{settings.BOOK_NAME}' + ) + if not chapter_list: + chapter_list = await app.initialise_chapter_list() + for chapter_item in chapter_list: + if chapter_item.get('number') == chapter_number: + chapter_id = chapter_item.get('id') + break + else: + raise BibleTUIFetchError(f"Chapter '{chapter_number}' not found in cache.") + + async with BibleAPI() as api: + if verse_number: + verse_meta = await fetch_verse_meta( + bible_id, + chapter_number, + verse_number, + chapter_id, + ) + response = await api.get_verse(bible_id, verse_meta.get('id')) + else: + response = await api.get_chapter(bible_id, chapter_id) + + data = response.get('data', {}) + if data: + cache_json(key, data) + return data + + +async def fetch_verse_meta( + bible_id: str, + chapter_number: str, + verse_number: str, + chapter_id: str, +): + """Fetch metadata for a specific verse from cache or API.""" + key = f'verses:content:{settings.BIBLE_NAME}:{settings.BOOK_NAME}:{chapter_number}:{verse_number}' + cached = load_json(key) + if cached: + return cached + + verses_key = ( + f'verses:list:{settings.BIBLE_NAME}:{settings.BOOK_NAME}:{chapter_number}' + ) + verses = load_json(verses_key) + if not verses: + async with BibleAPI() as api: + response = await api.get_verses(bible_id, chapter_id) + verses = response.get('data', []) + if not verses: + raise BibleTUIFetchError( + f"Unable to fetch list of verses for chapter '{chapter_number}' from the API." + ) + cache_json(verses_key, verses) + + target_ref = f'{settings.BOOK_NAME} {chapter_number}:{verse_number}' + for verse in verses: + if isinstance(verse, dict) and verse.get('reference') == target_ref: + verse_meta = verse + break + else: + verse_meta = None + if not verse_meta: + raise BibleTUIFetchError(f'Verse {target_ref} not found.') + + async with BibleAPI() as api: + response = await api.get_verse(bible_id, verse_meta.get('id')) + data = response.get('data', {}) + if data: + cache_json(key, data) + return data diff --git a/src/bible/tui/settings_modal.py b/src/bible/tui/settings_modal.py new file mode 100644 index 0000000..d3672a9 --- /dev/null +++ b/src/bible/tui/settings_modal.py @@ -0,0 +1,85 @@ +from textual.app import ComposeResult +from textual.containers import Vertical +from textual.screen import ModalScreen +from textual.widgets import Button, Input, Label + +from bible.settings import settings +from bible.sqlite import get_cached + +from . import util + + +class SettingsModal(ModalScreen): + """Modal window for editing persistent Bible settings.""" + + def compose(self) -> ComposeResult: + yield Vertical( + Label('Set Bible', id='bible_label'), + Input(value=settings.BIBLE_NAME, placeholder='Default Bible', id='bible'), + Label('Set Theme', id='theme_label'), + Input(value=settings.THEME, placeholder='Default Theme', id='theme'), + Button('Save', id='save', variant='success'), + Button('Cancel', id='cancel', variant='error'), + ) + + async def on_button_pressed(self, event: Button.Pressed) -> None: + """Handler for button press events in the settings modal. + + This method handles the logic for the "Save" and "Cancel" buttons. + If "Cancel" is pressed, the modal is dismissed without saving changes. + If "Save" is pressed, the input is validated and, if valid, the settings are updated and persisted. + + Args: + event (Button.Pressed): The button press event that triggered this handler. + """ + if event.button.id == 'cancel': + self.dismiss() + return + + if event.button.id == 'save': + bible = self.query_one('#bible', Input).value.strip() + + bible_input = self.query_one('#bible', Input) + if not await self.__validate_bible_name(bible): + bible_input.value = f'Invalid Bible name: {bible}' + bible_input.cursor_position = 0 + bible_input.selection = (0, 0) + return + + theme_input = self.query_one('#theme', Input) + theme = theme_input.value.strip() + + if not await self.__validate_theme(theme): + theme_input.value = f'Invalid theme: {theme}' + theme_input.cursor_position = 0 + theme_input.selection = (0, 0) + return + + util.save_env_value('BIBLE_BIBLE_NAME', bible) + util.save_env_value('BIBLE_THEME', theme) + + settings.BIBLE_NAME = bible + settings.THEME = theme + + await self.apply_theme(theme) + + self.dismiss() + + async def __validate_bible_name(self, bible: str) -> bool: + """Validate the Bible name input.""" + bibles = get_cached('bibles:list') + if bibles is None: + return False + return bible in bibles + + async def __validate_theme(self, theme: str) -> bool: + return theme in self.app.available_themes + + async def apply_theme(self, theme_name: str): + self.app.theme = theme_name + + await self.__validate_theme(theme_name) + + self.app.refresh_css(self.app.css_path) + + self.refresh() diff --git a/src/bible/tui/tui.py b/src/bible/tui/tui.py new file mode 100644 index 0000000..f74eae8 --- /dev/null +++ b/src/bible/tui/tui.py @@ -0,0 +1,314 @@ +import argparse +import typing +from typing import Any + +from rich.text import Text +from textual.app import App, ComposeResult +from textual.containers import VerticalScroll +from textual.theme import Theme +from textual.widgets import Footer, Header, Input, Link, Static + +from bible.api import BibleAPI +from bible.render import generate_summary, render_chapter, render_verse +from bible.settings import settings +from bible.sqlite import cache_json, get_cached, init_db, load_json + +from . import util +from .exceptions import BibleTUIFetchError +from .fetch import fetch_chapter_or_verse, fetch_multiple +from .settings_modal import SettingsModal + + +class BibleTUI(App): + """A Textual TUI for browsing and reading Bible chapters.""" + + CSS_PATH = 'tui.tcss' + TITLE = 'Bible TUI' + BINDINGS: typing.ClassVar[list[tuple[str, str, str]]] = [ + ('q', 'quit', 'Quit'), + ('s', 'open_settings', 'Settings'), + ('ctrl+s', 'open_settings', 'Settings'), + ('b', 'list_books', 'List Books'), + ('ctrl+b', 'list_books', 'List Books'), + ('l', 'list_bibles', 'List Bibles'), + ('ctrl+l', 'list_bibles', 'List Bibles'), + ] + + def __init__(self, theme_override: str | None = None, **kwargs): + super().__init__(**kwargs) + self.theme_override = theme_override + init_db() + self.call_later(self.initialise_bible_list) + + async def initialise_bible_list(self): + bibles_key = 'bibles:list' + if get_cached(bibles_key): + return + + async with BibleAPI() as api: + response = await api.get_bibles() + data = response.get('data', []) + if not data: + raise BibleTUIFetchError('Unable to fetch list of Bibles from the API.') + cache_json(bibles_key, data) + + async def initialise_book_list(self) -> Any: + books_key = f'books:list:{settings.BIBLE_NAME}' + if cached_books := load_json(books_key): + return cached_books + + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name') == settings.BIBLE_NAME: + bible_id = bible.get('id') + break + else: + raise BibleTUIFetchError( + f"Bible '{settings.BIBLE_NAME}' not found in cache." + ) + + async with BibleAPI() as api: + response = await api.get_books(bible_id) + data = response.get('data', []) + if not data: + raise BibleTUIFetchError( + f"Unable to fetch list of books for Bible '{settings.BIBLE_NAME}' from the API." + ) + cache_json(books_key, data) + return data + + async def initialise_chapter_list(self) -> Any: + chapters_key = f'chapters:list:{settings.BIBLE_NAME}:{settings.BOOK_NAME}' + if cached_chapters := load_json(chapters_key): + return cached_chapters + + bibles = load_json('bibles:list') + for bible in bibles: + if bible.get('name') == settings.BIBLE_NAME: + bible_id = bible.get('id') + break + else: + raise BibleTUIFetchError( + f"Bible '{settings.BIBLE_NAME}' not found in cache." + ) + + books_key = f'books:list:{settings.BIBLE_NAME}' + books = load_json(books_key) + if not books: + books = await self.initialise_book_list() + + for book in books: + if book.get('name') == settings.BOOK_NAME: + book_id = book.get('id') + break + else: + raise BibleTUIFetchError(f"Book '{settings.BOOK_NAME}' not found in cache.") + + async with BibleAPI() as api: + response = await api.get_chapters(bible_id, book_id) + data = response.get('data', []) + if not data: + raise BibleTUIFetchError( + f'Unable to fetch list of chapters for {settings.BOOK_NAME} from the API.' + ) + cache_json(chapters_key, data) + return data + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + self.input = Input( + placeholder='Enter book chapter or book chapter:verse (e.g., Genesis 1, Genesis 1:1)', + id='search', + ) + + yield Header() + yield self.input + yield VerticalScroll(Static('Welcome to the Bible TUI!', id='content')) + yield Link( + 'Powered by Bible.API', + url='https://api.bible', + tooltip='Click me', + ) + yield Footer() + + async def on_mount(self) -> None: + """Initial setup when the app starts.""" + self.query_one('#content', Static).update( + f'📖 Current Bible: {settings.BIBLE_NAME}\n' + 'Type a reference or press B to list books.' + ) + + theme_name = settings.THEME.lower() + + """ + # If it's a built-in theme, just apply it + if theme_name in self.available_themes: + self.theme = theme_name + return + """ + + light_theme = Theme( + name='onyx-light', + surface='#f5f5f5', + foreground='#1e1e1e', + accent='#4a90e2', + primary='#4a90e2', + secondary='#6bb3ff', + ) + self.register_theme(light_theme) + + dark_theme = Theme( + name='onyx-dark', + surface='#1e1e1e', + foreground='#e6e6e6', + accent='#4a90e2', + primary='#4a90e2', + secondary='#6bb3ff', + ) + self.register_theme(dark_theme) + + self.theme = theme_name + + async def action_open_settings(self) -> None: + await self.push_screen(SettingsModal()) + + async def action_quit(self) -> None: + self.exit() + + async def action_list_bibles(self) -> None: + """List all available Bibles.""" + + key = 'bibles:list' + bibles = load_json(key) + if not bibles: + bibles = await self.initialise_bible_list() + + bible_names = [bible.get('name', 'Unknown') for bible in bibles] + formatted = '\n'.join(bible_names) + self.query_one('#content', Static).update( + f'📚 Available Bibles:\n\n{formatted}' + ) + + async def action_list_books(self) -> None: + """List all books for the current Bible.""" + + key = f'books:list:{settings.BIBLE_NAME}' + books = load_json(key) + if not books: + books = await self.initialise_book_list() + + book_names = [book.get('name', 'Unknown') for book in books] + formatted = '\n'.join(book_names) + self.query_one('#content', Static).update( + f'📚 Books in {settings.BIBLE_NAME}:\n\n{formatted}' + ) + + async def action_show_verse(self) -> None: + verse_input = self.input.value.strip() + try: + book, chapter_verse = verse_input.split(maxsplit=1) + settings.BOOK_NAME = book + chapter, verse = chapter_verse.split(':') + try: + verse = await fetch_chapter_or_verse(self, chapter, verse) + if not verse: + self.query_one('#content', Static).update('⚠️ Verse not found.') + return + except BibleTUIFetchError as e: + self.query_one('#content', Static).update(f'⚠️ {e!s}') + return + except ValueError: + self.query_one('#content', Static).update( + '❌ Invalid format. Use: Genesis 1:1' + ) + return + + html = verse.get('content', '') + text = render_verse(html) + self.query_one('#content', Static).update(text) + + async def on_input_submitted(self, event: Input.Submitted) -> None: + """Handle user search input.""" + await self.action_show_reference() + + async def action_show_reference(self) -> None: + query = self.input.value.strip() + + if query.lower() == 'list books': + await self.action_list_books() + self.input.value = '' + return + + try: + book, chapters, verses = util.parse_reference(query) + if not await self.__validate_book(book): + await self.action_list_books() + raise ValueError( + f"Book '{book}' not found. Enter 'list books' to see available books." + ) + settings.BOOK_NAME = book + except ValueError as e: + self.query_one('#content', Static).update(f'❌ {e}') + return + + try: + items = await fetch_multiple(self, book, chapters, verses) + except BibleTUIFetchError as e: + self.query_one('#content', Static).update(f'⚠️ {e}') + return + + # Render everything + output = Text() + + for kind, chapter, verse, data in items: + if isinstance(data, Exception): + ref = ( + f'{book} {chapter}' + if verse is None + else f'{book} {chapter}:{verse}' + ) + output.append(f'⚠️ Error fetching {ref}: {data}\n') + continue + + html = data.get('content', '') + + if kind == 'verse': + rendered = render_verse(html) + output.append(rendered) + else: + summary = generate_summary(html) + metadata = { + 'Book': book, + 'Chapter': chapter, + 'Verses': data.get('verseCount'), + } + rendered = render_chapter( + html, + f'{book} {chapter}', + summary, + metadata, + ) + output.append(rendered) + + output.append('\n') + + self.query_one('#content', Static).update(output) + + async def __validate_book(self, book: str) -> bool: + books_key = f'books:list:{settings.BIBLE_NAME}' + cached_books = load_json(books_key) + if not cached_books: + cached_books = await self.initialise_book_list() + return book in [b.get('name') for b in cached_books] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description='Bible TUI — interact with Bible.API') + parser.add_argument('--theme', type=str, help='Specify the theme (light or dark)') + return parser.parse_args() + + +def main(): + args = parse_args() + app = BibleTUI(theme_override=args.theme) + app.run() diff --git a/src/bible/tui/tui.tcss b/src/bible/tui/tui.tcss new file mode 100644 index 0000000..befd472 --- /dev/null +++ b/src/bible/tui/tui.tcss @@ -0,0 +1,107 @@ +/* ===== Global Layout ===== */ + +Screen { + background: $surface; + color: $text; +} + +/* ===== Header / Footer ===== */ + +Header { + background: $accent; + color: $text; + padding: 1 2; + text-style: bold; +} + +Footer { + background: $surface-darken-3; + color: $text-muted; + border-top: tall $accent; +} + +/* ===== Input Bar ===== */ + +#input { + background: $surface-lighten-2; + border: tall $accent; +} + +/* ===== Main Content Area ===== */ + +#content { + background: $surface; + color: $text; + padding: 2 4; + overflow-y: auto; +} + +/* Make scripture text more readable */ +#content Text { + padding-top: 1; + padding-bottom: 1; +} + +/* Verse numbers */ +.vnum { + color: $accent; + text-style: bold; +} + +/* ===== Modal Window ===== */ + +/* Modal overlay */ +ModalScreen { + background: $surface-darken-3 40%; + align: center middle; +} + +/* Modal window */ +ModalScreen > Vertical { + background: $surface-lighten-2 85%; + border: thick $accent; + padding: 2 4; + width: 40%; + max-width: 60; + height: auto; +} + +ModalScreen Button { + margin-top: 1; + width: 100%; +} + +ModalScreen Label { + padding: 1 1; +} + + +/* ===== Buttons ===== */ + +Button { + background: $accent; + color: $text; + text-style: bold; +} + +Button:hover { + background: $accent-lighten-3; +} + +/* Footer */ +Footer { + background: $surface-darken-3; + color: $text-muted; + layout: horizontal; + min-height: 3; + padding: 0 1; +} + +/* ===== Attribution ===== */ + +#api-attribution { + dock: bottom; + padding: 1 2; + background: $surface; + color: $text-muted; +} \ No newline at end of file diff --git a/src/bible/tui/util.py b/src/bible/tui/util.py new file mode 100644 index 0000000..c3e0fb3 --- /dev/null +++ b/src/bible/tui/util.py @@ -0,0 +1,79 @@ +from pathlib import Path + + +def parse_reference(text: str): + """ + Parse references like: + Genesis 1 + Genesis 1:1 + Genesis 1-3 + Genesis 1:1-5 + Genesis 1-3:1-4 + Returns (book, chapter_numbers, verse_numbers) + """ + text = text.strip() + + try: + book, rest = text.split(maxsplit=1) + except ValueError: + raise ValueError('Invalid format. Use: Genesis 1 or Genesis 1:1') + + # Chapter and verse part + if ':' in rest: + chapter_part, verse_part = rest.split(':', 1) + chapter_items = chapter_part.split() + verse_items = verse_part.split() + chapters = expand_numbers(chapter_items) + verses = expand_numbers(verse_items) + return book, chapters, verses + + # Only chapters + chapter_items = rest.split() + chapters = expand_numbers(chapter_items) + return book, chapters, None + + +def expand_numbers(items: list[str | int]) -> list[int]: + """Expand numbers and ranges like ['1', '3-5'] into [1,3,4,5].""" + result = [] + + for item in items: + if isinstance(item, int): + result.append(item) + continue + + item = item.strip() + + if item.isdigit(): + result.append(int(item)) + continue + + if '-' in item: + start, end = item.split('-', 1) + if start.isdigit() and end.isdigit(): + result.extend(range(int(start), int(end) + 1)) + continue + + # Ignore invalid items silently + continue + + return sorted(set(result)) + + +def save_env_value(key: str, value: str): + env_path = Path('.env') + lines = env_path.read_text().splitlines() + new_lines = [] + found = False + + for line in lines: + if line.startswith(f'{key}='): + new_lines.append(f'{key}="{value}"') + found = True + else: + new_lines.append(line) + + if not found: + new_lines.append(f'{key}="{value}"') + + env_path.write_text('\n'.join(new_lines)) diff --git a/src/bible/util.py b/src/bible/util.py new file mode 100644 index 0000000..cfd6e79 --- /dev/null +++ b/src/bible/util.py @@ -0,0 +1,22 @@ +from bs4 import BeautifulSoup + + +def normalize_paragraph_classes(soup: BeautifulSoup) -> BeautifulSoup: + """ + Normalise paragraph class names across different Bible translations. + Ensures consistent 's' for section titles and 'p' for verse paragraphs. + """ + for p in soup.find_all('p'): + classes = p.get('class', []) + if not classes: + continue + + # Section titles (e.g. s1, s2 → s) + if any(cls.startswith('s') for cls in classes): + p['class'] = ['s'] + + # Verse paragraphs (e.g. m, b, li1, li2 → p) + elif any(cls.startswith(('m', 'b', 'li', 'p')) for cls in classes): + p['class'] = ['p'] + + return soup