diff --git a/.gitignore b/.gitignore index 4d985df..66f133f 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dmypy.json # Pyre type checker .pyre/ +docs/source/api/generated +docs/source/api/openapi.json diff --git a/.readthedocs.yml b/.readthedocs.yml index fe4c76e..bdc1c07 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,6 +13,9 @@ build: os: ubuntu-22.04 tools: python: "3.11" + jobs: + pre_build: + - python docs/source/api/generate_openapi.py # Optionally set the version of Python and requirements required to build your docs python: diff --git a/docs/requirements.txt b/docs/requirements.txt index 313830d..f15cf61 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,3 +6,4 @@ autodoc_pydantic myst-nb sphinx-design sphinx_github_changelog +sphinxcontrib-openapi diff --git a/docs/source/api/contributing.rst b/docs/source/api/contributing.rst new file mode 100644 index 0000000..8418b75 --- /dev/null +++ b/docs/source/api/contributing.rst @@ -0,0 +1,138 @@ +======================== +Contributing to Xpublish +======================== + +Contributions are highly welcomed and appreciated. Every little help counts, +so do not hesitate! + +.. contents:: Contribution links + :depth: 2 + +.. _submitfeedback: + +Feature requests and feedback +----------------------------- + +Do you like Xpublish? Share some love on Twitter or in your blog posts! + +We'd also like to hear about your propositions and suggestions. Feel free to +`submit them as issues `_ and: + +* Explain in detail how they should work. +* Keep the scope as narrow as possible. This will make it easier to implement. + +.. _reportbugs: + +Report bugs +----------- + +Report bugs for Xpublish in the `issue tracker `_. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting, + specifically the Python interpreter version, installed libraries, and Xpublish + version. +* Detailed steps to reproduce the bug. + +If you can write a demonstration test that currently fails but should pass +(xfail), that is a very useful commit to make as well, even if you cannot +fix the bug itself. + +.. _fixbugs: + +Fix bugs +-------- + +Look through the `GitHub issues for bugs `_. + +Talk to developers to find out how you can fix specific bugs. + +Write documentation +------------------- + +xpublish could always use more documentation. What exactly is needed? + +* More complementary documentation. Have you perhaps found something unclear? +* Docstrings. There can never be too many of them. +* Blog posts, articles and such -- they're all very appreciated. + +You can also edit documentation files directly in the GitHub web interface, +without using a local copy. This can be convenient for small fixes. + +To build the documentation locally, you first need to have a local development environment setup +by following the the steps in `Preparing Pull Requests <#pull-requests>`__ through the **Install +dependencies into a new conda environment** step. + +You can then build the documentation with the following commands:: + + $ conda activate xpublish-dev + $ cd docs + $ pip install -r requirements.txt + $ make html + +The built documentation should be available in the ``docs/_build/`` folder. + +.. _`pull requests`: +.. _pull-requests: + +Preparing Pull Requests +----------------------- + +#. Fork the + `xpublish GitHub repository `__. It's + fine to use ``xpublish`` as your fork repository name because it will live + under your user. + +#. Clone your fork locally using `git `_ and create a branch:: + + $ git clone git@github.com:YOUR_GITHUB_USERNAME/xpublish.git + $ cd xpublish + + # now, to fix a bug or add feature create your own branch off "main": + + $ git checkout -b your-bugfix-feature-branch-name main + +#. Install `pre-commit `_ and its hook on the Xpublish repo:: + + $ pip install --user pre-commit + $ pre-commit install + + Afterwards ``pre-commit`` will run whenever you commit. + + https://pre-commit.com/ is a framework for managing and maintaining multi-language pre-commit hooks + to ensure code-style and code formatting is consistent. + +#. Install dependencies into a new conda environment:: + + $ conda create -n xpublish-dev + $ conda activate xpublish-dev + $ pip install -r dev-requirements.txt + $ pip install --no-deps -e . + +#. Run all the tests + + Now running tests is as simple as issuing this command:: + + $ conda activate xpublish-dev + $ pytest + + This command will run tests via the "pytest" tool. + +#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. + + When committing, ``pre-commit`` will re-format the files if necessary. + +#. Commit and push once your tests pass and you are happy with your change(s):: + + $ git commit -a -m "" + $ git push -u + +#. Finally, submit a pull request through the GitHub website using this data:: + + head-fork: YOUR_GITHUB_USERNAME/xpublish + compare: your-branch-name + + base-fork: xpublish-community/xpublish + base: main diff --git a/docs/source/api/endpoints.md b/docs/source/api/endpoints.md new file mode 100644 index 0000000..5aaa26d --- /dev/null +++ b/docs/source/api/endpoints.md @@ -0,0 +1,6 @@ +# API Endpoints + +```{eval-rst} +.. openapi:: ./openapi.json + :group: +``` diff --git a/docs/source/api/generate_openapi.py b/docs/source/api/generate_openapi.py new file mode 100644 index 0000000..3d05e3b --- /dev/null +++ b/docs/source/api/generate_openapi.py @@ -0,0 +1,14 @@ +import json +from pathlib import Path + +import xpublish + +rest = xpublish.Rest({}) +app = rest.app +openapi_spec = app.openapi() + +script_path = Path(__file__) +spec_path = script_path.parent / 'openapi.json' + +with spec_path.open('w') as f: + json.dump(openapi_spec, f) diff --git a/docs/source/api/included_plugins.md b/docs/source/api/included_plugins.md new file mode 100644 index 0000000..167c621 --- /dev/null +++ b/docs/source/api/included_plugins.md @@ -0,0 +1,61 @@ +# Included Plugins + +Xpublish includes a set of built in plugins with associated endpoints. + +```{eval-rst} +.. currentmodule:: xpublish +``` + +## Dataset Info + +```{eval-rst} +.. autosummary:: + :toctree: generated/ + + plugins.included.dataset_info.DatasetInfoPlugin + +.. openapi:: ./openapi.json + :include: + /datasets/{dataset_id}/keys + /datasets/{dataset_id}/dict + /datasets/{dataset_id}/info +``` + +## Module Version + +```{eval-rst} +.. autosummary:: + :toctree: generated/ + + plugins.included.module_version.ModuleVersionPlugin + +.. openapi:: ./openapi.json + :include: + /versions +``` + +## Plugin Info + +```{eval-rst} +.. autosummary:: + :toctree: generated/ + + plugins.included.plugin_info.PluginInfoPlugin + +.. openapi:: ./openapi.json + :include: + /plugins +``` + +## Zarr + +```{eval-rst} +.. autosummary:: + :toctree: generated/ + + plugins.included.zarr.ZarrPlugin + +.. openapi:: ./openapi.json + :include: + /datasets/{dataset_id}/zarr/* +``` diff --git a/docs/source/api.md b/docs/source/api/index.md similarity index 88% rename from docs/source/api.md rename to docs/source/api/index.md index 39bc904..1e2434f 100644 --- a/docs/source/api.md +++ b/docs/source/api/index.md @@ -4,6 +4,15 @@ # API reference +```{toctree} +--- +hidden: +--- +endpoints +included_plugins +plugins +``` + ## Top-level Rest class The {class}`~xpublish.Rest` class can be used for publishing a @@ -136,25 +145,3 @@ passed in to the `Plugin.app_router` or `Plugin.dataset_router` method. get_plugins get_plugin_manager ``` - -## Plugins - -Plugins are inherit from the {class}`~xpublish.Plugin` class, and implement various hooks. - -```{eval-rst} -.. currentmodule:: xpublish -``` - -```{eval-rst} -.. autosummary:: - :toctree: generated/ - - Plugin - hookimpl - hookspec - Dependencies - plugins.hooks.PluginSpec - plugins.manage.find_default_plugins - plugins.manage.load_default_plugins - plugins.manage.configure_plugins -``` diff --git a/docs/source/api/plugins.md b/docs/source/api/plugins.md new file mode 100644 index 0000000..92d8a7a --- /dev/null +++ b/docs/source/api/plugins.md @@ -0,0 +1,25 @@ +```{eval-rst} +.. currentmodule:: xpublish +``` + +# Plugins + +Plugins are inherit from the {class}`~xpublish.Plugin` class, and implement various hooks. + +```{eval-rst} +.. currentmodule:: xpublish +``` + +```{eval-rst} +.. autosummary:: + :toctree: generated/ + + Plugin + hookimpl + hookspec + Dependencies + plugins.hooks.PluginSpec + plugins.manage.find_default_plugins + plugins.manage.load_default_plugins + plugins.manage.configure_plugins +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 727d286..291d0e1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -49,6 +49,7 @@ 'sphinx_design', 'myst_parser', 'sphinx_github_changelog', + 'sphinxcontrib.openapi', ] myst_enable_extensions = [] diff --git a/docs/source/index.md b/docs/source/index.md index e02c3f0..b02b851 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -62,7 +62,7 @@ with useful background information and explanation. ``` ```{grid-item-card} API Reference -:link: api +:link: api/index :link-type: doc The reference guide contains a detailed description of the Xpublish API/ @@ -109,7 +109,7 @@ maxdepth: 2 --- getting-started/index user-guide/index -api +api/index ecosystem/index Contributing changelog diff --git a/pyproject.toml b/pyproject.toml index c28f7d4..17af81a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,4 +58,4 @@ known-third-party = [ [tool.ruff.flake8-bugbear] # Allow fastapi.Depends and other dependency injection style function arguments -extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] +extend-immutable-calls = ["fastapi.Depends", "fastapi.Query", "fastapi.Path"] diff --git a/xpublish/plugins/included/dataset_info.py b/xpublish/plugins/included/dataset_info.py index 49b3907..78d6ffc 100644 --- a/xpublish/plugins/included/dataset_info.py +++ b/xpublish/plugins/included/dataset_info.py @@ -15,7 +15,7 @@ class DatasetInfoPlugin(Plugin): name = 'dataset_info' dataset_router_prefix: str = '' - dataset_router_tags: Sequence[str] = [] + dataset_router_tags: Sequence[str] = ['dataset_info'] @hookimpl def dataset_router(self, deps: Dependencies): diff --git a/xpublish/plugins/included/module_version.py b/xpublish/plugins/included/module_version.py index 4a19c31..77f1964 100644 --- a/xpublish/plugins/included/module_version.py +++ b/xpublish/plugins/included/module_version.py @@ -12,10 +12,12 @@ class ModuleVersionPlugin(Plugin): + """Share the currently loaded versions of key libraries""" + name = 'module_version' app_router_prefix: str = '' - app_router_tags: List[str] = [] + app_router_tags: List[str] = ['module_version'] @hookimpl def app_router(self): @@ -23,6 +25,7 @@ def app_router(self): @router.get('/versions') def get_versions(): + """Currently loaded versions of key libraries""" versions = dict(get_sys_info() + netcdf_and_hdf5_versions()) modules = [ 'xarray', diff --git a/xpublish/plugins/included/plugin_info.py b/xpublish/plugins/included/plugin_info.py index a433bb1..36968ac 100644 --- a/xpublish/plugins/included/plugin_info.py +++ b/xpublish/plugins/included/plugin_info.py @@ -16,10 +16,12 @@ class PluginInfo(BaseModel): class PluginInfoPlugin(Plugin): + """Expose plugin source and version""" + name = 'plugin_info' app_router_prefix: str = '' - app_router_tags: Sequence[str] = [] + app_router_tags: Sequence[str] = ['plugin_info'] @hookimpl def app_router(self, deps: Dependencies): @@ -29,6 +31,7 @@ def app_router(self, deps: Dependencies): def get_plugins( plugins: Dict[str, Plugin] = Depends(deps.plugins) ) -> Dict[str, PluginInfo]: + """Return the source and version of the currently loaded plugins""" plugin_info = {} for name, plugin in plugins.items(): diff --git a/xpublish/plugins/included/zarr.py b/xpublish/plugins/included/zarr.py index 86d9740..e899c1f 100644 --- a/xpublish/plugins/included/zarr.py +++ b/xpublish/plugins/included/zarr.py @@ -3,7 +3,7 @@ import cachey # type: ignore import xarray as xr -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Path from starlette.responses import Response # type: ignore from zarr.storage import array_meta_key, attrs_key, group_meta_key # type: ignore @@ -64,8 +64,8 @@ def get_zarr_attrs( @router.get('/{var}/{chunk}') def get_variable_chunk( - var: str, - chunk: str, + var: str = Path(default=None, description='Variable in dataset'), + chunk: str = Path(default=None, description='Zarr chunk'), dataset: xr.Dataset = Depends(deps.dataset), cache: cachey.Cache = Depends(deps.cache), ): diff --git a/xpublish/rest.py b/xpublish/rest.py index 65f8581..1998218 100644 --- a/xpublish/rest.py +++ b/xpublish/rest.py @@ -4,7 +4,7 @@ import pluggy import uvicorn import xarray as xr -from fastapi import APIRouter, FastAPI, HTTPException +from fastapi import APIRouter, FastAPI, HTTPException, Path from .dependencies import get_cache, get_dataset, get_dataset_ids, get_plugin_manager from .plugins import Dependencies, Plugin, PluginSpec, get_plugins, load_default_plugins @@ -118,7 +118,9 @@ def get_datasets_from_plugins(self) -> List[str]: return dataset_ids - def get_dataset_from_plugins(self, dataset_id: str) -> xr.Dataset: + def get_dataset_from_plugins( + self, dataset_id: str = Path(default=None, description='Unique ID of dataset') + ) -> xr.Dataset: """Attempt to load dataset from plugins, otherwise return dataset from passed in dictionary of datasets Parameters: diff --git a/xpublish/routers/common.py b/xpublish/routers/common.py index 7988c73..9de1df9 100644 --- a/xpublish/routers/common.py +++ b/xpublish/routers/common.py @@ -10,5 +10,6 @@ @dataset_collection_router.get('/datasets') -def get_dataset_collection_keys(ids: list = Depends(get_dataset_ids)): +def get_dataset_collection_keys(ids: list = Depends(get_dataset_ids)) -> list[str]: + """Return all the currently known Dataset IDs""" return ids