commit 6e5fd427f7bc59900d047570bb907f46615dded3 Author: onyx-and-iris Date: Tue Sep 15 00:16:23 2026 +0100 first commit 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 0000000..30888cd Binary files /dev/null and b/img/cli.png differ diff --git a/img/tui.png b/img/tui.png new file mode 100755 index 0000000..28338d4 Binary files /dev/null and b/img/tui.png differ 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