diff --git a/docs/api/monkeypatch.md b/docs/api/monkeypatch.md deleted file mode 100644 index d397869..0000000 --- a/docs/api/monkeypatch.md +++ /dev/null @@ -1,6 +0,0 @@ -# monkeypatch - -```{eval-rst} -.. automodule:: numpydantic.monkeypatch - :members: -``` \ No newline at end of file diff --git a/docs/api/serialization.md b/docs/api/serialization.md new file mode 100644 index 0000000..f070ea0 --- /dev/null +++ b/docs/api/serialization.md @@ -0,0 +1,7 @@ +# serialization + +```{eval-rst} +.. automodule:: numpydantic.serialization + :members: + :undoc-members: +``` \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 684602d..af6375e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,73 @@ ## 1.* +### 1.6.* + +#### 1.6.0 - 24-09-23 - Roundtrip JSON Serialization + +Roundtrip JSON serialization is here - with serialization to list of lists, +as well as file references that don't require copying the whole array if +used in data modeling, control over path relativization, and stamping of +interface version for the extra provenance conscious. + +Please see [serialization](./serialization.md) for narrative documentation :) + +**Potentially Breaking Changes** +- See [development](./development.md) for a statement about API stability +- An additional {meth}`.Interface.deserialize` method has been added to + {meth}`.Interface.validate` - downstream users are not intended to override the + `validate method`, but if they have, then JSON deserialization will not work for them. +- `Interface` subclasses now require a `name` attribute, a short string identifier for that interface, + and a `json_model` that inherits from {class}`.interface.JsonDict`. Interfaces without + these attributes will not be able to be instantiated. +- {meth}`.Interface.to_json` is now an abstract method that all interfaces must define. + +**Features** +- Roundtrip JSON serialization - by default dump to a list of list arrays, but + support the `round_trip` keyword in `model_dump_json` for provenance-preserving dumps +- JSON Schema generation has been separated from `core_schema` generation in {class}`.NDArray`. + Downstream interfaces can customize json schema generation without compromising ability to validate. +- All proxy classes must have an `__eq__` dunder method to compare equality - + in proxy classes, these compare equality of arguments, since the arrays that + are referenced on disk should be equal by definition. Direct array comparison + should use {func}`numpy.array_equal` +- Interfaces previously couldn't be instantiated without explicit shape and dtype arguments, + these have been given `Any` defaults. +- New {mod}`numpydantic.serialization` module to contain serialization logic. + +**New Classes** +See the docstrings for descriptions of each class +- `MarkMismatchError` for when an array serialized with `mark_interface` doesn't match + the interface that's deserializing it +- {class}`.interface.InterfaceMark` +- {class}`.interface.MarkedJson` +- {class}`.interface.JsonDict` + - {class}`.dask.DaskJsonDict` + - {class}`.hdf5.H5JsonDict` + - {class}`.numpy.NumpyJsonDict` + - {class}`.video.VideoJsonDict` + - {class}`.zarr.ZarrJsonDict` + +**Bugfix** +- [`#17`](https://github.com/p2p-ld/numpydantic/issues/17) - Arrays are re-validated as lists, rather than arrays +- Some proxy classes would fail to be serialized becauase they lacked an `__array__` method. + `__array__` methods have been added, and tests for coercing to an array to prevent regression. +- Some proxy classes lacked a `__name__` attribute, which caused failures to serialize + when the `__getattr__` methods attempted to pass it through. These have been added where needed. + +**Docs** +- Add statement about versioning and API stability to [development](./development.md) +- Add docs for serialization! +- Remove stranded docs from hooks and monkeypatch +- Added `myst_nb` to docs dependencies for direct rendering of code and output + +**Tests** +- Marks have been added for running subsets of the tests for a given interface, + package feature, etc. +- Tests for all the above functionality + + + ### 1.5.* #### 1.5.3 - 24-09-03 - Bugfix, type checking for empty HDF5 datasets diff --git a/docs/conf.py b/docs/conf.py index bae1f75..963b0b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ extensions = [ "sphinx.ext.doctest", "sphinx_design", "sphinxcontrib.mermaid", - "myst_parser", + "myst_nb", "sphinx.ext.todo", ] @@ -77,3 +77,8 @@ napoleon_attr_annotations = True # todo todo_include_todos = True todo_link_only = True + +# myst +# myst-nb +nb_render_markdown_format = "myst" +nb_execution_show_tb = True diff --git a/docs/data/test.avi b/docs/data/test.avi new file mode 100644 index 0000000..880cc68 Binary files /dev/null and b/docs/data/test.avi differ diff --git a/docs/data/test.h5 b/docs/data/test.h5 new file mode 100644 index 0000000..39fa06a Binary files /dev/null and b/docs/data/test.h5 differ diff --git a/docs/data/test.zarr/.zarray b/docs/data/test.zarr/.zarray new file mode 100644 index 0000000..8ec7941 --- /dev/null +++ b/docs/data/test.zarr/.zarray @@ -0,0 +1,22 @@ +{ + "chunks": [ + 2, + 2 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dtype": "=4.0.0; python_version < \"3.9\"", +] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -46,15 +52,55 @@ files = [ {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] +[[package]] +name = "appnope" +version = "0.1.4" +requires_python = ">=3.6" +summary = "Disable App Nap on macOS >= 10.9" +groups = ["dev", "docs"] +marker = "platform_system == \"Darwin\"" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + [[package]] name = "asciitree" version = "0.3.3" summary = "Draws ASCII trees." -groups = ["arrays", "dev", "tests"] +groups = ["arrays", "dev", "docs", "tests", "zarr"] files = [ {file = "asciitree-0.3.3.tar.gz", hash = "sha256:4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e"}, ] +[[package]] +name = "asttokens" +version = "2.4.1" +summary = "Annotate AST trees with source code positions" +groups = ["dev", "docs"] +dependencies = [ + "six>=1.12.0", + "typing; python_version < \"3.5\"", +] +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[[package]] +name = "attrs" +version = "24.2.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +groups = ["dev", "docs"] +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + [[package]] name = "autodoc-pydantic" version = "2.2.0" @@ -63,6 +109,7 @@ summary = "Seamlessly integrate pydantic models in your Sphinx documentation." groups = ["dev", "docs"] dependencies = [ "Sphinx>=4.0", + "importlib-metadata>1; python_version <= \"3.8\"", "pydantic-settings<3.0.0,>=2.0", "pydantic<3.0.0,>=2.0", ] @@ -76,6 +123,9 @@ version = "2.15.0" requires_python = ">=3.8" summary = "Internationalization utilities" groups = ["dev", "docs"] +dependencies = [ + "pytz>=2015.7; python_version < \"3.9\"", +] files = [ {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, @@ -142,6 +192,78 @@ files = [ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["dev", "docs"] +marker = "implementation_name == \"pypy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -218,9 +340,10 @@ name = "click" version = "8.1.7" requires_python = ">=3.7" summary = "Composable command line interface toolkit" -groups = ["arrays", "dask", "dev", "tests"] +groups = ["arrays", "dask", "dev", "docs", "tests"] dependencies = [ "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", ] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, @@ -232,7 +355,7 @@ name = "cloudpickle" version = "3.0.0" requires_python = ">=3.8" summary = "Pickler class to extend the standard pickle.Pickler functionality" -groups = ["arrays", "dask", "dev", "tests"] +groups = ["arrays", "dask", "dev", "docs", "tests"] files = [ {file = "cloudpickle-3.0.0-py3-none-any.whl", hash = "sha256:246ee7d0c295602a036e86369c77fecda4ab17b506496730f2f576d9016fd9c7"}, {file = "cloudpickle-3.0.0.tar.gz", hash = "sha256:996d9a482c6fb4f33c1a35335cf8afd065d2a56e973270364840712d9131a882"}, @@ -249,6 +372,20 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "comm" +version = "0.2.2" +requires_python = ">=3.8" +summary = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +groups = ["dev", "docs"] +dependencies = [ + "traitlets>=4", +] +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + [[package]] name = "coverage" version = "6.5.0" @@ -355,7 +492,7 @@ name = "dask" version = "2024.7.1" requires_python = ">=3.9" summary = "Parallel PyData with Task Scheduling" -groups = ["arrays", "dask", "dev", "tests"] +groups = ["arrays", "dask", "dev", "docs", "tests"] dependencies = [ "click>=8.1", "cloudpickle>=1.5.0", @@ -371,6 +508,44 @@ files = [ {file = "dask-2024.7.1.tar.gz", hash = "sha256:dbaef2d50efee841a9d981a218cfeb50392fc9a95e0403b6d680450e4f50d531"}, ] +[[package]] +name = "debugpy" +version = "1.8.5" +requires_python = ">=3.8" +summary = "An implementation of the Debug Adapter Protocol for Python" +groups = ["dev", "docs"] +files = [ + {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, + {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, + {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, + {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, + {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, + {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, + {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, + {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, + {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, + {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, + {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, + {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, + {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, + {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, + {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, + {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, + {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, + {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +requires_python = ">=3.5" +summary = "Decorators for Humans" +groups = ["dev", "docs"] +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + [[package]] name = "docopt" version = "0.6.2" @@ -396,31 +571,52 @@ name = "exceptiongroup" version = "1.2.2" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" -groups = ["dev", "tests"] +groups = ["dev", "docs", "tests"] marker = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] +[[package]] +name = "executing" +version = "2.1.0" +requires_python = ">=3.8" +summary = "Get the currently executing AST node of a frame, and other information" +groups = ["dev", "docs"] +files = [ + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, +] + [[package]] name = "fasteners" version = "0.19" requires_python = ">=3.6" summary = "A python package that provides useful locks" -groups = ["arrays", "dev", "tests"] +groups = ["arrays", "dev", "docs", "tests", "zarr"] marker = "sys_platform != \"emscripten\"" files = [ {file = "fasteners-0.19-py3-none-any.whl", hash = "sha256:758819cb5d94cdedf4e836988b74de396ceacb8e2794d21f82d131fd9ee77237"}, {file = "fasteners-0.19.tar.gz", hash = "sha256:b4f37c3ac52d8a445af3a66bce57b33b5e90b97c696b7b984f530cf8f0ded09c"}, ] +[[package]] +name = "fastjsonschema" +version = "2.20.0" +summary = "Fastest Python implementation of JSON schema" +groups = ["dev", "docs"] +files = [ + {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, + {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, +] + [[package]] name = "fsspec" version = "2024.6.1" requires_python = ">=3.8" summary = "File-system specification" -groups = ["arrays", "dask", "dev", "tests"] +groups = ["arrays", "dask", "dev", "docs", "tests"] files = [ {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, @@ -449,17 +645,87 @@ version = "1.2.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" summary = "A backport of fstrings to python<3.6" groups = ["dev", "tests"] +dependencies = [ + "tokenize-rt>=3; python_version < \"3.6\"", +] files = [ {file = "future_fstrings-1.2.0-py2.py3-none-any.whl", hash = "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63"}, {file = "future_fstrings-1.2.0.tar.gz", hash = "sha256:6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089"}, ] +[[package]] +name = "greenlet" +version = "3.1.1" +requires_python = ">=3.7" +summary = "Lightweight in-process concurrent programming" +groups = ["dev", "docs"] +marker = "(platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"" +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] + [[package]] name = "h11" version = "0.14.0" requires_python = ">=3.7" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" groups = ["dev"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -470,7 +736,7 @@ name = "h5py" version = "3.11.0" requires_python = ">=3.8" summary = "Read and write HDF5 files from Python" -groups = ["arrays", "dev", "hdf5", "tests"] +groups = ["arrays", "dev", "docs", "hdf5", "tests"] dependencies = [ "numpy>=1.17.3", ] @@ -522,8 +788,8 @@ version = "8.2.0" requires_python = ">=3.8" summary = "Read metadata from Python packages" groups = ["arrays", "dask", "dev", "docs", "tests"] -marker = "python_version < \"3.12\"" dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", "zipp>=0.5", ] files = [ @@ -542,6 +808,70 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "ipykernel" +version = "6.29.5" +requires_python = ">=3.8" +summary = "IPython Kernel for Jupyter" +groups = ["dev", "docs"] +dependencies = [ + "appnope; platform_system == \"Darwin\"", + "comm>=0.1.1", + "debugpy>=1.6.5", + "ipython>=7.23.1", + "jupyter-client>=6.1.12", + "jupyter-core!=5.0.*,>=4.12", + "matplotlib-inline>=0.1", + "nest-asyncio", + "packaging", + "psutil", + "pyzmq>=24", + "tornado>=6.1", + "traitlets>=5.4.0", +] +files = [ + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, +] + +[[package]] +name = "ipython" +version = "8.18.1" +requires_python = ">=3.9" +summary = "IPython: Productive Interactive Computing" +groups = ["dev", "docs"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "decorator", + "exceptiongroup; python_version < \"3.11\"", + "jedi>=0.16", + "matplotlib-inline", + "pexpect>4.3; sys_platform != \"win32\"", + "prompt-toolkit<3.1.0,>=3.0.41", + "pygments>=2.4.0", + "stack-data", + "traitlets>=5", + "typing-extensions; python_version < \"3.10\"", +] +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[[package]] +name = "jedi" +version = "0.19.1" +requires_python = ">=3.6" +summary = "An autocompletion tool for Python that can be used for text editors." +groups = ["dev", "docs"] +dependencies = [ + "parso<0.9.0,>=0.8.3", +] +files = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] + [[package]] name = "jinja2" version = "3.1.4" @@ -556,12 +886,102 @@ files = [ {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] +[[package]] +name = "jsonschema" +version = "4.23.0" +requires_python = ">=3.8" +summary = "An implementation of JSON Schema validation for Python" +groups = ["dev", "docs"] +dependencies = [ + "attrs>=22.2.0", + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "jsonschema-specifications>=2023.03.6", + "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", + "referencing>=0.28.4", + "rpds-py>=0.7.1", +] +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +requires_python = ">=3.8" +summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +groups = ["dev", "docs"] +dependencies = [ + "importlib-resources>=1.4.0; python_version < \"3.9\"", + "referencing>=0.31.0", +] +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[[package]] +name = "jupyter-cache" +version = "1.0.0" +requires_python = ">=3.9" +summary = "A defined interface for working with a cache of jupyter notebooks." +groups = ["dev", "docs"] +dependencies = [ + "attrs", + "click", + "importlib-metadata", + "nbclient>=0.2", + "nbformat", + "pyyaml", + "sqlalchemy<3,>=1.3.12", + "tabulate", +] +files = [ + {file = "jupyter_cache-1.0.0-py3-none-any.whl", hash = "sha256:594b1c4e29b488b36547e12477645f489dbdc62cc939b2408df5679f79245078"}, + {file = "jupyter_cache-1.0.0.tar.gz", hash = "sha256:d0fa7d7533cd5798198d8889318269a8c1382ed3b22f622c09a9356521f48687"}, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +requires_python = ">=3.8" +summary = "Jupyter protocol implementation and client libraries" +groups = ["dev", "docs"] +dependencies = [ + "importlib-metadata>=4.8.3; python_version < \"3.10\"", + "jupyter-core!=5.0.*,>=4.12", + "python-dateutil>=2.8.2", + "pyzmq>=23.0", + "tornado>=6.2", + "traitlets>=5.3", +] +files = [ + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, +] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +requires_python = ">=3.8" +summary = "Jupyter core package. A base package on which Jupyter projects rely." +groups = ["dev", "docs"] +dependencies = [ + "platformdirs>=2.5", + "pywin32>=300; sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"", + "traitlets>=5.3", +] +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + [[package]] name = "locket" version = "1.0.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" summary = "File-based locks for Python on Linux and Windows" -groups = ["arrays", "dask", "dev", "tests"] +groups = ["arrays", "dask", "dev", "docs", "tests"] files = [ {file = "locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3"}, {file = "locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632"}, @@ -631,6 +1051,20 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +requires_python = ">=3.8" +summary = "Inline Matplotlib backend for Jupyter" +groups = ["dev", "docs"] +dependencies = [ + "traitlets", +] +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + [[package]] name = "mdit-py-plugins" version = "0.4.1" @@ -667,6 +1101,29 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "myst-nb" +version = "1.1.1" +requires_python = ">=3.9" +summary = "A Jupyter Notebook Sphinx reader built on top of the MyST markdown parser." +groups = ["dev", "docs"] +dependencies = [ + "importlib-metadata", + "ipykernel", + "ipython", + "jupyter-cache>=0.5", + "myst-parser>=1.0.0", + "nbclient", + "nbformat>=5.0", + "pyyaml", + "sphinx>=5", + "typing-extensions", +] +files = [ + {file = "myst_nb-1.1.1-py3-none-any.whl", hash = "sha256:8b8f9085287d948eef46cb3764aafc21915e0e981882b8c742719f5b1a84c36f"}, + {file = "myst_nb-1.1.1.tar.gz", hash = "sha256:74227c11f76d03494f43b7788659b161b94f4dedef230a2912412bc8c3c9e553"}, +] + [[package]] name = "myst-parser" version = "2.0.0" @@ -686,6 +1143,51 @@ files = [ {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, ] +[[package]] +name = "nbclient" +version = "0.10.0" +requires_python = ">=3.8.0" +summary = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +groups = ["dev", "docs"] +dependencies = [ + "jupyter-client>=6.1.12", + "jupyter-core!=5.0.*,>=4.12", + "nbformat>=5.1", + "traitlets>=5.4", +] +files = [ + {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, + {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +requires_python = ">=3.8" +summary = "The Jupyter Notebook format" +groups = ["dev", "docs"] +dependencies = [ + "fastjsonschema>=2.15", + "jsonschema>=2.6", + "jupyter-core!=5.0.*,>=4.12", + "traitlets>=5.1", +] +files = [ + {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, + {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +requires_python = ">=3.5" +summary = "Patch asyncio to allow nested event loops" +groups = ["dev", "docs"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "networkx" version = "3.2.1" @@ -702,7 +1204,7 @@ name = "numcodecs" version = "0.12.1" requires_python = ">=3.8" summary = "A Python package providing buffer compression and transformation codecs for use in data storage and communication applications." -groups = ["arrays", "dev", "tests"] +groups = ["arrays", "dev", "docs", "tests", "zarr"] dependencies = [ "numpy>=1.7", ] @@ -731,7 +1233,7 @@ name = "numpy" version = "2.0.1" requires_python = ">=3.9" summary = "Fundamental package for array computing in Python" -groups = ["arrays", "default", "dev", "hdf5", "tests", "video"] +groups = ["default", "arrays", "dev", "docs", "hdf5", "tests", "video", "zarr"] files = [ {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, @@ -785,8 +1287,9 @@ name = "opencv-python" version = "4.10.0.84" requires_python = ">=3.6" summary = "Wrapper package for OpenCV python bindings." -groups = ["arrays", "dev", "tests", "video"] +groups = ["arrays", "dev", "docs", "tests", "video"] dependencies = [ + "numpy>=1.13.3; python_version < \"3.7\"", "numpy>=1.17.0; python_version >= \"3.7\"", "numpy>=1.17.3; python_version >= \"3.8\"", "numpy>=1.19.3; python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\"", @@ -818,12 +1321,23 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "parso" +version = "0.8.4" +requires_python = ">=3.6" +summary = "A Python Parser" +groups = ["dev", "docs"] +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + [[package]] name = "partd" version = "1.4.2" requires_python = ">=3.9" summary = "Appendable key-value storage" -groups = ["arrays", "dask", "dev", "tests"] +groups = ["arrays", "dask", "dev", "docs", "tests"] dependencies = [ "locket", "toolz", @@ -844,12 +1358,26 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pexpect" +version = "4.9.0" +summary = "Pexpect allows easy control of interactive console applications." +groups = ["dev", "docs"] +marker = "sys_platform != \"win32\"" +dependencies = [ + "ptyprocess>=0.5", +] +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + [[package]] name = "platformdirs" version = "4.2.2" requires_python = ">=3.8" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["dev"] +groups = ["dev", "docs"] files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, @@ -866,12 +1394,76 @@ files = [ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.47" +requires_python = ">=3.7.0" +summary = "Library for building powerful interactive command lines in Python" +groups = ["dev", "docs"] +dependencies = [ + "wcwidth", +] +files = [ + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, +] + +[[package]] +name = "psutil" +version = "6.0.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "Cross-platform lib for process and system monitoring in Python." +groups = ["dev", "docs"] +files = [ + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +summary = "Run a subprocess in a pseudo terminal" +groups = ["dev", "docs"] +marker = "sys_platform != \"win32\"" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +summary = "Safely evaluate AST nodes without side effects" +groups = ["dev", "docs"] +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["dev", "docs"] +marker = "implementation_name == \"pypy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.8.2" requires_python = ">=3.8" summary = "Data validation using Python type hints" -groups = ["arrays", "default", "dev", "docs", "tests"] +groups = ["default", "dev", "docs"] dependencies = [ "annotated-types>=0.4.0", "pydantic-core==2.20.1", @@ -888,7 +1480,7 @@ name = "pydantic-core" version = "2.20.1" requires_python = ">=3.8" summary = "Core functionality for Pydantic validation and serialization" -groups = ["arrays", "default", "dev", "docs", "tests"] +groups = ["default", "dev", "docs"] dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] @@ -1048,6 +1640,20 @@ files = [ {file = "pytest_depends-1.0.1-py3-none-any.whl", hash = "sha256:a1df072bcc93d77aca3f0946903f5fed8af2d9b0056db1dfc9ed5ac164ab0642"}, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["dev", "docs"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1059,6 +1665,25 @@ files = [ {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] +[[package]] +name = "pywin32" +version = "306" +summary = "Python for Window Extensions" +groups = ["dev", "docs"] +marker = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1100,6 +1725,113 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyzmq" +version = "26.2.0" +requires_python = ">=3.7" +summary = "Python bindings for 0MQ" +groups = ["dev", "docs"] +dependencies = [ + "cffi; implementation_name == \"pypy\"", +] +files = [ + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, +] + +[[package]] +name = "referencing" +version = "0.35.1" +requires_python = ">=3.8" +summary = "JSON Referencing + Python" +groups = ["dev", "docs"] +dependencies = [ + "attrs>=22.2.0", + "rpds-py>=0.7.0", +] +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + [[package]] name = "requests" version = "2.32.3" @@ -1117,6 +1849,121 @@ files = [ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] +[[package]] +name = "rich" +version = "13.8.1" +requires_python = ">=3.7.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["dev", "docs"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, +] + +[[package]] +name = "rpds-py" +version = "0.20.0" +requires_python = ">=3.8" +summary = "Python bindings to Rust's persistent data structures (rpds)" +groups = ["dev", "docs"] +files = [ + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, +] + [[package]] name = "ruff" version = "0.5.5" @@ -1144,6 +1991,17 @@ files = [ {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, ] +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +groups = ["dev", "docs"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1331,6 +2189,69 @@ files = [ {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.35" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +groups = ["dev", "docs"] +dependencies = [ + "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"", + "importlib-metadata; python_version < \"3.8\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b"}, + {file = "SQLAlchemy-2.0.35-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90"}, + {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea"}, + {file = "SQLAlchemy-2.0.35-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33"}, + {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9"}, + {file = "SQLAlchemy-2.0.35-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff"}, + {file = "SQLAlchemy-2.0.35-cp310-cp310-win32.whl", hash = "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b"}, + {file = "SQLAlchemy-2.0.35-cp310-cp310-win_amd64.whl", hash = "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e"}, + {file = "SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf"}, + {file = "SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-win32.whl", hash = "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3"}, + {file = "SQLAlchemy-2.0.35-cp39-cp39-win_amd64.whl", hash = "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f"}, + {file = "SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1"}, + {file = "sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +summary = "Extract data from python stack frames and tracebacks for informative displays" +groups = ["dev", "docs"] +dependencies = [ + "asttokens>=2.1.0", + "executing>=1.2.0", + "pure-eval", +] +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + [[package]] name = "starlette" version = "0.38.2" @@ -1346,6 +2267,17 @@ files = [ {file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"}, ] +[[package]] +name = "tabulate" +version = "0.9.0" +requires_python = ">=3.7" +summary = "Pretty-print tabular data" +groups = ["dev", "docs"] +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -1363,18 +2295,49 @@ name = "toolz" version = "0.12.1" requires_python = ">=3.7" summary = "List processing tools and functional utilities" -groups = ["arrays", "dask", "dev", "tests"] +groups = ["arrays", "dask", "dev", "docs", "tests"] files = [ {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, ] +[[package]] +name = "tornado" +version = "6.4.1" +requires_python = ">=3.8" +summary = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +groups = ["dev", "docs"] +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +requires_python = ">=3.8" +summary = "Traitlets Python configuration system" +groups = ["dev", "docs"] +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["arrays", "default", "dev", "docs", "tests"] +groups = ["default", "dev", "docs"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1482,6 +2445,19 @@ files = [ {file = "watchfiles-0.22.0.tar.gz", hash = "sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb"}, ] +[[package]] +name = "wcwidth" +version = "0.2.13" +summary = "Measures the displayed width of unicode strings in a terminal" +groups = ["dev", "docs"] +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "websockets" version = "12.0" @@ -1557,7 +2533,7 @@ name = "zarr" version = "2.18.2" requires_python = ">=3.9" summary = "An implementation of chunked, compressed, N-dimensional arrays for Python" -groups = ["arrays", "dev", "tests"] +groups = ["arrays", "dev", "docs", "tests", "zarr"] dependencies = [ "asciitree", "fasteners; sys_platform != \"emscripten\"", @@ -1575,7 +2551,6 @@ version = "3.19.2" requires_python = ">=3.8" summary = "Backport of pathlib-compatible object wrapper for zip files" groups = ["arrays", "dask", "dev", "docs", "tests"] -marker = "python_version < \"3.12\"" files = [ {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, diff --git a/pyproject.toml b/pyproject.toml index 4dddb69..0e6926b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "numpydantic" -version = "1.5.3" +version = "1.6.0" description = "Type and shape validation and serialization for arbitrary array types in pydantic models" authors = [ {name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"}, @@ -73,12 +73,15 @@ tests = [ "coveralls<4.0.0,>=3.3.1", ] docs = [ + "numpydantic[arrays]", "sphinx<8.0.0,>=7.2.6", "furo>=2024.1.29", "myst-parser<3.0.0,>=2.0.0", "autodoc-pydantic<3.0.0,>=2.0.1", "sphinx-design<1.0.0,>=0.5.0", "sphinxcontrib-mermaid>=0.9.2", + "myst-nb>=1.1.1", + "rich>=13.8.1", ] dev = [ "numpydantic[tests,docs]", @@ -109,6 +112,18 @@ filterwarnings = [ # nptyping's alias warnings 'ignore:.*deprecated alias.*Deprecated NumPy 1\.24.*' ] +markers = [ + "dtype: mark test related to dtype validation", + "shape: mark test related to shape validation", + "json_schema: mark test related to json schema generation", + "serialization: mark test related to serialization", + "proxy: test for proxy class in any interface", + "dask: dask interface", + "hdf5: hdf5 interface", + "numpy: numpy interface", + "video: video interface", + "zarr: zarr interface", +] [tool.ruff] target-version = "py311" diff --git a/src/numpydantic/exceptions.py b/src/numpydantic/exceptions.py index a61258f..c23b96f 100644 --- a/src/numpydantic/exceptions.py +++ b/src/numpydantic/exceptions.py @@ -25,3 +25,7 @@ class NoMatchError(MatchError): class TooManyMatchesError(MatchError): """Too many matches found by :class:`.Interface.match`""" + + +class MarkMismatchError(MatchError): + """A serialized :class:`.InterfaceMark` doesn't match the receiving interface""" diff --git a/src/numpydantic/interface/__init__.py b/src/numpydantic/interface/__init__.py index 0a0c490..36c7d97 100644 --- a/src/numpydantic/interface/__init__.py +++ b/src/numpydantic/interface/__init__.py @@ -4,15 +4,23 @@ Interfaces between nptyping types and array backends from numpydantic.interface.dask import DaskInterface from numpydantic.interface.hdf5 import H5Interface -from numpydantic.interface.interface import Interface +from numpydantic.interface.interface import ( + Interface, + InterfaceMark, + JsonDict, + MarkedJson, +) from numpydantic.interface.numpy import NumpyInterface from numpydantic.interface.video import VideoInterface from numpydantic.interface.zarr import ZarrInterface __all__ = [ - "Interface", "DaskInterface", "H5Interface", + "Interface", + "InterfaceMark", + "JsonDict", + "MarkedJson", "NumpyInterface", "VideoInterface", "ZarrInterface", diff --git a/src/numpydantic/interface/dask.py b/src/numpydantic/interface/dask.py index 7719e98..cd36a65 100644 --- a/src/numpydantic/interface/dask.py +++ b/src/numpydantic/interface/dask.py @@ -2,34 +2,73 @@ Interface for Dask arrays """ -from typing import Any, Optional +from typing import Any, Iterable, List, Literal, Optional, Union import numpy as np from pydantic import SerializationInfo -from numpydantic.interface.interface import Interface +from numpydantic.interface.interface import Interface, JsonDict from numpydantic.types import DtypeType, NDArrayType try: + from dask.array import from_array from dask.array.core import Array as DaskArray except ImportError: # pragma: no cover DaskArray = None +def _as_tuple(a_list: Any) -> tuple: + """Make a list of list into a tuple of tuples""" + return tuple( + [_as_tuple(item) if isinstance(item, list) else item for item in a_list] + ) + + +class DaskJsonDict(JsonDict): + """ + Round-trip json serialized form of a dask array + """ + + type: Literal["dask"] + name: str + chunks: Iterable[tuple[int, ...]] + dtype: str + array: list + + def to_array_input(self) -> DaskArray: + """Construct a dask array""" + np_array = np.array(self.array, dtype=self.dtype) + array = from_array( + np_array, + name=self.name, + chunks=_as_tuple(self.chunks), + ) + return array + + class DaskInterface(Interface): """ Interface for Dask :class:`~dask.array.core.Array` """ - input_types = (DaskArray,) + name = "dask" + input_types = (DaskArray, dict) return_type = DaskArray + json_model = DaskJsonDict @classmethod def check(cls, array: Any) -> bool: """ check if array is a dask array """ - return DaskArray is not None and isinstance(array, DaskArray) + if DaskArray is None: # pragma: no cover - no tests for interface deps atm + return False + elif isinstance(array, DaskArray): + return True + elif isinstance(array, dict): + return DaskJsonDict.is_valid(array) + else: + return False def get_object_dtype(self, array: NDArrayType) -> DtypeType: """Dask arrays require a compute() call to retrieve a single value""" @@ -43,7 +82,7 @@ class DaskInterface(Interface): @classmethod def to_json( cls, array: DaskArray, info: Optional[SerializationInfo] = None - ) -> list: + ) -> Union[List, DaskJsonDict]: """ Convert an array to a JSON serializable array by first converting to a numpy array and then to a list. @@ -56,4 +95,14 @@ class DaskInterface(Interface): method of serialization here using the python object itself rather than its JSON representation. """ - return np.array(array).tolist() + np_array = np.array(array) + as_json = np_array.tolist() + if info.round_trip: + as_json = DaskJsonDict( + type=cls.name, + array=as_json, + name=array.name, + chunks=array.chunks, + dtype=str(np_array.dtype), + ) + return as_json diff --git a/src/numpydantic/interface/hdf5.py b/src/numpydantic/interface/hdf5.py index 20cec0d..9215ec2 100644 --- a/src/numpydantic/interface/hdf5.py +++ b/src/numpydantic/interface/hdf5.py @@ -47,7 +47,7 @@ from typing import Any, Iterable, List, NamedTuple, Optional, Tuple, TypeVar, Un import numpy as np from pydantic import SerializationInfo -from numpydantic.interface.interface import Interface +from numpydantic.interface.interface import Interface, JsonDict from numpydantic.types import DtypeType, NDArrayType try: @@ -76,6 +76,20 @@ class H5ArrayPath(NamedTuple): """Refer to a specific field within a compound dtype""" +class H5JsonDict(JsonDict): + """Round-trip Json-able version of an HDF5 dataset""" + + file: str + path: str + field: Optional[str] = None + + def to_array_input(self) -> H5ArrayPath: + """Construct an :class:`.H5ArrayPath`""" + return H5ArrayPath( + **{k: v for k, v in self.model_dump().items() if k in H5ArrayPath._fields} + ) + + class H5Proxy: """ Proxy class to mimic numpy-like array behavior with an HDF5 array @@ -106,10 +120,11 @@ class H5Proxy: annotation_dtype: Optional[DtypeType] = None, ): self._h5f = None - self.file = Path(file) + self.file = Path(file).resolve() self.path = path self.field = field self._annotation_dtype = annotation_dtype + self._h5arraypath = H5ArrayPath(self.file, self.path, self.field) def array_exists(self) -> bool: """Check that there is in fact an array at :attr:`.path` within :attr:`.file`""" @@ -134,10 +149,20 @@ class H5Proxy: else: return obj.dtype[self.field] - def __getattr__(self, item: str): + def __array__(self) -> np.ndarray: + """To a numpy array""" with h5py.File(self.file, "r") as h5f: obj = h5f.get(self.path) - return getattr(obj, item) + return obj[:] + + def __getattr__(self, item: str): + if item == "__name__": + # special case for H5Proxies that don't refer to a real file during testing + return "H5Proxy" + with h5py.File(self.file, "r") as h5f: + obj = h5f.get(self.path) + val = getattr(obj, item) + return val def __getitem__( self, item: Union[int, slice, Tuple[Union[int, slice], ...]] @@ -205,6 +230,15 @@ class H5Proxy: """self.shape[0]""" return self.shape[0] + def __eq__(self, other: "H5Proxy") -> bool: + """ + Check that we are referring to the same hdf5 array + """ + if isinstance(other, H5Proxy): + return self._h5arraypath == other._h5arraypath + else: + raise ValueError("Can only compare equality of two H5Proxies") + def open(self, mode: str = "r") -> "h5py.Dataset": """ Return the opened :class:`h5py.Dataset` object @@ -244,8 +278,10 @@ class H5Interface(Interface): passthrough numpy-like interface to the dataset. """ + name = "hdf5" input_types = (H5ArrayPath, H5Arraylike, H5Proxy) return_type = H5Proxy + json_model = H5JsonDict @classmethod def enabled(cls) -> bool: @@ -261,6 +297,13 @@ class H5Interface(Interface): if isinstance(array, (H5ArrayPath, H5Proxy)): return True + if isinstance(array, dict): + if array.get("type", False) == cls.name: + return True + # continue checking if dict contains an hdf5 file + file = array.get("file", "") + array = (file, "") + if isinstance(array, (tuple, list)) and len(array) in (2, 3): # check that the first arg is an hdf5 file try: @@ -342,21 +385,27 @@ class H5Interface(Interface): @classmethod def to_json(cls, array: H5Proxy, info: Optional[SerializationInfo] = None) -> dict: """ - Dump to a dictionary containing + Render HDF5 array as JSON + + If ``round_trip == True``, we dump just the proxy info, a dictionary like: * ``file``: :attr:`.file` * ``path``: :attr:`.path` * ``attrs``: Any HDF5 attributes on the dataset * ``array``: The array as a list of lists + + Otherwise, we dump the array as a list of lists """ - try: - dset = array.open() - meta = { - "file": array.file, - "path": array.path, - "attrs": dict(dset.attrs), - "array": dset[:].tolist(), + if info.round_trip: + as_json = { + "type": cls.name, } - return meta - finally: - array.close() + as_json.update(array._h5arraypath._asdict()) + else: + try: + dset = array.open() + as_json = dset[:].tolist() + finally: + array.close() + + return as_json diff --git a/src/numpydantic/interface/interface.py b/src/numpydantic/interface/interface.py index 1ef307f..bee85b6 100644 --- a/src/numpydantic/interface/interface.py +++ b/src/numpydantic/interface/interface.py @@ -2,15 +2,20 @@ Base Interface metaclass """ +import inspect +import warnings from abc import ABC, abstractmethod +from functools import lru_cache +from importlib.metadata import PackageNotFoundError, version from operator import attrgetter from typing import Any, Generic, Optional, Tuple, Type, TypeVar, Union import numpy as np -from pydantic import SerializationInfo +from pydantic import BaseModel, SerializationInfo, ValidationError from numpydantic.exceptions import ( DtypeError, + MarkMismatchError, NoMatchError, ShapeError, TooManyMatchesError, @@ -19,6 +24,130 @@ from numpydantic.shape import check_shape from numpydantic.types import DtypeType, NDArrayType, ShapeType T = TypeVar("T", bound=NDArrayType) +U = TypeVar("U", bound="JsonDict") +V = TypeVar("V") # input type +W = TypeVar("W") # Any type in handle_input + + +class InterfaceMark(BaseModel): + """JSON-able mark to be able to round-trip json dumps""" + + module: str + cls: str + name: str + version: str + + def is_valid(self, cls: Type["Interface"], raise_on_error: bool = False) -> bool: + """ + Check that a given interface matches the mark. + + Args: + cls (Type): Interface type to check + raise_on_error (bool): Raise an ``MarkMismatchError`` when the match + is incorrect + + Returns: + bool + + Raises: + :class:`.MarkMismatchError` if requested by ``raise_on_error`` + for an invalid match + """ + mark = cls.mark_interface() + valid = self == mark + if not valid and raise_on_error: + raise MarkMismatchError( + "Mismatch between serialized mark and current interface, " + f"Serialized: {self}; current: {cls}" + ) + return valid + + def match_by_name(self) -> Optional[Type["Interface"]]: + """ + Try to find a matching interface by its name, returning it if found, + or None if not found. + """ + for i in Interface.interfaces(sort=False): + if i.name == self.name: + return i + return None + + +class JsonDict(BaseModel): + """ + Representation of array when dumped with round_trip == True. + """ + + type: str + + @abstractmethod + def to_array_input(self) -> V: + """ + Convert this roundtrip specifier to the relevant input class + (one of the ``input_types`` of an interface). + """ + + @classmethod + def is_valid(cls, val: dict, raise_on_error: bool = False) -> bool: + """ + Check whether a given dictionary matches this JsonDict specification + + Args: + val (dict): The dictionary to check for validity + raise_on_error (bool): If ``True``, raise the validation error + rather than returning a bool. (default: ``False``) + + Returns: + bool - true if valid, false if not + """ + try: + _ = cls.model_validate(val) + return True + except ValidationError as e: + if raise_on_error: + raise e + return False + + @classmethod + def handle_input(cls: Type[U], value: Union[dict, U, W]) -> Union[V, W]: + """ + Handle input that is the json serialized roundtrip version + (from :func:`~pydantic.BaseModel.model_dump` with ``round_trip=True``) + converting it to the input format with :meth:`.JsonDict.to_array_input` + or passing it through if not applicable + """ + if isinstance(value, dict): + value = cls(**value).to_array_input() + elif isinstance(value, cls): + value = value.to_array_input() + return value + + +class MarkedJson(BaseModel): + """ + Model of JSON dumped with an additional interface mark + with ``model_dump_json({'mark_interface': True})`` + """ + + interface: InterfaceMark + value: Union[list, dict] + """ + Inner value of the array, we don't validate for JsonDict here, + that should be downstream from us for performance reasons + """ + + @classmethod + def try_cast(cls, value: Union[V, dict]) -> Union[V, "MarkedJson"]: + """ + Try to cast to MarkedJson if applicable, otherwise return input + """ + if isinstance(value, dict) and "interface" in value and "value" in value: + try: + value = MarkedJson(**value) + except ValidationError: + # fine, just not a MarkedJson dict even if it looks like one + return value + return value class Interface(ABC, Generic[T]): @@ -30,7 +159,7 @@ class Interface(ABC, Generic[T]): return_type: Type[T] priority: int = 0 - def __init__(self, shape: ShapeType, dtype: DtypeType) -> None: + def __init__(self, shape: ShapeType = Any, dtype: DtypeType = Any) -> None: self.shape = shape self.dtype = dtype @@ -40,6 +169,7 @@ class Interface(ABC, Generic[T]): Calls the methods, in order: + * array = :meth:`.deserialize` (array) * array = :meth:`.before_validation` (array) * dtype = :meth:`.get_dtype` (array) - get the dtype from the array, override if eg. the dtype is not contained in ``array.dtype`` @@ -74,6 +204,8 @@ class Interface(ABC, Generic[T]): :class:`.DtypeError` and :class:`.ShapeError` (both of which are children of :class:`.InterfaceError` ) """ + array = self.deserialize(array) + array = self.before_validation(array) dtype = self.get_dtype(array) @@ -86,8 +218,32 @@ class Interface(ABC, Generic[T]): self.raise_for_shape(shape_valid, shape) array = self.after_validation(array) + return array + def deserialize(self, array: Any) -> Union[V, Any]: + """ + If given a JSON serialized version of the array, + deserialize it first. + + If a roundtrip-serialized :class:`.JsonDict`, + pass to :meth:`.JsonDict.handle_input`. + + If a roundtrip-serialized :class:`.MarkedJson`, + unpack mark, check for validity, warn if not, + and try to continue with validation + """ + if isinstance(marked_array := MarkedJson.try_cast(array), MarkedJson): + try: + marked_array.interface.is_valid(self.__class__, raise_on_error=True) + except MarkMismatchError as e: + warnings.warn( + str(e) + "\nAttempting to continue validation...", stacklevel=2 + ) + array = marked_array.value + + return self.json_model.handle_input(array) + def before_validation(self, array: Any) -> NDArrayType: """ Optional step pre-validation that coerces the input into a type that can be @@ -117,8 +273,6 @@ class Interface(ABC, Generic[T]): """ Validate the dtype of the given array, returning ``True`` if valid, ``False`` if not. - - """ if self.dtype is Any: return True @@ -211,17 +365,48 @@ class Interface(ABC, Generic[T]): installed, etc.) """ + @property + @abstractmethod + def name(self) -> str: + """ + Short name for this interface + """ + + @property + @abstractmethod + def json_model(self) -> JsonDict: + """ + The :class:`.JsonDict` model used for roundtripping + JSON serialization + """ + @classmethod - def to_json( - cls, array: Type[T], info: Optional[SerializationInfo] = None - ) -> Union[list, dict]: + @abstractmethod + def to_json(cls, array: Type[T], info: SerializationInfo) -> Union[list, JsonDict]: """ Convert an array of :attr:`.return_type` to a JSON-compatible format using base python types """ - if not isinstance(array, np.ndarray): # pragma: no cover - array = np.array(array) - return array.tolist() + + @classmethod + def mark_json(cls, array: Union[list, dict]) -> dict: + """ + When using ``model_dump_json`` with ``mark_interface: True`` in the ``context``, + add additional annotations that would allow the serialized array to be + roundtripped. + + Default is just to add an :class:`.InterfaceMark` + + Examples: + + >>> from pprint import pprint + >>> pprint(Interface.mark_json([1.0, 2.0])) + {'interface': {'cls': 'Interface', + 'module': 'numpydantic.interface.interface', + 'version': '1.2.2'}, + 'value': [1.0, 2.0]} + """ + return {"interface": cls.mark_interface(), "value": array} @classmethod def interfaces( @@ -274,6 +459,28 @@ class Interface(ABC, Generic[T]): return tuple(in_types) + @classmethod + def match_mark(cls, array: Any) -> Optional[Type["Interface"]]: + """ + Match a marked JSON dump of this array to the interface that it indicates. + + First find an interface that matches by name, and then run its + ``check`` method, because arrays can be dumped with a mark + but without ``round_trip == True`` (and thus can't necessarily + use the same interface that they were dumped with) + + Returns: + Interface if match found, None otherwise + """ + mark = MarkedJson.try_cast(array) + if not isinstance(mark, MarkedJson): + return None + + interface = mark.interface.match_by_name() + if interface is not None and interface.check(mark.value): + return interface + return None + @classmethod def match(cls, array: Any, fast: bool = False) -> Type["Interface"]: """ @@ -291,11 +498,18 @@ class Interface(ABC, Generic[T]): check each interface (as ordered by its ``priority`` , decreasing), and return on the first match. """ + # Shortcircuit match if this is a marked json dump + array = MarkedJson.try_cast(array) + if (match := cls.match_mark(array)) is not None: + return match + elif isinstance(array, MarkedJson): + array = array.value + # first try and find a non-numpy interface, since the numpy interface # will try and load the array into memory in its check method interfaces = cls.interfaces() - non_np_interfaces = [i for i in interfaces if i.__name__ != "NumpyInterface"] - np_interface = [i for i in interfaces if i.__name__ == "NumpyInterface"][0] + non_np_interfaces = [i for i in interfaces if i.name != "numpy"] + np_interface = [i for i in interfaces if i.name == "numpy"][0] if fast: matches = [] @@ -335,3 +549,29 @@ class Interface(ABC, Generic[T]): raise NoMatchError(f"No matching interfaces found for output {array}") else: return matches[0] + + @classmethod + @lru_cache(maxsize=32) + def mark_interface(cls) -> InterfaceMark: + """ + Create an interface mark indicating this interface for validation after + JSON serialization with ``round_trip==True`` + """ + interface_module = inspect.getmodule(cls) + interface_module = ( + None if interface_module is None else interface_module.__name__ + ) + try: + v = ( + None + if interface_module is None + else version(interface_module.split(".")[0]) + ) + except ( + PackageNotFoundError + ): # pragma: no cover - no tests for missing interface deps + v = None + + return InterfaceMark( + module=interface_module, cls=cls.__name__, name=cls.name, version=v + ) diff --git a/src/numpydantic/interface/numpy.py b/src/numpydantic/interface/numpy.py index 5ee988a..ad97474 100644 --- a/src/numpydantic/interface/numpy.py +++ b/src/numpydantic/interface/numpy.py @@ -2,9 +2,11 @@ Interface to numpy arrays """ -from typing import Any +from typing import Any, Literal, Union -from numpydantic.interface.interface import Interface +from pydantic import SerializationInfo + +from numpydantic.interface.interface import Interface, JsonDict try: import numpy as np @@ -18,13 +20,31 @@ except ImportError: # pragma: no cover np = None +class NumpyJsonDict(JsonDict): + """ + JSON-able roundtrip representation of numpy array + """ + + type: Literal["numpy"] + dtype: str + array: list + + def to_array_input(self) -> ndarray: + """ + Construct a numpy array + """ + return np.array(self.array, dtype=self.dtype) + + class NumpyInterface(Interface): """ Numpy :class:`~numpy.ndarray` s! """ + name = "numpy" input_types = (ndarray, list) return_type = ndarray + json_model = NumpyJsonDict priority = -999 """ The numpy interface is usually the interface of last resort. @@ -41,6 +61,8 @@ class NumpyInterface(Interface): """ if isinstance(array, ndarray): return True + elif isinstance(array, dict): + return NumpyJsonDict.is_valid(array) else: try: _ = np.array(array) @@ -61,3 +83,22 @@ class NumpyInterface(Interface): def enabled(cls) -> bool: """Check that numpy is present in the environment""" return ENABLED + + @classmethod + def to_json( + cls, array: ndarray, info: SerializationInfo = None + ) -> Union[list, JsonDict]: + """ + Convert an array of :attr:`.return_type` to a JSON-compatible format using + base python types + """ + if not isinstance(array, np.ndarray): # pragma: no cover + array = np.array(array) + + json_array = array.tolist() + + if info.round_trip: + json_array = NumpyJsonDict( + type=cls.name, dtype=str(array.dtype), array=json_array + ) + return json_array diff --git a/src/numpydantic/interface/video.py b/src/numpydantic/interface/video.py index f64457b..53a3ba5 100644 --- a/src/numpydantic/interface/video.py +++ b/src/numpydantic/interface/video.py @@ -3,10 +3,12 @@ Interface to support treating videos like arrays using OpenCV """ from pathlib import Path -from typing import Any, Optional, Tuple, Union +from typing import Any, Literal, Optional, Tuple, Union import numpy as np +from pydantic_core.core_schema import SerializationInfo +from numpydantic.interface import JsonDict from numpydantic.interface.interface import Interface try: @@ -19,6 +21,19 @@ except ImportError: # pragma: no cover VIDEO_EXTENSIONS = (".mp4", ".avi", ".mov", ".mkv") +class VideoJsonDict(JsonDict): + """Json-able roundtrip representation of a video file""" + + type: Literal["video"] + file: str + + def to_array_input(self) -> "VideoProxy": + """ + Construct a :class:`.VideoProxy` + """ + return VideoProxy(path=Path(self.file)) + + class VideoProxy: """ Passthrough proxy class to interact with videos as arrays @@ -33,7 +48,7 @@ class VideoProxy: ) if path is not None: - path = Path(path) + path = Path(path).resolve() self.path = path self._video = video # type: Optional[VideoCapture] @@ -52,6 +67,9 @@ class VideoProxy: "and it cant be reopened since source path cant be gotten " "from VideoCapture objects" ) + if not self.path.exists(): + raise FileNotFoundError(f"Video file {self.path} does not exist!") + self._video = VideoCapture(str(self.path)) return self._video @@ -137,6 +155,10 @@ class VideoProxy: slice_ = slice(0, slice_.stop, slice_.step) return slice_ + def __array__(self) -> np.ndarray: + """Whole video as a numpy array""" + return self[:] + def __getitem__(self, item: Union[int, slice, tuple]) -> np.ndarray: if isinstance(item, int): # want a single frame @@ -178,8 +200,16 @@ class VideoProxy: raise NotImplementedError("Setting pixel values on videos is not supported!") def __getattr__(self, item: str): + if item == "__name__": + return "VideoProxy" return getattr(self.video, item) + def __eq__(self, other: "VideoProxy") -> bool: + """Check if this is a proxy to the same video file""" + if not isinstance(other, VideoProxy): + raise TypeError("Can only compare equality of two VideoProxies") + return self.path == other.path + def __len__(self) -> int: """Number of frames in the video""" return self.shape[0] @@ -190,8 +220,10 @@ class VideoInterface(Interface): OpenCV interface to treat videos as arrays. """ + name = "video" input_types = (str, Path, VideoCapture, VideoProxy) return_type = VideoProxy + json_model = VideoJsonDict @classmethod def enabled(cls) -> bool: @@ -209,6 +241,9 @@ class VideoInterface(Interface): ): return True + if isinstance(array, dict): + array = array.get("file", "") + if isinstance(array, str): try: array = Path(array) @@ -227,3 +262,13 @@ class VideoInterface(Interface): else: proxy = VideoProxy(path=array) return proxy + + @classmethod + def to_json( + cls, array: VideoProxy, info: SerializationInfo + ) -> Union[list, VideoJsonDict]: + """Return a json-representation of a video""" + if info.round_trip: + return VideoJsonDict(type=cls.name, file=str(array.path)) + else: + return np.array(array).tolist() diff --git a/src/numpydantic/interface/zarr.py b/src/numpydantic/interface/zarr.py index 87f538a..41cad03 100644 --- a/src/numpydantic/interface/zarr.py +++ b/src/numpydantic/interface/zarr.py @@ -5,12 +5,12 @@ Interface to zarr arrays import contextlib from dataclasses import dataclass from pathlib import Path -from typing import Any, Optional, Sequence, Union +from typing import Any, Literal, Optional, Sequence, Union import numpy as np from pydantic import SerializationInfo -from numpydantic.interface.interface import Interface +from numpydantic.interface.interface import Interface, JsonDict from numpydantic.types import DtypeType try: @@ -56,13 +56,36 @@ class ZarrArrayPath: raise ValueError("Only len 1-2 iterables can be used for a ZarrArrayPath") +class ZarrJsonDict(JsonDict): + """Round-trip Json-able version of a Zarr Array""" + + info: dict[str, str] + type: Literal["zarr"] + file: Optional[str] = None + path: Optional[str] = None + array: Optional[list] = None + + def to_array_input(self) -> Union[ZarrArray, ZarrArrayPath]: + """ + Construct a ZarrArrayPath if file and path are present, + otherwise a ZarrArray + """ + if self.file: + array = ZarrArrayPath(file=self.file, path=self.path) + else: + array = zarr.array(self.array) + return array + + class ZarrInterface(Interface): """ Interface to in-memory or on-disk zarr arrays """ + name = "zarr" input_types = (Path, ZarrArray, ZarrArrayPath) return_type = ZarrArray + json_model = ZarrJsonDict @classmethod def enabled(cls) -> bool: @@ -71,7 +94,7 @@ class ZarrInterface(Interface): @staticmethod def _get_array( - array: Union[ZarrArray, str, Path, ZarrArrayPath, Sequence] + array: Union[ZarrArray, str, dict, ZarrJsonDict, Path, ZarrArrayPath, Sequence] ) -> ZarrArray: if isinstance(array, ZarrArray): return array @@ -92,6 +115,12 @@ class ZarrInterface(Interface): if isinstance(array, ZarrArray): return True + if isinstance(array, dict): + if array.get("type", False) == cls.name: + return True + # continue checking if dict contains a zarr file + array = array.get("file", "") + # See if can be coerced to ZarrArrayPath if isinstance(array, (Path, str)): array = ZarrArrayPath(file=array) @@ -135,26 +164,48 @@ class ZarrInterface(Interface): cls, array: Union[ZarrArray, str, Path, ZarrArrayPath, Sequence], info: Optional[SerializationInfo] = None, - ) -> dict: + ) -> Union[list, ZarrJsonDict]: """ - Dump just the metadata for an array from :meth:`zarr.core.Array.info_items` - plus the :meth:`zarr.core.Array.hexdigest`. + Dump a Zarr Array to JSON - The full array can be returned by passing ``'zarr_dump_array': True`` to the - serialization ``context`` :: + If ``info.round_trip == False``, dump the array as a list of lists. + This may be a memory-intensive operation. + + Otherwise, dump the metadata for an array from + :meth:`zarr.core.Array.info_items` + plus the :meth:`zarr.core.Array.hexdigest` as a :class:`.ZarrJsonDict` + + If either the ``dump_array`` value in the context dictionary is ``True`` + or the zarr array is an in-memory array, dump the array as well + (since without a persistent array it would be impossible to roundtrip and + dumping to JSON would be meaningless) + + Passing ```dump_array': True`` to the serialization ``context`` + looks like this:: model.model_dump_json(context={'zarr_dump_array': True}) """ - dump_array = False - if info is not None and info.context is not None: - dump_array = info.context.get("zarr_dump_array", False) - array = cls._get_array(array) - info = array.info_items() - info_dict = {i[0]: i[1] for i in info} - info_dict["hexdigest"] = array.hexdigest() - if dump_array: - info_dict["array"] = array[:].tolist() + if info.round_trip: + dump_array = False + if info is not None and info.context is not None: + dump_array = info.context.get("dump_array", False) + is_file = False - return info_dict + as_json = {"type": cls.name} + if hasattr(array.store, "dir_path"): + is_file = True + as_json["file"] = array.store.dir_path() + as_json["path"] = array.name + as_json["info"] = {i[0]: i[1] for i in array.info_items()} + as_json["info"]["hexdigest"] = array.hexdigest() + + if dump_array or not is_file: + as_json["array"] = array[:].tolist() + + as_json = ZarrJsonDict(**as_json) + else: + as_json = array[:].tolist() + + return as_json diff --git a/src/numpydantic/ndarray.py b/src/numpydantic/ndarray.py index d951d3a..fb81f69 100644 --- a/src/numpydantic/ndarray.py +++ b/src/numpydantic/ndarray.py @@ -24,11 +24,10 @@ from numpydantic.exceptions import InterfaceError from numpydantic.interface import Interface from numpydantic.maps import python_to_nptyping from numpydantic.schema import ( - _handler_type, - _jsonize_array, get_validate_interface, make_json_schema, ) +from numpydantic.serialization import jsonize_array from numpydantic.types import DtypeType, NDArrayType, ShapeType from numpydantic.vendor.nptyping.error import InvalidArgumentsError from numpydantic.vendor.nptyping.ndarray import NDArrayMeta as _NDArrayMeta @@ -41,6 +40,9 @@ from numpydantic.vendor.nptyping.typing_ import ( if TYPE_CHECKING: # pragma: no cover from nptyping.base_meta_classes import SubscriptableMeta + from pydantic._internal._schema_generation_shared import ( + CallbackGetCoreSchemaHandler, + ) from numpydantic import Shape @@ -164,33 +166,34 @@ class NDArray(NPTypingType, metaclass=NDArrayMeta): def __get_pydantic_core_schema__( cls, _source_type: "NDArray", - _handler: _handler_type, + _handler: "CallbackGetCoreSchemaHandler", ) -> core_schema.CoreSchema: shape, dtype = _source_type.__args__ shape: ShapeType dtype: DtypeType - # get pydantic core schema as a list of lists for JSON schema - list_schema = make_json_schema(shape, dtype, _handler) + # make core schema for json schema, store it and any model definitions + # note that there is a big of fragility in this function, + # as we need to access a private method of _handler to + # flatten out the json schema. See help(make_json_schema) + json_schema = make_json_schema(shape, dtype, _handler) - return core_schema.json_or_python_schema( - json_schema=list_schema, - python_schema=core_schema.with_info_plain_validator_function( - get_validate_interface(shape, dtype) - ), + return core_schema.with_info_plain_validator_function( + get_validate_interface(shape, dtype), serialization=core_schema.plain_serializer_function_ser_schema( - _jsonize_array, when_used="json", info_arg=True + jsonize_array, when_used="json", info_arg=True ), + metadata=json_schema, ) @classmethod def __get_pydantic_json_schema__( cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler ) -> core_schema.JsonSchema: - json_schema = handler(schema) + shape, dtype = cls.__args__ + json_schema = handler(schema["metadata"]) json_schema = handler.resolve_ref_schema(json_schema) - dtype = cls.__args__[1] if not isinstance(dtype, tuple) and dtype.__module__ not in ( "builtins", "typing", diff --git a/src/numpydantic/schema.py b/src/numpydantic/schema.py index d98f880..cafa1f4 100644 --- a/src/numpydantic/schema.py +++ b/src/numpydantic/schema.py @@ -5,10 +5,10 @@ Helper functions for use with :class:`~numpydantic.NDArray` - see the note in import hashlib import json -from typing import TYPE_CHECKING, Any, Callable, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Optional import numpy as np -from pydantic import BaseModel, SerializationInfo +from pydantic import BaseModel from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ListSchema, ValidationInfo @@ -19,13 +19,16 @@ from numpydantic.types import DtypeType, NDArrayType, ShapeType from numpydantic.vendor.nptyping.structure import StructureMeta if TYPE_CHECKING: # pragma: no cover + from pydantic._internal._schema_generation_shared import ( + CallbackGetCoreSchemaHandler, + ) + from numpydantic import Shape -_handler_type = Callable[[Any], core_schema.CoreSchema] -_UNSUPPORTED_TYPES = (complex,) - -def _numeric_dtype(dtype: DtypeType, _handler: _handler_type) -> CoreSchema: +def _numeric_dtype( + dtype: DtypeType, _handler: "CallbackGetCoreSchemaHandler" +) -> CoreSchema: """Make a numeric dtype that respects min/max values from extended numpy types""" if dtype in (np.number,): dtype = float @@ -36,14 +39,15 @@ def _numeric_dtype(dtype: DtypeType, _handler: _handler_type) -> CoreSchema: elif issubclass(dtype, np.integer): info = np.iinfo(dtype) schema = core_schema.int_schema(le=int(info.max), ge=int(info.min)) - else: schema = _handler.generate_schema(dtype) return schema -def _lol_dtype(dtype: DtypeType, _handler: _handler_type) -> CoreSchema: +def _lol_dtype( + dtype: DtypeType, _handler: "CallbackGetCoreSchemaHandler" +) -> CoreSchema: """Get the innermost dtype schema to use in the generated pydantic schema""" if isinstance(dtype, StructureMeta): # pragma: no cover raise NotImplementedError("Structured dtypes are currently unsupported") @@ -79,11 +83,12 @@ def _lol_dtype(dtype: DtypeType, _handler: _handler_type) -> CoreSchema: # does this need a warning? python_type = Any - if python_type in _UNSUPPORTED_TYPES: - array_type = core_schema.any_schema() - # TODO: warn and log here - elif python_type in (float, int): + if python_type in (float, int): array_type = _numeric_dtype(dtype, _handler) + elif python_type is bool: + array_type = core_schema.bool_schema() + elif python_type is Any: + array_type = core_schema.any_schema() else: array_type = _handler.generate_schema(python_type) @@ -208,14 +213,24 @@ def _unbounded_shape( def make_json_schema( - shape: ShapeType, dtype: DtypeType, _handler: _handler_type + shape: ShapeType, dtype: DtypeType, _handler: "CallbackGetCoreSchemaHandler" ) -> ListSchema: """ - Make a list of list JSON schema from a shape and a dtype. + Make a list of list pydantic core schema for an array from a shape and a dtype. + Used to generate JSON schema in the containing model, but not for validation, + which is handled by interfaces. First resolves the dtype into a pydantic ``CoreSchema`` , and then uses that with :func:`.list_of_lists_schema` . + .. admonition:: Potentially Fragile + + Uses a private method from the handler to flatten out nested definitions + (e.g. when dtype is a pydantic model) + so that they are present in the generated schema directly rather than + as references. Otherwise, at the time __get_pydantic_json_schema__ is called, + the definition references are lost. + Args: shape ( ShapeType ): Specification of a shape, as a tuple or an nptyping ``Shape`` @@ -234,6 +249,8 @@ def make_json_schema( else: list_schema = list_of_lists_schema(shape, dtype_schema) + list_schema = _handler._generate_schema.clean_schema(list_schema) + return list_schema @@ -252,9 +269,3 @@ def get_validate_interface(shape: ShapeType, dtype: DtypeType) -> Callable: return value return validate_interface - - -def _jsonize_array(value: Any, info: SerializationInfo) -> Union[list, dict]: - """Use an interface class to render an array as JSON""" - interface_cls = Interface.match_output(value) - return interface_cls.to_json(value, info) diff --git a/src/numpydantic/serialization.py b/src/numpydantic/serialization.py new file mode 100644 index 0000000..1f1edd0 --- /dev/null +++ b/src/numpydantic/serialization.py @@ -0,0 +1,128 @@ +""" +Serialization helpers for :func:`pydantic.BaseModel.model_dump` +and :func:`pydantic.BaseModel.model_dump_json` . +""" + +from pathlib import Path +from typing import Any, Callable, TypeVar, Union + +from pydantic_core.core_schema import SerializationInfo + +from numpydantic.interface import Interface, JsonDict + +T = TypeVar("T") +U = TypeVar("U") + + +def jsonize_array(value: Any, info: SerializationInfo) -> Union[list, dict]: + """Use an interface class to render an array as JSON""" + interface_cls = Interface.match_output(value) + array = interface_cls.to_json(value, info) + if isinstance(array, JsonDict): + array = array.model_dump(exclude_none=True) + + if info.context: + if info.context.get("mark_interface", False): + array = interface_cls.mark_json(array) + + if info.context.get("absolute_paths", False): + array = _absolutize_paths(array) + else: + relative_to = info.context.get("relative_to", ".") + array = _relativize_paths(array, relative_to) + else: + # relativize paths by default + array = _relativize_paths(array, ".") + + return array + + +def _relativize_paths(value: dict, relative_to: str = ".") -> dict: + """ + Make paths relative to either the current directory or the provided + ``relative_to`` directory, if provided in the context + """ + relative_to = Path(relative_to).resolve() + # pdb.set_trace() + + def _r_path(v: Any) -> Any: + try: + path = Path(v) + if not path.exists(): + return v + return str(relative_path(path, relative_to)) + except (TypeError, ValueError): + return v + + return _walk_and_apply(value, _r_path) + + +def _absolutize_paths(value: dict) -> dict: + def _a_path(v: Any) -> Any: + try: + path = Path(v) + if not path.exists(): + return v + return str(path.resolve()) + except (TypeError, ValueError): + return v + + return _walk_and_apply(value, _a_path) + + +def _walk_and_apply(value: T, f: Callable[[U], U]) -> T: + """ + Walk an object, applying a function + """ + if isinstance(value, dict): + for k, v in value.items(): + if isinstance(v, dict): + _walk_and_apply(v, f) + elif isinstance(v, list): + value[k] = [_walk_and_apply(sub_v, f) for sub_v in v] + else: + value[k] = f(v) + elif isinstance(value, list): + value = [_walk_and_apply(v, f) for v in value] + else: + value = f(value) + return value + + +def relative_path(self: Path, other: Path, walk_up: bool = True) -> Path: + """ + "Backport" of :meth:`pathlib.Path.relative_to` with ``walk_up=True`` + that's not available pre 3.12. + + Return the relative path to another path identified by the passed + arguments. If the operation is not possible (because this is not + related to the other path), raise ValueError. + + The *walk_up* parameter controls whether `..` may be used to resolve + the path. + + References: + https://github.com/python/cpython/blob/8a2baedc4bcb606da937e4e066b4b3a18961cace/Lib/pathlib/_abc.py#L244-L270 + """ + # pdb.set_trace() + if not isinstance(other, Path): # pragma: no cover - ripped from cpython + other = Path(other) + self_parts = self.parts + other_parts = other.parts + anchor0, parts0 = self_parts[0], list(reversed(self_parts[1:])) + anchor1, parts1 = other_parts[0], list(reversed(other_parts[1:])) + if anchor0 != anchor1: + raise ValueError(f"{self!r} and {other!r} have different anchors") + while parts0 and parts1 and parts0[-1] == parts1[-1]: + parts0.pop() + parts1.pop() + for part in parts1: # pragma: no cover - not testing, ripped off from cpython + if not part or part == ".": + pass + elif not walk_up: + raise ValueError(f"{self!r} is not in the subpath of {other!r}") + elif part == "..": + raise ValueError(f"'..' segment in {other!r} cannot be walked") + else: + parts0.append("..") + return Path(*reversed(parts0)) diff --git a/tests/conftest.py b/tests/conftest.py index 0467f25..c9035f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import pdb import sys import pytest diff --git a/tests/test_interface/conftest.py b/tests/test_interface/conftest.py index 63bdc4a..5d36fa5 100644 --- a/tests/test_interface/conftest.py +++ b/tests/test_interface/conftest.py @@ -1,6 +1,6 @@ import pytest -from typing import Tuple, Callable +from typing import Callable, Tuple, Type import numpy as np import dask.array as da import zarr @@ -12,27 +12,47 @@ from numpydantic import interface, NDArray @pytest.fixture( scope="function", params=[ - ([[1, 2], [3, 4]], interface.NumpyInterface), - (np.zeros((3, 4)), interface.NumpyInterface), - ("hdf5_array", interface.H5Interface), - (da.random.random((10, 10)), interface.DaskInterface), - (zarr.ones((10, 10)), interface.ZarrInterface), - ("zarr_nested_array", interface.ZarrInterface), - ("zarr_array", interface.ZarrInterface), - ("avi_video", interface.VideoInterface), - ], - ids=[ - "numpy_list", - "numpy", - "H5ArrayPath", - "dask", - "zarr_memory", - "zarr_nested", - "zarr_array", - "video", + pytest.param( + ([[1, 2], [3, 4]], interface.NumpyInterface), + marks=pytest.mark.numpy, + id="numpy-list", + ), + pytest.param( + (np.zeros((3, 4)), interface.NumpyInterface), + marks=pytest.mark.numpy, + id="numpy", + ), + pytest.param( + ("hdf5_array", interface.H5Interface), + marks=pytest.mark.hdf5, + id="h5-array-path", + ), + pytest.param( + (da.random.random((10, 10)), interface.DaskInterface), + marks=pytest.mark.dask, + id="dask", + ), + pytest.param( + (zarr.ones((10, 10)), interface.ZarrInterface), + marks=pytest.mark.zarr, + id="zarr-memory", + ), + pytest.param( + ("zarr_nested_array", interface.ZarrInterface), + marks=pytest.mark.zarr, + id="zarr-nested", + ), + pytest.param( + ("zarr_array", interface.ZarrInterface), + marks=pytest.mark.zarr, + id="zarr-array", + ), + pytest.param( + ("avi_video", interface.VideoInterface), marks=pytest.mark.video, id="video" + ), ], ) -def interface_type(request) -> Tuple[NDArray, interface.Interface]: +def interface_type(request) -> Tuple[NDArray, Type[interface.Interface]]: """ Test cases for each interface's ``check`` method - each input should match the provided interface and that interface only diff --git a/tests/test_interface/test_dask.py b/tests/test_interface/test_dask.py index fb1e4cb..c3b70e0 100644 --- a/tests/test_interface/test_dask.py +++ b/tests/test_interface/test_dask.py @@ -1,5 +1,3 @@ -import pdb - import pytest import json @@ -11,6 +9,8 @@ from numpydantic.exceptions import DtypeError, ShapeError from tests.conftest import ValidationCase +pytestmark = pytest.mark.dask + def dask_array(case: ValidationCase) -> da.Array: if issubclass(case.dtype, BaseModel): @@ -42,14 +42,17 @@ def test_dask_check(interface_type): assert not DaskInterface.check(interface_type[0]) +@pytest.mark.shape def test_dask_shape(shape_cases): _test_dask_case(shape_cases) +@pytest.mark.dtype def test_dask_dtype(dtype_cases): _test_dask_case(dtype_cases) +@pytest.mark.serialization def test_dask_to_json(array_model): array_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] array = da.array(array_list) diff --git a/tests/test_interface/test_dunder.py b/tests/test_interface/test_dunder.py deleted file mode 100644 index 60f42d8..0000000 --- a/tests/test_interface/test_dunder.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Tests for dunder methods on all interfaces -""" - - -def test_dunder_len(all_interfaces): - """ - Each interface or proxy type should support __len__ - """ - assert len(all_interfaces.array) == all_interfaces.array.shape[0] diff --git a/tests/test_interface/test_hdf5.py b/tests/test_interface/test_hdf5.py index 9ca9e94..bd94810 100644 --- a/tests/test_interface/test_hdf5.py +++ b/tests/test_interface/test_hdf5.py @@ -14,6 +14,8 @@ from numpydantic.exceptions import DtypeError, ShapeError from tests.conftest import ValidationCase +pytestmark = pytest.mark.hdf5 + def hdf5_array_case( case: ValidationCase, array_func, compound: bool = False @@ -72,11 +74,13 @@ def test_hdf5_check_not_hdf5(tmp_path): assert not H5Interface.check(spec) +@pytest.mark.shape @pytest.mark.parametrize("compound", [True, False]) def test_hdf5_shape(shape_cases, hdf5_array, compound): _test_hdf5_case(shape_cases, hdf5_array, compound) +@pytest.mark.dtype @pytest.mark.parametrize("compound", [True, False]) def test_hdf5_dtype(dtype_cases, hdf5_array, compound): _test_hdf5_case(dtype_cases, hdf5_array, compound) @@ -90,6 +94,7 @@ def test_hdf5_dataset_not_exists(hdf5_array, model_blank): assert "no array found" in e +@pytest.mark.proxy def test_assignment(hdf5_array, model_blank): array = hdf5_array() @@ -101,7 +106,9 @@ def test_assignment(hdf5_array, model_blank): assert (model.array[1:3, 2:4] == 10).all() -def test_to_json(hdf5_array, array_model): +@pytest.mark.serialization +@pytest.mark.parametrize("round_trip", (True, False)) +def test_to_json(hdf5_array, array_model, round_trip): """ Test serialization of HDF5 arrays to JSON Args: @@ -115,15 +122,19 @@ def test_to_json(hdf5_array, array_model): instance = model(array=array) # type: BaseModel - json_str = instance.model_dump_json() - json_dict = json.loads(json_str)["array"] - - assert json_dict["file"] == str(array.file) - assert json_dict["path"] == str(array.path) - assert json_dict["attrs"] == {} - assert json_dict["array"] == instance.array[:].tolist() + json_str = instance.model_dump_json( + round_trip=round_trip, context={"absolute_paths": True} + ) + json_dumped = json.loads(json_str)["array"] + if round_trip: + assert json_dumped["file"] == str(array.file) + assert json_dumped["path"] == str(array.path) + else: + assert json_dumped == instance.array[:].tolist() +@pytest.mark.dtype +@pytest.mark.proxy def test_compound_dtype(tmp_path): """ hdf5 proxy indexes compound dtypes as single fields when field is given @@ -158,6 +169,8 @@ def test_compound_dtype(tmp_path): assert all(instance.array[1] == 2) +@pytest.mark.dtype +@pytest.mark.proxy @pytest.mark.parametrize("compound", [True, False]) def test_strings(hdf5_array, compound): """ @@ -177,6 +190,8 @@ def test_strings(hdf5_array, compound): assert all(instance.array[1] == "sup") +@pytest.mark.dtype +@pytest.mark.proxy @pytest.mark.parametrize("compound", [True, False]) def test_datetime(hdf5_array, compound): """ @@ -218,3 +233,29 @@ def test_empty_dataset(dtype, tmp_path): array: NDArray[Any, dtype] _ = MyModel(array=(array_path, "/data")) + + +@pytest.mark.proxy +@pytest.mark.parametrize( + "comparison,valid", + [ + (H5Proxy(file="test_file.h5", path="/subpath", field="sup"), True), + (H5Proxy(file="test_file.h5", path="/subpath"), False), + (H5Proxy(file="different_file.h5", path="/subpath"), False), + (("different_file.h5", "/subpath", "sup"), ValueError), + ("not even a proxy-like thing", ValueError), + ], +) +def test_proxy_eq(comparison, valid): + """ + test the __eq__ method of H5ArrayProxy matches proxies to the same + dataset (and path), or raises a ValueError + """ + proxy_a = H5Proxy(file="test_file.h5", path="/subpath", field="sup") + if valid is True: + assert proxy_a == comparison + elif valid is False: + assert proxy_a != comparison + else: + with pytest.raises(valid): + assert proxy_a == comparison diff --git a/tests/test_interface/test_interface_base.py b/tests/test_interface/test_interface_base.py index baacc60..0b99ae6 100644 --- a/tests/test_interface/test_interface_base.py +++ b/tests/test_interface/test_interface_base.py @@ -4,11 +4,32 @@ for tests that should apply to all interfaces, use ``test_interfaces.py`` """ import gc +from typing import Literal import pytest import numpy as np -from numpydantic.interface import Interface +from numpydantic.interface import ( + Interface, + JsonDict, + InterfaceMark, + NumpyInterface, + MarkedJson, +) +from pydantic import ValidationError + +from numpydantic.interface.interface import V + + +class MyJsonDict(JsonDict): + type: Literal["my_json_dict"] + field: str + number: int + + def to_array_input(self) -> V: + dumped = self.model_dump() + dumped["extra_input_param"] = True + return dumped @pytest.fixture(scope="module") @@ -162,3 +183,66 @@ def test_interface_recursive(interfaces): assert issubclass(interfaces.interface3, interfaces.interface1) assert issubclass(interfaces.interface1, Interface) assert interfaces.interface4 in ifaces + + +@pytest.mark.serialization +def test_jsondict_is_valid(): + """ + A JsonDict should return a bool true/false if it is valid or not, + and raise an error when requested + """ + invalid = {"doesnt": "have", "the": "props"} + valid = {"type": "my_json_dict", "field": "a_field", "number": 1} + assert MyJsonDict.is_valid(valid) + assert not MyJsonDict.is_valid(invalid) + with pytest.raises(ValidationError): + assert not MyJsonDict.is_valid(invalid, raise_on_error=True) + + +@pytest.mark.serialization +def test_jsondict_handle_input(): + """ + JsonDict should be able to parse a valid dict and return it to the input format + """ + valid = {"type": "my_json_dict", "field": "a_field", "number": 1} + instantiated = MyJsonDict(**valid) + expected = { + "type": "my_json_dict", + "field": "a_field", + "number": 1, + "extra_input_param": True, + } + + for item in (valid, instantiated): + result = MyJsonDict.handle_input(item) + assert result == expected + + +@pytest.mark.serialization +@pytest.mark.parametrize("interface", Interface.interfaces()) +def test_interface_mark_match_by_name(interface): + """ + Interface mark should match an interface by its name + """ + # other parts don't matter + mark = InterfaceMark(module="fake", cls="fake", version="fake", name=interface.name) + fake_mark = InterfaceMark( + module="fake", cls="fake", version="fake", name="also_fake" + ) + assert mark.match_by_name() is interface + assert fake_mark.match_by_name() is None + + +@pytest.mark.serialization +def test_marked_json_try_cast(): + """ + MarkedJson.try_cast should try and cast to a markedjson! + returning the value unchanged if it's not a match + """ + valid = {"interface": NumpyInterface.mark_interface(), "value": [[1, 2], [3, 4]]} + invalid = [1, 2, 3, 4, 5] + mimic = {"interface": "not really", "value": "still not really"} + + assert isinstance(MarkedJson.try_cast(valid), MarkedJson) + assert MarkedJson.try_cast(invalid) is invalid + assert MarkedJson.try_cast(mimic) is mimic diff --git a/tests/test_interface/test_interfaces.py b/tests/test_interface/test_interfaces.py index 36308c9..faec0d8 100644 --- a/tests/test_interface/test_interfaces.py +++ b/tests/test_interface/test_interfaces.py @@ -2,6 +2,41 @@ Tests that should be applied to all interfaces """ +import pytest +from typing import Callable +from importlib.metadata import version +import json + +import numpy as np +import dask.array as da +from zarr.core import Array as ZarrArray +from pydantic import BaseModel + +from numpydantic.interface import Interface, InterfaceMark, MarkedJson + + +def _test_roundtrip(source: BaseModel, target: BaseModel, round_trip: bool): + """Test model equality for roundtrip tests""" + if round_trip: + assert type(target.array) is type(source.array) + if isinstance(source.array, (np.ndarray, ZarrArray)): + assert np.array_equal(target.array, np.array(source.array)) + elif isinstance(source.array, da.Array): + assert np.all(da.equal(target.array, source.array)) + else: + assert target.array == source.array + + assert target.array.dtype == source.array.dtype + else: + assert np.array_equal(target.array, np.array(source.array)) + + +def test_dunder_len(all_interfaces): + """ + Each interface or proxy type should support __len__ + """ + assert len(all_interfaces.array) == all_interfaces.array.shape[0] + def test_interface_revalidate(all_interfaces): """ @@ -10,3 +45,86 @@ def test_interface_revalidate(all_interfaces): See: https://github.com/p2p-ld/numpydantic/pull/14 """ _ = type(all_interfaces)(array=all_interfaces.array) + + +def test_interface_rematch(interface_type): + """ + All interfaces should match the results of the object they return after validation + """ + array, interface = interface_type + if isinstance(array, Callable): + array = array() + + assert Interface.match(interface().validate(array)) is interface + + +def test_interface_to_numpy_array(all_interfaces): + """ + All interfaces should be able to have the output of their validation stage + coerced to a numpy array with np.array() + """ + _ = np.array(all_interfaces.array) + + +@pytest.mark.serialization +def test_interface_dump_json(all_interfaces): + """ + All interfaces should be able to dump to json + """ + all_interfaces.model_dump_json() + + +@pytest.mark.serialization +@pytest.mark.parametrize("round_trip", [True, False]) +def test_interface_roundtrip_json(all_interfaces, round_trip): + """ + All interfaces should be able to roundtrip to and from json + """ + dumped_json = all_interfaces.model_dump_json(round_trip=round_trip) + model = all_interfaces.model_validate_json(dumped_json) + _test_roundtrip(all_interfaces, model, round_trip) + + +@pytest.mark.serialization +@pytest.mark.parametrize("an_interface", Interface.interfaces()) +def test_interface_mark_interface(an_interface): + """ + All interfaces should be able to mark the current version and interface info + """ + mark = an_interface.mark_interface() + assert isinstance(mark, InterfaceMark) + assert mark.name == an_interface.name + assert mark.cls == an_interface.__name__ + assert mark.module == an_interface.__module__ + assert mark.version == version(mark.module.split(".")[0]) + + +@pytest.mark.serialization +@pytest.mark.parametrize("valid", [True, False]) +@pytest.mark.parametrize("round_trip", [True, False]) +@pytest.mark.filterwarnings("ignore:Mismatch between serialized mark") +def test_interface_mark_roundtrip(all_interfaces, valid, round_trip): + """ + All interfaces should be able to roundtrip with the marked interface, + and a mismatch should raise a warning and attempt to proceed + """ + dumped_json = all_interfaces.model_dump_json( + round_trip=round_trip, context={"mark_interface": True} + ) + + data = json.loads(dumped_json) + + # ensure that we are a MarkedJson + _ = MarkedJson.model_validate_json(json.dumps(data["array"])) + + if not valid: + # ruin the version + data["array"]["interface"]["version"] = "v99999999" + dumped_json = json.dumps(data) + + with pytest.warns(match="Mismatch.*"): + model = all_interfaces.model_validate_json(dumped_json) + else: + model = all_interfaces.model_validate_json(dumped_json) + + _test_roundtrip(all_interfaces, model, round_trip) diff --git a/tests/test_interface/test_numpy.py b/tests/test_interface/test_numpy.py index 6a34b98..bfb4c4d 100644 --- a/tests/test_interface/test_numpy.py +++ b/tests/test_interface/test_numpy.py @@ -5,6 +5,8 @@ from numpydantic.exceptions import DtypeError, ShapeError from tests.conftest import ValidationCase +pytestmark = pytest.mark.numpy + def numpy_array(case: ValidationCase) -> np.ndarray: if issubclass(case.dtype, BaseModel): @@ -22,10 +24,12 @@ def _test_np_case(case: ValidationCase): case.model(array=array) +@pytest.mark.shape def test_numpy_shape(shape_cases): _test_np_case(shape_cases) +@pytest.mark.dtype def test_numpy_dtype(dtype_cases): _test_np_case(dtype_cases) diff --git a/tests/test_interface/test_video.py b/tests/test_interface/test_video.py index d5ef1b3..5f03a57 100644 --- a/tests/test_interface/test_video.py +++ b/tests/test_interface/test_video.py @@ -14,6 +14,8 @@ from numpydantic import NDArray, Shape from numpydantic import dtype as dt from numpydantic.interface.video import VideoProxy +pytestmark = pytest.mark.video + @pytest.mark.parametrize("input_type", [str, Path]) def test_video_validation(avi_video, input_type): @@ -49,6 +51,7 @@ def test_video_from_videocapture(avi_video): opened_vid.release() +@pytest.mark.shape def test_video_wrong_shape(avi_video): shape = (100, 50) @@ -65,6 +68,7 @@ def test_video_wrong_shape(avi_video): instance = MyModel(array=vid) +@pytest.mark.proxy def test_video_getitem(avi_video): """ Should be able to get individual frames and slices as if it were a normal array @@ -127,6 +131,7 @@ def test_video_getitem(avi_video): instance.array[5] = 10 +@pytest.mark.proxy def test_video_attrs(avi_video): """Should be able to access opencv properties""" shape = (100, 50) @@ -142,6 +147,7 @@ def test_video_attrs(avi_video): assert int(instance.array.get(cv2.CAP_PROP_POS_FRAMES)) == 5 +@pytest.mark.proxy def test_video_close(avi_video): """Should close and reopen video file if needed""" shape = (100, 50) @@ -158,3 +164,42 @@ def test_video_close(avi_video): assert instance.array._video is None # reopen assert isinstance(instance.array.video, cv2.VideoCapture) + + +@pytest.mark.proxy +def test_video_not_exists(tmp_path): + """ + A video file that doesn't exist should raise an error + """ + video = VideoProxy(tmp_path / "not_real.avi") + with pytest.raises(FileNotFoundError): + _ = video.video + + +@pytest.mark.proxy +@pytest.mark.parametrize( + "comparison,valid", + [ + (VideoProxy("test_video.avi"), True), + (VideoProxy("not_real_video.avi"), False), + ("not even a video proxy", TypeError), + ], +) +def test_video_proxy_eq(comparison, valid): + """ + Comparing a video proxy's equality should be valid if the path matches + Args: + comparison: + valid: + + Returns: + + """ + proxy_a = VideoProxy("test_video.avi") + if valid is True: + assert proxy_a == comparison + elif valid is False: + assert proxy_a != comparison + else: + with pytest.raises(valid): + assert proxy_a == comparison diff --git a/tests/test_interface/test_zarr.py b/tests/test_interface/test_zarr.py index 2e465f2..ed5c252 100644 --- a/tests/test_interface/test_zarr.py +++ b/tests/test_interface/test_zarr.py @@ -6,13 +6,14 @@ import zarr from pydantic import BaseModel, ValidationError from numcodecs import Pickle - from numpydantic.interface import ZarrInterface from numpydantic.interface.zarr import ZarrArrayPath from numpydantic.exceptions import DtypeError, ShapeError from tests.conftest import ValidationCase +pytestmark = pytest.mark.zarr + @pytest.fixture() def dir_array(tmp_output_dir_func) -> zarr.DirectoryStore: @@ -87,10 +88,12 @@ def test_zarr_check(interface_type): assert not ZarrInterface.check(interface_type[0]) +@pytest.mark.shape def test_zarr_shape(store, shape_cases): _test_zarr_case(shape_cases, store) +@pytest.mark.dtype def test_zarr_dtype(dtype_cases, store): _test_zarr_case(dtype_cases, store) @@ -123,7 +126,10 @@ def test_zarr_array_path_from_iterable(zarr_array): assert apath.path == inner_path -def test_zarr_to_json(store, model_blank): +@pytest.mark.serialization +@pytest.mark.parametrize("dump_array", [True, False]) +@pytest.mark.parametrize("roundtrip", [True, False]) +def test_zarr_to_json(store, model_blank, roundtrip, dump_array): expected_fields = ( "Type", "Data type", @@ -137,17 +143,22 @@ def test_zarr_to_json(store, model_blank): array = zarr.array(lol_array, store=store) instance = model_blank(array=array) - as_json = json.loads(instance.model_dump_json())["array"] - assert "array" not in as_json - for field in expected_fields: - assert field in as_json - assert len(as_json["hexdigest"]) == 40 - # dump the array itself too - as_json = json.loads(instance.model_dump_json(context={"zarr_dump_array": True}))[ - "array" - ] - for field in expected_fields: - assert field in as_json - assert len(as_json["hexdigest"]) == 40 - assert as_json["array"] == lol_array + context = {"dump_array": dump_array} + as_json = json.loads( + instance.model_dump_json(round_trip=roundtrip, context=context) + )["array"] + + if roundtrip: + if dump_array: + assert as_json["array"] == lol_array + else: + if as_json.get("file", False): + assert "array" not in as_json + + for field in expected_fields: + assert field in as_json["info"] + assert len(as_json["info"]["hexdigest"]) == 40 + + else: + assert as_json == lol_array diff --git a/tests/test_ndarray.py b/tests/test_ndarray.py index f92a66d..cda092c 100644 --- a/tests/test_ndarray.py +++ b/tests/test_ndarray.py @@ -1,5 +1,3 @@ -import pdb - import pytest from typing import Union, Optional, Any @@ -15,6 +13,7 @@ from numpydantic import dtype from numpydantic.dtype import Number +@pytest.mark.json_schema def test_ndarray_type(): class Model(BaseModel): array: NDArray[Shape["2 x, * y"], Number] @@ -40,6 +39,8 @@ def test_ndarray_type(): instance = Model(array=np.zeros((2, 3)), array_any=np.ones((3, 4, 5))) +@pytest.mark.dtype +@pytest.mark.json_schema def test_schema_unsupported_type(): """ Complex numbers should just be made with an `any` schema @@ -55,9 +56,11 @@ def test_schema_unsupported_type(): } +@pytest.mark.dtype +@pytest.mark.json_schema def test_schema_tuple(): """ - Types specified as tupled should have their schemas as a union + Types specified as tuples should have their schemas as a union """ class Model(BaseModel): @@ -72,6 +75,8 @@ def test_schema_tuple(): assert all([i["minimum"] == 0 for i in conditions]) +@pytest.mark.dtype +@pytest.mark.json_schema def test_schema_number(): """ np.numeric should just be the float schema @@ -115,12 +120,12 @@ def test_ndarray_union(): instance = Model(array=np.random.random((5, 10, 4, 6))) +@pytest.mark.shape +@pytest.mark.dtype @pytest.mark.parametrize("dtype", dtype.Number) def test_ndarray_unparameterized(dtype): """ NDArray without any parameters is any shape, any type - Returns: - """ class Model(BaseModel): @@ -134,6 +139,7 @@ def test_ndarray_unparameterized(dtype): _ = Model(array=np.zeros(dim_sizes, dtype=dtype)) +@pytest.mark.shape def test_ndarray_any(): """ using :class:`typing.Any` in for the shape means any shape @@ -164,6 +170,19 @@ def test_ndarray_coercion(): amod = Model(array=["a", "b", "c"]) +@pytest.mark.shape +def test_shape_ellipsis(): + """ + Test that ellipsis is a wildcard, rather than "repeat the last index" + """ + + class MyModel(BaseModel): + array: NDArray[Shape["1, 2, ..."], Number] + + _ = MyModel(array=np.zeros((1, 2, 3, 4, 5))) + + +@pytest.mark.serialization def test_ndarray_serialize(): """ Arrays should be dumped to a list when using json, but kept as ndarray otherwise @@ -188,6 +207,7 @@ _json_schema_types = [ ] +@pytest.mark.json_schema def test_json_schema_basic(array_model): """ NDArray types should correctly generate a list of lists JSON schema @@ -210,6 +230,8 @@ def test_json_schema_basic(array_model): assert inner["items"]["type"] == "number" +@pytest.mark.dtype +@pytest.mark.json_schema @pytest.mark.parametrize("dtype", [*dtype.Integer, *dtype.Float]) def test_json_schema_dtype_single(dtype, array_model): """ @@ -240,6 +262,8 @@ def test_json_schema_dtype_single(dtype, array_model): ) +@pytest.mark.dtype +@pytest.mark.json_schema @pytest.mark.parametrize( "dtype,expected", [ @@ -266,6 +290,8 @@ def test_json_schema_dtype_builtin(dtype, expected, array_model): assert inner_type["type"] == expected +@pytest.mark.dtype +@pytest.mark.json_schema def test_json_schema_dtype_model(): """ Pydantic models can be used in arrays as dtypes @@ -314,6 +340,8 @@ def _recursive_array(schema): assert any_of[1]["minimum"] == 0 +@pytest.mark.shape +@pytest.mark.json_schema def test_json_schema_ellipsis(): """ NDArray types should create a recursive JSON schema for any-shaped arrays diff --git a/tests/test_serialization.py b/tests/test_serialization.py new file mode 100644 index 0000000..702dc1a --- /dev/null +++ b/tests/test_serialization.py @@ -0,0 +1,95 @@ +""" +Test serialization-specific functionality that doesn't need to be +applied across every interface (use test_interface/test_interfaces for that +""" + +import h5py +import pytest +from pathlib import Path +from typing import Callable +import numpy as np +import json + +pytestmark = pytest.mark.serialization + + +@pytest.fixture(scope="module") +def hdf5_at_path() -> Callable[[Path], None]: + _path = "" + + def _hdf5_at_path(path: Path) -> None: + nonlocal _path + _path = path + h5f = h5py.File(path, "w") + _ = h5f.create_dataset("/data", data=np.array([[1, 2], [3, 4]])) + _ = h5f.create_dataset("subpath/to/dataset", data=np.array([[1, 2], [4, 5]])) + h5f.close() + + yield _hdf5_at_path + + Path(_path).unlink(missing_ok=True) + + +def test_relative_path(hdf5_at_path, tmp_output_dir, model_blank): + """ + By default, we should make all paths relative to the cwd + """ + out_path = tmp_output_dir / "relative.h5" + hdf5_at_path(out_path) + model = model_blank(array=(out_path, "/data")) + rt = model.model_dump_json(round_trip=True) + file = json.loads(rt)["array"]["file"] + + # should not be absolute + assert not Path(file).is_absolute() + # should be relative to cwd + out_file = (Path.cwd() / file).resolve() + assert out_file == out_path.resolve() + + +def test_relative_to_path(hdf5_at_path, tmp_output_dir, model_blank): + """ + When explicitly passed a path to be ``relative_to`` , + relative to that instead of cwd + """ + out_path = tmp_output_dir / "relative.h5" + relative_to_path = Path(__file__) / "fake_dir" / "sub_fake_dir" + expected_path = "../../../__tmp__/relative.h5" + + hdf5_at_path(out_path) + model = model_blank(array=(out_path, "/data")) + rt = model.model_dump_json( + round_trip=True, context={"relative_to": str(relative_to_path)} + ) + data = json.loads(rt)["array"] + file = data["file"] + + # should not be absolute + assert not Path(file).is_absolute() + # should be expected path and reach the file + assert file == expected_path + assert (relative_to_path / file).resolve() == out_path.resolve() + + # we shouldn't have touched `/data` even though it is pathlike + assert data["path"] == "/data" + + +def test_relative_to_path(hdf5_at_path, tmp_output_dir, model_blank): + """ + When told, we make paths absolute + """ + out_path = tmp_output_dir / "relative.h5" + expected_dataset = "subpath/to/dataset" + + hdf5_at_path(out_path) + model = model_blank(array=(out_path, expected_dataset)) + rt = model.model_dump_json(round_trip=True, context={"absolute_paths": True}) + data = json.loads(rt)["array"] + file = data["file"] + + # should be absolute and equal to out_path + assert Path(file).is_absolute() + assert Path(file) == out_path.resolve() + + # shouldn't have absolutized subpath even if it's pathlike + assert data["path"] == expected_dataset diff --git a/tests/test_shape.py b/tests/test_shape.py index 3abff19..b521054 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -1,5 +1,3 @@ -import pdb - import pytest from typing import Any @@ -9,6 +7,8 @@ import numpy as np from numpydantic import NDArray, Shape +pytestmark = pytest.mark.shape + @pytest.mark.parametrize( "shape,valid",