diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3de9751..f7cae66 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,3 +57,39 @@ jobs: # with: # # If you use a different name, update COMMENT_ARTIFACT_NAME accordingly # name: python-coverage-comment-action + + docs: + needs: [pre-commit, pytest] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + # - name: Download coverage svg + # uses: actions/download-artifact@v3 + # with: + # name: coverage-badge + # path: docs/assets/ + # + # - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + # name: Update cache_id + # + - name: Apply mkdocs cache + uses: actions/cache@v3 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + + - name: Install doc dependencies via poetry + run: | + pip install poetry + poetry install --with dev + + - name: Build docs with gh-deploy --force + run: | + poetry run mkdocs gh-deploy --force diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py new file mode 100644 index 0000000..8629b57 --- /dev/null +++ b/docs/gen_ref_pages.py @@ -0,0 +1,69 @@ +"""Generate documentation pages from docstrings.""" + +from logging import getLogger +from pathlib import Path +from typing import Generator + +import mkdocs_gen_files + +logger = getLogger(__name__) + + +CODE_PATH: Path = Path() +CODE_DOC_REGEX: str = "*.py" +DOC_SUFFIX: str = ".md" +GEN_DOC_PATH: str = "reference" +DOC_NAV_FILE_NAME: str = "DOC_STRINGS.md" +DOC_NAV_FILE_PATH: Path = Path(str(GEN_DOC_PATH)) / str(DOC_NAV_FILE_NAME) + +EXCLUDE_PATHS: tuple[str, ...] = ( + "azure", + "data", + "docker", + "meeting_notes", + "notebooks", + "docs", + "tests", +) + +nav: mkdocs_gen_files.Nav = mkdocs_gen_files.Nav() + + +for path in sorted(CODE_PATH.rglob(CODE_DOC_REGEX)): + module_path: Path = path.relative_to(str(CODE_PATH)).with_suffix("") + doc_path: Path = path.relative_to(str(CODE_PATH)).with_suffix(DOC_SUFFIX) + + full_doc_path: Path = Path(str(GEN_DOC_PATH), doc_path) + + parts: tuple[str, ...] = tuple(module_path.parts) + logger.debug(f"Managing module path {parts}") + + if parts[-1] == "__init__": + parts = parts[:-1] + if len(parts) == 0: + logger.debug(f"Skipping {parts}") + continue + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1] == "__main__" or set(EXCLUDE_PATHS) & set(parts): + logger.debug(f"Skipping module path {parts}") + continue + + logger.debug(f"Will write to {full_doc_path}") + + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + doc_md_path: str = ".".join(parts) + section_heading: str = "::: " + doc_md_path + logger.debug(f"Adding {section_heading}") + fd.write(section_heading) + + mkdocs_gen_files.set_edit_path(full_doc_path, path) + + +with mkdocs_gen_files.open(DOC_NAV_FILE_PATH, "w") as nav_file: + logger.warning(f"Opening {DOC_NAV_FILE_NAME} to generate navigation file.") + literate_nav_str: Generator = nav.build_literate_nav() + logger.debug(f"Writing:\n{literate_nav_str}") + nav_file.writelines(literate_nav_str) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..3a5c082 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,94 @@ +site_name: 'Reginald: a simple Slack bot written in Python' +dev_addr: '127.0.0.1:9000' +docs_dir: . + +nav: + - Home: README.md + - Models: MODELS.md + - Configuration: ENVIRONMENT_VARIABLES.md + - Issues: https://github.com/alan-turing-institute/reginald/issues + - Reference: reference/ + +repo_url: https://github.com/alan-turing-institute/reginald/ + +watch: + - docs + +theme: + name: material + features: + - content.code.copy + - content.tabs.link + palette: + # Palette toggle for automatic mode + - media: '(prefers-color-scheme)' + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: '(prefers-color-scheme: light)' + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +plugins: + - search: + lang: en + - same-dir + # optional + # - include-markdown + # - markdown-exec + - gen-files: + scripts: + - docs/gen_ref_pages.py + - literate-nav: + nav_file: DOC_STRINGS.md + - section-index + - autorefs + + - mkdocstrings: + handlers: + python: + paths: [.] + options: + docstring_style: numpy + separate_signature: true + show_signature_annotations: true + annotations_path: brief + line_length: 80 + signature_crossrefs: true + merge_init_into_classes: true + +markdown_extensions: + - smarty + - admonition + - pymdownx.details + - abbr + - attr_list + - tables + - footnotes + - pymdownx.arithmatex: + generic: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + use_pygments: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.magiclink +# +# extra_css: +# - css/code_select.css diff --git a/poetry.lock b/poetry.lock index b68a58e..bb1f2b5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -279,6 +279,20 @@ typing-extensions = ">=4.6.0" [package.extras] aio = ["azure-core[aio] (>=1.28.0)"] +[[package]] +name = "babel" +version = "2.15.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -1119,6 +1133,23 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe, test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] tqdm = ["tqdm"] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + [[package]] name = "gitdb" version = "4.0.11" @@ -1222,6 +1253,20 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] +[[package]] +name = "griffe" +version = "0.45.3" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-0.45.3-py3-none-any.whl", hash = "sha256:ed1481a680ae3e28f91a06e0d8a51a5c9b97555aa2527abc2664447cc22337d6"}, + {file = "griffe-0.45.3.tar.gz", hash = "sha256:02ee71cc1a5035864b97bd0dbfff65c33f6f2c8854d3bd48a791905c2b8a44b9"}, +] + +[package.dependencies] +colorama = ">=0.4" + [[package]] name = "grpcio" version = "1.60.1" @@ -2032,6 +2077,21 @@ files = [ httpx = ">=0.20.0" pydantic = ">=1.10" +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -2169,6 +2229,17 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + [[package]] name = "minijinja" version = "2.0.1" @@ -2186,6 +2257,205 @@ files = [ {file = "minijinja-2.0.1.tar.gz", hash = "sha256:e774beffebfb8a1ad17e638ef70917cf5e94593f79acb8a8fff7d983169f3a4e"}, ] +[[package]] +name = "mkdocs" +version = "1.6.0" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.0.1" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, + {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-gen-files" +version = "0.5.0" +description = "MkDocs plugin to programmatically generate documentation pages during the build" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs_gen_files-0.5.0-py3-none-any.whl", hash = "sha256:7ac060096f3f40bd19039e7277dd3050be9a453c8ac578645844d4d91d7978ea"}, + {file = "mkdocs_gen_files-0.5.0.tar.gz", hash = "sha256:4c7cf256b5d67062a788f6b1d035e157fc1a9498c2399be9af5257d4ff4d19bc"}, +] + +[package.dependencies] +mkdocs = ">=1.0.3" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-literate-nav" +version = "0.6.1" +description = "MkDocs plugin to specify the navigation in Markdown instead of YAML" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs_literate_nav-0.6.1-py3-none-any.whl", hash = "sha256:e70bdc4a07050d32da79c0b697bd88e9a104cf3294282e9cb20eec94c6b0f401"}, + {file = "mkdocs_literate_nav-0.6.1.tar.gz", hash = "sha256:78a7ab6d878371728acb0cdc6235c9b0ffc6e83c997b037f4a5c6ff7cef7d759"}, +] + +[package.dependencies] +mkdocs = ">=1.0.3" + +[[package]] +name = "mkdocs-material" +version = "9.5.26" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.26-py3-none-any.whl", hash = "sha256:5d01fb0aa1c7946a1e3ae8689aa2b11a030621ecb54894e35aabb74c21016312"}, + {file = "mkdocs_material-9.5.26.tar.gz", hash = "sha256:56aeb91d94cffa43b6296fa4fbf0eb7c840136e563eecfd12c2d9e92e50ba326"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocs-same-dir" +version = "0.1.3" +description = "MkDocs plugin to allow placing mkdocs.yml in the same directory as documentation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_same_dir-0.1.3-py3-none-any.whl", hash = "sha256:3d094649e2e47efcf90a8b0051a4c2b837aaf4137a28c8e334ba9465804a317e"}, + {file = "mkdocs_same_dir-0.1.3.tar.gz", hash = "sha256:c849556b1d79ae270947f41bb89d442aa1e858ab6ec6423eb178ae76a7f984fc"}, +] + +[package.dependencies] +mkdocs = ">=1.0.3" + +[[package]] +name = "mkdocs-section-index" +version = "0.3.9" +description = "MkDocs plugin to allow clickable sections that lead to an index page" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_section_index-0.3.9-py3-none-any.whl", hash = "sha256:5e5eb288e8d7984d36c11ead5533f376fdf23498f44e903929d72845b24dfe34"}, + {file = "mkdocs_section_index-0.3.9.tar.gz", hash = "sha256:b66128d19108beceb08b226ee1ba0981840d14baf8a652b6c59e650f3f92e4f8"}, +] + +[package.dependencies] +mkdocs = ">=1.2" + +[[package]] +name = "mkdocstrings" +version = "0.25.1" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings-0.25.1-py3-none-any.whl", hash = "sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51"}, + {file = "mkdocstrings-0.25.1.tar.gz", hash = "sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf"}, +] + +[package.dependencies] +click = ">=7.0" +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.4" +mkdocs-autorefs = ">=0.3.1" +platformdirs = ">=2.2.0" +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.10.3" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.10.3-py3-none-any.whl", hash = "sha256:11ff6d21d3818fb03af82c3ea6225b1534837e17f790aa5f09626524171f949b"}, + {file = "mkdocstrings_python-1.10.3.tar.gz", hash = "sha256:321cf9c732907ab2b1fedaafa28765eaa089d89320f35f7206d00ea266889d03"}, +] + +[package.dependencies] +griffe = ">=0.44" +mkdocstrings = ">=0.25" + [[package]] name = "mkl" version = "2021.4.0" @@ -2731,6 +3001,16 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + [[package]] name = "pandas" version = "2.2.2" @@ -3366,6 +3646,24 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pymdown-extensions" +version = "10.8.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] name = "pypdf" version = "4.2.0" @@ -3552,6 +3850,20 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "rapidfuzz" version = "3.9.3" @@ -4081,13 +4393,13 @@ files = [ [[package]] name = "slack-sdk" -version = "3.28.0" +version = "3.29.0" description = "The Slack API Platform SDK for Python" optional = false python-versions = ">=3.6" files = [ - {file = "slack_sdk-3.28.0-py2.py3-none-any.whl", hash = "sha256:1a47700ae20566575ce494d1d1b6f594b011d06aad28e3b8e28c052cad1d6c4c"}, - {file = "slack_sdk-3.28.0.tar.gz", hash = "sha256:e6ece5cb70850492637e002e3b0d26d307939f4a33203b88cb274f7475c9a144"}, + {file = "slack_sdk-3.29.0-py2.py3-none-any.whl", hash = "sha256:1857300b9bdd5cd43eb527a0dd8c6415aa0623cd76e153bf167e93968efd5121"}, + {file = "slack_sdk-3.29.0.tar.gz", hash = "sha256:a6ad02fd23a7c70ded7e48e0fe317e269820e64e55069699cc75d64fb2ebf5a1"}, ] [package.extras] @@ -4809,6 +5121,50 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "watchdog" +version = "4.0.1" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, + {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, + {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, + {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, + {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -5097,4 +5453,4 @@ azure = ["azure-storage-file-share", "pulumi", "pulumi-azure-native"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "4d7330aee8ed44e7b403e2a21fc304c310e2ae5f9fba6a72079257869506a085" +content-hash = "94a6b1070b9c28f4eb543bf3001b677856b25ec7fd0535a078f0181fc2a3492a" diff --git a/pyproject.toml b/pyproject.toml index ff73adc..8216d62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,12 @@ pytest = "^8.2.2" ipython = "^8.25.0" pytest-cov = "^5.0.0" poetry = "^1.8.3" +mkdocs-material = "^9.5.26" +mkdocstrings-python = "^1.10.3" +mkdocs-gen-files = "^0.5.0" +mkdocs-literate-nav = "^0.6.1" +mkdocs-section-index = "^0.3.9" +mkdocs-same-dir = "^0.1.3" [tool.poetry.extras] api_bot = [ diff --git a/reginald/models/chat_interact.py b/reginald/models/chat_interact.py index 1bc5cfa..cc28584 100644 --- a/reginald/models/chat_interact.py +++ b/reginald/models/chat_interact.py @@ -1,9 +1,18 @@ import readline +from typing import Final from reginald.models.base import ResponseModel from reginald.models.setup_llm import setup_llm -art = """ +from ..utils import REGINALD_PROMPT + +INPUT_PROMPT: Final[str] = ">>> " +EXIT_STRS: set[str] = {"exit", "exit()", "quit()", "bye Reginald"} +CLEAR_HISTORY_STRS: set[str] = {"clear_history", r"\clear_history"} + +ART: Final[ + str +] = r""" (`-') (`-') _ _ <-. (`-')_(`-') _ _(`-') <-.(OO ) ( OO).-/ .-> (_) \( OO) (OO ).-/ <-. ( (OO ).-> ,------,(,------.,---(`-'),-(`-',--./ ,--// ,---. ,--. ) \ .'_ @@ -19,23 +28,23 @@ def run_chat_interact(streaming: bool = False, **kwargs) -> ResponseModel: # set up response model response_model = setup_llm(**kwargs) user_id = "command_line_chat" - print(art) + print(ART) while True: - message = input(">>> ") - if message in ["exit", "exit()", "quit()", "bye Reginald"]: + message = input(INPUT_PROMPT) + if message in EXIT_STRS: return response_model if message == "": continue - if message in ["clear_history", "\clear_history"]: + if message in ["clear_history", r"\clear_history"]: if ( response_model.mode == "chat" and response_model.chat_engine.get(user_id) is not None ): response_model.chat_engine[user_id].reset() - print("\nReginald: History cleared.") + print(f"\n{REGINALD_PROMPT}History cleared.") else: - print("\nReginald: No history to clear.") + print(f"\n{REGINALD_PROMPT}No history to clear.") continue if streaming: @@ -43,4 +52,4 @@ def run_chat_interact(streaming: bool = False, **kwargs) -> ResponseModel: print("") else: response = response_model.direct_message(message=message, user_id=user_id) - print(f"\nReginald: {response.message}") + print(f"\n{REGINALD_PROMPT}{response.message}") diff --git a/reginald/utils.py b/reginald/utils.py index 720e520..359a39b 100644 --- a/reginald/utils.py +++ b/reginald/utils.py @@ -5,85 +5,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn -REGINAL_PROMPT: Final[str] = "Reginald: " - - -def stream_iter_progress_wrapper( - streamer: Iterable | Callable | chain, - task_str: str = REGINAL_PROMPT, - progress_bar: bool = True, - end: str = "", - *args, - **kwargs, -) -> Iterable: - """Add a progress bar for iteration. - - Examples - -------- - >>> from time import sleep - >>> def sleeper(naps: int = 3) -> Generator[str, None, None]: - ... for nap in range(naps): - ... sleep(1) - ... yield f'nap: {nap}' - >>> tuple(stream_iter_progress_wrapper(streamer=sleeper)) - - Reginald: ('nap: 0', 'nap: 1', 'nap: 2') - >>> tuple(stream_iter_progress_wrapper( - ... streamer=sleeper, progress_bar=False)) - Reginald: ('nap: 0', 'nap: 1', 'nap: 2') - """ - if isinstance(streamer, Callable): - streamer = streamer(*args, **kwargs) - if progress_bar: - with Progress( - TextColumn("{task.description}[progress.description]"), - SpinnerColumn(), - transient=True, - ) as progress: - if isinstance(streamer, list | tuple): - streamer = (item for item in streamer) - assert isinstance(streamer, Generator) - progress.add_task(task_str) - first_item = next(streamer) - streamer = chain((first_item,), streamer) - print(task_str, end=end) - return streamer - - -def stream_progress_wrapper( - streamer: Callable, - task_str: str = REGINAL_PROMPT, - progress_bar: bool = True, - end: str = "\n", - *args, - **kwargs, -) -> Any: - """Add a progress bar for iteration. - - Examples - -------- - >>> from time import sleep - >>> def sleeper(seconds: int = 3) -> str: - ... sleep(seconds) - ... return f'{seconds} seconds nap' - >>> stream_progress_wrapper(sleeper) - - Reginald: - '3 seconds nap' - """ - if progress_bar: - with Progress( - TextColumn("{task.description}[progress.description]"), - SpinnerColumn(), - transient=True, - ) as progress: - progress.add_task(task_str) - results: Any = streamer(*args, **kwargs) - print(task_str, end=end) - return results - else: - print(task_str, end=end) - return streamer(*args, **kwargs) +REGINALD_PROMPT: Final[str] = "Reginald: " def get_env_var( @@ -140,3 +62,119 @@ def create_folder(folder: str) -> None: os.makedirs(folder) else: logging.info(f"Folder '{folder}' already exists") + + +def stream_progress_wrapper( + streamer: Callable, + task_str: str = REGINALD_PROMPT, + use_spinner: bool = True, + end: str = "\n", + *args, + **kwargs, +) -> Any: + """Add a progress bar for iteration. + + Parameters + ---------- + streamer + Funciton to add the `SpinnerColumn` while running + task_str + What to print whether `use_spinner` is `True` or not, + and if `use_spinner` is `True` is printed prior to + the `SpinningColumn`. + use_spinner + Whether to print the `SpinnerColumn` or not. + end + What to pass to the `end` parameter of `print` calls. + args + Any arguments to pass to `streamer` + kwargs + Any keyward arguments to pass to `streamer`. + + Examples + -------- + >>> from time import sleep + >>> def sleeper(seconds: int = 3) -> str: + ... sleep(seconds) + ... return f'{seconds} seconds nap' + >>> stream_progress_wrapper(sleeper) + + Reginald: + '3 seconds nap' + >>> stream_progress_wrapper(sleeper, use_spinner=False, end='') + Reginald: '3 seconds nap' + """ + if use_spinner: + with Progress( + TextColumn("{task.description}[progress.description]"), + SpinnerColumn(), + transient=True, + ) as progress: + progress.add_task(task_str) + results: Any = streamer(*args, **kwargs) + print(task_str, end=end) + return results + else: + print(task_str, end=end) + return streamer(*args, **kwargs) + + +def stream_iter_progress_wrapper( + streamer: Iterable | Callable | chain, + task_str: str = REGINALD_PROMPT, + use_spinner: bool = True, + end: str = "", + *args, + **kwargs, +) -> Iterable: + """Add a progress bar for iteration. + + Parameters + ---------- + streamer + `Iterable`, `Callable` or `chain` to add the `SpinnerColumn` + while iteraing over. A `Callabe` will be converted to a + `Generator`. + task_str + What to print whether `use_spinner` is `True` or not, + and if `use_spinner` is `True` is printed prior to + the `SpinningColumn`. + use_spinner + Whether to print the `SpinnerColumn` or not. + end + What to pass to the `end` parameter of `print` calls. + args + Any arguments to pass to `streamer` + kwargs + Any keyward arguments to pass to `streamer`. + + Examples + -------- + >>> from time import sleep + >>> def sleeper(naps: int = 3) -> Generator[str, None, None]: + ... for nap in range(naps): + ... sleep(1) + ... yield f'nap: {nap}' + >>> tuple(stream_iter_progress_wrapper(streamer=sleeper)) + + Reginald: ('nap: 0', 'nap: 1', 'nap: 2') + >>> tuple(stream_iter_progress_wrapper( + ... streamer=sleeper, use_spinner=False)) + Reginald: ('nap: 0', 'nap: 1', 'nap: 2') + """ + if isinstance(streamer, Callable): + streamer = streamer(*args, **kwargs) + if use_spinner: + with Progress( + TextColumn("{task.description}[progress.description]"), + SpinnerColumn(), + transient=True, + ) as progress: + if isinstance(streamer, list | tuple): + streamer = (item for item in streamer) + assert isinstance(streamer, Generator) + progress.add_task(task_str) + first_item = next(streamer) + streamer = chain((first_item,), streamer) + print(task_str, end=end) + return streamer diff --git a/tests/test_chat_interact.py b/tests/test_chat_interact.py index 1cb68a0..9afbd7d 100644 --- a/tests/test_chat_interact.py +++ b/tests/test_chat_interact.py @@ -5,11 +5,18 @@ from typer.testing import CliRunner from reginald.cli import cli -from reginald.models.chat_interact import art, run_chat_interact +from reginald.models.chat_interact import ( + ART, + CLEAR_HISTORY_STRS, + EXIT_STRS, + INPUT_PROMPT, + REGINALD_PROMPT, + run_chat_interact, +) from reginald.models.simple.hello import Hello runner = CliRunner() -art_split = art.splitlines() +art_split = ART.splitlines() def test_chat_cli(): @@ -17,9 +24,11 @@ def test_chat_cli(): result = runner.invoke(cli, ["chat"], input="What's up dock?\nexit\n") term_stdout_lines: list[str] = result.stdout.split("\n") assert term_stdout_lines[: len(art_split)] == art_split - assert term_stdout_lines[len(art_split) + 1] == ">>> " - assert term_stdout_lines[len(art_split) + 2] == "Reginald: Hello! How are you?" - assert term_stdout_lines[len(art_split) + 3] == ">>> " + assert term_stdout_lines[len(art_split) + 1] == INPUT_PROMPT + assert ( + term_stdout_lines[len(art_split) + 2] == f"{REGINALD_PROMPT}Hello! How are you?" + ) + assert term_stdout_lines[len(art_split) + 3] == INPUT_PROMPT def test_chat_cli_no_stream(): @@ -29,51 +38,29 @@ def test_chat_cli_no_stream(): ) term_stdout_lines: list[str] = result.stdout.split("\n") assert term_stdout_lines[: len(art_split)] == art_split - assert term_stdout_lines[len(art_split) + 1] == ">>> " + assert term_stdout_lines[len(art_split) + 1] == INPUT_PROMPT assert ( term_stdout_lines[len(art_split) + 2] - == "Reginald: Let's discuss this in a channel!" + == f"{REGINALD_PROMPT}Let's discuss this in a channel!" ) - assert term_stdout_lines[len(art_split) + 3] == ">>> " - - -def test_chat_interact_exit(): - with mock.patch.object(builtins, "input", lambda _: "exit"): - interaction = run_chat_interact(model="hello") - assert isinstance(interaction, Hello) + assert term_stdout_lines[len(art_split) + 3] == INPUT_PROMPT -def test_chat_interact_exit_with_bracket(): - with mock.patch.object(builtins, "input", lambda _: "exit()"): +@pytest.mark.parametrize("input", EXIT_STRS) +def test_chat_interact_exit(input: str): + with mock.patch.object(builtins, "input", lambda _: input): interaction = run_chat_interact(model="hello") assert isinstance(interaction, Hello) -def test_chat_interact_quit_with_bracket(): - with mock.patch.object(builtins, "input", lambda _: "quit()"): - interaction = run_chat_interact(model="hello") - assert isinstance(interaction, Hello) - - -def test_chat_interact_bye(): - with mock.patch.object(builtins, "input", lambda _: "bye Reginald"): - interaction = run_chat_interact(model="hello") - assert isinstance(interaction, Hello) - - -def test_chat_interact_clear_history(): - result = runner.invoke(cli, ["chat"], input="clear_history\n") - term_stdout_lines: list[str] = result.stdout.split("\n") - assert term_stdout_lines[: len(art_split)] == art_split - assert term_stdout_lines[len(art_split) + 1] == ">>> " - assert term_stdout_lines[len(art_split) + 2] == "Reginald: No history to clear." - assert term_stdout_lines[len(art_split) + 3] == ">>> " - - -def test_chat_interact_slash_clear_history(): - result = runner.invoke(cli, ["chat"], input="\clear_history\n") +@pytest.mark.parametrize("input", CLEAR_HISTORY_STRS) +def test_chat_interact_clear_history(input: str): + result = runner.invoke(cli, ["chat"], input=input) term_stdout_lines: list[str] = result.stdout.split("\n") assert term_stdout_lines[: len(art_split)] == art_split - assert term_stdout_lines[len(art_split) + 1] == ">>> " - assert term_stdout_lines[len(art_split) + 2] == "Reginald: No history to clear." - assert term_stdout_lines[len(art_split) + 3] == ">>> " + assert term_stdout_lines[len(art_split) + 1] == INPUT_PROMPT + assert ( + term_stdout_lines[len(art_split) + 2] + == f"{REGINALD_PROMPT}No history to clear." + ) + assert term_stdout_lines[len(art_split) + 3] == INPUT_PROMPT