Skip to content

Commit

Permalink
Merge pull request #97 from silx-kit/types
Browse files Browse the repository at this point in the history
Define metadata return types
  • Loading branch information
axelboc authored Aug 29, 2024
2 parents 922dc0f + c3fc2b8 commit 116a629
Show file tree
Hide file tree
Showing 16 changed files with 193 additions and 102 deletions.
5 changes: 3 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
from __future__ import annotations

import os
import sys
from typing import List

sys.path.insert(0, os.path.abspath(".."))

Expand Down Expand Up @@ -47,7 +48,7 @@
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns: List[str] = []
exclude_patterns: list[str] = []


# -- Options for HTML output -------------------------------------------------
Expand Down
108 changes: 56 additions & 52 deletions h5grove/content.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from __future__ import annotations
from collections.abc import Callable, Sequence
from typing import (
Any,
Generic,
TypeVar,
cast,
)

import contextlib
from pathlib import Path
from typing import Any, Callable, Dict, Generic, Optional, Sequence, TypeVar, Union
import h5py
import numpy as np

Expand All @@ -9,7 +17,19 @@
except ImportError:
pass

from .models import LinkResolution, Selection
from .models import (
LinkResolution,
Selection,
EntityMetadata,
ExternalLinkMetadata,
SoftLinkMetadata,
AttributeMetadata,
ResolvedEntityMetadata,
GroupMetadata,
DatasetMetadata,
DatatypeMetadata,
Stats,
)
from .utils import (
NotFoundError,
QueryArgumentError,
Expand All @@ -35,21 +55,18 @@ class EntityContent:
def __init__(self, path: str):
self._path = path

def metadata(self) -> Dict[str, str]:
"""Entity metadata
:returns: {"name": str, "kind": str}
"""
def metadata(self) -> EntityMetadata:
"""Entity metadata"""
return {"name": self.name, "kind": self.kind}

@property
def name(self) -> str:
"""Entity name. Last member of the path."""
"""Entity name (last path segment)"""
return self._path.split("/")[-1]

@property
def path(self) -> str:
"""Path in the file."""
"""Path in the file"""
return self._path


Expand All @@ -61,11 +78,8 @@ def __init__(self, path: str, link: h5py.ExternalLink):
self._target_file = link.filename
self._target_path = link.path

def metadata(self, depth=None):
"""External link metadata
:returns: {"name": str, "target_file": str, "target_path": str, "kind": str}
"""
def metadata(self, depth=None) -> ExternalLinkMetadata:
"""External link metadata"""
return sorted_dict(
("target_file", self._target_file),
("target_path", self._target_path),
Expand All @@ -89,12 +103,10 @@ class SoftLinkContent(EntityContent):
def __init__(self, path: str, link: h5py.SoftLink) -> None:
super().__init__(path)
self._target_path = link.path
""" The target path of the link """
"""The target path of the link"""

def metadata(self, depth=None):
"""
:returns: {"name": str, "target_path": str, "kind": str}
"""
def metadata(self, depth=None) -> SoftLinkMetadata:
"""Soft link metadata"""
return sorted_dict(
("target_path", self._target_path), *super().metadata().items()
)
Expand All @@ -114,19 +126,19 @@ class ResolvedEntityContent(EntityContent, Generic[T]):
def __init__(self, path: str, h5py_entity: T):
super().__init__(path)
self._h5py_entity = h5py_entity
""" Resolved h5py entity """
"""Resolved h5py entity"""

def attributes(self, attr_keys: Optional[Sequence[str]] = None):
def attributes(
self, attr_keys: Sequence[str] | None = None
) -> dict[str, AttributeMetadata]:
"""Attributes of the h5py entity. Can be filtered by keys."""
if attr_keys is None:
return dict((*self._h5py_entity.attrs.items(),))

return dict((key, self._h5py_entity.attrs[key]) for key in attr_keys)

def metadata(self, depth=None):
"""
:returns: {"attributes": AttributeMetadata, "name": str, "kind": str}
"""
def metadata(self, depth=None) -> ResolvedEntityMetadata:
"""Resolved entity metadata"""
attribute_names = sorted(self._h5py_entity.attrs.keys())
return sorted_dict(
(
Expand All @@ -143,10 +155,8 @@ def metadata(self, depth=None):
class DatasetContent(ResolvedEntityContent[h5py.Dataset]):
kind = "dataset"

def metadata(self, depth=None):
"""
:returns: {"attributes": AttributeMetadata, chunks": tuple, "filters": tuple, "kind": str, "name": str, "shape": tuple, "type": TypeMetadata}
"""
def metadata(self, depth=None) -> DatasetMetadata:
"""Dataset metadata"""
return sorted_dict(
("chunks", self._h5py_entity.chunks),
("filters", get_filters(self._h5py_entity)),
Expand All @@ -157,9 +167,9 @@ def metadata(self, depth=None):

def data(
self,
selection: Selection = None,
selection: Selection | None = None,
flatten: bool = False,
dtype: Optional[str] = "origin",
dtype: str | None = "origin",
):
"""Dataset data.
Expand All @@ -177,13 +187,10 @@ def data(

return result

def data_stats(
self, selection: Selection = None
) -> Dict[str, Union[float, int, None]]:
def data_stats(self, selection: Selection | None = None) -> Stats:
"""Statistics on the data. Providing a selection will compute stats only on the selected slice.
:param selection: NumPy-like indexing to define a selection as a slice
:returns: {"strict_positive_min": number | None, "positive_min": number | None, "min": number | None, "max": number | None, "mean": number | None, "std": number | None}
"""
data = self._get_finite_data(selection)

Expand All @@ -208,7 +215,7 @@ class GroupContent(ResolvedEntityContent[h5py.Group]):
def __init__(self, path: str, h5py_entity: h5py.Group, h5file: h5py.File):
super().__init__(path, h5py_entity)
self._h5file = h5file
""" File in which the entity was resolved. This is needed to resolve child entity. """
"""File in which the entity was resolved. This is needed to resolve child entity."""

def _get_child_metadata_content(self, depth=0):
return [
Expand All @@ -218,14 +225,13 @@ def _get_child_metadata_content(self, depth=0):
for child_path in self._h5py_entity.keys()
]

def metadata(self, depth: int = 1):
def metadata(self, depth: int = 1) -> GroupMetadata:
"""Metadata of the group. Recursively includes child metadata if depth > 0.
:parameter depth: The level of child metadata resolution.
:returns: {"attributes": AttributeMetadata, "children": ChildMetadata, "name": str, "kind": str}
"""
if depth <= 0:
return super().metadata()
return cast(GroupMetadata, super().metadata())

return sorted_dict(
("children", self._get_child_metadata_content(depth - 1)),
Expand All @@ -236,10 +242,8 @@ def metadata(self, depth: int = 1):
class DatatypeContent(ResolvedEntityContent[h5py.Datatype]):
kind = "datatype"

def metadata(self, depth=None):
"""
:returns: {"attributes": AttributeMetadata, "kind": str, "name": str, "type": TypeMetadata}
"""
def metadata(self, depth=None) -> DatatypeMetadata:
"""Datatype metadata"""
return sorted_dict(
("type", get_type_metadata(self._h5py_entity.id)),
*super().metadata().items(),
Expand All @@ -248,7 +252,7 @@ def metadata(self, depth=None):

def create_content(
h5file: h5py.File,
path: Optional[str],
path: str | None,
resolve_links: LinkResolution = LinkResolution.ONLY_VALID,
):
"""
Expand Down Expand Up @@ -287,11 +291,11 @@ def create_content(

@contextlib.contextmanager
def get_content_from_file(
filepath: Union[str, Path],
path: Optional[str],
filepath: str | Path,
path: str | None,
create_error: Callable[[int, str], Exception],
resolve_links_arg: Optional[str] = LinkResolution.ONLY_VALID,
h5py_options: Dict[str, Any] = {},
resolve_links_arg: str | None = LinkResolution.ONLY_VALID,
h5py_options: dict[str, Any] = {},
):
f = open_file_with_error_fallback(filepath, create_error, h5py_options)

Expand All @@ -316,11 +320,11 @@ def get_content_from_file(

@contextlib.contextmanager
def get_list_of_paths(
filepath: Union[str, Path],
base_path: Optional[str],
filepath: str | Path,
base_path: str | None,
create_error: Callable[[int, str], Exception],
resolve_links_arg: Optional[str] = LinkResolution.ONLY_VALID,
h5py_options: Dict[str, Any] = {},
resolve_links_arg: str | None = LinkResolution.ONLY_VALID,
h5py_options: dict[str, Any] = {},
):
f = open_file_with_error_fallback(filepath, create_error, h5py_options)

Expand Down
15 changes: 9 additions & 6 deletions h5grove/encoders.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations
from collections.abc import Callable
from typing import Any

import io
from typing import Any, Callable, Dict, Optional, Union
import numpy as np
import orjson
import h5py
Expand All @@ -17,7 +20,7 @@ def bin_encode(array: np.ndarray) -> bytes:
return array.tobytes()


def orjson_default(o: Any) -> Union[list, float, str, None]:
def orjson_default(o: Any) -> list | float | str | None:
"""Converts Python objects to JSON-serializable objects.
:raises TypeError: if the object is not supported."""
Expand All @@ -37,7 +40,7 @@ def orjson_default(o: Any) -> Union[list, float, str, None]:
raise TypeError


def orjson_encode(content: Any, default: Optional[Callable] = None) -> bytes:
def orjson_encode(content: Any, default: Callable | None = None) -> bytes:
"""Encode in JSON using orjson.
:param: content: Content to encode
Expand Down Expand Up @@ -82,15 +85,15 @@ def tiff_encode(data: np.ndarray) -> bytes:
class Response:
content: bytes
""" Encoded `content` as bytes """
headers: Dict[str, str]
headers: dict[str, str]
""" Associated headers """

def __init__(self, content: bytes, headers: Dict[str, str]):
def __init__(self, content: bytes, headers: dict[str, str]):
self.content = content
self.headers = {**headers, "Content-Length": str(len(content))}


def encode(content: Any, encoding: Optional[str] = "json") -> Response:
def encode(content: Any, encoding: str | None = "json") -> Response:
"""Encode content in given encoding.
Warning: Not all encodings supports all types of content.
Expand Down
8 changes: 5 additions & 3 deletions h5grove/fastapi_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Helpers for usage with `FastAPI <https://fastapi.tiangolo.com/>`_"""

from __future__ import annotations
from collections.abc import Callable

from fastapi import APIRouter, Depends, Response, Query, Request
from fastapi.routing import APIRoute
from pydantic_settings import BaseSettings
from typing import List, Optional, Union, Callable

from .content import (
DatasetContent,
Expand Down Expand Up @@ -46,7 +48,7 @@ async def custom_route_handler(request: Request) -> Response:


class Settings(BaseSettings):
base_dir: Union[str, None] = None
base_dir: str | None = None


settings = Settings()
Expand Down Expand Up @@ -86,7 +88,7 @@ async def get_root():
async def get_attr(
file: str = Depends(add_base_path),
path: str = "/",
attr_keys: Optional[List[str]] = Query(default=None),
attr_keys: list[str] | None = Query(default=None),
):
"""`/attr/` endpoint handler"""
with get_content_from_file(file, path, create_error) as content:
Expand Down
8 changes: 5 additions & 3 deletions h5grove/flask_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Helpers for usage with `Flask <https://flask.palletsprojects.com/>`_"""

from __future__ import annotations
from collections.abc import Callable, Mapping
from typing import Any

from werkzeug.exceptions import HTTPException
from flask import Blueprint, current_app, request, Response, Request
import os
from typing import Any, Callable, Mapping, Optional


from .content import (
DatasetContent,
Expand All @@ -29,7 +31,7 @@


def make_encoded_response(
content, format_arg: Optional[str] = "json", status: Optional[int] = None
content, format_arg: str | None = "json", status: int | None = None
) -> Response:
"""Prepare flask Response according to format"""
h5grove_response = encode(content, format_arg)
Expand Down
Loading

0 comments on commit 116a629

Please sign in to comment.