simple old style nwb-linkml test
This commit is contained in:
sneakers-the-rat 2024-02-05 15:39:29 -08:00
parent d5a3a09bed
commit 5d3cd95d84
Signed by untrusted user who does not match committer: jonny
GPG key ID: 6DCB96EF1E4D232D
20 changed files with 335 additions and 46 deletions

9
docs/api/index.md Normal file
View file

@ -0,0 +1,9 @@
# numpydantic
Top-level API contents
```{eval-rst}
.. automodule:: numpydantic
:members:
:imported-members:
```

10
docs/api/linkml/index.md Normal file
View file

@ -0,0 +1,10 @@
# linkml
```{toctree}
:caption: LinkML
ndarraygen
pydanticgen
template
```

View file

@ -0,0 +1,6 @@
# ndarraygen
```{eval-rst}
.. automodule:: numpydantic.linkml.ndarraygen
:members:
```

View file

@ -0,0 +1,6 @@
# pydanticgen
```{eval-rst}
.. automodule:: numpydantic.linkml.pydanticgen
:members:
```

View file

@ -0,0 +1,6 @@
# template
```{eval-rst}
.. automodule:: numpydantic.linkml.template
:members:
```

6
docs/api/maps.md Normal file
View file

@ -0,0 +1,6 @@
# maps
```{eval-rst}
.. automodule:: numpydantic.maps
:members:
```

6
docs/api/monkeypatch.md Normal file
View file

@ -0,0 +1,6 @@
# monkeypatch
```{eval-rst}
.. automodule:: numpydantic.monkeypatch
:members:
```

6
docs/api/ndarray.md Normal file
View file

@ -0,0 +1,6 @@
# ndarray
```{eval-rst}
.. automodule:: numpydantic.ndarray
:members:
```

6
docs/api/proxy.md Normal file
View file

@ -0,0 +1,6 @@
# proxy
```{eval-rst}
.. automodule:: numpydantic.proxy
:members:
```

View file

@ -6,48 +6,53 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'numpydantic' project = "numpydantic"
copyright = '2024, Jonny Saunders' copyright = "2024, Jonny Saunders"
author = 'Jonny Saunders' author = "Jonny Saunders"
release = 'v0.0.0' release = "v0.0.0"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [ extensions = [
'sphinx.ext.napoleon', "sphinx.ext.napoleon",
'sphinx.ext.autodoc', "sphinx.ext.autodoc",
'sphinxcontrib.autodoc_pydantic', "sphinxcontrib.autodoc_pydantic",
'sphinx.ext.intersphinx', "sphinx.ext.intersphinx",
"sphinx_design", "sphinx_design",
'myst_parser', "myst_parser",
'sphinx.ext.todo' "sphinx.ext.todo",
] ]
templates_path = ['_templates'] templates_path = ["_templates"]
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
intersphinx_mapping = { intersphinx_mapping = {
'python': ('https://docs.python.org/3', None), "python": ("https://docs.python.org/3", None),
'numpy': ('https://numpy.org/doc/stable/', None), "numpy": ("https://numpy.org/doc/stable/", None),
'pydantic': ('https://docs.pydantic.dev/latest/', None), "pydantic": ("https://docs.pydantic.dev/latest/", None),
'linkml': ('https://linkml.io/linkml/', None), "linkml": ("https://linkml.io/linkml/", None),
'linkml_runtime': ('https://linkml.io/linkml/', None), "linkml_runtime": ("https://linkml.io/linkml/", None),
'linkml-runtime': ('https://linkml.io/linkml/', None) "linkml-runtime": ("https://linkml.io/linkml/", None),
} }
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo' html_theme = "furo"
html_static_path = ['_static'] html_static_path = ["_static"]
# autodoc # autodoc
autodoc_pydantic_model_show_json_error_strategy = 'coerce' autodoc_pydantic_model_show_json_error_strategy = "coerce"
autodoc_pydantic_model_show_json = False autodoc_pydantic_model_show_json = False
autodoc_mock_imports = [] autodoc_mock_imports = [
"dask",
"h5py",
"linkml",
"linkml-runtime",
]
autoclass_content = "both" autoclass_content = "both"
autodoc_member_order='bysource' autodoc_member_order = "bysource"
add_module_names = False add_module_names = False
# Napoleon settings # Napoleon settings

View file

