diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..aeb668b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + branches: + - main + + +permissions: + contents: read + +jobs: + ruff: + name: Ruff Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: chartboost/ruff-action@v1 + + black: + name: Black Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31f586e..63979cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,14 +2,34 @@ name: Tests on: push: + branches: + - main + pull_request: + branches: + - main jobs: test: strategy: matrix: - python-version: ["3.9", "3.11", "3.12"] + platform: ["ubuntu-latest", "macos-latest", "windows-latest"] + numpy-version: ["<2.0.0", ">=2.0.0"] + python-version: ["3.9", "3.10", "3.11", "3.12"] + exclude: + - numpy-version: "<2.0.0" + python-version: "3.10" + - numpy-version: "<2.0.0" + python-version: "3.11" + - platform: "macos-latest" + python-version: "3.10" + - platform: "macos-latest" + python-version: "3.11" + - platform: "windows-latest" + python-version: "3.10" + - platform: "windows-latest" + python-version: "3.11" - runs-on: ubuntu-latest + runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 @@ -21,21 +41,28 @@ jobs: cache: 'pip' - name: Install dependencies - run: pip install -e ".[tests,linkml]" + run: pip install -e ".[tests]" + + - name: Install numpy version + run: pip install "numpy${{ matrix.numpy-version }}" - name: Run Tests run: pytest - - name: Report coverage - if: ${{ matrix.python-version }} == "3.11" - run: "coveralls --service=github" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Coveralls Parallel + uses: coverallsapp/github-action@v2.3.0 + if: runner.os != 'macOS' + with: + flag-name: run-${{ join(matrix.*, '-') }} + parallel: true + debug: true - lint: + finish-coverage: + needs: test + if: ${{ always() }} runs-on: ubuntu-latest - continue-on-error: true steps: - - uses: actions/checkout@v3 - - uses: chartboost/ruff-action@v1 - - uses: psf/black@stable \ No newline at end of file + - name: Coveralls Finished + uses: coverallsapp/github-action@v2.3.0 + with: + parallel-finished: true \ No newline at end of file diff --git a/README.md b/README.md index e69c8b2..b7ceeff 100644 --- a/README.md +++ b/README.md @@ -416,4 +416,11 @@ dumped = instance.model_dump_json(context={'zarr_dump_array': True}) "hexdigest": "c51604eace325fe42bbebf39146c0956bd2ed13c" } } -``` \ No newline at end of file +``` + +## Vendored Dependencies + +We have vendored dependencies in the `src/numpydantic/vendor` package, +and reproduced their licenses in the `licenses` directory. + +- [nptyping](https://github.com/ramonhagenaars/nptyping) - `numpydantic.vendor.nptyping` - `/licenses/nptyping.txt` \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 389fc07..2df9c76 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,33 @@ ## 1.* +### 1.2.3 - 24-07-31 - Vendor `nptyping` + +`nptyping` vendored into `numpydantic.vendor.nptyping` - +`nptyping` is no longer maintained, and pins `numpy<2`. +It also has many obnoxious warnings and we have to monkeypatch it +so it performs halfway decently. Since we are en-route to deprecating +usage of `nptyping` anyway, in the meantime we have just vendored it in +(it is MIT licensed, included) so that we can make those changes ourselves +and have to patch less of it. Currently the whole package is vendored with +modifications, but will be whittled away until we have replaced it with +updated type specification system :) + +Bugfix: +- [#2](https://github.com/p2p-ld/numpydantic/issues/2) - Support `numpy>=2` +- Remove deprecated numpy dtypes + +CI: +- Add windows and mac tests +- Add testing with numpy>=2 and <2 + +DevOps: +- Make a tox file for local testing, not used in CI. + +Tidying: +- Remove `monkeypatch` module! we don't need it anymore! + everything has either been upstreamed or vendored. + ### 1.2.2 - 24-07-31 Add `datetime` map to numpy's :class:`numpy.datetime64` type diff --git a/licenses/nptyping.txt b/licenses/nptyping.txt new file mode 100644 index 0000000..5a89c4e --- /dev/null +++ b/licenses/nptyping.txt @@ -0,0 +1,7 @@ +Copyright 2022, Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pdm.lock b/pdm.lock index ed47804..41818a0 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "arrays", "dask", "dev", "docs", "hdf5", "tests", "video"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.2" -content_hash = "sha256:221d8b83742574a9be8f41986d0def3392a7b248d1bb4fa5d3676e5fd2ebbefb" +content_hash = "sha256:62c153ae2eb738b173ac882538810a727eee2ac364ea674c0d8a1ef310bbefd6" [[package]] name = "alabaster" @@ -133,13 +133,13 @@ files = [ [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." groups = ["dev", "docs", "tests"] files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -352,7 +352,7 @@ files = [ [[package]] name = "dask" -version = "2024.6.2" +version = "2024.7.1" requires_python = ">=3.9" summary = "Parallel PyData with Task Scheduling" groups = ["arrays", "dask", "dev", "tests"] @@ -362,13 +362,13 @@ dependencies = [ "fsspec>=2021.09.0", "importlib-metadata>=4.13.0; python_version < \"3.12\"", "packaging>=20.0", - "partd>=1.2.0", + "partd>=1.4.0", "pyyaml>=5.3.1", "toolz>=0.10.0", ] files = [ - {file = "dask-2024.6.2-py3-none-any.whl", hash = "sha256:81b80ee015b2e057b93bb2d1bf13a866136e762e2b24bf54b6b621e8b86b7708"}, - {file = "dask-2024.6.2.tar.gz", hash = "sha256:d429d6b19e85fd1306ac37c188aaf99d03bbe69a6fe59d2b42882b2ac188686f"}, + {file = "dask-2024.7.1-py3-none-any.whl", hash = "sha256:dd046840050376c317de90629db5c6197adda820176cf3e2df10c3219d11951f"}, + {file = "dask-2024.7.1.tar.gz", hash = "sha256:dbaef2d50efee841a9d981a218cfeb50392fc9a95e0403b6d680450e4f50d531"}, ] [[package]] @@ -393,14 +393,14 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" groups = ["dev", "tests"] marker = "python_version < \"3.11\"" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [[package]] @@ -428,7 +428,7 @@ files = [ [[package]] name = "furo" -version = "2024.5.6" +version = "2024.7.18" requires_python = ">=3.8" summary = "A clean customisable Sphinx documentation theme." groups = ["dev", "docs"] @@ -439,8 +439,8 @@ dependencies = [ "sphinx<8.0,>=6.0", ] files = [ - {file = "furo-2024.5.6-py3-none-any.whl", hash = "sha256:490a00d08c0a37ecc90de03ae9227e8eb5d6f7f750edf9807f398a2bdf2358de"}, - {file = "furo-2024.5.6.tar.gz", hash = "sha256:81f205a6605ebccbb883350432b4831c0196dd3d1bc92f61e1f459045b3d2b0b"}, + {file = "furo-2024.7.18-py3-none-any.whl", hash = "sha256:b192c7c1f59805494c8ed606d9375fdac6e6ba8178e747e72bc116745fb7e13f"}, + {file = "furo-2024.7.18.tar.gz", hash = "sha256:37b08c5fccc95d46d8712c8be97acd46043963895edde05b0f4f135d58325c83"}, ] [[package]] @@ -518,7 +518,7 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.0.0" +version = "8.2.0" requires_python = ">=3.8" summary = "Read metadata from Python packages" groups = ["arrays", "dask", "dev", "docs", "tests"] @@ -527,8 +527,8 @@ dependencies = [ "zipp>=0.5", ] files = [ - {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, - {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, + {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, + {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, ] [[package]] @@ -697,21 +697,6 @@ files = [ {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, ] -[[package]] -name = "nptyping" -version = "2.5.0" -requires_python = ">=3.7" -summary = "Type hints for NumPy." -groups = ["arrays", "default", "dev", "tests"] -dependencies = [ - "numpy<2.0.0,>=1.20.0; python_version >= \"3.8\"", - "typing-extensions<5.0.0,>=4.0.0; python_version < \"3.10\"", -] -files = [ - {file = "nptyping-2.5.0-py3-none-any.whl", hash = "sha256:764e51836faae33a7ae2e928af574cfb701355647accadcc89f2ad793630b7c8"}, - {file = "nptyping-2.5.0.tar.gz", hash = "sha256:e3d35b53af967e6fb407c3016ff9abae954d3a0568f7cc13a461084224e8e20a"}, -] - [[package]] name = "numcodecs" version = "0.12.1" @@ -743,47 +728,56 @@ files = [ [[package]] name = "numpy" -version = "1.26.4" +version = "2.0.1" requires_python = ">=3.9" summary = "Fundamental package for array computing in Python" groups = ["arrays", "default", "dev", "hdf5", "tests", "video"] files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, + {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"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, + {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, + {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, + {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, + {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, + {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, + {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, + {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, + {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, + {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, ] [[package]] @@ -874,23 +868,24 @@ files = [ [[package]] name = "pydantic" -version = "2.7.4" +version = "2.8.2" requires_python = ">=3.8" summary = "Data validation using Python type hints" groups = ["arrays", "default", "dev", "docs", "tests"] dependencies = [ "annotated-types>=0.4.0", - "pydantic-core==2.18.4", - "typing-extensions>=4.6.1", + "pydantic-core==2.20.1", + "typing-extensions>=4.12.2; python_version >= \"3.13\"", + "typing-extensions>=4.6.1; python_version < \"3.13\"", ] files = [ - {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, - {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [[package]] name = "pydantic-core" -version = "2.18.4" +version = "2.20.1" requires_python = ">=3.8" summary = "Core functionality for Pydantic validation and serialization" groups = ["arrays", "default", "dev", "docs", "tests"] @@ -898,78 +893,88 @@ dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"}, - {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"}, - {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"}, - {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"}, - {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"}, - {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"}, - {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"}, - {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"}, - {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"}, - {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"}, - {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"}, - {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"}, - {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"}, - {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"}, - {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"}, - {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"}, - {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"}, - {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"}, - {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"}, - {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"}, - {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"}, - {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"}, - {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"}, - {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"}, - {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"}, - {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"}, - {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, ] [[package]] name = "pydantic-settings" -version = "2.3.4" +version = "2.4.0" requires_python = ">=3.8" summary = "Settings management using Pydantic" groups = ["dev", "docs"] @@ -978,8 +983,8 @@ dependencies = [ "python-dotenv>=0.21.0", ] files = [ - {file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"}, - {file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"}, + {file = "pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315"}, + {file = "pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88"}, ] [[package]] @@ -995,7 +1000,7 @@ files = [ [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" groups = ["dev", "tests"] @@ -1004,12 +1009,12 @@ dependencies = [ "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", "iniconfig", "packaging", - "pluggy<2.0,>=1.5", + "pluggy<2,>=1.5", "tomli>=1; python_version < \"3.11\"", ] files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [[package]] @@ -1114,29 +1119,29 @@ files = [ [[package]] name = "ruff" -version = "0.5.0" +version = "0.5.5" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["dev"] files = [ - {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, - {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, - {file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, - {file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, - {file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, - {file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, - {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, + {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"}, + {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"}, + {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"}, + {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"}, + {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"}, + {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"}, + {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, ] [[package]] @@ -1173,22 +1178,22 @@ files = [ [[package]] name = "sphinx" -version = "7.3.7" +version = "7.4.7" requires_python = ">=3.9" summary = "Python documentation generator" groups = ["dev", "docs"] dependencies = [ - "Jinja2>=3.0", - "Pygments>=2.14", + "Jinja2>=3.1", + "Pygments>=2.17", "alabaster~=0.7.14", - "babel>=2.9", - "colorama>=0.4.5; sys_platform == \"win32\"", - "docutils<0.22,>=0.18.1", + "babel>=2.13", + "colorama>=0.4.6; sys_platform == \"win32\"", + "docutils<0.22,>=0.20", "imagesize>=1.3", - "importlib-metadata>=4.8; python_version < \"3.10\"", - "packaging>=21.0", - "requests>=2.25.0", - "snowballstemmer>=2.0", + "importlib-metadata>=6.0; python_version < \"3.10\"", + "packaging>=23.0", + "requests>=2.30.0", + "snowballstemmer>=2.2", "sphinxcontrib-applehelp", "sphinxcontrib-devhelp", "sphinxcontrib-htmlhelp>=2.0.0", @@ -1198,8 +1203,8 @@ dependencies = [ "tomli>=2; python_version < \"3.11\"", ] files = [ - {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, - {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, + {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, + {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, ] [[package]] @@ -1251,35 +1256,35 @@ files = [ [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.8" +version = "2.0.0" requires_python = ">=3.9" summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" groups = ["dev", "docs"] files = [ - {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, - {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.6" +version = "2.0.0" requires_python = ">=3.9" summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" groups = ["dev", "docs"] files = [ - {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, - {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.5" +version = "2.1.0" requires_python = ">=3.9" summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" groups = ["dev", "docs"] files = [ - {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, - {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [[package]] @@ -1306,29 +1311,29 @@ files = [ [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.7" +version = "2.0.0" requires_python = ">=3.9" summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" groups = ["dev", "docs"] files = [ - {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, - {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.10" +version = "2.0.0" requires_python = ">=3.9" summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" groups = ["dev", "docs"] files = [ - {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, - {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [[package]] name = "starlette" -version = "0.37.2" +version = "0.38.2" requires_python = ">=3.8" summary = "The little ASGI library that shines." groups = ["dev"] @@ -1337,8 +1342,8 @@ dependencies = [ "typing-extensions>=3.10.0; python_version < \"3.10\"", ] files = [ - {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, - {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, + {file = "starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff"}, + {file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"}, ] [[package]] @@ -1388,7 +1393,7 @@ files = [ [[package]] name = "uvicorn" -version = "0.30.1" +version = "0.30.4" requires_python = ">=3.8" summary = "The lightning-fast ASGI server." groups = ["dev"] @@ -1398,8 +1403,8 @@ dependencies = [ "typing-extensions>=4.0; python_version < \"3.11\"", ] files = [ - {file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"}, - {file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"}, + {file = "uvicorn-0.30.4-py3-none-any.whl", hash = "sha256:06b00e3087e58c6865c284143c0c42f810b32ff4f265ab19d08c566f74a08728"}, + {file = "uvicorn-0.30.4.tar.gz", hash = "sha256:00db9a9e3711a5fa59866e2b02fac69d8dc70ce0814aaec9a66d1d9e5c832a30"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 68ef104..3da4cd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [project] name = "numpydantic" -version = "1.2.2" +version = "1.2.3" description = "Type and shape validation and serialization for numpy arrays in pydantic models" authors = [ {name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"}, ] dependencies = [ "pydantic>=2.3.0", - "nptyping>=2.5.0", "numpy>=1.24.0", + "typing-extensions>=4.11.0;python_version<'3.11'" ] homepage = "https://numpydantic.readthedocs.io" requires-python = "<4.0,>=3.9" @@ -40,7 +40,6 @@ tests = [ "coverage>=6.1.1", "pytest-cov<5.0.0,>=4.1.0", "coveralls<4.0.0,>=3.3.1", - "typing-extensions>=4.11.0;python_version<'3.11'", ] docs = [ "sphinx<8.0.0,>=7.2.6", @@ -101,6 +100,10 @@ select = [ "I", # annotations "ANN", + # perf + "PERF", + # numpy + "NPY", ## ---------- # pydocstyle # undocumented public objects @@ -131,4 +134,9 @@ fixable = ["ALL"] [tool.mypy] plugins = [ "pydantic.mypy" +] + +[tool.coverage.run] +omit = [ + "src/numpydantic/vendor/*" ] \ No newline at end of file diff --git a/src/numpydantic/__init__.py b/src/numpydantic/__init__.py index d251f8d..803d8d2 100644 --- a/src/numpydantic/__init__.py +++ b/src/numpydantic/__init__.py @@ -1,10 +1,6 @@ -# ruff: noqa: E402 # ruff: noqa: F401 # ruff: noqa: I001 # ruff: noqa: D104 -from numpydantic.monkeypatch import apply_patches - -apply_patches() from numpydantic.ndarray import NDArray from numpydantic.meta import update_ndarray_stub diff --git a/src/numpydantic/dtype.py b/src/numpydantic/dtype.py index 55bcca6..12d766a 100644 --- a/src/numpydantic/dtype.py +++ b/src/numpydantic/dtype.py @@ -69,9 +69,7 @@ Half = np.half Single = np.single Double = np.double LongDouble = np.longdouble -LongFloat = np.longfloat Float = ( - np.float_, np.float16, np.float32, np.float64, @@ -80,36 +78,26 @@ Float = ( ) Floating = Float -ComplexFloating = np.complexfloating Complex64 = np.complex64 Complex128 = np.complex128 CSingle = np.csingle -SingleComplex = np.singlecomplex CDouble = np.cdouble -CFloat = np.cfloat CLongDouble = np.clongdouble -CLongFloat = np.clongfloat Complex = ( - np.complex_, - np.complexfloating, np.complex64, np.complex128, np.csingle, - np.singlecomplex, np.cdouble, - np.cfloat, np.clongdouble, - np.clongfloat, ) -LongComplex = np.longcomplex Flexible = np.flexible Void = np.void Character = np.character Bytes = np.bytes_ Str = np.str_ -String = np.string_ -Unicode = np.unicode_ +String = np.str_ +Unicode = np.str_ Number = tuple( [ diff --git a/src/numpydantic/interface/video.py b/src/numpydantic/interface/video.py index 0e1a50d..4f2048d 100644 --- a/src/numpydantic/interface/video.py +++ b/src/numpydantic/interface/video.py @@ -144,9 +144,9 @@ class VideoProxy: elif isinstance(item, slice): # slice of frames item = self._complete_slice(item) - frames = [] - for i in range(item.start, item.stop, item.step): - frames.append(self._get_frame(i)) + frames = [ + self._get_frame(i) for i in range(item.start, item.stop, item.step) + ] return np.stack(frames) else: # slices are passed as tuples diff --git a/src/numpydantic/maps.py b/src/numpydantic/maps.py index 38e4925..d80d33e 100644 --- a/src/numpydantic/maps.py +++ b/src/numpydantic/maps.py @@ -21,7 +21,7 @@ np_to_python = { **{n: int for n in dt.Integer}, **{n: float for n in dt.Float}, **{n: complex for n in dt.Complex}, - **{n: str for n in (np.character, np.str_, np.string_, np.unicode_)}, + **{n: str for n in (np.character, np.str_, np.bytes_, np.str_)}, } """Map from python types to numpy""" diff --git a/src/numpydantic/monkeypatch.py b/src/numpydantic/monkeypatch.py deleted file mode 100644 index 05607e5..0000000 --- a/src/numpydantic/monkeypatch.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Functions to monkeypatch dependent packages - most notably nptyping -""" - -# ruff: noqa: ANN001 - - -def patch_npytyping_perf() -> None: - """ - npytyping makes an expensive call to inspect.stack() - that makes imports of pydantic models take ~200x longer than - they should: - - References: - - https://github.com/ramonhagenaars/nptyping/issues/110 - """ - import inspect - from types import FrameType - - from nptyping import base_meta_classes, ndarray, recarray - from nptyping.pandas_ import dataframe - - # make a new __module__ methods for the affected classes - - def new_module_ndarray(cls) -> str: # pragma: no cover - return cls._get_module(inspect.currentframe(), "nptyping.ndarray") - - def new_module_recarray(cls) -> str: # pragma: no cover - return cls._get_module(inspect.currentframe(), "nptyping.recarray") - - def new_module_dataframe(cls) -> str: # pragma: no cover - return cls._get_module(inspect.currentframe(), "nptyping.pandas_.dataframe") - - # and a new _get_module method for the parent class - def new_get_module(cls, stack: FrameType, module: str) -> str: # pragma: no cover - return ( - "typing" - if inspect.getframeinfo(stack.f_back).function == "formatannotation" - else module - ) - - # now apply the patches - ndarray.NDArrayMeta.__module__ = property(new_module_ndarray) - recarray.RecArrayMeta.__module__ = property(new_module_recarray) - dataframe.DataFrameMeta.__module__ = property(new_module_dataframe) - base_meta_classes.SubscriptableMeta._get_module = new_get_module - - -def patch_nptyping_warnings() -> None: - """ - nptyping shits out a bunch of numpy deprecation warnings from using - olde aliases - - References: - - https://github.com/ramonhagenaars/nptyping/issues/113 - - https://github.com/ramonhagenaars/nptyping/issues/102 - """ - import warnings - - warnings.filterwarnings("ignore", category=DeprecationWarning, module="nptyping.*") - - -def apply_patches() -> None: - """Apply all monkeypatches!""" - patch_npytyping_perf() - patch_nptyping_warnings() diff --git a/src/numpydantic/ndarray.py b/src/numpydantic/ndarray.py index 5ad42ba..42fc3f8 100644 --- a/src/numpydantic/ndarray.py +++ b/src/numpydantic/ndarray.py @@ -16,14 +16,6 @@ Extension of nptyping NDArray for pydantic that allows for JSON-Schema serializa from typing import TYPE_CHECKING, Any, Tuple import numpy as np -from nptyping.error import InvalidArgumentsError -from nptyping.ndarray import NDArrayMeta as _NDArrayMeta -from nptyping.nptyping_type import NPTypingType -from nptyping.structure import Structure -from nptyping.structure_expression import check_type_names -from nptyping.typing_ import ( - dtype_per_name, -) from pydantic import GetJsonSchemaHandler from pydantic_core import core_schema @@ -38,6 +30,14 @@ from numpydantic.schema import ( make_json_schema, ) from numpydantic.types import DtypeType, ShapeType +from numpydantic.vendor.nptyping.error import InvalidArgumentsError +from numpydantic.vendor.nptyping.ndarray import NDArrayMeta as _NDArrayMeta +from numpydantic.vendor.nptyping.nptyping_type import NPTypingType +from numpydantic.vendor.nptyping.structure import Structure +from numpydantic.vendor.nptyping.structure_expression import check_type_names +from numpydantic.vendor.nptyping.typing_ import ( + dtype_per_name, +) if TYPE_CHECKING: # pragma: no cover from nptyping.base_meta_classes import SubscriptableMeta diff --git a/src/numpydantic/schema.py b/src/numpydantic/schema.py index 084ac7c..9636190 100644 --- a/src/numpydantic/schema.py +++ b/src/numpydantic/schema.py @@ -7,7 +7,6 @@ import hashlib import json from typing import TYPE_CHECKING, Any, Callable, Optional, Union -import nptyping.structure import numpy as np from pydantic import SerializationInfo from pydantic_core import CoreSchema, core_schema @@ -17,8 +16,9 @@ from numpydantic import dtype as dt from numpydantic.interface import Interface from numpydantic.maps import np_to_python from numpydantic.types import DtypeType, NDArrayType, ShapeType +from numpydantic.vendor.nptyping.structure import StructureMeta -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from numpydantic import Shape _handler_type = Callable[[Any], core_schema.CoreSchema] @@ -45,12 +45,12 @@ def _numeric_dtype(dtype: DtypeType, _handler: _handler_type) -> CoreSchema: def _lol_dtype(dtype: DtypeType, _handler: _handler_type) -> CoreSchema: """Get the innermost dtype schema to use in the generated pydantic schema""" - if isinstance(dtype, nptyping.structure.StructureMeta): # pragma: no cover + if isinstance(dtype, StructureMeta): # pragma: no cover raise NotImplementedError("Structured dtypes are currently unsupported") if isinstance(dtype, tuple): # if it's a meta-type that refers to a generic float/int, just make that - if dtype == dt.Float: + if dtype in (dt.Float, dt.Number): array_type = core_schema.float_schema() elif dtype == dt.Integer: array_type = core_schema.int_schema() @@ -143,7 +143,7 @@ def list_of_lists_schema(shape: "Shape", array_type: CoreSchema) -> ListSchema: arg = int(arg) arg_min = arg arg_max = arg - except ValueError as e: + except ValueError as e: # pragma: no cover raise ValueError( "Array shapes must be integers, wildcards, ellipses, or " diff --git a/src/numpydantic/shape.py b/src/numpydantic/shape.py index 366e572..62a567f 100644 --- a/src/numpydantic/shape.py +++ b/src/numpydantic/shape.py @@ -29,15 +29,15 @@ from abc import ABC from functools import lru_cache from typing import Any, Dict, List, Union -from nptyping.base_meta_classes import ContainerMeta -from nptyping.error import InvalidShapeError, NPTypingError -from nptyping.nptyping_type import NPTypingType -from nptyping.shape_expression import ( +from numpydantic.vendor.nptyping.base_meta_classes import ContainerMeta +from numpydantic.vendor.nptyping.error import InvalidShapeError, NPTypingError +from numpydantic.vendor.nptyping.nptyping_type import NPTypingType +from numpydantic.vendor.nptyping.shape_expression import ( get_dimensions, normalize_shape_expression, remove_labels, ) -from nptyping.typing_ import ShapeExpression, ShapeTuple +from numpydantic.vendor.nptyping.typing_ import ShapeExpression, ShapeTuple class ShapeMeta(ContainerMeta, implementation="Shape"): diff --git a/src/numpydantic/types.py b/src/numpydantic/types.py index c629fbf..ef01cee 100644 --- a/src/numpydantic/types.py +++ b/src/numpydantic/types.py @@ -8,7 +8,7 @@ Note that these are types as in python typing types, not classes. from typing import Any, Protocol, Tuple, Union, runtime_checkable -from nptyping import DType +from numpydantic.vendor.nptyping import DType ShapeType = Union[Tuple[int, ...], Any] DtypeType = Union[str, type, Any, DType] diff --git a/src/numpydantic/vendor/__init__.py b/src/numpydantic/vendor/__init__.py new file mode 100644 index 0000000..9af2c76 --- /dev/null +++ b/src/numpydantic/vendor/__init__.py @@ -0,0 +1,10 @@ +""" +Vendored modules - see licenses in /licenses. + +Currently consists just of nptyping, as it is no longer maintained +and pins a version of numpy<2, and we have to do an increasing +number of awkward monkeypatches for perf and customization reasons. + +This vendored module will slowly be worked out of the code and +all its functionality replaced. +""" diff --git a/src/numpydantic/vendor/nptyping/__init__.py b/src/numpydantic/vendor/nptyping/__init__.py new file mode 100644 index 0000000..4ca8fdb --- /dev/null +++ b/src/numpydantic/vendor/nptyping/__init__.py @@ -0,0 +1,182 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from numpydantic.vendor.nptyping.assert_isinstance import assert_isinstance +from numpydantic.vendor.nptyping.error import ( + InvalidArgumentsError, + InvalidDTypeError, + InvalidShapeError, + InvalidStructureError, + NPTypingError, +) +from numpydantic.vendor.nptyping.ndarray import NDArray +from numpydantic.vendor.nptyping.package_info import __version__ +from numpydantic.vendor.nptyping.pandas_.dataframe import DataFrame +from numpydantic.vendor.nptyping.recarray import RecArray +from numpydantic.vendor.nptyping.shape import Shape +from numpydantic.vendor.nptyping.shape_expression import ( + normalize_shape_expression, + validate_shape_expression, +) +from numpydantic.vendor.nptyping.structure import Structure +from numpydantic.vendor.nptyping.typing_ import ( + Bool, + Byte, + Bytes, + CDouble, + CFloat, + Character, + CLongDouble, + CLongFloat, + Complex, + Complex64, + Complex128, + ComplexFloating, + CSingle, + Datetime64, + Double, + DType, + Flexible, + Float, + Float16, + Float32, + Float64, + Floating, + Half, + Inexact, + Int, + Int8, + Int16, + Int32, + Int64, + IntC, + Integer, + IntP, + LongComplex, + LongDouble, + LongFloat, + LongLong, + Number, + Object, + Short, + SignedInteger, + Single, + SingleComplex, + String, + Timedelta64, + UByte, + UInt, + UInt8, + UInt16, + UInt32, + UInt64, + UIntC, + UIntP, + ULongLong, + Unicode, + UnsignedInteger, + UShort, + Void, +) + +__all__ = [ + "NDArray", + "RecArray", + "assert_isinstance", + "validate_shape_expression", + "normalize_shape_expression", + "NPTypingError", + "InvalidArgumentsError", + "InvalidShapeError", + "InvalidStructureError", + "InvalidDTypeError", + "Shape", + "Structure", + "__version__", + "DType", + "Number", + "Bool", + "Bool8", + "Object", + "Object0", + "Datetime64", + "Integer", + "SignedInteger", + "Int8", + "Int16", + "Int32", + "Int64", + "Byte", + "Short", + "IntC", + "IntP", + "Int0", + "Int", + "LongLong", + "Timedelta64", + "UnsignedInteger", + "UInt8", + "UInt16", + "UInt32", + "UInt64", + "UByte", + "UShort", + "UIntC", + "UIntP", + "UInt0", + "UInt", + "ULongLong", + "Inexact", + "Floating", + "Float16", + "Float32", + "Float64", + "Half", + "Single", + "Double", + "Float", + "LongDouble", + "LongFloat", + "ComplexFloating", + "Complex64", + "Complex128", + "CSingle", + "SingleComplex", + "CDouble", + "Complex", + "CFloat", + "CLongDouble", + "CLongFloat", + "LongComplex", + "Flexible", + "Void", + "Void0", + "Character", + "Bytes", + "String", + "Bytes0", + "Unicode", + "Str0", + "DataFrame", +] diff --git a/src/numpydantic/vendor/nptyping/assert_isinstance.py b/src/numpydantic/vendor/nptyping/assert_isinstance.py new file mode 100644 index 0000000..e6d4187 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/assert_isinstance.py @@ -0,0 +1,53 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from typing import ( + Any, + Optional, + Type, + TypeVar, +) + +try: + from typing import TypeGuard # type: ignore[attr-defined] +except ImportError: # pragma: no cover + from typing_extensions import TypeGuard # type: ignore[attr-defined] + +TYPE = TypeVar("TYPE") + + +def assert_isinstance( + instance: Any, cls: Type[TYPE], message: Optional[str] = None +) -> TypeGuard[TYPE]: + """ + A TypeGuard function that is equivalent to `assert instance, cls, message` + that hides nasty MyPy or IDE warnings. + :param instance: the instance that is checked against cls. + :param cls: the class + :param message: any message that is displayed when the assert check fails. + :return: the type of cls. + """ + message = message or f"instance={instance!r}, cls={cls!r}" + assert isinstance(instance, cls), message + return True diff --git a/src/numpydantic/vendor/nptyping/base_meta_classes.py b/src/numpydantic/vendor/nptyping/base_meta_classes.py new file mode 100644 index 0000000..337e629 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/base_meta_classes.py @@ -0,0 +1,249 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from abc import ABCMeta, abstractmethod +from inspect import getframeinfo +from types import FrameType +from typing import ( + Any, + Dict, + Optional, + Set, + Tuple, + TypeVar, +) + +from numpydantic.vendor.nptyping.error import InvalidArgumentsError, NPTypingError + +_T = TypeVar("_T") + + +class InconstructableMeta(ABCMeta): + """ + Makes it impossible for a class to get instantiated. + """ + + def __call__(cls, *_: Any, **__: Any) -> None: + """Raise an error if constructed""" + raise NPTypingError( + f"Cannot instantiate nptyping.{cls.__name__}. Did you mean to use [ ] ?" + ) + + +class ImmutableMeta(ABCMeta): + """ + Makes it impossible to changes values on a class. + """ + + def __setattr__(cls, key: str, value: Any) -> None: + if key not in ("_abc_impl", "__abstractmethods__"): + raise NPTypingError(f"Cannot set values to nptyping.{cls.__name__}.") + + +class FinalMeta(ABCMeta): + """ + Makes it impossible for classes to inherit from some class. + + An concrete inheriting meta class requires to define a name for its + implementation. The class with this name will be the only class that is + allowed to use that concrete meta class. + """ + + _name_per_meta_cls: Dict[type, Optional[str]] = {} + + def __init_subclass__(cls, implementation: Optional[str] = None) -> None: + # implementation is made Optional here, to allow other meta classes to + # inherit. + cls._name_per_meta_cls[cls] = implementation + + def __new__(cls, name: str, *args: Any, **kwargs: Any) -> type: + """Prevent subclasses, return from internal dict instead""" + if name == cls._name_per_meta_cls[cls]: + assert name, "cls_name not set" + return type.__new__(cls, name, *args, **kwargs) + + raise NPTypingError(f"Cannot subclass nptyping.{cls._name_per_meta_cls[cls]}.") + + +class MaybeCheckableMeta(ABCMeta): + """ + Makes instance and subclass checks raise by default. + """ + + def __instancecheck__(cls, instance: Any) -> bool: + raise NPTypingError( + f"Instance checking is not supported for nptyping.{cls.__name__}." + ) + + def __subclasscheck__(cls, subclass: Any) -> bool: + raise NPTypingError( + f"Subclass checking is not supported for nptyping.{cls.__name__}." + ) + + +class PrintableMeta(ABCMeta): + """ + Ensures that a class can be printed nicely. + """ + + @abstractmethod + def __str__(cls) -> str: ... # pragma: no cover + + def __repr__(cls) -> str: + return str(cls) + + +class SubscriptableMeta(ABCMeta): + """ + Makes a class subscriptable: it accepts arguments between brackets and a + new type is returned for every unique set of arguments. + """ + + _all_types: Dict[Tuple[type, Tuple[Any, ...]], type] = {} + _parameterized: bool = False + + @abstractmethod + def _get_item(cls, item: Any) -> Tuple[Any, ...]: ... # pragma: no cover + + def _get_module(cls, stack: FrameType, module: str) -> str: + # The magic below makes Python's help function display a meaningful + # text with nptyping types. + return ( + "typing" + if getframeinfo(stack.f_back).function == "formatannotation" + else module + ) + + def _get_additional_values( + cls, item: Any # pylint: disable=unused-argument + ) -> Dict[str, Any]: + # This method is invoked after _get_item and right before returning + # the result of __getitem__. It can be overridden to provide extra + # values that are to be set as attributes on the new type. + return {} + + def __getitem__(cls, item: Any) -> type: + if getattr(cls, "_parameterized", False): + raise NPTypingError(f"Type nptyping.{cls} is already parameterized.") + + args = cls._get_item(item) + additional_values = cls._get_additional_values(item) + assert hasattr(cls, "__args__"), "A SubscriptableMeta must have __args__." + if args != cls.__args__: # type: ignore[attr-defined] + result = cls._create_type(args, additional_values) + else: + result = cls + + return result + + def _create_type( + cls, args: Tuple[Any, ...], additional_values: Dict[str, Any] + ) -> type: + key = (cls, args) + if key not in cls._all_types: + cls._all_types[key] = type( + cls.__name__, + (cls,), + {"__args__": args, "_parameterized": True, **additional_values}, + ) + return cls._all_types[key] + + +class ComparableByArgsMeta(ABCMeta): + """ + Makes a class comparable by means of its __args__. + """ + + __args__: Tuple[Any, ...] + + def __eq__(cls, other: Any) -> bool: + return ( + hasattr(cls, "__args__") + and hasattr(other, "__args__") + and cls.__args__ == other.__args__ + ) + + def __hash__(cls) -> int: + return hash(cls.__args__) + + +class ContainerMeta( + InconstructableMeta, + ImmutableMeta, + FinalMeta, + MaybeCheckableMeta, + PrintableMeta, + SubscriptableMeta, + ComparableByArgsMeta, + ABCMeta, +): + """ + Base meta class for "containers" such as Shape and Structure. + """ + + _known_expressions: Set[str] = set() + __args__: Tuple[str, ...] + + @abstractmethod + def _validate_expression(cls, item: str) -> None: ... # pragma: no cover + + @abstractmethod + def _normalize_expression(cls, item: str) -> str: ... # pragma: no cover + + def _get_item(cls, item: Any) -> Tuple[Any, ...]: + if not isinstance(item, str): + raise InvalidArgumentsError( + f"Unexpected argument of type {type(item)}, expecting a string." + ) + + if item in cls._known_expressions: + # No need to do costly validations and normalizations if it has been done + # before. + return (item,) + + cls._validate_expression(item) + norm_shape_expression = cls._normalize_expression(item) + cls._known_expressions.add(norm_shape_expression) + return (norm_shape_expression,) + + def __subclasscheck__(cls, subclass: Any) -> bool: + type_match = type(subclass) is type(cls) + return type_match and ( + subclass.__args__ == cls.__args__ or not cls._parameterized + ) + + def __str__(cls) -> str: + return f"{cls.__name__}['{cls.__args__[0]}']" + + def __eq__(cls, other: Any) -> bool: + result = cls is other + if not result and hasattr(cls, "__args__") and hasattr(other, "__args__"): + normalized_args = tuple( + cls._normalize_expression(str(arg)) for arg in other.__args__ + ) + result = cls.__args__ == normalized_args + return result + + def __hash__(cls) -> int: + return hash(cls.__args__) diff --git a/src/numpydantic/vendor/nptyping/error.py b/src/numpydantic/vendor/nptyping/error.py new file mode 100644 index 0000000..237864e --- /dev/null +++ b/src/numpydantic/vendor/nptyping/error.py @@ -0,0 +1,47 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +class NPTypingError(Exception): + """Base error for all NPTyping errors.""" + + +class InvalidArgumentsError(NPTypingError): + """Raised when a invalid arguments are provided to an nptyping type.""" + + +class InvalidShapeError(NPTypingError): + """Raised when a shape is considered not valid.""" + + +class InvalidStructureError(NPTypingError): + """Raised when a structure is considered not valid.""" + + +class InvalidDTypeError(NPTypingError): + """Raised when an argument is not a DType.""" + + +class DependencyError(NPTypingError): + """Raised when a dependency has not been installed.""" diff --git a/src/numpydantic/vendor/nptyping/ndarray.py b/src/numpydantic/vendor/nptyping/ndarray.py new file mode 100644 index 0000000..90a4793 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/ndarray.py @@ -0,0 +1,200 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import inspect +from abc import ABC +from typing import Any, Tuple + +import numpy as np + +from numpydantic.vendor.nptyping.base_meta_classes import ( + FinalMeta, + ImmutableMeta, + InconstructableMeta, + MaybeCheckableMeta, + PrintableMeta, + SubscriptableMeta, +) +from numpydantic.vendor.nptyping.error import InvalidArgumentsError +from numpydantic.vendor.nptyping.nptyping_type import NPTypingType +from numpydantic.vendor.nptyping.shape import Shape +from numpydantic.vendor.nptyping.shape_expression import check_shape +from numpydantic.vendor.nptyping.structure import Structure +from numpydantic.vendor.nptyping.structure_expression import ( + check_structure, + check_type_names, +) +from numpydantic.vendor.nptyping.typing_ import ( + DType, + dtype_per_name, + name_per_dtype, +) + + +class NDArrayMeta( + SubscriptableMeta, + InconstructableMeta, + ImmutableMeta, + FinalMeta, + MaybeCheckableMeta, + PrintableMeta, + implementation="NDArray", +): + """ + Metaclass that is coupled to nptyping.NDArray. It contains all actual logic + such as instance checking. + """ + + __args__: Tuple[Shape, DType] + _parameterized: bool + + @property + def __module__(cls) -> str: + return cls._get_module(inspect.currentframe(), "nptyping.ndarray") + + def _get_item(cls, item: Any) -> Tuple[Any, ...]: + cls._check_item(item) + shape, dtype = cls._get_from_tuple(item) + return shape, dtype + + def __instancecheck__( # pylint: disable=bad-mcs-method-argument + self, instance: Any + ) -> bool: + shape, dtype = self.__args__ + dtype_is_structure = issubclass(dtype, Structure) + structure_is_ok = dtype_is_structure and check_structure( + instance.dtype, dtype, dtype_per_name + ) + return ( + isinstance(instance, np.ndarray) + and (shape is Any or check_shape(instance.shape, shape)) + and ( + dtype is Any + or structure_is_ok + or issubclass(instance.dtype.type, dtype) + ) + ) + + def __str__(cls) -> str: + shape, dtype = cls.__args__ + return ( + f"{cls.__name__}[{cls._shape_expression_to_str(shape)}, " + f"{cls._dtype_to_str(dtype)}]" + ) + + def _is_literal_like(cls, item: Any) -> bool: + # item is a Literal or "Literal enough" (ducktyping). + return hasattr(item, "__args__") + + def _check_item(cls, item: Any) -> None: + # Check if the item is what we expect and raise if it is not. + if not isinstance(item, tuple): + raise InvalidArgumentsError(f"Unexpected argument of type {type(item)}.") + if len(item) > 2: + raise InvalidArgumentsError(f"Unexpected argument {item[2]}.") + + def _get_from_tuple(cls, item: Tuple[Any, ...]) -> Tuple[Shape, DType]: + # Return the Shape Expression and DType from a tuple. + shape = cls._get_shape(item[0]) + dtype = cls._get_dtype(item[1]) + return shape, dtype + + def _get_shape(cls, dtype_candidate: Any) -> Shape: + if dtype_candidate is Any or dtype_candidate is Shape: + shape = Any + elif issubclass(dtype_candidate, Shape): + shape = dtype_candidate + elif cls._is_literal_like(dtype_candidate): + shape_expression = dtype_candidate.__args__[0] + shape = Shape[shape_expression] + else: + raise InvalidArgumentsError( + f"Unexpected argument '{dtype_candidate}', expecting" + " Shape[]" + " or Literal[]" + " or typing.Any." + ) + return shape + + def _get_dtype(cls, dtype_candidate: Any) -> DType: + is_dtype = isinstance(dtype_candidate, type) and issubclass( + dtype_candidate, np.generic + ) + if dtype_candidate is Any: + dtype = Any + elif is_dtype: + dtype = dtype_candidate + elif issubclass(dtype_candidate, Structure): + dtype = dtype_candidate + check_type_names(dtype, dtype_per_name) + elif cls._is_literal_like(dtype_candidate): + structure_expression = dtype_candidate.__args__[0] + dtype = Structure[structure_expression] + check_type_names(dtype, dtype_per_name) + else: + raise InvalidArgumentsError( + f"Unexpected argument '{dtype_candidate}', expecting" + " Structure[]" + " or Literal[]" + " or a dtype" + " or typing.Any." + ) + return dtype + + def _dtype_to_str(cls, dtype: Any) -> str: + if dtype is Any: + result = "Any" + elif issubclass(dtype, Structure): + result = str(dtype) + else: + result = name_per_dtype[dtype] + return result + + def _shape_expression_to_str(cls, shape_expression: Any) -> str: + return "Any" if shape_expression is Any else str(shape_expression) + + +class NDArray(NPTypingType, ABC, metaclass=NDArrayMeta): + """ + An nptyping equivalent of numpy ndarray. + + ## No arguments means an NDArray with any DType and any shape. + >>> NDArray + NDArray[Any, Any] + + ## You can provide a DType and a Shape Expression. + >>> from nptyping import Int32, Shape + >>> NDArray[Shape["2, 2"], Int32] + NDArray[Shape['2, 2'], Int32] + + ## Instance checking can be done and the shape is also checked. + >>> import numpy as np + >>> isinstance(np.array([[1, 2], [3, 4]]), NDArray[Shape['2, 2'], Int32]) + True + >>> isinstance(np.array([[1, 2], [3, 4], [5, 6]]), NDArray[Shape['2, 2'], Int32]) + False + + """ + + __args__ = (Any, Any) diff --git a/src/numpydantic/vendor/nptyping/nptyping_type.py b/src/numpydantic/vendor/nptyping/nptyping_type.py new file mode 100644 index 0000000..f8d90f0 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/nptyping_type.py @@ -0,0 +1,31 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from abc import ABC + + +class NPTypingType(ABC): # noqa: B024 + """ + Baseclass for all nptyping types. + """ diff --git a/src/numpydantic/vendor/nptyping/package_info.py b/src/numpydantic/vendor/nptyping/package_info.py new file mode 100644 index 0000000..d72b631 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/package_info.py @@ -0,0 +1,38 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +__title__ = "nptyping" +__version__ = "2.5.0" +__author__ = "Ramon Hagenaars" +__author_email__ = "ramon.hagenaars@gmail.com" +__description__ = "Type hints for NumPy." +__url__ = "https://github.com/ramonhagenaars/nptyping" +__license__ = "MIT" +__python_versions__ = [ + "3.7", + "3.8", + "3.9", + "3.10", + "3.11", +] diff --git a/src/numpydantic/vendor/nptyping/pandas_/__init__.py b/src/numpydantic/vendor/nptyping/pandas_/__init__.py new file mode 100644 index 0000000..c74bb5a --- /dev/null +++ b/src/numpydantic/vendor/nptyping/pandas_/__init__.py @@ -0,0 +1,3 @@ +""" +Pandas dataframe types +""" diff --git a/src/numpydantic/vendor/nptyping/pandas_/dataframe.py b/src/numpydantic/vendor/nptyping/pandas_/dataframe.py new file mode 100644 index 0000000..f734acb --- /dev/null +++ b/src/numpydantic/vendor/nptyping/pandas_/dataframe.py @@ -0,0 +1,140 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import inspect +from abc import ABC +from typing import Any, Tuple + +import numpy as np + +from numpydantic.vendor.nptyping import InvalidArgumentsError +from numpydantic.vendor.nptyping.base_meta_classes import ( + FinalMeta, + ImmutableMeta, + InconstructableMeta, + MaybeCheckableMeta, + PrintableMeta, + SubscriptableMeta, +) +from numpydantic.vendor.nptyping.error import DependencyError +from numpydantic.vendor.nptyping.nptyping_type import NPTypingType +from numpydantic.vendor.nptyping.pandas_.typing_ import dtype_per_name +from numpydantic.vendor.nptyping.structure import Structure +from numpydantic.vendor.nptyping.structure_expression import check_structure + +try: + import pandas as pd +except ImportError: # pragma: no cover + pd = None # type: ignore[misc, assignment] + + +class DataFrameMeta( + SubscriptableMeta, + InconstructableMeta, + ImmutableMeta, + FinalMeta, + MaybeCheckableMeta, + PrintableMeta, + implementation="DataFrame", +): + """ + Metaclass that is coupled to nptyping.DataFrame. It contains all actual logic + such as instance checking. + """ + + __args__: Tuple[Structure] + _parameterized: bool + + def __instancecheck__( # pylint: disable=bad-mcs-method-argument + self, instance: Any + ) -> bool: + structure = self.__args__[0] + + if pd is None: + raise DependencyError( # pragma: no cover + "Pandas needs to be installed for instance checking. Use `pip " + "install nptyping[pandas]` or `pip install nptyping[complete]`" + ) + + if not isinstance(instance, pd.DataFrame): + return False + + if structure is Any: + return True + + structured_dtype = np.dtype( + [(column, dtype.str) for column, dtype in instance.dtypes.items()] + ) + return check_structure(structured_dtype, structure, dtype_per_name) + + def _get_item(cls, item: Any) -> Tuple[Structure]: + if item is Any: + return (Any,) + cls._check_item(item) + return (Structure[item.__args__[0]],) + + def __str__(cls) -> str: + structure = cls.__args__[0] + structure_str = "Any" if structure is Any else structure.__args__[0] + return f"{cls.__name__}[{structure_str}]" + + def __repr__(cls) -> str: + structure = cls.__args__[0] + structure_str = "Any" if structure is Any else structure + return f"{cls.__name__}[{structure_str}]" + + @property + def __module__(cls) -> str: + return cls._get_module(inspect.currentframe(), "nptyping.ndarray") + + def _check_item(cls, item: Any) -> None: + # Check if the item is what we expect and raise if it is not. + if not hasattr(item, "__args__"): + raise InvalidArgumentsError(f"Unexpected argument of type {type(item)}.") + + +class DataFrame(NPTypingType, ABC, metaclass=DataFrameMeta): + """ + An nptyping equivalent of pandas DataFrame. + + ## No arguments means a DataFrame of any structure. + >>> DataFrame + DataFrame[Any] + + ## You can use Structure Expression. + >>> from nptyping import DataFrame, Structure + >>> DataFrame[Structure["x: Int, y: Int"]] + DataFrame[Structure['[x, y]: Int']] + + ## Instance checking can be done and the structure is also checked. + >>> import pandas as pd + >>> df = pd.DataFrame({'x': [1, 2, 3], 'y': [4., 5., 6.]}) + >>> isinstance(df, DataFrame[Structure['x: Int, y: Float']]) + True + >>> isinstance(df, DataFrame[Structure['x: Float, y: Int']]) + False + + """ + + __args__ = (Any,) diff --git a/src/numpydantic/vendor/nptyping/pandas_/dataframe.pyi b/src/numpydantic/vendor/nptyping/pandas_/dataframe.pyi new file mode 100644 index 0000000..edab03f --- /dev/null +++ b/src/numpydantic/vendor/nptyping/pandas_/dataframe.pyi @@ -0,0 +1,27 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import pandas as pd + +DataFrame = pd.DataFrame diff --git a/src/numpydantic/vendor/nptyping/pandas_/typing_.py b/src/numpydantic/vendor/nptyping/pandas_/typing_.py new file mode 100644 index 0000000..461fa1f --- /dev/null +++ b/src/numpydantic/vendor/nptyping/pandas_/typing_.py @@ -0,0 +1,34 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from numpydantic.vendor.nptyping.typing_ import Object +from numpydantic.vendor.nptyping.typing_ import dtype_per_name as dtype_per_name_default + +dtype_per_name = { + **dtype_per_name_default, # type: ignore[arg-type] + # Override the `String` and `Str` to point to `Object`. Pandas uses Object + # for string types in Dataframes and Series. + "String": Object, + "Str": Object, +} diff --git a/src/numpydantic/vendor/nptyping/py.typed b/src/numpydantic/vendor/nptyping/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/numpydantic/vendor/nptyping/recarray.py b/src/numpydantic/vendor/nptyping/recarray.py new file mode 100644 index 0000000..931a135 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/recarray.py @@ -0,0 +1,79 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import inspect +from typing import Any, Tuple + +import numpy as np + +from numpydantic.vendor.nptyping.error import InvalidArgumentsError +from numpydantic.vendor.nptyping.ndarray import NDArray, NDArrayMeta +from numpydantic.vendor.nptyping.structure import Structure +from numpydantic.vendor.nptyping.typing_ import DType + + +class RecArrayMeta(NDArrayMeta, implementation="RecArray"): + """ + Metaclass that is coupled to nptyping.RecArray. It takes most of its logic + from NDArrayMeta. + """ + + def _get_item(cls, item: Any) -> Tuple[Any, ...]: + cls._check_item(item) + shape, dtype = cls._get_from_tuple(item) + return shape, dtype + + def _get_dtype(cls, dtype_candidate: Any) -> DType: + if not issubclass(dtype_candidate, Structure) and dtype_candidate is not Any: + raise InvalidArgumentsError( + f"Unexpected argument {dtype_candidate}. Expecting a Structure." + ) + return dtype_candidate + + @property + def __module__(cls) -> str: + return cls._get_module(inspect.currentframe(), "nptyping.ndarray") + + def __instancecheck__( # pylint: disable=bad-mcs-method-argument + self, instance: Any + ) -> bool: + return isinstance(instance, np.recarray) and NDArrayMeta.__instancecheck__( + self, instance + ) + + +class RecArray(NDArray, metaclass=RecArrayMeta): + """ + An nptyping equivalent of numpy recarray. + + ## RecArrays can take a Shape and must take a Structure + >>> from nptyping import Shape, Structure + >>> RecArray[Shape["2, 2"], Structure["x: Float, y: Float"]] + RecArray[Shape['2, 2'], Structure['[x, y]: Float']] + + ## Or Any + >>> from typing import Any + >>> RecArray[Shape["2, 2"], Any] + RecArray[Shape['2, 2'], Any] + """ diff --git a/src/numpydantic/vendor/nptyping/recarray.pyi b/src/numpydantic/vendor/nptyping/recarray.pyi new file mode 100644 index 0000000..4e67f8a --- /dev/null +++ b/src/numpydantic/vendor/nptyping/recarray.pyi @@ -0,0 +1,27 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import numpy as np + +RecArray = np.recarray diff --git a/src/numpydantic/vendor/nptyping/shape.py b/src/numpydantic/vendor/nptyping/shape.py new file mode 100644 index 0000000..b4d4698 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/shape.py @@ -0,0 +1,76 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from abc import ABC +from typing import Any, Dict + +from numpydantic.vendor.nptyping.base_meta_classes import ContainerMeta +from numpydantic.vendor.nptyping.nptyping_type import NPTypingType +from numpydantic.vendor.nptyping.shape_expression import ( + get_dimensions, + normalize_shape_expression, + remove_labels, + validate_shape_expression, +) + + +class ShapeMeta(ContainerMeta, implementation="Shape"): + """ + Metaclass that is coupled to nptyping.Shape. + """ + + def _validate_expression(cls, item: str) -> None: + validate_shape_expression(item) + + def _normalize_expression(cls, item: str) -> str: + return normalize_shape_expression(item) + + def _get_additional_values(cls, item: Any) -> Dict[str, Any]: + dim_strings = get_dimensions(item) + dim_string_without_labels = remove_labels(dim_strings) + return {"prepared_args": dim_string_without_labels} + + +class Shape(NPTypingType, ABC, metaclass=ShapeMeta): + """ + A container for shape expressions that describe the shape of an multi + dimensional array. + + Simple example: + + >>> Shape['2, 2'] + Shape['2, 2'] + + A Shape can be compared to a typing.Literal. You can use Literals in + NDArray as well. + + >>> from typing import Literal + + >>> Shape['2, 2'] == Literal['2, 2'] + True + + """ + + __args__ = ("*, ...",) + prepared_args = "*, ..." diff --git a/src/numpydantic/vendor/nptyping/shape.pyi b/src/numpydantic/vendor/nptyping/shape.pyi new file mode 100644 index 0000000..eeb64ed --- /dev/null +++ b/src/numpydantic/vendor/nptyping/shape.pyi @@ -0,0 +1,37 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +try: + from typing import Literal # type: ignore[attr-defined] +except ImportError: + from typing_extensions import Literal # type: ignore[attr-defined,misc,assignment] + +from typing import Any, cast + +# For MyPy: +Shape = cast(Literal, Shape) # type: ignore[has-type,misc,valid-type] + +# For PyRight: +class Shape: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> Any: ... diff --git a/src/numpydantic/vendor/nptyping/shape_expression.py b/src/numpydantic/vendor/nptyping/shape_expression.py new file mode 100644 index 0000000..6234031 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/shape_expression.py @@ -0,0 +1,191 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import re +import string +from functools import lru_cache +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Union, +) + +from numpydantic.vendor.nptyping.error import InvalidShapeError +from numpydantic.vendor.nptyping.typing_ import ShapeExpression, ShapeTuple + +if TYPE_CHECKING: + from nptyping.shape import Shape # pragma: no cover + + +@lru_cache +def check_shape(shape: ShapeTuple, target: "Shape") -> bool: + """ + Check whether the given shape corresponds to the given shape_expression. + :param shape: the shape in question. + :param target: the shape expression to which shape is tested. + :return: True if the given shape corresponds to shape_expression. + """ + target_shape = _handle_ellipsis(shape, target.prepared_args) + return _check_dimensions_against_shape(shape, target_shape) + + +def validate_shape_expression(shape_expression: Union[ShapeExpression, Any]) -> None: + """ + Validate shape_expression and raise an InvalidShapeError if it is not + considered valid. + :param shape_expression: the shape expression to validate. + :return: None. + """ + shape_expression_no_quotes = shape_expression.replace("'", "").replace('"', "") + if shape_expression is not Any and not re.match( + _REGEX_SHAPE_EXPRESSION, shape_expression_no_quotes + ): + raise InvalidShapeError( + f"'{shape_expression}' is not a valid shape expression." + ) + + +def normalize_shape_expression(shape_expression: ShapeExpression) -> ShapeExpression: + """ + Normalize the given shape expression, e.g. by removing whitespaces, making + similar expressions look the same. + :param shape_expression: the shape expression that is to be normalized. + :return: a normalized shape expression. + """ + shape_expression = shape_expression.replace("'", "").replace('"', "") + # Replace whitespaces right before labels with $. + shape_expression = re.sub(rf"\s*{_REGEX_LABEL}", r"$\1", shape_expression) + # Let all commas be followed by a $. + shape_expression = shape_expression.replace(",", ",$") + # Remove all whitespaces left. + shape_expression = re.sub(r"\s*", "", shape_expression) + # Remove $ right after a bracket. + shape_expression = re.sub(r"\[\$+", "[", shape_expression) + # Replace $ with a single space. + shape_expression = re.sub(r"\$+", " ", shape_expression) + return shape_expression + + +def get_dimensions(shape_expression: str) -> List[str]: + """ + Find all "break downs" (the parts between brackets) in a shape expressions + and replace them with mere dimension sizes. + + :param shape_expression: the shape expression that gets the break downs replaced. + :return: a list of dimensions without break downs. + """ + shape_expression_without_breakdowns = shape_expression + for dim_breakdown in re.findall( + r"(\[[^\]]+\])", shape_expression_without_breakdowns + ): + dim_size = len(dim_breakdown.split(",")) + shape_expression_without_breakdowns = ( + shape_expression_without_breakdowns.replace(dim_breakdown, str(dim_size)) + ) + return shape_expression_without_breakdowns.split(",") + + +def remove_labels(dimensions: List[str]) -> List[str]: + """ + Remove all labels (words that start with a lowercase). + + :param dimensions: a list of dimensions. + :return: a copy of the given list without labels. + """ + return [re.sub(r"\b[a-z]\w*", "", dim).strip() for dim in dimensions] + + +def _check_dimensions_against_shape(shape: ShapeTuple, target: List[str]) -> bool: + # Walk through the shape and test them against the given target, + # taking into consideration variables, wildcards, etc. + + if len(shape) != len(target): + return False + shape_as_strings = (str(dim) for dim in shape) + variables: Dict[str, str] = {} + for dim, target_dim in zip(shape_as_strings, target): + if _is_wildcard(target_dim) or _is_assignable_var(dim, target_dim, variables): + continue + if dim != target_dim: + return False + return True + + +def _handle_ellipsis(shape: ShapeTuple, target: List[str]) -> List[str]: + # Let the ellipsis allows for any number of dimensions by replacing the + # ellipsis with the dimension size repeated the number of times that + # corresponds to the shape of the instance. + if target[-1] == "...": + dim_to_repeat = target[-2] + target = target[0:-1] + if len(shape) > len(target): + difference = len(shape) - len(target) + target += difference * [dim_to_repeat] + return target + + +def _is_assignable_var(dim: str, target_dim: str, variables: Dict[str, str]) -> bool: + # Return whether target_dim is a variable and can be assigned with dim. + return _is_variable(target_dim) and _can_assign_variable(dim, target_dim, variables) + + +def _is_variable(dim: str) -> bool: + # Return whether dim is a variable. + return dim[0] in string.ascii_uppercase + + +def _can_assign_variable(dim: str, target_dim: str, variables: Dict[str, str]) -> bool: + # Check and assign a variable. + assignable = variables.get(target_dim) in (None, dim) + variables[target_dim] = dim + return assignable + + +def _is_wildcard(dim: str) -> bool: + # Return whether dim is a wildcard (i.e. the character that takes any + # dimension size). + return dim == "*" + + +_REGEX_SEPARATOR = r"(\s*,\s*)" +_REGEX_DIMENSION_SIZE = r"(\s*[0-9]+\s*)" +_REGEX_VARIABLE = r"(\s*\b[A-Z]\w*\s*)" +_REGEX_LABEL = r"(\s*\b[a-z]\w*\s*)" +_REGEX_LABELS = rf"({_REGEX_LABEL}({_REGEX_SEPARATOR}{_REGEX_LABEL})*)" +_REGEX_WILDCARD = r"(\s*\*\s*)" +_REGEX_DIMENSION_BREAKDOWN = rf"(\s*\[{_REGEX_LABELS}\]\s*)" +_REGEX_DIMENSION = ( + rf"({_REGEX_DIMENSION_SIZE}" + rf"|{_REGEX_VARIABLE}" + rf"|{_REGEX_WILDCARD}" + rf"|{_REGEX_DIMENSION_BREAKDOWN})" +) +_REGEX_DIMENSION_WITH_LABEL = rf"({_REGEX_DIMENSION}(\s+{_REGEX_LABEL})*)" +_REGEX_DIMENSIONS = ( + rf"{_REGEX_DIMENSION_WITH_LABEL}({_REGEX_SEPARATOR}{_REGEX_DIMENSION_WITH_LABEL})*" +) +_REGEX_DIMENSIONS_ELLIPSIS = rf"({_REGEX_DIMENSIONS}{_REGEX_SEPARATOR}\.\.\.\s*)" +_REGEX_SHAPE_EXPRESSION = rf"^({_REGEX_DIMENSIONS}|{_REGEX_DIMENSIONS_ELLIPSIS})$" diff --git a/src/numpydantic/vendor/nptyping/structure.py b/src/numpydantic/vendor/nptyping/structure.py new file mode 100644 index 0000000..8ad5d5b --- /dev/null +++ b/src/numpydantic/vendor/nptyping/structure.py @@ -0,0 +1,108 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from abc import ABC +from typing import ( + Any, + Dict, + List, +) + +from numpydantic.vendor.nptyping.base_meta_classes import ContainerMeta +from numpydantic.vendor.nptyping.nptyping_type import NPTypingType +from numpydantic.vendor.nptyping.structure_expression import ( + create_name_to_type_dict, + normalize_structure_expression, + validate_structure_expression, +) + + +class StructureMeta(ContainerMeta, implementation="Structure"): + """ + Metaclass that is coupled to nptyping.Structure. + """ + + __args__ = tuple() + + def _validate_expression(cls, item: str) -> None: + validate_structure_expression(item) + + def _normalize_expression(cls, item: str) -> str: + return normalize_structure_expression(item) + + def _get_additional_values(cls, item: Any) -> Dict[str, Any]: + return { + "_type_per_name": create_name_to_type_dict(item), + "_has_wildcard": item.replace(" ", "").endswith(",*"), + } + + +class Structure(NPTypingType, ABC, metaclass=StructureMeta): + """ + A container for structure expressions that describe the structured dtype of + an array. + + Simple example: + + >>> Structure["x: Float, y: Float"] + Structure['[x, y]: Float'] + + """ + + _type_per_name = {} + _has_wildcard = False + + @classmethod + def has_wildcard(cls) -> bool: + """ + Returns whether this Structure has a wildcard for any other columns. + :return: True if this Structure expresses "any other columns". + """ + return cls._has_wildcard + + @classmethod + def get_types(cls) -> List[str]: + """ + Return a list of all types (strings) in this Structure. + :return: a list of all types in this Structure. + """ + return list(set(cls._type_per_name.values())) + + @classmethod + def get_names(cls) -> List[str]: + """ + Return a list of all names in this Structure. + :return: a list of all names in this Structure. + """ + return list(cls._type_per_name.keys()) + + @classmethod + def get_type(cls, name: str) -> str: + """ + Get the type (str) that corresponds to the given name. For example for + Structure["x: Float"], get_type("x") would give "Float". + :param name: the name of which the type is to be returned. + :return: the type as a string that corresponds to that name. + """ + return cls._type_per_name[name] diff --git a/src/numpydantic/vendor/nptyping/structure.pyi b/src/numpydantic/vendor/nptyping/structure.pyi new file mode 100644 index 0000000..a0ba8eb --- /dev/null +++ b/src/numpydantic/vendor/nptyping/structure.pyi @@ -0,0 +1,48 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +try: + from typing import Literal # type: ignore[attr-defined] +except ImportError: + from typing_extensions import Literal # type: ignore[attr-defined,misc,assignment] + +from typing import Any, Dict, cast + +import numpy as np +from numpydantic.vendor.nptyping.base_meta_classes import ContainerMeta + +class StructureMeta(ContainerMeta, implementation="Structure"): + + __args__ = tuple() + + def _validate_expression(cls, item: str) -> None: ... + def _normalize_expression(cls, item: str) -> str: ... + def _get_additional_values(cls, item: Any) -> Dict[str, Any]: ... + +# For MyPy: +Structure = cast(Literal, Structure) # type: ignore[has-type,misc,valid-type] + +# For PyRight: +class Structure(np.dtype[Any]): # type: ignore[no-redef,misc] + def __class_getitem__(cls, item: Any) -> Any: ... diff --git a/src/numpydantic/vendor/nptyping/structure_expression.py b/src/numpydantic/vendor/nptyping/structure_expression.py new file mode 100644 index 0000000..625f770 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/structure_expression.py @@ -0,0 +1,339 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import re +from collections import Counter, defaultdict +from difflib import get_close_matches +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, + List, + Mapping, + Tuple, + Type, + Union, +) + +import numpy as np + +from numpydantic.vendor.nptyping.error import InvalidShapeError, InvalidStructureError +from numpydantic.vendor.nptyping.shape import Shape +from numpydantic.vendor.nptyping.shape_expression import ( + check_shape, + normalize_shape_expression, + validate_shape_expression, +) +from numpydantic.vendor.nptyping.typing_ import StructureExpression + +if TYPE_CHECKING: + from nptyping.structure import Structure # pragma: no cover + + +def validate_structure_expression( + structure_expression: Union[StructureExpression, Any] +) -> None: + """ + Validate the given structure_expression and raise an InvalidStructureError + if it is deemed invalid. + :param structure_expression: the structure expression in question. + :return: None. + """ + if structure_expression is not Any: + if not re.match(_REGEX_STRUCTURE_EXPRESSION, structure_expression): + raise InvalidStructureError( + f"'{structure_expression}' is not a valid structure expression." + ) + _validate_structure_expression_contains_no_multiple_field_names( + structure_expression + ) + _validate_sub_array_expressions(structure_expression) + + +def check_structure( + structured_dtype: np.dtype, # type: ignore[type-arg] + target: "Structure", + type_per_name: Dict[str, type], +) -> bool: + """ + Check the given structured_dtype against the given target Structure and + return whether it corresponds (True) or not (False). The given dictionary + contains the vocabulary context for the check. + :param structured_dtype: the dtype in question. + :param target: the target Structure that is checked against. + :param type_per_name: a dict that holds the types by their names as they + occur in a structure expression. + :return: True if the given dtype is valid with the given target. + """ + fields: Mapping[str, Any] = structured_dtype.fields or {} # type: ignore[assignment] + + # Add the wildcard to the lexicon. We want to do this here to keep + # knowledge on wildcards in one place (this module). + type_per_name_with_wildcard: Dict[str, type] = { + **type_per_name, + "*": object, + } # type: ignore[arg-type] + + if target.has_wildcard(): + # Check from the Target's perspective. All fields in the Target should be + # in the subject. + def iterator() -> Generator[Tuple[str, Tuple[np.dtype, int]], None, None]: # type: ignore[type-arg] # pylint: disable=line-too-long + for name_ in target.get_names(): + yield name_, fields.get(name_) # type: ignore[misc] + + else: + # Check from the subject's perspective. All fields in the subject + # should be in the target. + if set(target.get_names()) != set(fields.keys()): + return False + + def iterator() -> Generator[Tuple[str, Tuple[np.dtype, int]], None, None]: # type: ignore[type-arg] # pylint: disable=line-too-long + yield from fields.items() + + for name, dtype_tuple in iterator(): + field_in_target_not_in_subject = dtype_tuple is None + if field_in_target_not_in_subject or not _check_structure_field( + name, dtype_tuple, target, type_per_name_with_wildcard + ): + return False + return True + + +def _check_structure_field( + name: str, + dtype_tuple: Tuple[np.dtype, int], # type: ignore[type-arg] + target: "Structure", + type_per_name_with_wildcard: Dict[str, type], +) -> bool: + dtype = dtype_tuple[0] + target_type_name = target.get_type(name) + target_type_shape_match = re.search(_REGEX_FIELD_SHAPE, target_type_name) + actual_type = dtype.type + if target_type_shape_match: + if not dtype.subdtype: + # the dtype does not contain a shape. + return False + actual_type = dtype.subdtype[0].type + target_type_shape = target_type_shape_match.group(1) + shape_corresponds = check_shape(dtype.shape, Shape[target_type_shape]) + if not shape_corresponds: + return False + target_type_name = target_type_name.replace( + target_type_shape_match.group(0), "" + ) + check_type_name(target_type_name, type_per_name_with_wildcard) + target_type = type_per_name_with_wildcard[target_type_name] + return issubclass(actual_type, target_type) + + +def check_type_names( + structure: "Structure", type_per_name: Dict[str, Type[object]] +) -> None: + """ + Check the given structure for any invalid type names in the given context + of type_per_name. Raises an InvalidStructureError if a type name is + invalid. + :param structure: the Structure that is checked. + :param type_per_name: the context that determines which type names are valid. + :return: None. + """ + for type_ in structure.get_types(): + check_type_name(type_, type_per_name) + + +def check_type_name(type_name: str, type_per_name: Dict[str, Type[object]]) -> None: + """ + Check if the given type_name is in type_per_name and raise a meaningful + error if not. + :param type_name: the key that is checked to be in type_per_name. + :param type_per_name: a dict that is looked in for type_name. + :return: None. + """ + # Remove any subarray stuff here. + type_name = type_name.split("[")[0] + if type_name not in type_per_name: + close_matches = get_close_matches( + type_name, type_per_name.keys(), 3, cutoff=0.4 + ) + close_matches_str = ", ".join(f"'{match}'" for match in close_matches) + extra_help = "" + if len(close_matches) > 1: + extra_help = f" Did you mean one of {close_matches_str}?" + elif close_matches: + extra_help = f" Did you mean {close_matches_str}?" + raise InvalidStructureError( # pylint: disable=raise-missing-from + f"Type '{type_name}' is not valid in this context.{extra_help}" + ) + + +def normalize_structure_expression( + structure_expression: StructureExpression, +) -> StructureExpression: + """ + Normalize the given structure expression, e.g. by removing whitespaces, + making similar expressions look the same. + :param structure_expression: the structure expression that is to be normalized. + :return: a normalized structure expression. + """ + structure_expression = re.sub(r"\s*", "", structure_expression) + type_to_names_dict = _create_type_to_names_dict(structure_expression) + normalized_structure_expression = _type_to_names_dict_to_str(type_to_names_dict) + result = normalized_structure_expression.replace(",", ", ").replace(" ", " ") + has_wildcard_end = structure_expression.replace(" ", "").endswith(",*") + if has_wildcard_end: + result += ", *" + return result + + +def create_name_to_type_dict( + structure_expression: StructureExpression, +) -> Dict[str, str]: + """ + Create a dict with a name as key and a type (str) as value from the given + structure expression. Structure["x: Int, y: Float"] would yield + {"x: "Int", "y": "Float"}. + :param structure_expression: the structure expression from which the dict + is extracted. + :return: a dict with names and their types, both as strings. + """ + type_to_names_dict = _create_type_to_names_dict(structure_expression) + return { + name.strip(): type_.strip() + for type_, names in type_to_names_dict.items() + for name in names + } + + +def _validate_structure_expression_contains_no_multiple_field_names( + structure_expression: StructureExpression, +) -> None: + # Validate that there are not multiple occurrences of the same field names. + matches = re.findall(_REGEX_FIELD, re.sub(r"\s*", "", structure_expression)) + field_name_combinations = [match[0].split(":")[0] for match in matches] + field_names: List[str] = [] + for field_name_combination in field_name_combinations: + field_name_combination_match = re.match( + _REGEX_FIELD_NAMES_COMBINATION, field_name_combination + ) + if field_name_combination_match: + field_names += field_name_combination_match.group(2).split(_SEPARATOR) + else: + field_names.append(field_name_combination) + field_name_counter = Counter(field_names) + field_names_occurring_multiple_times = [ + field_name for field_name, amount in field_name_counter.items() if amount > 1 + ] + if field_names_occurring_multiple_times: + # If there are multiple, just raise about the first. Otherwise the + # error message gets bloated. + field_name_under_fire = field_names_occurring_multiple_times[0] + raise InvalidStructureError( + f"Field names may occur only once in a structure expression." + f" Field name '{field_name_under_fire}' occurs" + f" {field_name_counter[field_name_under_fire]} times in" + f" '{structure_expression}'." + ) + + +def _validate_sub_array_expressions(structure_expression: str) -> None: + # Validate that the given structure expression does not contain any shape + # expressions for sub arrays that are invalid. + for field_match in re.findall(_REGEX_FIELD, structure_expression): + field_type = field_match[0].split(_FIELD_TYPE_POINTER)[1] + type_shape_match = re.search(_REGEX_FIELD_SHAPE, field_type) + if type_shape_match: + type_shape = type_shape_match[1] + try: + validate_shape_expression(type_shape) + except InvalidShapeError as err: + raise InvalidStructureError( + f"'{structure_expression}' is not a valid structure" + f" expression; {str(err)}" + ) from err + + +def _create_type_to_names_dict( + structure_expression: StructureExpression, +) -> Dict[str, List[str]]: + # Create a dictionary with field names per type, sorted by type and then by + # name. + names_per_type: Dict[str, List[str]] = defaultdict(list) + for field_match in re.findall(_REGEX_FIELD, structure_expression): + field_name_combination, field_type = field_match[0].split(_FIELD_TYPE_POINTER) + field_name_combination_match = re.match( + _REGEX_FIELD_NAMES_COMBINATION, field_name_combination + ) + field_type_shape_match = re.search(_REGEX_FIELD_SHAPE, field_type) + if field_name_combination_match: + field_names = field_name_combination_match.group(2).split(_SEPARATOR) + else: + field_names = [field_name_combination] + if field_type_shape_match: + type_shape = field_type_shape_match.group(1) + normalized_type_shape = normalize_shape_expression(type_shape) + field_type = field_type.replace( + field_type_shape_match.group(0), f"[{normalized_type_shape}]" + ) + names_per_type[field_type] += field_names + return { + field_type: sorted(names_per_type[field_type]) + for field_type in sorted(names_per_type.keys()) + } + + +def _type_to_names_dict_to_str(type_to_names_dict: Dict[str, List[str]]) -> str: + # Turn the given dict into a structure expression. + field_strings = [] + for field_type, field_names in type_to_names_dict.items(): + field_names_joined = f"{_SEPARATOR}".join(field_names) + if len(field_names) > 1: + field_names_joined = f"[{field_names_joined}]" + field_strings.append(f"{field_names_joined}{_FIELD_TYPE_POINTER} {field_type}") + return f"{_SEPARATOR}".join(field_strings) + + +_SEPARATOR = "," +_FIELD_TYPE_POINTER = ":" +_REGEX_SEPARATOR = rf"(\s*{_SEPARATOR}\s*)" +_REGEX_FIELD_NAME = r"(\s*[a-zA-Z]\w*\s*)" +_REGEX_FIELD_NAMES = rf"({_REGEX_FIELD_NAME}({_REGEX_SEPARATOR}{_REGEX_FIELD_NAME})+)" +_REGEX_FIELD_NAMES_COMBINATION = rf"(\s*\[{_REGEX_FIELD_NAMES}\]\s*)" +_REGEX_FIELD_LEFT = rf"({_REGEX_FIELD_NAME}|{_REGEX_FIELD_NAMES_COMBINATION})" +_REGEX_FIELD_TYPE = r"(\s*[a-zA-Z]\w*\s*)" +_REGEX_FIELD_TYPE_WILDCARD = r"(\s*\*\s*)" +_REGEX_FIELD_SHAPE = r"\[([^\]]+)\]" +_REGEX_FIELD_SHAPE_MAYBE = rf"\s*({_REGEX_FIELD_SHAPE})?\s*" +_REGEX_FIELD_RIGHT = ( + rf"({_REGEX_FIELD_TYPE}|{_REGEX_FIELD_TYPE_WILDCARD}){_REGEX_FIELD_SHAPE_MAYBE}" +) +_REGEX_FIELD_TYPE_POINTER = rf"(\s*{_FIELD_TYPE_POINTER}\s*)" +_REGEX_FIELD = ( + rf"(\s*{_REGEX_FIELD_LEFT}{_REGEX_FIELD_TYPE_POINTER}{_REGEX_FIELD_RIGHT}\s*)" +) +_REGEX_STRUCTURE_EXPRESSION = ( + rf"^({_REGEX_FIELD}" + rf"({_REGEX_SEPARATOR}{_REGEX_FIELD})*" + rf"({_REGEX_SEPARATOR}{_REGEX_FIELD_TYPE_WILDCARD})?)$" +) diff --git a/src/numpydantic/vendor/nptyping/typing_.py b/src/numpydantic/vendor/nptyping/typing_.py new file mode 100644 index 0000000..6caf3b9 --- /dev/null +++ b/src/numpydantic/vendor/nptyping/typing_.py @@ -0,0 +1,162 @@ +""" +MIT License + +Copyright (c) 2023 Ramon Hagenaars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import sys +from typing import Tuple, Union + +if sys.version_info.minor >= 10: + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + +import numpy as np + +ShapeExpression: TypeAlias = str +StructureExpression: TypeAlias = str +DType: TypeAlias = Union[np.generic, StructureExpression] +ShapeTuple: TypeAlias = Tuple[int, ...] + +Number = np.number +Bool = np.bool_ +Obj = np.object_ # Obj is a common abbreviation and should be usable. +Object = np.object_ +Datetime64 = np.datetime64 +Integer = np.integer +SignedInteger = np.signedinteger +Int8 = np.int8 +Int16 = np.int16 +Int32 = np.int32 +Int64 = np.int64 +Byte = np.byte +Short = np.short +IntC = np.intc +IntP = np.intp +Int = np.integer # Int should translate to the "generic" int type. +Int_ = np.int_ +LongLong = np.longlong +Timedelta64 = np.timedelta64 +UnsignedInteger = np.unsignedinteger +UInt8 = np.uint8 +UInt16 = np.uint16 +UInt32 = np.uint32 +UInt64 = np.uint64 +UByte = np.ubyte +UShort = np.ushort +UIntC = np.uintc +UIntP = np.uintp +UInt = np.uint +ULongLong = np.ulonglong +Inexact = np.inexact +Floating = np.floating +Float16 = np.float16 +Float32 = np.float32 +Float64 = np.float64 +Half = np.half +Single = np.single +Double = np.double +Float = np.float64 +LongDouble = np.longdouble +LongFloat = np.longdouble +ComplexFloating = np.complexfloating +Complex64 = np.complex64 +Complex128 = np.complex128 +CSingle = np.csingle +SingleComplex = np.complex64 +CDouble = np.cdouble +Complex = np.complex128 +CFloat = np.complex128 +CLongDouble = np.clongdouble +CLongFloat = np.clongdouble +LongComplex = np.clongdouble +Flexible = np.flexible +Void = np.void +Character = np.character +Bytes = np.bytes_ +Str = np.str_ +String = np.str_ +Unicode = np.str_ + +dtypes = [ + (Number, "Number"), + (Bool, "Bool"), + (Obj, "Obj"), + (Object, "Object"), + (Datetime64, "Datetime64"), + (Integer, "Integer"), + (SignedInteger, "SignedInteger"), + (Int8, "Int8"), + (Int16, "Int16"), + (Int32, "Int32"), + (Int64, "Int64"), + (Byte, "Byte"), + (Short, "Short"), + (IntC, "IntC"), + (IntP, "IntP"), + (Int, "Int"), + (LongLong, "LongLong"), + (Timedelta64, "Timedelta64"), + (UnsignedInteger, "UnsignedInteger"), + (UInt8, "UInt8"), + (UInt16, "UInt16"), + (UInt32, "UInt32"), + (UInt64, "UInt64"), + (UByte, "UByte"), + (UShort, "UShort"), + (UIntC, "UIntC"), + (UIntP, "UIntP"), + (UInt, "UInt"), + (ULongLong, "ULongLong"), + (Inexact, "Inexact"), + (Floating, "Floating"), + (Float16, "Float16"), + (Float32, "Float32"), + (Float64, "Float64"), + (Half, "Half"), + (Single, "Single"), + (Double, "Double"), + (Float, "Float"), + (LongDouble, "LongDouble"), + (LongFloat, "LongFloat"), + (ComplexFloating, "ComplexFloating"), + (Complex64, "Complex64"), + (Complex128, "Complex128"), + (CSingle, "CSingle"), + (SingleComplex, "SingleComplex"), + (CDouble, "CDouble"), + (Complex, "Complex"), + (CFloat, "CFloat"), + (CLongDouble, "CLongDouble"), + (CLongFloat, "CLongFloat"), + (LongComplex, "LongComplex"), + (Flexible, "Flexible"), + (Void, "Void"), + (Character, "Character"), + (Bytes, "Bytes"), + (String, "String"), + (Str, "Str"), + (Unicode, "Unicode"), +] + +name_per_dtype = dict(dtypes) +dtype_per_name = {name: dtype for dtype, name in dtypes} diff --git a/tests/fixtures.py b/tests/fixtures.py index e780058..7c14b35 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,6 +1,7 @@ import shutil from pathlib import Path from typing import Any, Callable, Optional, Tuple, Type, Union +from warnings import warn import h5py import numpy as np @@ -25,7 +26,13 @@ def tmp_output_dir(request: pytest.FixtureRequest) -> Path: yield path if not request.config.getvalue("--with-output"): - shutil.rmtree(str(path)) + try: + shutil.rmtree(str(path)) + except PermissionError as e: + # sporadic error on windows machines... + warn( + f"Temporary directory could not be removed due to a permissions error: \n{str(e)}" + ) @pytest.fixture(scope="function") diff --git a/tests/test_ndarray.py b/tests/test_ndarray.py index 9e45c1e..7b512ef 100644 --- a/tests/test_ndarray.py +++ b/tests/test_ndarray.py @@ -7,11 +7,12 @@ import json import numpy as np from pydantic import BaseModel, ValidationError, Field -from nptyping import Number + from numpydantic import NDArray, Shape from numpydantic.exceptions import ShapeError, DtypeError from numpydantic import dtype +from numpydantic.dtype import Number def test_ndarray_type(): @@ -39,6 +40,53 @@ def test_ndarray_type(): instance = Model(array=np.zeros((2, 3)), array_any=np.ones((3, 4, 5))) +def test_schema_unsupported_type(): + """ + Complex numbers should just be made with an `any` schema + """ + + class Model(BaseModel): + array: NDArray[Shape["2 x, * y"], complex] + + schema = Model.model_json_schema() + assert schema["properties"]["array"]["items"] == { + "items": {}, + "type": "array", + } + + +def test_schema_tuple(): + """ + Types specified as tupled should have their schemas as a union + """ + + class Model(BaseModel): + array: NDArray[Shape["2 x, * y"], (np.uint8, np.uint16)] + + schema = Model.model_json_schema() + assert "anyOf" in schema["properties"]["array"]["items"]["items"] + conditions = schema["properties"]["array"]["items"]["items"]["anyOf"] + + assert all([i["type"] == "integer" for i in conditions]) + assert sorted([i["maximum"] for i in conditions]) == [255, 65535] + assert all([i["minimum"] == 0 for i in conditions]) + + +def test_schema_number(): + """ + np.numeric should just be the float schema + """ + + class Model(BaseModel): + array: NDArray[Shape["2 x, * y"], np.number] + + schema = Model.model_json_schema() + assert schema["properties"]["array"]["items"] == { + "items": {"type": "number"}, + "type": "array", + } + + def test_ndarray_union(): class Model(BaseModel): array: Optional[ diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9f1fefc --- /dev/null +++ b/tox.ini @@ -0,0 +1,14 @@ +[tox] +requires = + tox>=4 +env_list = py{39,310,311,312}-numpy{1,2} + +[testenv] +package = editable +extras = + tests +deps = + py{39,310,311,312}-numpy1: numpy<2.0.0 + py{39,310,311,312}-numpy2: numpy>=2.0.0 +commands = + python -m pytest