Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pydantic >=2.0 support #215

Merged
merged 29 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3be143d
Update dev-environment.txt so one can install with conda/mamba or pip
xaviernogueira Jul 19, 2023
f659445
Improve code clarity, adding trailing commas
xaviernogueira Jul 19, 2023
225aa57
Adding annotation for Zarr plugin + function typehints + small readab…
xaviernogueira Jul 19, 2023
84df486
Add annotation for plugin_info, one typehint, and small readability c…
xaviernogueira Jul 19, 2023
356d433
Add annotation to module_version
xaviernogueira Jul 19, 2023
bfbc5d7
Add annotation to dataset info, and small readability updates. Fixed …
xaviernogueira Jul 19, 2023
3098b98
Typehints and some clarity
xaviernogueira Jul 19, 2023
9cc6ff0
Make code formatting consistent
xaviernogueira Jul 19, 2023
4436cbe
Add return typehints
xaviernogueira Jul 19, 2023
7d41b68
Adding typehints to functions
xaviernogueira Jul 19, 2023
34d3130
Adding typehints to functions (i poked around dependencies to assure …
xaviernogueira Jul 19, 2023
a296da5
Switch to proper static variable and type variable syntax
xaviernogueira Jul 19, 2023
14a2e04
Pre-commit formatting (see notes)
xaviernogueira Jul 19, 2023
823e386
Update docs to include plugin annotation syntax
xaviernogueira Jul 19, 2023
3906550
updating docs with plugin name annotations
xaviernogueira Jul 19, 2023
b9d9c6b
fixing annotation in test plugins
xaviernogueira Jul 19, 2023
55c1c97
switching from List to Sequence for consistency with other plugins
xaviernogueira Jul 19, 2023
19f34a7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2023
e5d37de
Getting rid of python >= 3.10 Union syntax
xaviernogueira Jul 19, 2023
7bc96d7
Making sure return is a JSONResponse or response (removing dict retur…
xaviernogueira Jul 19, 2023
301ffea
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2023
c93aa7d
undo response encoding causing tests to fail
xaviernogueira Jul 19, 2023
e482d74
Pin docs to old pydantic (for now, until auto_pydantic upgrades)
xaviernogueira Jul 19, 2023
905688c
W/ pydantic 2.0 Optional class vars on BaseModel subclasses needed a …
xaviernogueira Jul 19, 2023
9a502bd
BaseModel.dict() is deprecated -> switched to BaseModel.model_dump()
xaviernogueira Jul 19, 2023
59ca91e
(maybe) fixing autodoc build
xaviernogueira Jul 19, 2023
038c1a5
Supporting both v1 and v2 pydantic
xaviernogueira Jul 20, 2023
47dc840
Explanatory comment for try/except
xaviernogueira Jul 20, 2023
c017917
Requested changes (swapping return typehints to Pythonic return type)
xaviernogueira Jul 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ build:
# Optionally set the version of Python and requirements required to build your docs
python:
install:
- requirements: docs/requirements.txt
- method: pip
path: .
- requirements: docs/requirements.txt
11 changes: 10 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
cachey
dask
fastapi>=0.78.0
fsspec
httpx
importlib-metadata
netcdf4
numcodecs
numpy
pluggy
pooch
pytest
pytest-mock
pytest-sugar
pytest-cov
requests
-r requirements.txt
toolz
uvicorn
xarray
zarr
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
pydantic<2.0
sphinx-autosummary-accessors
pydata-sphinx-theme
sphinx-autodoc-typehints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


class TutorialDataset(Plugin):
name = 'xarray-tutorial-dataset'
name: str = 'xarray-tutorial-dataset'

@hookimpl
def get_datasets(self):
Expand Down
8 changes: 5 additions & 3 deletions docs/source/getting-started/tutorial/dataset-router-plugin.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from typing import Sequence

import xarray as xr
from fastapi import APIRouter, Depends, HTTPException

from xpublish import Dependencies, Plugin, SingleDatasetRest, hookimpl


class MeanPlugin(Plugin):
name = 'mean'
name: str = 'mean'

dataset_router_prefix = ''
dataset_router_tags = ['mean']
dataset_router_prefix: str = ''
dataset_router_tags: Sequence[str] = ['mean']

@hookimpl
def dataset_router(self, deps: Dependencies):
Expand Down
14 changes: 7 additions & 7 deletions docs/source/user-guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ from xpublish import Plugin
class HelloWorldPlugin(Plugin):
name = "hello_world"
name: str = "hello_world"
```

At the minimum, a plugin needs to specify a `name` attribute.
At the minimum, a plugin needs to specify a `name` attribute with a type annotation. For example, `name: str = my_plugin_name`.

### Marking implementation methods

Expand All @@ -66,7 +66,7 @@ from xpublish import Plugin, hookimpl
from fastapi import APIRouter
class HelloWorldPlugin(Plugin):
name = "hello_world"
name: str = "hello_world"
@hookimpl
def app_router(self):
Expand Down Expand Up @@ -188,7 +188,7 @@ from xpublish import Plugin, Dependencies, hookimpl


class DatasetAttrs(Plugin):
name = "dataset-attrs"
name: str = "dataset-attrs"

@hookimpl
def dataset_router(self, deps: Dependencies):
Expand Down Expand Up @@ -224,7 +224,7 @@ from xpublish import Plugin, Dependencies, hookimpl


class DatasetInfoPlugin(Plugin):
name = "dataset-info"
name: str = "dataset-info"

dataset_router_prefix = "/info"
dataset_router_tags = ["info"]
Expand Down Expand Up @@ -257,7 +257,7 @@ from xpublish import Plugin, Dependencies, hookimpl


class PluginInfo(Plugin):
name = "plugin_info"
name: str = "plugin_info"

app_router_prefix = "/info"
app_router_tags = ["info"]
Expand Down Expand Up @@ -298,7 +298,7 @@ from xpublish import Plugin, hookimpl


class TutorialDataset(Plugin):
name = "xarray-tutorial-dataset"
name: str = "xarray-tutorial-dataset"

@hookimpl
def get_datasets(self):
Expand Down
6 changes: 3 additions & 3 deletions tests/test_rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def get_dims(dataset: xr.Dataset = Depends(get_dataset)):
@pytest.fixture(scope='function')
def dataset_plugin(airtemp_ds):
class AirtempPlugin(Plugin):
name = 'airtemp'
name: str = 'airtemp'

@hookimpl
def get_dataset(self, dataset_id: str):
Expand All @@ -86,7 +86,7 @@ def hello(self):
pass

class HookSpecPlugin(Plugin):
name = 'hook_spec'
name: str = 'hook_spec'

@hookimpl
def register_hookspec(self):
Expand All @@ -98,7 +98,7 @@ def register_hookspec(self):
@pytest.fixture(scope='function')
def hook_implementation_plugin():
class HookImplementationPlugin(Plugin):
name = 'hook_implementation'
name: str = 'hook_implementation'

@hookimpl
def hello(self):
Expand Down
4 changes: 2 additions & 2 deletions xpublish/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from fastapi import Depends

from .utils.api import DATASET_ID_ATTR_KEY
from .utils.zarr import create_zmetadata, create_zvariables, zarr_metadata_key
from .utils.zarr import ZARR_METADATA_KEY, create_zmetadata, create_zvariables

if TYPE_CHECKING:
from .plugins import Plugin # pragma: no cover
Expand Down Expand Up @@ -90,7 +90,7 @@ def get_zmetadata(
):
"""FastAPI dependency that returns a consolidated zmetadata dictionary."""

cache_key = dataset.attrs.get(DATASET_ID_ATTR_KEY, '') + '/' + zarr_metadata_key
cache_key = dataset.attrs.get(DATASET_ID_ATTR_KEY, '') + '/' + ZARR_METADATA_KEY
zmeta = cache.get(cache_key)

if zmeta is None:
Expand Down
26 changes: 19 additions & 7 deletions xpublish/plugins/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,24 @@ class Dependencies(BaseModel):
"""

dataset_ids: Callable[..., List[str]] = Field(
get_dataset_ids, description='Returns a list of all valid dataset ids'
get_dataset_ids,
description='Returns a list of all valid dataset ids',
)
dataset: Callable[[str], xr.Dataset] = Field(
get_dataset, description='Returns a dataset using ``/<dataset_id>/`` in the path.'
get_dataset,
description='Returns a dataset using ``/<dataset_id>/`` in the path.',
)
cache: Callable[..., cachey.Cache] = Field(
get_cache, description='Provide access to :py:class:`cachey.Cache`'
get_cache,
description='Provide access to :py:class:`cachey.Cache`',
)
plugins: Callable[..., Dict[str, 'Plugin']] = Field(
get_plugins, description='A dictionary of plugins allowing direct access'
get_plugins,
description='A dictionary of plugins allowing direct access',
)
plugin_manager: Callable[..., pluggy.PluginManager] = Field(
get_plugin_manager, description='The plugin manager itself, allowing for maximum creativity'
get_plugin_manager,
description='The plugin manager itself, allowing for maximum creativity',
)

def __hash__(self):
Expand All @@ -64,7 +69,13 @@ def __hash__(self):
"""Make sure that the plugin is hashable to load with pluggy"""
things_to_hash = []

for e in self.dict():
# try/except is for pydantic backwards compatibility
try:
model_dict = self.model_dump()
except AttributeError:
model_dict = self.dict()

for e in model_dict:
if isinstance(e, list):
things_to_hash.append(tuple(e)) # pragma: no cover
else:
Expand Down Expand Up @@ -115,7 +126,8 @@ def get_datasets(self) -> Iterable[str]: # type: ignore
"""Return an iterable of dataset ids that the plugin can provide"""

@hookspec(firstresult=True)
def get_dataset(self, dataset_id: str) -> Optional[xr.Dataset]: # type: ignore
# type: ignore
def get_dataset(self, dataset_id: str) -> Optional[xr.Dataset]:
"""Return a dataset by requested dataset_id.

If the plugin does not have the dataset, return None
Expand Down
16 changes: 10 additions & 6 deletions xpublish/plugins/included/dataset_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@
class DatasetInfoPlugin(Plugin):
"""Dataset metadata"""

name = 'dataset_info'
name: str = 'dataset_info'

dataset_router_prefix: str = ''
dataset_router_tags: Sequence[str] = ['dataset_info']

@hookimpl
def dataset_router(self, deps: Dependencies):
router = APIRouter(prefix=self.dataset_router_prefix, tags=list(self.dataset_router_tags))
def dataset_router(self, deps: Dependencies) -> APIRouter:
router = APIRouter(
prefix=self.dataset_router_prefix,
tags=list(self.dataset_router_tags),
)

@router.get('/')
def html_representation(
dataset=Depends(deps.dataset),
):
) -> HTMLResponse:
"""Returns the xarray HTML representation of the dataset."""

with xr.set_options(display_style='html'):
Expand All @@ -43,15 +46,15 @@ def list_keys(
@router.get('/dict')
def to_dict(
dataset=Depends(deps.dataset),
):
) -> dict:
"""The full dataset as a dictionary"""
return JSONResponse(dataset.to_dict(data=False))

@router.get('/info')
def info(
dataset=Depends(deps.dataset),
cache=Depends(deps.cache),
):
) -> dict:
"""Dataset schema (close to the NCO-JSON schema)."""

zvariables = get_zvariables(dataset, cache)
Expand All @@ -66,6 +69,7 @@ def info(
for name, var in zvariables.items():
attrs = meta[f'{name}/{attrs_key}'].copy()
attrs.pop('_ARRAY_DIMENSIONS')

info['variables'][name] = {
'type': var.data.dtype.name,
'dimensions': list(var.dims),
Expand Down
15 changes: 9 additions & 6 deletions xpublish/plugins/included/module_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
import importlib
import sys
from typing import List
from typing import Sequence

from fastapi import APIRouter

Expand All @@ -14,17 +14,20 @@
class ModuleVersionPlugin(Plugin):
"""Share the currently loaded versions of key libraries"""

name = 'module_version'
name: str = 'module_version'

app_router_prefix: str = ''
app_router_tags: List[str] = ['module_version']
app_router_tags: Sequence[str] = ['module_version']

@hookimpl
def app_router(self):
router = APIRouter(prefix=self.app_router_prefix, tags=self.app_router_tags)
def app_router(self) -> APIRouter:
router = APIRouter(
prefix=self.app_router_prefix,
tags=self.app_router_tags,
)

@router.get('/versions')
def get_versions():
def get_versions() -> dict:
"""Currently loaded versions of key libraries"""
versions = dict(get_sys_info() + netcdf_and_hdf5_versions())
modules = [
Expand Down
14 changes: 9 additions & 5 deletions xpublish/plugins/included/plugin_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@

class PluginInfo(BaseModel):
path: str
version: Optional[str]
version: Optional[str] = None


class PluginInfoPlugin(Plugin):
"""Expose plugin source and version"""

name = 'plugin_info'
name: str = 'plugin_info'

app_router_prefix: str = ''
app_router_tags: Sequence[str] = ['plugin_info']

@hookimpl
def app_router(self, deps: Dependencies):
router = APIRouter(prefix=self.app_router_prefix, tags=list(self.app_router_tags))
def app_router(self, deps: Dependencies) -> APIRouter:
router = APIRouter(
prefix=self.app_router_prefix,
tags=list(self.app_router_tags),
)

@router.get('/plugins')
def get_plugins(
Expand All @@ -44,7 +47,8 @@ def get_plugins(
version = None # pragma: no cover

plugin_info[name] = PluginInfo(
path=f'{plugin_type.__module__}.{plugin.__repr_name__()}', version=version
path=f'{plugin_type.__module__}.{plugin.__repr_name__()}',
version=version,
)

return plugin_info
Expand Down
Loading