@ -1,5 +1,11 @@
# Hooks # Hooks
## TODO What hooks do we want to expose to downstream users so they can use this without needing
to override everything?
- nwb compatibility: allowable precision map in dtype check ```{todo}
**NWB Compatibility**
**Precision:** NWB allows for a sort of hierarchy of type specification -
a less precise type also allows the data to be specified in a more precise type
```

View file

@ -14,30 +14,32 @@ It does two primary things:
- **Generate models from LinkML** - extend the LinkML pydantic generator to create models that - **Generate models from LinkML** - extend the LinkML pydantic generator to create models that
that use the [linkml-arrays](https://github.com/linkml/linkml-arrays) syntax that use the [linkml-arrays](https://github.com/linkml/linkml-arrays) syntax
## Overview
The Python type annotation system is weird and not like the rest of Python!
(at least until [PEP 0649](https://peps.python.org/pep-0649/) gets mainlined).
Similarly, Pydantic 2's core_schema system is wonderful but still relatively poorly
documented for custom types! This package does the work of plugging them in
together to make some kind of type validation frankenstein.
The first problem is that type annotations are evaluated statically by python, mypy,
etc. This means you can't use typical python syntax for declaring types - it has to
be present at the time `__new__` is called, rather than `__init__`.
- pydantic schema
- validation
- serialization
- lazy loading
- compression
```{toctree} ```{toctree}
:maxdepth: 2 :maxdepth: 2
:caption: Contents: :caption: Contents
:hidden: :hidden: true
overview
ndarray
linkml
hooks hooks
todo
```
```{toctree}
:maxdepth: 2
:caption: API
:hidden: true
api/index
api/ndarray
api/proxy
api/linkml/index
api/maps
api/monkeypatch
``` ```

2
docs/linkml.md Normal file
View file

@ -0,0 +1,2 @@
# LinkML Generation

134
docs/ndarray.md Normal file
View file

@ -0,0 +1,134 @@
# Constrained Arrays
## Implementation details
```{todo}
**Docs:**
Describe implementation details!
```
## Examples
### Declaration
Type with a single {class}`~numpydantic.NDArray` class, or use a {class}`~typing.Union`
to express more complex array constraints.
This package is effectively a Pydantic interface to [nptyping](https://github.com/ramonhagenaars/nptyping),
so any array syntax is valid there. (see [TODO](todo) for caveats)
```python
from typing import Union
from pydantic import BaseModel
from numpydantic import NDArray, Shape, UInt8, Float, Int
class Image(BaseModel):
"""
Data values. Data can be in 1-D, 2-D, 3-D, or 4-D. The first dimension should always represent time. This can also be used to store binary data (e.g., image frames). This can also be a link to data stored in an external file.
"""
array: Union[
NDArray[Shape["* x, * y"], UInt8],
NDArray[Shape["* x, * y, 3 rgb"], UInt8],
NDArray[Shape["* x, * y, 4 rgba"], UInt8],
NDArray[Shape["* t, * x, * y, 3 rgb"], UInt8],
NDArray[Shape["* t, * x, * y, 4 rgba"], Float]
]
```
### Validation:
```python
import numpy as np
# works
frame_gray = Image(array=np.ones((1280, 720), dtype=np.uint8))
frame_rgb = Image(array=np.ones((1280, 720, 3), dtype=np.uint8))
frame_rgba = Image(array=np.ones((1280, 720, 4), dtype=np.uint8))
video_rgb = Image(array=np.ones((100, 1280, 720, 3), dtype=np.uint8))
# fails
wrong_n_dimensions = Image(array=np.ones((1280,), dtype=np.uint8))
wrong_shape = Image(array=np.ones((1280,720,10), dtype=np.uint8))
wrong_type = Image(array=np.ones((1280,720,3), dtype=np.float64))
# shapes and types are checked together
float_video = Image(array=np.ones((100, 1280, 720, 4),dtype=float))
wrong_shape_float_video = Image(array=np.ones((100, 1280, 720, 3),dtype=float))
```
### JSON schema generation:
```python
class MyArray(BaseModel):
array: NDArray[Shape["2 x, * y, 4 z"], Float]
```
```python
>>> print(json.dumps(MyArray.model_json_schema(), indent=2))
```
```json
{
"properties": {
"array": {
"items": {
"items": {
"items": {
"type": "number"
},
"maxItems": 4,
"minItems": 4,
"type": "array"
},
"type": "array"
},
"maxItems": 2,
"minItems": 2,
"title": "Array",
"type": "array"
}
},
"required": [
"array"
],
"title": "MyArray",
"type": "object"
}
```
### Serialization
```python
class SmolArray(BaseModel):
array: NDArray[Shape["2 x, 2 y"], Int]
class BigArray(BaseModel):
array: NDArray[Shape["1000 x, 1000 y"], Int]
```
Serialize small arrays as lists of lists, and big arrays as a b64-encoded blosc compressed string
```python
>>> smol = SmolArray(array=np.array([[1,2],[3,4]], dtype=int))
>>> big = BigArray(array=np.random.randint(0,255,(1000,1000),int))
>>> print(smol.model_dump_json())
{"array":[[1,2],[3,4]]}
>>> print(big.model_dump_json())
{
"array": "( long b64 encoded string )",
"shape": [1000, 1000],
"dtype": "int64",
"unpack_fns": ["base64.b64decode", "blosc2.unpack_array2"],
}
```
## TODO
```{todo}
Implement structured arrays
```
```{todo}
Implement pandas dataframe validation?
```

17
docs/overview.md Normal file
View file

@ -0,0 +1,17 @@
# Overview
The Python type annotation system is weird and not like the rest of Python!
(at least until [PEP 0649](https://peps.python.org/pep-0649/) gets mainlined).
Similarly, Pydantic 2's core_schema system is wonderful but still relatively poorly
documented for custom types! This package does the work of plugging them in
together to make some kind of type validation frankenstein.
The first problem is that type annotations are evaluated statically by python, mypy,
etc. This means you can't use typical python syntax for declaring types - it has to
be present at the time `__new__` is called, rather than `__init__`.
- pydantic schema
- validation
- serialization
- lazy loading
- compression

5
docs/todo.md Normal file
View file

@ -0,0 +1,5 @@
# TODO
```{todolist}
```

View file

@ -1,12 +1,13 @@
# ruff: noqa: E402 # ruff: noqa: E402
# ruff: noqa: F401 # ruff: noqa: F401
# ruff: noqa: I001
from numpydantic.monkeypatch import apply_patches from numpydantic.monkeypatch import apply_patches
apply_patches() apply_patches()
from numpydantic.ndarray import NDArray
# convenience imports for typing - finish this! # convenience imports for typing - finish this!
from typing import Any from typing import Any
from nptyping import Float, Int, Number, Shape, UInt8 from nptyping import Float, Int, Number, Shape, UInt8
from numpydantic.ndarray import NDArray

View file

@ -0,0 +1,8 @@
# ruff: noqa: E402
# ruff: noqa: F401
from numpydantic.linkml.ndarraygen import (
ArrayFormat,
LinkMLDataArray,
LinkMLNDArray,
NWBLinkMLArraylike,
)

View file

@ -2,6 +2,7 @@ import shutil
from pathlib import Path from pathlib import Path
import pytest import pytest
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
@ -38,3 +39,36 @@ def tmp_output_dir_mod(tmp_output_dir) -> Path:
shutil.rmtree(str(subpath)) shutil.rmtree(str(subpath))
subpath.mkdir() subpath.mkdir()
return subpath return subpath
@pytest.fixture()
def nwb_linkml_array() -> tuple[ClassDefinition, str]:
classdef = ClassDefinition(
name="NWB_Linkml Array",
description="Main class's array",
is_a="Arraylike",
attributes=[
SlotDefinition(name="x", range="numeric", required=True),
SlotDefinition(name="y", range="numeric", required=True),
SlotDefinition(
name="z",
range="numeric",
required=False,
maximum_cardinality=3,
minimum_cardinality=3,
),
SlotDefinition(
name="a",
range="numeric",
required=False,
minimum_cardinality=4,
maximum_cardinality=4,
),
],
)
generated = """Union[
NDArray[Shape["* x, * y"], Number],
NDArray[Shape["* x, * y, 3 z"], Number],
NDArray[Shape["* x, * y, 3 z, 4 a"], Number]
]"""
return classdef, generated

View file

@ -0,0 +1,14 @@
import pytest
from numpydantic.linkml import ArrayFormat, NWBLinkMLArraylike
from ..fixtures import nwb_linkml_array
def test_nwb_linkml_array(nwb_linkml_array):
classdef, generated = nwb_linkml_array
assert ArrayFormat.is_array(classdef)
assert NWBLinkMLArraylike.check(classdef)
assert ArrayFormat.get(classdef) is NWBLinkMLArraylike
assert generated == NWBLinkMLArraylike.make(classdef)