Skip to content

Commit

Permalink
Add methods to API docs and use typehints (#167)
Browse files Browse the repository at this point in the history
* Add methods to API docs and use typehints

Closes #160 #161

* Add make live command for using with sphinx-autobuild

* Remove duplicate typing from register_plugin
  • Loading branch information
abkfenris authored Mar 21, 2023
1 parent 29f79f2 commit f2171bb
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 29 deletions.
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
sphinx>=3.1
sphinx-autosummary-accessors
sphinx_rtd_theme>=1.0
sphinx-autodoc-typehints
autodoc_pydantic
41 changes: 38 additions & 3 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,43 @@ Top-level Rest class
The :class:`~xpublish.Rest` class can be used for publishing a
a collection of :class:`xarray.Dataset` objects.

The main interfaces to Xpublish that many users may use.

.. autosummary::
:toctree: generated/

Rest
Rest.app
Rest.cache
Rest.plugins
Rest.serve
Rest.register_plugin
Rest.dependencies

There are also a handful of methods that are more likely to be used
when subclassing `xpublish.Rest` to modify functionality, or are used
by plugin dependencies.

.. autosummary::
:toctree: generated/

Rest.setup_datasets
Rest.get_datasets_from_plugins
Rest.get_dataset_from_plugins
Rest.setup_plugins
Rest.init_cache_kwargs
Rest.init_app_kwargs
Rest.plugin_routers

There is also a specialized version of :class:`xpublish.Rest` for use
when only a single dataset is being served, instead of a collection
of datasets.

.. autosummary::
:toctree: generated/

SingleDatasetRest
SingleDatasetRest.setup_datasets

For serving a single dataset the :class:`~xpublish.SingleDatasetRest` is used instead.

Expand All @@ -29,8 +59,8 @@ Dataset.rest (xarray accessor)
==============================

This accessor extends :py:class:`xarray.Dataset` with the same interface than
:class:`~xpublish.Rest` or :class:`~xpublish.SingleDatasetRest`. It is a convenient
method for publishing one single dataset. Proper use of this accessor should be like:
:class:`~xpublish.SingleDatasetRest`. It is a convenient method for publishing one single
dataset. Proper use of this accessor should be like:

.. code-block:: python
Expand Down Expand Up @@ -72,7 +102,10 @@ FastAPI dependencies

The functions below are defined in module ``xpublish.dependencies`` and can
be used as `FastAPI dependencies <https://fastapi.tiangolo.com/tutorial/dependencies>`_
when creating custom API endpoints.
when creating custom API endpoints directly.

When creating routers with plugins, instead use ``xpublish.Dependency`` that will be
passed in to the ``Plugin.app_router`` or ``Plugin.dataset_router`` method.

.. currentmodule:: xpublish.dependencies

Expand All @@ -84,6 +117,8 @@ when creating custom API endpoints.
get_cache
get_zvariables
get_zmetadata
get_plugins
get_plugin_manager

Plugins
=======
Expand Down
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'sphinx.ext.napoleon',
'sphinxcontrib.autodoc_pydantic',
'sphinx_autosummary_accessors',
'sphinx_autodoc_typehints',
]

