├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── example.py ├── performance.py ├── pyproject.toml ├── setup.py ├── src └── covjson_pydantic │ ├── __init__.py │ ├── base_models.py │ ├── coverage.py │ ├── domain.py │ ├── i18n.py │ ├── ndarray.py │ ├── observed_property.py │ ├── parameter.py │ ├── py.typed │ ├── reference_system.py │ └── unit.py └── tests ├── __init__.py ├── test_coverage.py └── test_data ├── categorical-data-parameter.json ├── continuous-data-parameter.json ├── coverage-json.json ├── coverage-mixed-type-ndarray.json ├── doc-example-coverage-collection.json ├── doc-example-coverage.json ├── example_py.json ├── grid-domain-no-y.json ├── grid-domain.json ├── mixed-type-axes-2.json ├── mixed-type-axes.json ├── mixed-type-ndarray-1.json ├── mixed-type-ndarray-2.json ├── mixed-type-ndarray-3.json ├── ndarray-float.json ├── ndarray-integer.json ├── ndarray-string.json ├── parameters.json ├── point-series-domain-custom.json ├── point-series-domain-more-z.json ├── point-series-domain-no-t.json ├── polygon-series-coverage-collection.json ├── spec-axes.json ├── spec-domain-grid.json ├── spec-domain-multipoint-series.json ├── spec-domain-multipoint.json ├── spec-domain-point-compact.json ├── spec-domain-point-series.json ├── spec-domain-point.json ├── spec-domain-polygon-series.json ├── spec-domain-trajectory.json ├── spec-domain-vertical-profile.json ├── spec-ndarray.json ├── spec-parametergroup.json ├── spec-reference-system-identifierrs.json ├── spec-tiled-ndarray.json ├── spec-trajectory-coverage.json ├── spec-vertical-profile-coverage.json ├── str-axes.json └── temporalrs-no-calendar.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | env: 11 | LATEST_PY_VERSION: '3.10' 12 | 13 | jobs: 14 | tests: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install .["test"] 31 | 32 | - name: Run pre-commit 33 | if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} 34 | run: | 35 | python -m pip install pre-commit 36 | pre-commit run --all-files 37 | 38 | - name: Run tests 39 | run: python -m pytest --cov covjson_pydantic --cov-report xml --cov-report term-missing 40 | 41 | - name: Upload Results 42 | if: ${{ matrix.python-version == env.LATEST_PY_VERSION }} 43 | uses: codecov/codecov-action@v4 44 | env: 45 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 46 | with: 47 | file: ./coverage.xml 48 | flags: unittests 49 | name: ${{ matrix.python-version }} 50 | fail_ci_if_error: false 51 | 52 | publish: 53 | needs: [tests] 54 | runs-on: ubuntu-latest 55 | if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' 56 | steps: 57 | - uses: actions/checkout@v4 58 | - name: Set up Python 59 | uses: actions/setup-python@v5 60 | with: 61 | python-version: ${{ env.LATEST_PY_VERSION }} 62 | 63 | - name: Install dependencies 64 | run: | 65 | python -m pip install --upgrade pip 66 | python -m pip install flit 67 | python -m pip install . 68 | 69 | - name: Set tag version 70 | id: tag 71 | run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 72 | 73 | - name: Set module version 74 | id: module 75 | run: echo "version=$(python -c 'from importlib.metadata import version; print(version("covjson_pydantic"))')" >> $GITHUB_OUTPUT 76 | 77 | - name: Build and publish 78 | if: steps.tag.outputs.tag == steps.module.outputs.version 79 | env: 80 | FLIT_USERNAME: __token__ 81 | FLIT_PASSWORD: ${{ secrets.PYPI_TOKEN }} 82 | run: flit publish 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Code Editors 2 | ## Jet Brains 3 | .idea/ 4 | .run/ 5 | __pycache__/ 6 | 7 | # Python 8 | ## MyPy 9 | .mypy_cache 10 | ## Virtual Environment 11 | venv/ 12 | 13 | # unit test results 14 | .coverage 15 | .pytest_cache 16 | TEST-*-*.xml 17 | coverage.json 18 | coverage.xml 19 | htmlcov/ 20 | junit-report.xml 21 | 22 | # Ignore package 23 | *.egg-info/ 24 | .env 25 | build/ 26 | dist/ 27 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | # Formatting 6 | - id: end-of-file-fixer # Makes sure files end in a newline and only a newline. 7 | - id: pretty-format-json 8 | args: [ 9 | '--autofix', 10 | '--indent=4', 11 | '--no-ensure-ascii', 12 | '--no-sort-keys' 13 | ] # Formats and sorts your JSON files. 14 | - id: trailing-whitespace # Trims trailing whitespace. 15 | # Checks 16 | - id: check-json # Attempts to load all json files to verify syntax. 17 | - id: check-merge-conflict # Check for files that contain merge conflict strings. 18 | - id: check-shebang-scripts-are-executable # Checks that scripts with shebangs are executable. 19 | - id: check-yaml 20 | # only checks syntax not load the yaml: 21 | # https://stackoverflow.com/questions/59413979/how-exclude-ref-tag-from-check-yaml-git-hook 22 | args: [ '--unsafe' ] # Parse the yaml files for syntax. 23 | 24 | # reorder-python-imports ~ sort python imports 25 | - repo: https://github.com/asottile/reorder_python_imports 26 | rev: v2.6.0 27 | hooks: 28 | - id: reorder-python-imports 29 | 30 | # black ~ Formats Python code 31 | - repo: https://github.com/psf/black 32 | rev: 22.3.0 33 | hooks: 34 | - id: black 35 | args: [ 36 | '--line-length=120' 37 | ] 38 | 39 | # flake8 ~ Enforces the Python PEP8 style guide 40 | # Configure the pep8-naming flake plugin to recognise @classmethod, @validator, @root_validator as classmethod. 41 | # Ignore the unused imports (F401) for the __init__ files, the imports are not always used inside the file, 42 | # but used to setup how other files can import it in a more convenient way. 43 | - repo: https://github.com/pycqa/flake8 44 | rev: 4.0.1 45 | hooks: 46 | - id: flake8 47 | args: [ 48 | '--classmethod-decorators=classmethod,validator,root_validator', 49 | '--ignore=E203,W503', 50 | '--max-line-length=120', 51 | '--per-file-ignores=__init__.py:F401' 52 | ] 53 | additional_dependencies: [ 'pep8-naming==0.12.1' ] 54 | 55 | - repo: https://github.com/pre-commit/mirrors-mypy 56 | rev: v1.5.1 57 | hooks: 58 | - id: mypy 59 | language_version: python 60 | # No reason to run if only tests have changed. They intentionally break typing. 61 | exclude: tests/.* 62 | # Pass mypy the entire folder because a change in one file can break others. 63 | args: [--config-file=pyproject.toml, src/] 64 | # Don't pass it the individual filenames because it is already doing the whole folder. 65 | pass_filenames: false 66 | additional_dependencies: 67 | - pydantic 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Koninklijk Nederlands Meteorologisch Instituut (KNMI) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoverageJSON Pydantic 2 | 3 |

4 | 5 | Test 6 | 7 | 8 | Coverage 9 | 10 | 11 | Package version 12 | 13 | 14 | Downloads 15 | 16 | 17 | License 18 | 19 |

20 | 21 | 22 | This repository contains the coveragejson-pydantic Python package. It provides [Pydantic](https://pydantic-docs.helpmanual.io/) models for [CoverageJSON](https://covjson.org/). This can, for example, be used to develop an API using FastAPI serving or receiving CoverageJSON. 23 | 24 | ## Install 25 | ```shell 26 | pip install covjson-pydantic 27 | ``` 28 | 29 | Or you can install directly from source: 30 | 31 | ```shell 32 | pip install git+https://github.com/KNMI/covjson-pydantic.git 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```python 38 | from datetime import datetime, timezone 39 | from pydantic import AwareDatetime 40 | from covjson_pydantic.coverage import Coverage 41 | from covjson_pydantic.domain import Domain, Axes, ValuesAxis, DomainType 42 | from covjson_pydantic.ndarray import NdArrayFloat 43 | 44 | c = Coverage( 45 | domain=Domain( 46 | domainType=DomainType.point_series, 47 | axes=Axes( 48 | x=ValuesAxis[float](values=[1.23]), 49 | y=ValuesAxis[float](values=[4.56]), 50 | t=ValuesAxis[AwareDatetime](values=[datetime(2024, 8, 1, tzinfo=timezone.utc)]), 51 | ), 52 | ), 53 | ranges={ 54 | "temperature": NdArrayFloat(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0]) 55 | } 56 | ) 57 | 58 | print(c.model_dump_json(exclude_none=True, indent=4)) 59 | ``` 60 | Will print 61 | ```json 62 | { 63 | "type": "Coverage", 64 | "domain": { 65 | "type": "Domain", 66 | "domainType": "PointSeries", 67 | "axes": { 68 | "x": { 69 | "values": [ 70 | 1.23 71 | ] 72 | }, 73 | "y": { 74 | "values": [ 75 | 4.56 76 | ] 77 | }, 78 | "t": { 79 | "values": [ 80 | "2024-08-01T00:00:00Z" 81 | ] 82 | } 83 | } 84 | }, 85 | "ranges": { 86 | "temperature": { 87 | "type": "NdArray", 88 | "dataType": "float", 89 | "axisNames": [ 90 | "x", 91 | "y", 92 | "t" 93 | ], 94 | "shape": [ 95 | 1, 96 | 1, 97 | 1 98 | ], 99 | "values": [ 100 | 42.0 101 | ] 102 | } 103 | } 104 | } 105 | ``` 106 | 107 | ## Contributing 108 | 109 | Make an editable installation from within the repository root 110 | 111 | ```shell 112 | pip install -e '.[test]' 113 | ``` 114 | 115 | ### Running tests 116 | 117 | ```shell 118 | pytest tests/ 119 | ``` 120 | 121 | ### Linting and typing 122 | 123 | Linting and typing (mypy) is done using [pre-commit](https://pre-commit.com) hooks. 124 | 125 | ```shell 126 | pip install pre-commit 127 | pre-commit install 128 | pre-commit run 129 | ``` 130 | 131 | ## Related packages 132 | 133 | * [edr-pydantic](https://github.com/KNMI/edr-pydantic) - Pydantic data models for the Environmental Data Retrieval (EDR) API 134 | * [geojson-pydantic](https://github.com/developmentseed/geojson-pydantic) - Pydantic data models for the GeoJSON spec 135 | 136 | ## Real world usage 137 | 138 | This library is used to build an OGC Environmental Data Retrieval (EDR) API, serving automatic weather data station data from The Royal Netherlands Meteorological Institute (KNMI). See the [KNMI Data Platform EDR API](https://developer.dataplatform.knmi.nl/edr-api). 139 | 140 | ## TODOs 141 | Help is wanted in the following areas to fully implement the CovJSON spec: 142 | * The `Polygon`, `MultiPolygon` and `MultiPolygonSeries` domain types are not supported. 143 | * The `Section` domain type is not supported. 144 | * Not all requirements in the spec relating different fields are implemented. 145 | 146 | ## License 147 | 148 | Apache License, Version 2.0 149 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from datetime import timezone 3 | 4 | from covjson_pydantic.coverage import Coverage 5 | from covjson_pydantic.domain import Axes 6 | from covjson_pydantic.domain import Domain 7 | from covjson_pydantic.domain import DomainType 8 | from covjson_pydantic.domain import ValuesAxis 9 | from covjson_pydantic.ndarray import NdArrayFloat 10 | from pydantic import AwareDatetime 11 | 12 | c = Coverage( 13 | domain=Domain( 14 | domainType=DomainType.point_series, 15 | axes=Axes( 16 | x=ValuesAxis[float](values=[1.23]), 17 | y=ValuesAxis[float](values=[4.56]), 18 | t=ValuesAxis[AwareDatetime](values=[datetime(2024, 8, 1, tzinfo=timezone.utc)]), 19 | ), 20 | ), 21 | ranges={"temperature": NdArrayFloat(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0])}, 22 | ) 23 | 24 | print(c.model_dump_json(exclude_none=True, indent=4)) 25 | -------------------------------------------------------------------------------- /performance.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | from pathlib import Path 3 | 4 | filename = Path(__file__).parent.resolve() / "tests" / "test_data" / "coverage-json.json" 5 | 6 | setup = f""" 7 | import json 8 | from covjson_pydantic.coverage import Coverage 9 | 10 | file = "{filename}" 11 | # Put JSON in default unindented format 12 | with open(file, "r") as f: 13 | data = json.load(f) 14 | json_string = json.dumps(data, separators=(",", ":")) 15 | cj = Coverage.model_validate_json(json_string) 16 | """ 17 | 18 | # This can be used to quickly check performance. The first call checks JSON to Python conversion 19 | # The second call checks Python to JSON conversion 20 | # Consider generating a larger CoverageJSON file 21 | print(timeit.timeit("Coverage.model_validate_json(json_string)", setup, number=1000)) 22 | print(timeit.timeit("cj.model_dump_json(exclude_none=True)", setup, number=1000)) 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "covjson-pydantic" 3 | description = "The Pydantic models for CoverageJSON" 4 | readme = "README.md" 5 | requires-python = ">=3.8" 6 | license = {file = "LICENSE"} 7 | authors = [ 8 | {name = "KNMI Data Platform Team", email = "opendata@knmi.nl"}, 9 | ] 10 | keywords = ["covjson", "Pydantic"] 11 | classifiers = [ 12 | "Intended Audience :: Information Technology", 13 | "Intended Audience :: Science/Research", 14 | "License :: OSI Approved :: Apache Software License", 15 | "Programming Language :: Python :: 3.8", 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Programming Language :: Python :: 3.13", 21 | "Topic :: Scientific/Engineering :: GIS", 22 | "Typing :: Typed", 23 | ] 24 | version = "0.7.0" 25 | dependencies = ["pydantic>=2.3,<3"] 26 | 27 | [project.optional-dependencies] 28 | test = ["pytest", "pytest-cov"] 29 | dev = ["pre-commit"] 30 | 31 | [project.urls] 32 | Source = "https://github.com/knmi/covjson-pydantic" 33 | 34 | [build-system] 35 | requires = ["flit>=3.2,<4"] 36 | build-backend = "flit_core.buildapi" 37 | 38 | [tool.flit.module] 39 | name = "covjson_pydantic" 40 | 41 | [tool.flit.sdist] 42 | exclude = [ 43 | "test/", 44 | ".github/", 45 | ] 46 | 47 | [tool.mypy] 48 | plugins = [ 49 | "pydantic.mypy" 50 | ] 51 | 52 | [tool.pydantic-mypy] 53 | warn_untyped_fields = true 54 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import setuptools 5 | 6 | logging.basicConfig() 7 | logger = logging.getLogger(__name__) 8 | logger.setLevel(os.environ.get("LOG_LEVEL", "INFO")) 9 | 10 | if __name__ == "__main__": 11 | setuptools.setup() 12 | -------------------------------------------------------------------------------- /src/covjson_pydantic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNMI/covjson-pydantic/932cb4700a2e530cb48b2dcd5d8f65692ed19746/src/covjson_pydantic/__init__.py -------------------------------------------------------------------------------- /src/covjson_pydantic/base_models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel as PydanticBaseModel 2 | from pydantic import ConfigDict 3 | 4 | 5 | class CovJsonBaseModel(PydanticBaseModel): 6 | model_config = ConfigDict( 7 | str_strip_whitespace=True, 8 | str_min_length=1, 9 | extra="forbid", 10 | validate_default=True, 11 | validate_assignment=True, 12 | strict=True, 13 | ) 14 | -------------------------------------------------------------------------------- /src/covjson_pydantic/coverage.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info < (3, 9): 4 | from typing_extensions import Annotated 5 | else: 6 | from typing import Annotated 7 | 8 | from typing import Dict 9 | from typing import List 10 | from typing import Literal 11 | from typing import Optional 12 | from typing import Union 13 | 14 | from pydantic import AnyUrl 15 | from pydantic import Field 16 | 17 | from .base_models import CovJsonBaseModel 18 | from .domain import Domain 19 | from .domain import DomainType 20 | from .ndarray import NdArrayFloat 21 | from .ndarray import NdArrayInt 22 | from .ndarray import NdArrayStr 23 | from .ndarray import TiledNdArrayFloat 24 | from .parameter import Parameters 25 | from .parameter import ParameterGroup 26 | from .reference_system import ReferenceSystemConnectionObject 27 | 28 | NdArrayTypes = Annotated[Union[NdArrayFloat, NdArrayInt, NdArrayStr], Field(discriminator="dataType")] 29 | 30 | 31 | class Coverage(CovJsonBaseModel, extra="allow"): 32 | id: Optional[str] = None 33 | type: Literal["Coverage"] = "Coverage" 34 | domain: Domain 35 | parameters: Optional[Parameters] = None 36 | parameterGroups: Optional[List[ParameterGroup]] = None # noqa: N815 37 | ranges: Dict[str, Union[NdArrayTypes, TiledNdArrayFloat, AnyUrl]] 38 | 39 | 40 | class CoverageCollection(CovJsonBaseModel, extra="allow"): 41 | type: Literal["CoverageCollection"] = "CoverageCollection" 42 | domainType: Optional[DomainType] = None # noqa: N815 43 | coverages: List[Coverage] 44 | parameters: Optional[Parameters] = None 45 | parameterGroups: Optional[List[ParameterGroup]] = None # noqa: N815 46 | referencing: Optional[List[ReferenceSystemConnectionObject]] = None 47 | -------------------------------------------------------------------------------- /src/covjson_pydantic/domain.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Generic 3 | from typing import List 4 | from typing import Literal 5 | from typing import Optional 6 | from typing import Tuple 7 | from typing import TypeVar 8 | from typing import Union 9 | 10 | from pydantic import AwareDatetime 11 | from pydantic import field_validator 12 | from pydantic import model_validator 13 | from pydantic import PositiveInt 14 | 15 | from .base_models import CovJsonBaseModel 16 | from .reference_system import ReferenceSystemConnectionObject 17 | 18 | 19 | class CompactAxis(CovJsonBaseModel): 20 | start: float 21 | stop: float 22 | num: PositiveInt 23 | 24 | @model_validator(mode="after") 25 | def single_value_case(self): 26 | if self.num == 1 and self.start != self.stop: 27 | raise ValueError("If the value of 'num' is 1, then 'start' and 'stop' MUST have identical values.") 28 | return self 29 | 30 | 31 | ValuesT = TypeVar("ValuesT") 32 | 33 | 34 | # Combination between Generics (ValuesT) and datetime and strict mode causes issues between JSON <-> Pydantic 35 | # conversions. Strict mode has been disabled. Issue: https://github.com/KNMI/covjson-pydantic/issues/4 36 | class ValuesAxis(CovJsonBaseModel, Generic[ValuesT], extra="allow", strict=False): 37 | dataType: Optional[str] = None # noqa: N815 38 | coordinates: Optional[List[str]] = None 39 | values: List[ValuesT] 40 | bounds: Optional[List[ValuesT]] = None 41 | 42 | @model_validator(mode="after") 43 | def bounds_length(self): 44 | if self.bounds is not None and len(self.bounds) != 2 * len(self.values): 45 | raise ValueError("If provided, the length of 'bounds' should be twice that of 'values'.") 46 | return self 47 | 48 | 49 | class DomainType(str, Enum): 50 | grid = "Grid" 51 | vertical_profile = "VerticalProfile" 52 | point_series = "PointSeries" 53 | point = "Point" 54 | multi_point_series = "MultiPointSeries" 55 | multi_point = "MultiPoint" 56 | trajectory = "Trajectory" 57 | polygon_series = "PolygonSeries" 58 | 59 | 60 | class Axes(CovJsonBaseModel): 61 | x: Optional[Union[ValuesAxis[float], ValuesAxis[str], CompactAxis]] = None 62 | y: Optional[Union[ValuesAxis[float], ValuesAxis[str], CompactAxis]] = None 63 | z: Optional[Union[ValuesAxis[float], ValuesAxis[str], CompactAxis]] = None 64 | t: Optional[ValuesAxis[AwareDatetime]] = None 65 | # TODO: Add better support for 'polygon' and 'tuple' composite axes 66 | composite: Optional[ValuesAxis[Tuple]] = None 67 | 68 | @model_validator(mode="after") 69 | def at_least_one_axes(self): 70 | if self.x is None and self.y is None and self.z is None and self.t is None and self.composite is None: 71 | raise ValueError("At least one axis of x,y,z,t or composite must be given.") 72 | return self 73 | 74 | 75 | class Domain(CovJsonBaseModel, extra="allow"): 76 | type: Literal["Domain"] = "Domain" 77 | domainType: Optional[DomainType] = None # noqa: N815 78 | axes: Axes 79 | referencing: Optional[List[ReferenceSystemConnectionObject]] = None 80 | 81 | # TODO: This is a workaround to allow domainType to work in strict mode, in combination with FastAPI. 82 | # See: https://github.com/tiangolo/fastapi/discussions/9868 83 | # And: https://github.com/KNMI/covjson-pydantic/issues/5 84 | @field_validator("domainType", mode="before") 85 | @classmethod 86 | def value_to_enum(cls, v): 87 | if isinstance(v, str): 88 | return DomainType(v) 89 | return v 90 | 91 | @staticmethod 92 | def check_axis(domain_type, axes, required_axes, allowed_axes, single_value_axes): 93 | # Check required axes 94 | for axis_name in required_axes: 95 | axis = getattr(axes, axis_name) 96 | if axis is None: 97 | raise ValueError(f"A '{domain_type.value}' must have a '{axis_name}'-axis.") 98 | if axis_name in single_value_axes: 99 | if isinstance(axis, ValuesAxis) and len(axis.values) != 1: 100 | raise ValueError( 101 | f"The 'values' field of the ValuesAxis '{axis_name}'-axis " 102 | f"of a '{domain_type.value}' domain must contain a single value." 103 | ) 104 | if isinstance(axis, CompactAxis) and axis.num != 1: 105 | raise ValueError( 106 | f"The 'num' field of the CompactAxis '{axis_name}'-axis " 107 | f"of a '{domain_type.value}' domain must be 1." 108 | ) 109 | 110 | # Check allowed axes 111 | all_axis = {"x", "y", "z", "t", "composite"} 112 | for axis_name in all_axis - required_axes - allowed_axes: 113 | axis = getattr(axes, axis_name) 114 | if axis is not None: 115 | raise ValueError(f"A '{domain_type.value}' domain can not have a '{axis_name}'-axis.") 116 | 117 | # Check for single value of allowed axes 118 | for axis_name in allowed_axes: 119 | axis = getattr(axes, axis_name) 120 | if axis is not None and axis_name in single_value_axes: 121 | if isinstance(axis, ValuesAxis) and len(axis.values) != 1: 122 | raise ValueError( 123 | f"If provided, the 'values' field of the ValuesAxis '{axis_name}'-axis " 124 | f"of a '{domain_type.value}' domain must contain a single value." 125 | ) 126 | if isinstance(axis, CompactAxis) and axis.num != 1: 127 | raise ValueError( 128 | f"If provided, the 'num' field of the CompactAxis '{axis_name}'-axis " 129 | f"of a '{domain_type.value}' domain must be 1." 130 | ) 131 | 132 | @model_validator(mode="after") 133 | def check_domain_consistent(self): 134 | domain_type = self.domainType 135 | axes = self.axes 136 | 137 | if domain_type == DomainType.grid: 138 | Domain.check_axis( 139 | domain_type, axes, required_axes={"x", "y"}, allowed_axes={"z", "t"}, single_value_axes=set() 140 | ) 141 | 142 | if domain_type == DomainType.vertical_profile: 143 | Domain.check_axis( 144 | domain_type, axes, required_axes={"x", "y", "z"}, allowed_axes={"t"}, single_value_axes={"x", "y", "t"} 145 | ) 146 | 147 | if domain_type == DomainType.point_series: 148 | Domain.check_axis( 149 | domain_type, axes, required_axes={"x", "y", "t"}, allowed_axes={"z"}, single_value_axes={"x", "y", "z"} 150 | ) 151 | 152 | if domain_type == DomainType.polygon_series: 153 | Domain.check_axis( 154 | domain_type, 155 | axes, 156 | required_axes={"composite", "t"}, 157 | allowed_axes={"z"}, 158 | single_value_axes={"z", "composite"}, 159 | ) 160 | 161 | if domain_type == DomainType.point: 162 | Domain.check_axis( 163 | domain_type, 164 | axes, 165 | required_axes={"x", "y"}, 166 | allowed_axes={"z", "t"}, 167 | single_value_axes={"x", "y", "z", "t"}, 168 | ) 169 | 170 | if domain_type == DomainType.multi_point_series: 171 | Domain.check_axis( 172 | domain_type, axes, required_axes={"composite", "t"}, allowed_axes=set(), single_value_axes=set() 173 | ) 174 | 175 | if domain_type == DomainType.multi_point: 176 | Domain.check_axis( 177 | domain_type, axes, required_axes={"composite"}, allowed_axes={"t"}, single_value_axes={"t"} 178 | ) 179 | 180 | if domain_type == DomainType.trajectory: 181 | Domain.check_axis( 182 | domain_type, axes, required_axes={"composite"}, allowed_axes={"z"}, single_value_axes={"z"} 183 | ) 184 | 185 | return self 186 | -------------------------------------------------------------------------------- /src/covjson_pydantic/i18n.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Dict 3 | 4 | 5 | class LanguageTag(str, Enum): 6 | dutch = "nl" 7 | english = "en" 8 | german = "de" 9 | undefined = "und" 10 | 11 | 12 | # TODO: This was throwing warning: 13 | # Expected `definition-ref` but got `LanguageTag` - serialized value may not be as expected 14 | # This may be a bug in Pydantic: https://github.com/pydantic/pydantic/issues/6467 15 | # or: https://github.com/pydantic/pydantic/issues/6422 16 | # So, for now, reverted to a less strict type 17 | # See issue: https://github.com/KNMI/covjson-pydantic/issues/3 18 | # i18n = Dict[LanguageTag, str] 19 | i18n = Dict[str, str] 20 | -------------------------------------------------------------------------------- /src/covjson_pydantic/ndarray.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | from typing import Literal 4 | from typing import Optional 5 | 6 | from pydantic import model_validator 7 | 8 | from .base_models import CovJsonBaseModel 9 | 10 | 11 | class NdArray(CovJsonBaseModel, extra="allow"): 12 | type: Literal["NdArray"] = "NdArray" 13 | dataType: str # Kept here to ensure order of output in JSON # noqa: N815 14 | axisNames: Optional[List[str]] = None # noqa: N815 15 | shape: Optional[List[int]] = None 16 | 17 | @model_validator(mode="before") 18 | @classmethod 19 | def validate_is_sub_class(cls, values): 20 | if cls is NdArray: 21 | raise TypeError( 22 | "NdArray cannot be instantiated directly, please use a NdArrayFloat, NdArrayInt or NdArrayStr" 23 | ) 24 | return values 25 | 26 | @model_validator(mode="after") 27 | def check_field_dependencies(self): 28 | if len(self.values) > 1 and (self.axisNames is None or len(self.axisNames) == 0): 29 | raise ValueError("'axisNames' must to be provided if array is not 0D") 30 | 31 | if len(self.values) > 1 and (self.shape is None or len(self.shape) == 0): 32 | raise ValueError("'shape' must to be provided if array is not 0D") 33 | 34 | if self.axisNames is not None and self.shape is not None and len(self.axisNames) != len(self.shape): 35 | raise ValueError("'axisNames' and 'shape' should have equal length") 36 | 37 | if self.shape is not None and len(self.shape) >= 1: 38 | prod = math.prod(self.shape) 39 | if len(self.values) != prod: 40 | raise ValueError( 41 | "Where 'shape' is present and non-empty, the product of its values MUST equal " 42 | "the number of elements in the 'values' array." 43 | ) 44 | 45 | return self 46 | 47 | 48 | class NdArrayFloat(NdArray): 49 | dataType: Literal["float"] = "float" # noqa: N815 50 | values: List[Optional[float]] 51 | 52 | 53 | class NdArrayInt(NdArray): 54 | dataType: Literal["integer"] = "integer" # noqa: N815 55 | values: List[Optional[int]] 56 | 57 | 58 | class NdArrayStr(NdArray): 59 | dataType: Literal["string"] = "string" # noqa: N815 60 | values: List[Optional[str]] 61 | 62 | 63 | class TileSet(CovJsonBaseModel): 64 | tileShape: List[Optional[int]] # noqa: N815 65 | urlTemplate: str # noqa: N815 66 | 67 | 68 | # TODO: Validation of field dependencies 69 | # TODO: Support string and integer type TiledNdArray 70 | class TiledNdArrayFloat(CovJsonBaseModel, extra="allow"): 71 | type: Literal["TiledNdArray"] = "TiledNdArray" 72 | dataType: Literal["float"] = "float" # noqa: N815 73 | axisNames: List[str] # noqa: N815 74 | shape: List[int] 75 | tileSets: List[TileSet] # noqa: N815 76 | -------------------------------------------------------------------------------- /src/covjson_pydantic/observed_property.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from typing import Optional 3 | 4 | from .base_models import CovJsonBaseModel 5 | from .i18n import i18n 6 | 7 | 8 | class Category(CovJsonBaseModel): 9 | id: str 10 | label: i18n 11 | description: Optional[i18n] = None 12 | 13 | 14 | class ObservedProperty(CovJsonBaseModel): 15 | id: Optional[str] = None 16 | label: i18n 17 | description: Optional[i18n] = None 18 | categories: Optional[List[Category]] = None 19 | -------------------------------------------------------------------------------- /src/covjson_pydantic/parameter.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from typing import List 3 | from typing import Literal 4 | from typing import Optional 5 | from typing import Union 6 | 7 | from pydantic import model_validator 8 | from pydantic import RootModel 9 | 10 | from .base_models import CovJsonBaseModel 11 | from .i18n import i18n 12 | from .observed_property import ObservedProperty 13 | from .unit import Unit 14 | 15 | 16 | class Parameter(CovJsonBaseModel, extra="allow"): 17 | type: Literal["Parameter"] = "Parameter" 18 | id: Optional[str] = None 19 | label: Optional[i18n] = None 20 | description: Optional[i18n] = None 21 | observedProperty: ObservedProperty # noqa: N815 22 | categoryEncoding: Optional[Dict[str, Union[int, List[int]]]] = None # noqa: N815 23 | unit: Optional[Unit] = None 24 | 25 | @model_validator(mode="after") 26 | def must_not_have_unit_if_observed_property_has_categories(self): 27 | if self.unit is not None and self.observedProperty is not None and self.observedProperty.categories is not None: 28 | raise ValueError( 29 | "A parameter object MUST NOT have a 'unit' member " 30 | "if the 'observedProperty' member has a 'categories' member." 31 | ) 32 | 33 | return self 34 | 35 | 36 | class Parameters(RootModel): 37 | root: Dict[str, Parameter] 38 | 39 | def __iter__(self): 40 | return iter(self.root) 41 | 42 | def __getitem__(self, key): 43 | return self.root[key] 44 | 45 | def get(self, key, default=None): 46 | return self.root.get(key, default) 47 | 48 | 49 | class ParameterGroup(CovJsonBaseModel, extra="allow"): 50 | type: Literal["ParameterGroup"] = "ParameterGroup" 51 | id: Optional[str] = None 52 | label: Optional[i18n] = None 53 | description: Optional[i18n] = None 54 | observedProperty: Optional[ObservedProperty] = None # noqa: N815 55 | members: List[str] 56 | 57 | @model_validator(mode="after") 58 | def must_have_label_and_or_observed_property(self): 59 | if self.label is None and self.observedProperty is None: 60 | raise ValueError( 61 | "A parameter group object MUST have either or both the members 'label' or/and 'observedProperty'" 62 | ) 63 | return self 64 | -------------------------------------------------------------------------------- /src/covjson_pydantic/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNMI/covjson-pydantic/932cb4700a2e530cb48b2dcd5d8f65692ed19746/src/covjson_pydantic/py.typed -------------------------------------------------------------------------------- /src/covjson_pydantic/reference_system.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from typing import List 3 | from typing import Literal 4 | from typing import Optional 5 | from typing import Union 6 | 7 | from pydantic import AnyUrl 8 | from pydantic import model_validator 9 | 10 | from .base_models import CovJsonBaseModel 11 | from .i18n import i18n 12 | 13 | 14 | class TargetConcept(CovJsonBaseModel): 15 | id: Optional[str] = None # Not in spec, but needed for example in spec for 'Identifier-based Reference Systems' 16 | label: i18n 17 | description: Optional[i18n] = None 18 | 19 | 20 | class ReferenceSystem(CovJsonBaseModel, extra="allow"): 21 | type: Literal["GeographicCRS", "ProjectedCRS", "VerticalCRS", "TemporalRS", "IdentifierRS"] 22 | id: Optional[str] = None 23 | description: Optional[i18n] = None 24 | 25 | # Only for TemporalRS 26 | calendar: Optional[Union[Literal["Gregorian"], AnyUrl]] = None 27 | timeScale: Optional[AnyUrl] = None # noqa: N815 28 | 29 | # Only for IdentifierRS 30 | label: Optional[i18n] = None 31 | targetConcept: Optional[TargetConcept] = None # noqa: N815 32 | identifiers: Optional[Dict[str, TargetConcept]] = None 33 | 34 | @model_validator(mode="after") 35 | def check_type_specific_fields(self): 36 | if self.type != "TemporalRS" and (self.calendar is not None or self.timeScale is not None): 37 | raise ValueError("'calendar' and 'timeScale' fields can only be used for type 'TemporalRS'") 38 | 39 | if self.type == "TemporalRS" and self.calendar is None: 40 | raise ValueError("A temporal RS object MUST have a member 'calendar' with value 'Gregorian' or a URI") 41 | 42 | if self.type != "IdentifierRS" and ( 43 | self.label is not None or self.targetConcept is not None or self.identifiers is not None 44 | ): 45 | raise ValueError( 46 | "'label', 'targetConcept' and 'identifiers' fields can only be used for type 'IdentifierRS'" 47 | ) 48 | 49 | if self.type == "IdentifierRS" and self.targetConcept is None: 50 | raise ValueError("An identifier RS object MUST have a member 'targetConcept'") 51 | 52 | return self 53 | 54 | 55 | class ReferenceSystemConnectionObject(CovJsonBaseModel): 56 | coordinates: List[str] 57 | system: ReferenceSystem 58 | -------------------------------------------------------------------------------- /src/covjson_pydantic/unit.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from typing import Union 3 | 4 | from pydantic import model_validator 5 | 6 | from .base_models import CovJsonBaseModel 7 | from .i18n import i18n 8 | 9 | 10 | class Symbol(CovJsonBaseModel): 11 | value: str 12 | type: str 13 | 14 | 15 | class Unit(CovJsonBaseModel): 16 | id: Optional[str] = None 17 | label: Optional[i18n] = None 18 | symbol: Optional[Union[str, Symbol]] = None 19 | 20 | @model_validator(mode="after") 21 | def check_either_label_or_symbol(self): 22 | if self.label is None and self.symbol is None: 23 | raise ValueError("Either 'label' or 'symbol' should be set") 24 | 25 | return self 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KNMI/covjson-pydantic/932cb4700a2e530cb48b2dcd5d8f65692ed19746/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_coverage.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from io import StringIO 4 | from pathlib import Path 5 | 6 | import pytest 7 | from covjson_pydantic.coverage import Coverage 8 | from covjson_pydantic.coverage import CoverageCollection 9 | from covjson_pydantic.domain import Axes 10 | from covjson_pydantic.domain import Domain 11 | from covjson_pydantic.ndarray import NdArray 12 | from covjson_pydantic.ndarray import NdArrayFloat 13 | from covjson_pydantic.ndarray import NdArrayInt 14 | from covjson_pydantic.ndarray import NdArrayStr 15 | from covjson_pydantic.ndarray import TiledNdArrayFloat 16 | from covjson_pydantic.parameter import Parameter 17 | from covjson_pydantic.parameter import ParameterGroup 18 | from covjson_pydantic.parameter import Parameters 19 | from covjson_pydantic.reference_system import ReferenceSystem 20 | from covjson_pydantic.reference_system import ReferenceSystemConnectionObject 21 | from pydantic import ValidationError 22 | 23 | 24 | happy_cases = [ 25 | ("spec-axes.json", Axes), 26 | ("str-axes.json", Axes), 27 | ("coverage-json.json", Coverage), 28 | ("coverage-mixed-type-ndarray.json", Coverage), 29 | ("doc-example-coverage.json", Coverage), 30 | ("example_py.json", Coverage), 31 | ("spec-vertical-profile-coverage.json", Coverage), 32 | ("spec-trajectory-coverage.json", Coverage), 33 | ("doc-example-coverage-collection.json", CoverageCollection), 34 | ("polygon-series-coverage-collection.json", CoverageCollection), 35 | ("grid-domain.json", Domain), 36 | ("point-series-domain-custom.json", Domain), 37 | ("spec-domain-grid.json", Domain), 38 | ("spec-domain-vertical-profile.json", Domain), 39 | ("spec-domain-point-series.json", Domain), 40 | ("spec-domain-point.json", Domain), 41 | ("spec-domain-point-compact.json", Domain), 42 | ("spec-domain-multipoint-series.json", Domain), 43 | ("spec-domain-multipoint.json", Domain), 44 | ("spec-domain-trajectory.json", Domain), 45 | ("spec-domain-polygon-series.json", Domain), 46 | ("ndarray-float.json", NdArrayFloat), 47 | ("ndarray-string.json", NdArrayStr), 48 | ("ndarray-integer.json", NdArrayInt), 49 | ("spec-ndarray.json", NdArrayFloat), 50 | ("spec-tiled-ndarray.json", TiledNdArrayFloat), 51 | ("continuous-data-parameter.json", Parameter), 52 | ("categorical-data-parameter.json", Parameter), 53 | ("parameters.json", Parameters), 54 | ("spec-parametergroup.json", ParameterGroup), 55 | ("spec-reference-system-identifierrs.json", ReferenceSystem), 56 | ] 57 | 58 | 59 | @pytest.mark.parametrize("file_name, object_type", happy_cases) 60 | def test_happy_cases(file_name, object_type): 61 | file = Path(__file__).parent.resolve() / "test_data" / file_name 62 | # Put JSON in default unindented format 63 | with open(file, "r") as f: 64 | data = json.load(f) 65 | json_string = json.dumps(data, separators=(",", ":"), ensure_ascii=False) 66 | 67 | # Round-trip 68 | assert object_type.model_validate_json(json_string).model_dump_json(exclude_none=True) == json_string 69 | 70 | 71 | error_cases = [ 72 | ("grid-domain-no-y.json", Domain, r"A 'Grid' must have a 'y'-axis"), 73 | ( 74 | "point-series-domain-more-z.json", 75 | Domain, 76 | r"If provided, the 'values' field of the ValuesAxis 'z'-axis of a 'PointSeries' " 77 | + "domain must contain a single value.", 78 | ), 79 | ("point-series-domain-no-t.json", Domain, r"A 'PointSeries' must have a 't'-axis."), 80 | ("mixed-type-axes.json", Axes, r"Input should be a valid number"), 81 | ("mixed-type-axes-2.json", Axes, r"Input should be a valid string"), 82 | ("mixed-type-ndarray-1.json", NdArrayFloat, r"Input should be a valid number"), 83 | ("mixed-type-ndarray-1.json", NdArrayStr, r"Input should be 'string'"), 84 | ("mixed-type-ndarray-2.json", NdArrayFloat, r"Input should be a valid number"), 85 | ("mixed-type-ndarray-2.json", NdArrayStr, r"Input should be 'string'"), 86 | ("mixed-type-ndarray-3.json", NdArrayInt, r"Input should be a valid integer"), 87 | ("mixed-type-ndarray-3.json", NdArrayFloat, r"Input should be 'float'"), 88 | ( 89 | "temporalrs-no-calendar.json", 90 | ReferenceSystemConnectionObject, 91 | r"A temporal RS object MUST have a member 'calendar'", 92 | ), 93 | ] 94 | 95 | 96 | @pytest.mark.parametrize("file_name, object_type, error_message", error_cases) 97 | def test_error_cases(file_name, object_type, error_message): 98 | file = Path(__file__).parent.resolve() / "test_data" / file_name 99 | # Put JSON in default unindented format 100 | with open(file, "r") as f: 101 | data = json.load(f) 102 | json_string = json.dumps(data, separators=(",", ":")) 103 | 104 | with pytest.raises(ValidationError, match=error_message): 105 | object_type.model_validate_json(json_string) 106 | 107 | 108 | def test_ndarray_directly(): 109 | with pytest.raises(TypeError, match="NdArray cannot be instantiated directly"): 110 | NdArray(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0]) 111 | 112 | 113 | def test_example_py(): 114 | file = Path(__file__).parent.parent.resolve() / "example.py" 115 | 116 | with open(file, "r") as f: 117 | code = f.read() 118 | 119 | old_stdout = sys.stdout 120 | sys.stdout = my_stdout = StringIO() 121 | exec(code) 122 | sys.stdout = old_stdout 123 | 124 | file = Path(__file__).parent.resolve() / "test_data" / "example_py.json" 125 | with open(file, "r") as f: 126 | assert my_stdout.getvalue() == f.read() 127 | 128 | 129 | def test_parameters_root_model(): 130 | file = Path(__file__).parent.resolve() / "test_data" / "parameters.json" 131 | with open(file, "r") as f: 132 | parameters = Parameters.model_validate_json(f.read()) 133 | 134 | assert parameters["PSAL"].observedProperty.label["en"] == "Sea Water Salinity" 135 | assert parameters.get("POTM").observedProperty.label["en"] == "Sea Water Potential Temperature" 136 | assert len([p for p in parameters]) == 2 137 | -------------------------------------------------------------------------------- /tests/test_data/categorical-data-parameter.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Parameter", 3 | "description": { 4 | "en": "The land cover category." 5 | }, 6 | "observedProperty": { 7 | "id": "http://example.com/land_cover", 8 | "label": { 9 | "en": "Land Cover" 10 | }, 11 | "description": { 12 | "en": "longer description..." 13 | }, 14 | "categories": [ 15 | { 16 | "id": "http://example.com/land_cover/categories/grass", 17 | "label": { 18 | "en": "Grass" 19 | }, 20 | "description": { 21 | "en": "Very green grass." 22 | } 23 | }, 24 | { 25 | "id": "http://example.com/land_cover/categories/forest", 26 | "label": { 27 | "en": "Forest" 28 | } 29 | } 30 | ] 31 | }, 32 | "categoryEncoding": { 33 | "http://example.com/land_cover/categories/grass": 1, 34 | "http://example.com/land_cover/categories/forest": [ 35 | 2, 36 | 3 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/test_data/continuous-data-parameter.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Parameter", 3 | "description": { 4 | "en": "The sea surface temperature in degrees Celsius." 5 | }, 6 | "observedProperty": { 7 | "id": "http://vocab.nerc.ac.uk/standard_name/sea_surface_temperature/", 8 | "label": { 9 | "en": "Sea Surface Temperature" 10 | }, 11 | "description": { 12 | "en": "The temperature of sea water near the surface (including the part under sea-ice, if any), and not the skin temperature." 13 | } 14 | }, 15 | "unit": { 16 | "label": { 17 | "en": "Degree Celsius" 18 | }, 19 | "symbol": { 20 | "value": "Cel", 21 | "type": "http://www.opengis.net/def/uom/UCUM/" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/test_data/coverage-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Coverage", 3 | "domain": { 4 | "type": "Domain", 5 | "domainType": "PointSeries", 6 | "axes": { 7 | "x": { 8 | "values": [ 9 | 5.3 10 | ] 11 | }, 12 | "y": { 13 | "values": [ 14 | 53.2 15 | ] 16 | }, 17 | "t": { 18 | "values": [ 19 | "2022-01-01T04:10:00Z", 20 | "2022-01-01T04:20:00Z", 21 | "2022-01-01T04:30:00Z", 22 | "2022-01-01T04:40:00Z", 23 | "2022-01-01T04:50:00Z", 24 | "2022-01-01T05:00:00Z" 25 | ] 26 | } 27 | } 28 | }, 29 | "parameters": { 30 | "temperature": { 31 | "type": "Parameter", 32 | "description": { 33 | "en": "This is the air temperature" 34 | }, 35 | "observedProperty": { 36 | "label": { 37 | "en": "temperature" 38 | } 39 | }, 40 | "unit": { 41 | "label": { 42 | "en": "Degree Celsius" 43 | }, 44 | "symbol": { 45 | "value": "Cel", 46 | "type": "http://www.opengis.net/def/uom/UCUM" 47 | } 48 | } 49 | }, 50 | "dewpoint": { 51 | "type": "Parameter", 52 | "description": { 53 | "en": "This is the air dewpoint" 54 | }, 55 | "observedProperty": { 56 | "label": { 57 | "en": "dewpoint" 58 | } 59 | }, 60 | "unit": { 61 | "label": { 62 | "en": "Degree Celsius" 63 | }, 64 | "symbol": { 65 | "value": "Cel", 66 | "type": "http://www.opengis.net/def/uom/UCUM" 67 | } 68 | } 69 | } 70 | }, 71 | "ranges": { 72 | "temperature": { 73 | "type": "NdArray", 74 | "dataType": "float", 75 | "axisNames": [ 76 | "x", 77 | "y", 78 | "t" 79 | ], 80 | "shape": [ 81 | 1, 82 | 1, 83 | 6 84 | ], 85 | "values": [ 86 | 64.27437704538298, 87 | 64.70702358086481, 88 | 65.13680141101983, 89 | 65.56289242989419, 90 | 65.98448554858814, 91 | 66.40077824091921 92 | ] 93 | }, 94 | "dewpoint": { 95 | "type": "NdArray", 96 | "dataType": "float", 97 | "axisNames": [ 98 | "x", 99 | "y", 100 | "t" 101 | ], 102 | "shape": [ 103 | 1, 104 | 1, 105 | 6 106 | ], 107 | "values": [ 108 | 62.27437704538298, 109 | 62.707023580864806, 110 | 63.136801411019825, 111 | 63.56289242989419, 112 | 63.98448554858814, 113 | 64.40077824091921 114 | ] 115 | } 116 | }, 117 | "extra:extra": "extra fields allowed" 118 | } 119 | -------------------------------------------------------------------------------- /tests/test_data/coverage-mixed-type-ndarray.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Coverage", 3 | "domain": { 4 | "type": "Domain", 5 | "domainType": "PointSeries", 6 | "axes": { 7 | "x": { 8 | "values": [ 9 | 5.3 10 | ] 11 | }, 12 | "y": { 13 | "values": [ 14 | 53.2 15 | ] 16 | }, 17 | "t": { 18 | "values": [ 19 | "2022-01-01T04:10:00Z", 20 | "2022-01-01T04:20:00Z", 21 | "2022-01-01T04:30:00Z" 22 | ] 23 | } 24 | } 25 | }, 26 | "parameters": { 27 | "float-parameter": { 28 | "type": "Parameter", 29 | "observedProperty": { 30 | "label": { 31 | "en": "float" 32 | } 33 | } 34 | }, 35 | "string-parameter": { 36 | "type": "Parameter", 37 | "observedProperty": { 38 | "label": { 39 | "en": "string" 40 | } 41 | } 42 | }, 43 | "integer-parameter": { 44 | "type": "Parameter", 45 | "observedProperty": { 46 | "label": { 47 | "en": "integer" 48 | } 49 | } 50 | }, 51 | "null-parameter": { 52 | "type": "Parameter", 53 | "observedProperty": { 54 | "label": { 55 | "en": "null" 56 | } 57 | } 58 | } 59 | }, 60 | "ranges": { 61 | "string-parameter": { 62 | "type": "NdArray", 63 | "dataType": "string", 64 | "axisNames": [ 65 | "x", 66 | "y", 67 | "t" 68 | ], 69 | "shape": [ 70 | 1, 71 | 1, 72 | 3 73 | ], 74 | "values": [ 75 | null, 76 | "foo", 77 | "bar" 78 | ] 79 | }, 80 | "float-parameter": { 81 | "type": "NdArray", 82 | "dataType": "float", 83 | "axisNames": [ 84 | "x", 85 | "y", 86 | "t" 87 | ], 88 | "shape": [ 89 | 1, 90 | 1, 91 | 3 92 | ], 93 | "values": [ 94 | 62.0, 95 | null, 96 | 63.136801411019825 97 | ] 98 | }, 99 | "integer-parameter": { 100 | "type": "NdArray", 101 | "dataType": "integer", 102 | "axisNames": [ 103 | "x", 104 | "y", 105 | "t" 106 | ], 107 | "shape": [ 108 | 1, 109 | 1, 110 | 3 111 | ], 112 | "values": [ 113 | 1, 114 | null, 115 | 3 116 | ] 117 | }, 118 | "null-parameter": { 119 | "type": "NdArray", 120 | "dataType": "integer", 121 | "axisNames": [ 122 | "x", 123 | "y", 124 | "t" 125 | ], 126 | "shape": [ 127 | 1, 128 | 1, 129 | 3 130 | ], 131 | "values": [ 132 | null, 133 | null, 134 | null 135 | ] 136 | } 137 | }, 138 | "extra:extra": "extra fields allowed" 139 | } 140 | -------------------------------------------------------------------------------- /tests/test_data/doc-example-coverage-collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "CoverageCollection", 3 | "domainType": "VerticalProfile", 4 | "coverages": [ 5 | { 6 | "type": "Coverage", 7 | "domain": { 8 | "type": "Domain", 9 | "axes": { 10 | "x": { 11 | "values": [ 12 | -10.1 13 | ] 14 | }, 15 | "y": { 16 | "values": [ 17 | -40.2 18 | ] 19 | }, 20 | "z": { 21 | "values": [ 22 | 5.0, 23 | 8.0, 24 | 14.0 25 | ] 26 | }, 27 | "t": { 28 | "values": [ 29 | "2013-01-13T11:12:20Z" 30 | ] 31 | } 32 | } 33 | }, 34 | "ranges": { 35 | "PSAL": { 36 | "type": "NdArray", 37 | "dataType": "float", 38 | "axisNames": [ 39 | "z" 40 | ], 41 | "shape": [ 42 | 3 43 | ], 44 | "values": [ 45 | 43.7, 46 | 43.8, 47 | 43.9 48 | ] 49 | } 50 | } 51 | }, 52 | { 53 | "type": "Coverage", 54 | "domain": { 55 | "type": "Domain", 56 | "axes": { 57 | "x": { 58 | "values": [ 59 | -11.1 60 | ] 61 | }, 62 | "y": { 63 | "values": [ 64 | -45.2 65 | ] 66 | }, 67 | "z": { 68 | "values": [ 69 | 4.0, 70 | 7.0, 71 | 9.0 72 | ] 73 | }, 74 | "t": { 75 | "values": [ 76 | "2013-01-13T12:12:20Z" 77 | ] 78 | } 79 | } 80 | }, 81 | "ranges": { 82 | "PSAL": { 83 | "type": "NdArray", 84 | "dataType": "float", 85 | "axisNames": [ 86 | "z" 87 | ], 88 | "shape": [ 89 | 3 90 | ], 91 | "values": [ 92 | 42.7, 93 | 41.8, 94 | 40.9 95 | ] 96 | } 97 | } 98 | } 99 | ], 100 | "parameters": { 101 | "PSAL": { 102 | "type": "Parameter", 103 | "description": { 104 | "en": "The measured salinity, in practical salinity units (psu) of the sea water" 105 | }, 106 | "observedProperty": { 107 | "id": "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/", 108 | "label": { 109 | "en": "Sea Water Salinity" 110 | } 111 | }, 112 | "unit": { 113 | "symbol": "psu" 114 | } 115 | } 116 | }, 117 | "referencing": [ 118 | { 119 | "coordinates": [ 120 | "x", 121 | "y" 122 | ], 123 | "system": { 124 | "type": "GeographicCRS", 125 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 126 | } 127 | }, 128 | { 129 | "coordinates": [ 130 | "z" 131 | ], 132 | "system": { 133 | "type": "VerticalCRS", 134 | "cs": { 135 | "csAxes": [ 136 | { 137 | "name": { 138 | "en": "Pressure" 139 | }, 140 | "direction": "down", 141 | "unit": { 142 | "symbol": "Pa" 143 | } 144 | } 145 | ] 146 | } 147 | } 148 | }, 149 | { 150 | "coordinates": [ 151 | "t" 152 | ], 153 | "system": { 154 | "type": "TemporalRS", 155 | "calendar": "Gregorian" 156 | } 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /tests/test_data/doc-example-coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Coverage", 3 | "domain": { 4 | "type": "Domain", 5 | "domainType": "Grid", 6 | "axes": { 7 | "x": { 8 | "start": -179.5, 9 | "stop": 179.5, 10 | "num": 360 11 | }, 12 | "y": { 13 | "start": -89.5, 14 | "stop": 89.5, 15 | "num": 180 16 | }, 17 | "t": { 18 | "values": [ 19 | "2013-01-13T00:00:00Z" 20 | ] 21 | } 22 | }, 23 | "referencing": [ 24 | { 25 | "coordinates": [ 26 | "x", 27 | "y" 28 | ], 29 | "system": { 30 | "type": "GeographicCRS", 31 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 32 | } 33 | }, 34 | { 35 | "coordinates": [ 36 | "t" 37 | ], 38 | "system": { 39 | "type": "TemporalRS", 40 | "calendar": "Gregorian" 41 | } 42 | } 43 | ] 44 | }, 45 | "parameters": { 46 | "TEMP": { 47 | "type": "Parameter", 48 | "description": { 49 | "en": "The air temperature measured in degrees Celsius." 50 | }, 51 | "observedProperty": { 52 | "id": "http://vocab.nerc.ac.uk/standard_name/air_temperature/", 53 | "label": { 54 | "en": "Air temperature", 55 | "de": "Lufttemperatur" 56 | } 57 | }, 58 | "unit": { 59 | "label": { 60 | "en": "Degree Celsius" 61 | }, 62 | "symbol": { 63 | "value": "Cel", 64 | "type": "http://www.opengis.net/def/uom/UCUM/" 65 | } 66 | } 67 | } 68 | }, 69 | "ranges": { 70 | "TEMP": "http://example.com/coverages/123/TEMP" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/test_data/example_py.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Coverage", 3 | "domain": { 4 | "type": "Domain", 5 | "domainType": "PointSeries", 6 | "axes": { 7 | "x": { 8 | "values": [ 9 | 1.23 10 | ] 11 | }, 12 | "y": { 13 | "values": [ 14 | 4.56 15 | ] 16 | }, 17 | "t": { 18 | "values": [ 19 | "2024-08-01T00:00:00Z" 20 | ] 21 | } 22 | } 23 | }, 24 | "ranges": { 25 | "temperature": { 26 | "type": "NdArray", 27 | "dataType": "float", 28 | "axisNames": [ 29 | "x", 30 | "y", 31 | "t" 32 | ], 33 | "shape": [ 34 | 1, 35 | 1, 36 | 1 37 | ], 38 | "values": [ 39 | 42.0 40 | ] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/test_data/grid-domain-no-y.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "Grid", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 1.0, 8 | 2.0, 9 | 3.0 10 | ] 11 | }, 12 | "z": { 13 | "values": [ 14 | 1.0 15 | ] 16 | }, 17 | "t": { 18 | "values": [ 19 | "2008-01-01T04:00:00Z" 20 | ] 21 | } 22 | }, 23 | "referencing": [ 24 | { 25 | "coordinates": [ 26 | "t" 27 | ], 28 | "system": { 29 | "type": "TemporalRS", 30 | "calendar": "Gregorian" 31 | } 32 | }, 33 | { 34 | "coordinates": [ 35 | "x", 36 | "z" 37 | ], 38 | "system": { 39 | "type": "GeographicCRS", 40 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /tests/test_data/grid-domain.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "Grid", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 1.0, 8 | 2.0, 9 | 3.0 10 | ] 11 | }, 12 | "y": { 13 | "values": [ 14 | 20.0, 15 | 21.0 16 | ] 17 | }, 18 | "z": { 19 | "values": [ 20 | 1.0 21 | ] 22 | }, 23 | "t": { 24 | "values": [ 25 | "2008-01-01T04:00:00Z", 26 | "2008-01-01T05:00:00Z", 27 | "2008-01-01T06:00:00Z" 28 | ] 29 | } 30 | }, 31 | "referencing": [ 32 | { 33 | "coordinates": [ 34 | "t" 35 | ], 36 | "system": { 37 | "type": "TemporalRS", 38 | "calendar": "Gregorian" 39 | } 40 | }, 41 | { 42 | "coordinates": [ 43 | "y", 44 | "x", 45 | "z" 46 | ], 47 | "system": { 48 | "type": "GeographicCRS", 49 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /tests/test_data/mixed-type-axes-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "x": { 3 | "values": [ 4 | "foo", 5 | 42.0 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_data/mixed-type-axes.json: -------------------------------------------------------------------------------- 1 | { 2 | "x": { 3 | "values": [ 4 | 42.0, 5 | "foo" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_data/mixed-type-ndarray-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "NdArray", 3 | "dataType": "float", 4 | "axisNames": [ 5 | "y", 6 | "x" 7 | ], 8 | "shape": [ 9 | 2 10 | ], 11 | "values": [ 12 | "42.0", 13 | 123 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/test_data/mixed-type-ndarray-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "NdArray", 3 | "dataType": "float", 4 | "axisNames": [ 5 | "y", 6 | "x" 7 | ], 8 | "shape": [ 9 | 2 10 | ], 11 | "values": [ 12 | "foo", 13 | "bar" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/test_data/mixed-type-ndarray-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "NdArray", 3 | "dataType": "integer", 4 | "axisNames": [ 5 | "y", 6 | "x" 7 | ], 8 | "shape": [ 9 | 2 10 | ], 11 | "values": [ 12 | 1, 13 | 1.42 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/test_data/ndarray-float.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "NdArray", 3 | "dataType": "float", 4 | "axisNames": [ 5 | "t", 6 | "y", 7 | "x" 8 | ], 9 | "shape": [ 10 | 1, 11 | 2, 12 | 3 13 | ], 14 | "values": [ 15 | 27.1, 16 | 24.1, 17 | null, 18 | 25.1, 19 | 26.7, 20 | 23.2 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/test_data/ndarray-integer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "NdArray", 3 | "dataType": "integer", 4 | "axisNames": [ 5 | "t", 6 | "y", 7 | "x" 8 | ], 9 | "shape": [ 10 | 1, 11 | 1, 12 | 3 13 | ], 14 | "values": [ 15 | 1, 16 | 2, 17 | 42 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/test_data/ndarray-string.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "NdArray", 3 | "dataType": "string", 4 | "axisNames": [ 5 | "t", 6 | "y", 7 | "x" 8 | ], 9 | "shape": [ 10 | 1, 11 | 2, 12 | 3 13 | ], 14 | "values": [ 15 | "ABC", 16 | "DEF", 17 | null, 18 | "XYZ", 19 | "a123", 20 | "qwerty" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/test_data/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "PSAL": { 3 | "type": "Parameter", 4 | "description": { 5 | "en": "The measured salinity, in practical salinity units (psu) of the sea water" 6 | }, 7 | "observedProperty": { 8 | "id": "https://vocab.nerc.ac.uk/standard_name/sea_water_salinity/", 9 | "label": { 10 | "en": "Sea Water Salinity" 11 | } 12 | }, 13 | "unit": { 14 | "symbol": "psu" 15 | } 16 | }, 17 | "POTM": { 18 | "type": "Parameter", 19 | "description": { 20 | "en": "The potential temperature, in degrees Celsius, of the sea water" 21 | }, 22 | "observedProperty": { 23 | "id": "https://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/", 24 | "label": { 25 | "en": "Sea Water Potential Temperature" 26 | } 27 | }, 28 | "unit": { 29 | "symbol": "°C" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_data/point-series-domain-custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "PointSeries", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 5.3 8 | ] 9 | }, 10 | "y": { 11 | "values": [ 12 | 53.2 13 | ] 14 | }, 15 | "t": { 16 | "dataType": "knmi:range", 17 | "values": [ 18 | "2022-01-01T04:03:00Z", 19 | "2022-01-01T05:09:00Z" 20 | ], 21 | "knmi:num": 10 22 | } 23 | }, 24 | "referencing": [ 25 | { 26 | "coordinates": [ 27 | "t" 28 | ], 29 | "system": { 30 | "type": "TemporalRS", 31 | "calendar": "Gregorian" 32 | } 33 | }, 34 | { 35 | "coordinates": [ 36 | "y", 37 | "x" 38 | ], 39 | "system": { 40 | "type": "GeographicCRS", 41 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /tests/test_data/point-series-domain-more-z.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "PointSeries", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 5.3 8 | ] 9 | }, 10 | "y": { 11 | "values": [ 12 | 53.2 13 | ] 14 | }, 15 | "z": { 16 | "values": [ 17 | 53.2, 18 | 54.2 19 | ] 20 | }, 21 | "t": { 22 | "values": [ 23 | "2022-01-01T04:03:00Z", 24 | "2022-01-01T05:09:00Z" 25 | ] 26 | } 27 | }, 28 | "referencing": [ 29 | { 30 | "coordinates": [ 31 | "t" 32 | ], 33 | "system": { 34 | "type": "TemporalRS", 35 | "calendar": "Gregorian" 36 | } 37 | }, 38 | { 39 | "coordinates": [ 40 | "y", 41 | "x", 42 | "z" 43 | ], 44 | "system": { 45 | "type": "GeographicCRS", 46 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 47 | } 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /tests/test_data/point-series-domain-no-t.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "PointSeries", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 5.3 8 | ] 9 | }, 10 | "y": { 11 | "values": [ 12 | 53.2 13 | ] 14 | } 15 | }, 16 | "referencing": [ 17 | { 18 | "coordinates": [ 19 | "y", 20 | "x" 21 | ], 22 | "system": { 23 | "type": "GeographicCRS", 24 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tests/test_data/polygon-series-coverage-collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "CoverageCollection", 3 | "coverages": [ 4 | { 5 | "type": "Coverage", 6 | "domain": { 7 | "type": "Domain", 8 | "domainType": "PolygonSeries", 9 | "axes": { 10 | "t": { 11 | "values": [ 12 | "2016-01-01T00:00:00Z", 13 | "2016-02-01T00:00:00Z", 14 | "2016-03-01T00:00:00Z" 15 | ] 16 | }, 17 | "composite": { 18 | "dataType": "polygon", 19 | "coordinates": [ 20 | "x", 21 | "y" 22 | ], 23 | "values": [ 24 | [ 25 | [ 26 | [ 27 | -105.67217, 28 | 36.02425 29 | ], 30 | [ 31 | -105.88091, 32 | 35.24744 33 | ], 34 | [ 35 | -105.01299, 36 | 32.0286 37 | ], 38 | [ 39 | -103.54082, 40 | 32.07516 41 | ], 42 | [ 43 | -104.06816, 44 | 34.75247 45 | ], 46 | [ 47 | -105.67217, 48 | 36.02425 49 | ] 50 | ] 51 | ] 52 | ] 53 | } 54 | } 55 | }, 56 | "ranges": { 57 | "Lake/Reservoir Storage End of Month": { 58 | "type": "NdArray", 59 | "dataType": "float", 60 | "axisNames": [ 61 | "t" 62 | ], 63 | "shape": [ 64 | 3 65 | ], 66 | "values": [ 67 | 1.0, 68 | 2.0, 69 | 42.0 70 | ] 71 | } 72 | } 73 | } 74 | ], 75 | "parameters": { 76 | "Lake/Reservoir Storage End of Month": { 77 | "type": "Parameter", 78 | "description": { 79 | "en": "Instant daily lake/reservoir storage volume in acre-feet. Monthly refers to one measurement on the last day of each month." 80 | }, 81 | "observedProperty": { 82 | "id": "1470", 83 | "label": { 84 | "en": "Lake/Reservoir Storage End of Month" 85 | } 86 | }, 87 | "unit": { 88 | "symbol": "af" 89 | } 90 | } 91 | }, 92 | "referencing": [ 93 | { 94 | "coordinates": [ 95 | "x", 96 | "y" 97 | ], 98 | "system": { 99 | "type": "GeographicCRS", 100 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 101 | } 102 | }, 103 | { 104 | "coordinates": [ 105 | "z" 106 | ], 107 | "system": { 108 | "type": "VerticalCRS", 109 | "cs": { 110 | "csAxes": [ 111 | { 112 | "name": { 113 | "en": "time" 114 | }, 115 | "direction": "down", 116 | "unit": { 117 | "symbol": "time" 118 | } 119 | } 120 | ] 121 | } 122 | } 123 | }, 124 | { 125 | "coordinates": [ 126 | "t" 127 | ], 128 | "system": { 129 | "type": "TemporalRS", 130 | "calendar": "Gregorian" 131 | } 132 | } 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /tests/test_data/spec-axes.json: -------------------------------------------------------------------------------- 1 | { 2 | "x": { 3 | "values": [ 4 | 20.0, 5 | 21.0 6 | ], 7 | "bounds": [ 8 | 19.5, 9 | 20.5, 10 | 20.5, 11 | 21.5 12 | ] 13 | }, 14 | "y": { 15 | "start": 0.0, 16 | "stop": 5.0, 17 | "num": 6 18 | }, 19 | "t": { 20 | "values": [ 21 | "2008-01-01T04:00:00Z", 22 | "2008-01-02T04:00:00Z" 23 | ] 24 | }, 25 | "composite": { 26 | "dataType": "tuple", 27 | "coordinates": [ 28 | "t", 29 | "x", 30 | "y" 31 | ], 32 | "values": [ 33 | [ 34 | "2008-01-01T04:00:00Z", 35 | 1, 36 | 20 37 | ], 38 | [ 39 | "2008-01-01T04:30:00Z", 40 | 2, 41 | 21 42 | ] 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-grid.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "Grid", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 1.0, 8 | 2.0, 9 | 3.0 10 | ] 11 | }, 12 | "y": { 13 | "values": [ 14 | 20.0, 15 | 21.0 16 | ] 17 | }, 18 | "z": { 19 | "values": [ 20 | 1.0 21 | ] 22 | }, 23 | "t": { 24 | "values": [ 25 | "2008-01-01T04:00:00Z" 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-multipoint-series.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "MultiPointSeries", 4 | "axes": { 5 | "t": { 6 | "values": [ 7 | "2008-01-01T04:00:00Z", 8 | "2008-01-01T05:00:00Z" 9 | ] 10 | }, 11 | "composite": { 12 | "dataType": "tuple", 13 | "coordinates": [ 14 | "x", 15 | "y", 16 | "z" 17 | ], 18 | "values": [ 19 | [ 20 | 1, 21 | 20, 22 | 1 23 | ], 24 | [ 25 | 2, 26 | 21, 27 | 3 28 | ] 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-multipoint.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "MultiPoint", 4 | "axes": { 5 | "t": { 6 | "values": [ 7 | "2008-01-01T04:00:00Z" 8 | ] 9 | }, 10 | "composite": { 11 | "dataType": "tuple", 12 | "coordinates": [ 13 | "x", 14 | "y", 15 | "z" 16 | ], 17 | "values": [ 18 | [ 19 | 1.0, 20 | 20.0, 21 | 1.0 22 | ], 23 | [ 24 | 2.0, 25 | 21.0, 26 | 3.0 27 | ] 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-point-compact.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "Point", 4 | "axes": { 5 | "x": { 6 | "start": 1.0, 7 | "stop": 1.0, 8 | "num": 1 9 | }, 10 | "y": { 11 | "start": 20.0, 12 | "stop": 20.0, 13 | "num": 1 14 | }, 15 | "z": { 16 | "start": 1.8, 17 | "stop": 1.8, 18 | "num": 1 19 | }, 20 | "t": { 21 | "values": [ 22 | "2008-01-01T04:00:00Z" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-point-series.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "PointSeries", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 1.0 8 | ] 9 | }, 10 | "y": { 11 | "values": [ 12 | 20.0 13 | ] 14 | }, 15 | "z": { 16 | "values": [ 17 | 1.0 18 | ] 19 | }, 20 | "t": { 21 | "values": [ 22 | "2008-01-01T04:00:00Z", 23 | "2008-01-01T05:00:00Z" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-point.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "Point", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 1.0 8 | ] 9 | }, 10 | "y": { 11 | "values": [ 12 | 20.0 13 | ] 14 | }, 15 | "z": { 16 | "values": [ 17 | 1.8 18 | ] 19 | }, 20 | "t": { 21 | "values": [ 22 | "2008-01-01T04:00:00Z" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-polygon-series.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "PolygonSeries", 4 | "axes": { 5 | "z": { 6 | "values": [ 7 | 2.0 8 | ] 9 | }, 10 | "t": { 11 | "values": [ 12 | "2008-01-01T04:00:00Z", 13 | "2008-01-01T05:00:00Z" 14 | ] 15 | }, 16 | "composite": { 17 | "dataType": "polygon", 18 | "coordinates": [ 19 | "x", 20 | "y" 21 | ], 22 | "values": [ 23 | [ 24 | [ 25 | [ 26 | 100.0, 27 | 0.0 28 | ], 29 | [ 30 | 101.0, 31 | 0.0 32 | ], 33 | [ 34 | 101.0, 35 | 1.0 36 | ], 37 | [ 38 | 100.0, 39 | 1.0 40 | ], 41 | [ 42 | 100.0, 43 | 0.0 44 | ] 45 | ] 46 | ] 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-trajectory.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "Trajectory", 4 | "axes": { 5 | "composite": { 6 | "dataType": "tuple", 7 | "coordinates": [ 8 | "t", 9 | "x", 10 | "y" 11 | ], 12 | "values": [ 13 | [ 14 | "2008-01-01T04:00:00Z", 15 | 1, 16 | 20 17 | ], 18 | [ 19 | "2008-01-01T04:30:00Z", 20 | 2, 21 | 21 22 | ] 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/test_data/spec-domain-vertical-profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Domain", 3 | "domainType": "VerticalProfile", 4 | "axes": { 5 | "x": { 6 | "values": [ 7 | 1.0 8 | ] 9 | }, 10 | "y": { 11 | "values": [ 12 | 21.0 13 | ] 14 | }, 15 | "z": { 16 | "values": [ 17 | 1.0, 18 | 5.0, 19 | 20.0 20 | ] 21 | }, 22 | "t": { 23 | "values": [ 24 | "2008-01-01T04:00:00Z" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/test_data/spec-ndarray.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "NdArray", 3 | "dataType": "float", 4 | "axisNames": [ 5 | "y", 6 | "x" 7 | ], 8 | "shape": [ 9 | 4, 10 | 2 11 | ], 12 | "values": [ 13 | 12.3, 14 | 12.5, 15 | 11.5, 16 | 23.1, 17 | null, 18 | null, 19 | 10.1, 20 | 9.1 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/test_data/spec-parametergroup.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "ParameterGroup", 3 | "label": { 4 | "en": "Daily sea surface temperature with uncertainty information" 5 | }, 6 | "observedProperty": { 7 | "id": "http://vocab.nerc.ac.uk/standard_name/sea_surface_temperature/", 8 | "label": { 9 | "en": "Sea surface temperature" 10 | } 11 | }, 12 | "members": [ 13 | "SST_mean", 14 | "SST_stddev" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/test_data/spec-reference-system-identifierrs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "IdentifierRS", 3 | "id": "https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2", 4 | "label": { 5 | "en": "ISO 3166-1 alpha-2 codes" 6 | }, 7 | "targetConcept": { 8 | "id": "http://dbpedia.org/resource/Country", 9 | "label": { 10 | "en": "Country", 11 | "de": "Land" 12 | } 13 | }, 14 | "identifiers": { 15 | "de": { 16 | "id": "http://dbpedia.org/resource/Germany", 17 | "label": { 18 | "de": "Deutschland", 19 | "en": "Germany" 20 | } 21 | }, 22 | "gb": { 23 | "id": "http://dbpedia.org/resource/United_Kingdom", 24 | "label": { 25 | "de": "Vereinigtes Konigreich", 26 | "en": "United Kingdom" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/test_data/spec-tiled-ndarray.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "TiledNdArray", 3 | "dataType": "float", 4 | "axisNames": [ 5 | "t", 6 | "y", 7 | "x" 8 | ], 9 | "shape": [ 10 | 2, 11 | 5, 12 | 10 13 | ], 14 | "tileSets": [ 15 | { 16 | "tileShape": [ 17 | null, 18 | null, 19 | null 20 | ], 21 | "urlTemplate": "http://example.com/a/all.covjson" 22 | }, 23 | { 24 | "tileShape": [ 25 | 1, 26 | null, 27 | null 28 | ], 29 | "urlTemplate": "http://example.com/b/{t}.covjson" 30 | }, 31 | { 32 | "tileShape": [ 33 | null, 34 | 2, 35 | 3 36 | ], 37 | "urlTemplate": "http://example.com/c/{y}-{x}.covjson" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /tests/test_data/spec-trajectory-coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Coverage", 3 | "domain": { 4 | "type": "Domain", 5 | "domainType": "Trajectory", 6 | "axes": { 7 | "composite": { 8 | "dataType": "tuple", 9 | "coordinates": [ 10 | "t", 11 | "x", 12 | "y", 13 | "z" 14 | ], 15 | "values": [ 16 | [ 17 | "2008-01-01T04:00:00Z", 18 | 1, 19 | 20, 20 | 1 21 | ], 22 | [ 23 | "2008-01-01T04:30:00Z", 24 | 2, 25 | 21, 26 | 3 27 | ] 28 | ] 29 | } 30 | }, 31 | "referencing": [ 32 | { 33 | "coordinates": [ 34 | "x", 35 | "y" 36 | ], 37 | "system": { 38 | "type": "GeographicCRS", 39 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 40 | } 41 | }, 42 | { 43 | "coordinates": [ 44 | "z" 45 | ], 46 | "system": { 47 | "type": "VerticalCRS", 48 | "cs": { 49 | "csAxes": [ 50 | { 51 | "name": { 52 | "en": "Pressure" 53 | }, 54 | "direction": "down", 55 | "unit": { 56 | "symbol": "Pa" 57 | } 58 | } 59 | ] 60 | } 61 | } 62 | }, 63 | { 64 | "coordinates": [ 65 | "t" 66 | ], 67 | "system": { 68 | "type": "TemporalRS", 69 | "calendar": "Gregorian" 70 | } 71 | } 72 | ] 73 | }, 74 | "parameters": { 75 | "temperature": { 76 | "type": "Parameter", 77 | "description": { 78 | "en": "This is the air temperature" 79 | }, 80 | "observedProperty": { 81 | "label": { 82 | "en": "temperature" 83 | } 84 | }, 85 | "unit": { 86 | "label": { 87 | "en": "Degree Celsius" 88 | }, 89 | "symbol": { 90 | "value": "Cel", 91 | "type": "http://www.opengis.net/def/uom/UCUM" 92 | } 93 | } 94 | } 95 | }, 96 | "ranges": { 97 | "temperature": { 98 | "type": "NdArray", 99 | "dataType": "float", 100 | "axisNames": [ 101 | "composite" 102 | ], 103 | "shape": [ 104 | 2 105 | ], 106 | "values": [ 107 | 10.1, 108 | 11.3 109 | ] 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/test_data/spec-vertical-profile-coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Coverage", 3 | "domain": { 4 | "type": "Domain", 5 | "domainType": "VerticalProfile", 6 | "axes": { 7 | "x": { 8 | "values": [ 9 | -10.1 10 | ] 11 | }, 12 | "y": { 13 | "values": [ 14 | -40.2 15 | ] 16 | }, 17 | "z": { 18 | "values": [ 19 | 5.4562, 20 | 8.9282, 21 | 14.8802, 22 | 20.832, 23 | 26.7836, 24 | 32.735, 25 | 38.6863, 26 | 44.6374, 27 | 50.5883, 28 | 56.5391, 29 | 62.4897, 30 | 68.4401, 31 | 74.3903, 32 | 80.3404, 33 | 86.2902, 34 | 92.24, 35 | 98.1895, 36 | 104.1389, 37 | 110.0881, 38 | 116.0371, 39 | 121.9859 40 | ] 41 | }, 42 | "t": { 43 | "values": [ 44 | "2013-01-13T11:12:20Z" 45 | ] 46 | } 47 | }, 48 | "referencing": [ 49 | { 50 | "coordinates": [ 51 | "x", 52 | "y" 53 | ], 54 | "system": { 55 | "type": "GeographicCRS", 56 | "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" 57 | } 58 | }, 59 | { 60 | "coordinates": [ 61 | "z" 62 | ], 63 | "system": { 64 | "type": "VerticalCRS", 65 | "cs": { 66 | "csAxes": [ 67 | { 68 | "name": { 69 | "en": "Pressure" 70 | }, 71 | "direction": "down", 72 | "unit": { 73 | "symbol": "Pa" 74 | } 75 | } 76 | ] 77 | } 78 | } 79 | }, 80 | { 81 | "coordinates": [ 82 | "t" 83 | ], 84 | "system": { 85 | "type": "TemporalRS", 86 | "calendar": "Gregorian" 87 | } 88 | } 89 | ] 90 | }, 91 | "parameters": { 92 | "PSAL": { 93 | "type": "Parameter", 94 | "description": { 95 | "en": "The measured salinity, in practical salinity units (psu) of the sea water" 96 | }, 97 | "observedProperty": { 98 | "id": "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/", 99 | "label": { 100 | "en": "Sea Water Salinity" 101 | } 102 | }, 103 | "unit": { 104 | "symbol": "psu" 105 | } 106 | }, 107 | "POTM": { 108 | "type": "Parameter", 109 | "description": { 110 | "en": "The potential temperature, in degrees celcius, of the sea water" 111 | }, 112 | "observedProperty": { 113 | "id": "http://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/", 114 | "label": { 115 | "en": "Sea Water Potential Temperature" 116 | } 117 | }, 118 | "unit": { 119 | "symbol": "deg C" 120 | } 121 | } 122 | }, 123 | "ranges": { 124 | "PSAL": { 125 | "type": "NdArray", 126 | "dataType": "float", 127 | "axisNames": [ 128 | "z" 129 | ], 130 | "shape": [ 131 | 21 132 | ], 133 | "values": [ 134 | 43.9599, 135 | 43.9599, 136 | 43.964, 137 | 43.964, 138 | 43.9679, 139 | 43.9879, 140 | 44.004, 141 | 44.012, 142 | 44.012, 143 | 44.0159, 144 | 44.032, 145 | 44.032, 146 | 44.048, 147 | 44.0559, 148 | 44.0559, 149 | 44.0579, 150 | 44.068, 151 | 44.074, 152 | 44.0779, 153 | 44.088, 154 | 44.094 155 | ] 156 | }, 157 | "POTM": { 158 | "type": "NdArray", 159 | "dataType": "float", 160 | "axisNames": [ 161 | "z" 162 | ], 163 | "shape": [ 164 | 21 165 | ], 166 | "values": [ 167 | 23.8, 168 | 23.7, 169 | 23.5, 170 | 23.4, 171 | 23.2, 172 | 22.4, 173 | 21.8, 174 | 21.7, 175 | 21.5, 176 | 21.3, 177 | 21.0, 178 | 20.6, 179 | 20.1, 180 | 19.7, 181 | 19.4, 182 | 19.1, 183 | 18.9, 184 | 18.8, 185 | 18.7, 186 | 18.6, 187 | 18.5 188 | ] 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/test_data/str-axes.json: -------------------------------------------------------------------------------- 1 | { 2 | "x": { 3 | "values": [ 4 | "foo", 5 | "bar" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_data/temporalrs-no-calendar.json: -------------------------------------------------------------------------------- 1 | { 2 | "coordinates": [ 3 | "t" 4 | ], 5 | "system": { 6 | "type": "TemporalRS", 7 | "id": "http://www.opengis.net/def/crs/OGC/1.3/" 8 | } 9 | } 10 | --------------------------------------------------------------------------------