extlinks = {
Expand Down
16 changes: 15 additions & 1 deletion xpublish/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def get_dataset_ids() -> List[str]:
This dummy dependency will be overridden when creating the FastAPI
application.
Returns:
A list of unique keys for datasets
"""
return [] # pragma: no cover

Expand All @@ -38,6 +41,13 @@ def get_dataset(dataset_id: str) -> xr.Dataset:
This dummy dependency will be overridden when creating the FastAPI
application.
Parameters:
dataset_id:
Unique path-safe key identifying dataset
Returns:
Requested Xarray dataset
"""
return xr.Dataset() # pragma: no cover

Expand Down Expand Up @@ -93,7 +103,11 @@ def get_zmetadata(


def get_plugins() -> Dict[str, 'Plugin']:
"""FastAPI dependency that returns the a dictionary of loaded plugins"""
"""FastAPI dependency that returns the a dictionary of loaded plugins
Returns:
Dictionary of names to initialized plugins.
"""

return {} # pragma: no cover

Expand Down
100 changes: 75 additions & 25 deletions xpublish/rest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Optional
from typing import Dict, List, Optional, Tuple

import cachey
import pluggy
Expand All @@ -16,6 +16,9 @@
normalize_datasets,
)

RouterKwargs = Dict
RouterAndKwargs = Tuple[APIRouter, RouterKwargs]


class Rest:
"""Used to publish multiple Xarray Datasets via a REST API (FastAPI application).
Expand All @@ -27,10 +30,10 @@ class Rest:
Parameters
----------
datasets : dict
datasets :
A mapping of datasets objects to be served. If a mapping is given, keys
are used as dataset ids and are converted to strings. See also the notes below.
routers : list, optional
routers :
A list of dataset-specific :class:`fastapi.APIRouter` instances to
include in the fastAPI application. These routers are in addition
to any loaded via plugins.
Expand All @@ -39,15 +42,15 @@ class Rest:
the 1st tuple element is a :class:`fastapi.APIRouter` instance and the
2nd element is a dictionary that is used to pass keyword arguments to
:meth:`fastapi.FastAPI.include_router`.
cache_kws : dict, optional
cache_kws :
Dictionary of keyword arguments to be passed to
:meth:`cachey.Cache.__init__()`.
By default, the cache size is set to 1MB, but this can be changed with
``available_bytes``.
app_kws : dict, optional
app_kws :
Dictionary of keyword arguments to be passed to
:meth:`fastapi.FastAPI.__init__()`.
plugins : dict, optional
plugins :
Optional dictionary of loaded, configured plugins.
Overrides automatic loading of plugins.
If no plugins are desired, set to an empty dict.
Expand All @@ -67,8 +70,8 @@ def __init__(
self,
datasets: Optional[Dict[str, xr.Dataset]] = None,
routers: Optional[APIRouter] = None,
cache_kws=None,
app_kws=None,
cache_kws: Optional[Dict] = None,
app_kws: Optional[Dict] = None,
plugins: Optional[Dict[str, Plugin]] = None,
):
self.setup_datasets(datasets or {})
Expand All @@ -81,25 +84,49 @@ def __init__(
self.init_app_kwargs(app_kws)
self.init_cache_kwargs(cache_kws)

def setup_datasets(self, datasets: Dict[str, xr.Dataset]):
"""Initialize datasets and getter functions"""
def setup_datasets(self, datasets: Dict[str, xr.Dataset]) -> str:
"""Initialize datasets and dataset accessor function
Returns:
Prefix for dataset routers
"""
self._datasets = normalize_datasets(datasets)

self._get_dataset_func = self.get_dataset_from_plugins
self._dataset_route_prefix = '/datasets/{dataset_id}'
return self._dataset_route_prefix

def get_datasets_from_plugins(self):
"""Get dataset ids from directly loaded datasets and plugins"""
def get_datasets_from_plugins(self) -> List[str]:
"""Return dataset ids from directly loaded datasets and plugins
Used as a FastAPI dependency in dataset router plugins
via :meth:`Rest.dependencies`.
Returns:
Dataset IDs from plugins and datasets loaded into
:class:`xpublish.Rest` at initialization.
"""
dataset_ids = list(self._datasets)

for plugin_dataset_ids in self.pm.hook.get_datasets():
dataset_ids.extend(plugin_dataset_ids)

return dataset_ids

def get_dataset_from_plugins(self, dataset_id: str):
"""Attempt to load dataset from plugins, otherwise load"""
def get_dataset_from_plugins(self, dataset_id: str) -> xr.Dataset:
"""Attempt to load dataset from plugins, otherwise return dataset from passed in dictionary of datasets
Parameters:
dataset_id:
Unique key of dataset to attempt to load from plugins or
those provided to :class:`xpublish.Rest` at initialization.
Returns:
Dataset for selected ``dataset_id``
Raises:
FastAPI.HTTPException: When a dataset is not found a 404 error is returned.
"""
dataset = self.pm.hook.get_dataset(dataset_id=dataset_id)

if dataset:
Expand All @@ -111,7 +138,16 @@ def get_dataset_from_plugins(self, dataset_id: str):
return self._datasets[dataset_id]

def setup_plugins(self, plugins: Optional[Dict[str, Plugin]] = None):
"""Initialize and load plugins from entry_points"""
"""Initialize and load plugins from entry_points unless explicitly provided
Parameters:
plugins:
If a dictionary of initialized plugins is provided,
then the automatic loading of plugins is disabled.
Providing an empty dictionary will also disable
automatic loading of plugins.
"""
if plugins is None:
plugins = load_default_plugins()

Expand All @@ -131,9 +167,9 @@ def register_plugin(
Register a plugin with the xpublish system
Args:
plugin (Plugin): Instantiated Plugin object
plugin_name (str, optional): Plugin name
overwrite (bool, optional): If a plugin of the same name exist,
plugin: Instantiated Plugin object
plugin_name: Plugin name
overwrite: If a plugin of the same name exist,
setting this to True will remove the existing plugin before
registering the new plugin. Defaults to False.
Expand Down Expand Up @@ -207,8 +243,13 @@ def _init_routers(self, dataset_routers: Optional[APIRouter]):

self._app_routers = app_routers

def plugin_routers(self):
"""Load the app and dataset routers for plugins"""
def plugin_routers(self) -> Tuple[List[RouterAndKwargs], List[RouterAndKwargs]]:
"""Load the app and dataset routers for plugins
Returns:
A tuple containing a list of top-level routers from plugins
and a list of per-dataset routers from plugins
"""
app_routers = []
dataset_routers = []

Expand All @@ -223,6 +264,7 @@ def plugin_routers(self):
return app_routers, dataset_routers

def dependencies(self) -> Dependencies:
"""FastAPI dependencies to pass to plugin router methods"""
deps = Dependencies(
dataset_ids=self.get_datasets_from_plugins,
dataset=self._get_dataset_func,
Expand Down Expand Up @@ -258,21 +300,27 @@ def _init_app(self):

@property
def app(self) -> FastAPI:
"""Returns the :class:`fastapi.FastAPI` application instance."""
"""Returns the :class:`fastapi.FastAPI` application instance.
Notes
-----
Plugins registered with :meth:`xpublish.Rest.register_plugin` after :meth:`xpublish.Rest.app`
is accessed or :meth:`xpublish.Rest.serve` is called once may not take effect.
"""
if self._app is None:
self._app = self._init_app()
return self._app

def serve(self, host='0.0.0.0', port=9000, log_level='debug', **kwargs):
def serve(self, host: str = '0.0.0.0', port: int = 9000, log_level: str = 'debug', **kwargs):
"""Serve this FastAPI application via :func:`uvicorn.run`.
Parameters
----------
host : str
host :
Bind socket to this host.
port : int
port :
Bind socket to this port.
log_level : str
log_level :
App logging level, valid options are
{'critical', 'error', 'warning', 'info', 'debug', 'trace'}.
**kwargs :
Expand Down Expand Up @@ -309,6 +357,8 @@ def __init__(
super().__init__({}, routers, cache_kws, app_kws, plugins)

def setup_datasets(self, datasets):
"""Modifies the dataset loading to instead connect to the
single dataset"""
self._dataset_route_prefix = ''
self._datasets = {}

Expand Down

0 comments on commit f2171bb

Please sign in to comment.