Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integer and string support for NdArray values (second attempt) #20

Merged
merged 23 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
75a904c
Add string support to NdArray.
PaulVanSchayck Nov 14, 2024
4ade6d1
Improve mixed type test cases
PaulVanSchayck Nov 14, 2024
a25348a
Add integer type for NdArray
PaulVanSchayck Nov 14, 2024
685c767
Add coverage-mixed-type-ndarray.json
PaulVanSchayck Nov 14, 2024
6df19b7
Make NdArray.dataType required. Add test case for NdArray with all nu…
PaulVanSchayck Nov 14, 2024
4a0ecc1
Default value for values should be []
PaulVanSchayck Nov 14, 2024
f4ec066
Fancy logic to auto specify NdArray.dataType from NdArrayType
PaulVanSchayck Nov 14, 2024
94c14fe
Update README
PaulVanSchayck Nov 14, 2024
8a37e6c
Allow None dataType
PaulVanSchayck Nov 14, 2024
4fffc8b
Support Python 3.8
PaulVanSchayck Nov 14, 2024
2ae9ec5
Add tests for set_datatype
PaulVanSchayck Nov 14, 2024
329d0f7
Even simpler form to set NdArray types
PaulVanSchayck Nov 15, 2024
69241ee
Rewrite TiledNdArray to TiledNdArrayFloat for consistency
PaulVanSchayck Nov 15, 2024
ec4cbba
Make coverage-mixed-type-ndarray.json according to spec
PaulVanSchayck Nov 15, 2024
dd67e0b
Handle more cases in tests
PaulVanSchayck Nov 15, 2024
d33e25f
Bump to version 0.5.0
PaulVanSchayck Nov 18, 2024
a764d6d
Update TODOs in README
PaulVanSchayck Nov 18, 2024
75d0de4
Use discriminated union to speed up parsing of JSON.
PaulVanSchayck Nov 19, 2024
e9acfdb
Fix example
PaulVanSchayck Nov 19, 2024
bc24fe2
Fix for Python 3.8
PaulVanSchayck Nov 19, 2024
ce0d5a0
Better fix for Python 3.8
PaulVanSchayck Nov 19, 2024
7a6dda3
Add test to test example.py
PaulVanSchayck Nov 22, 2024
831727c
Add file to test performance
PaulVanSchayck Nov 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ from datetime import datetime, timezone
from pydantic import AwareDatetime
from covjson_pydantic.coverage import Coverage
from covjson_pydantic.domain import Domain, Axes, ValuesAxis, DomainType
from covjson_pydantic.ndarray import NdArray
from covjson_pydantic.ndarray import NdArrayFloat

c = Coverage(
domain=Domain(
domainType=DomainType.point_series,
axes=Axes(
x=ValuesAxis[float](values=[1.23]),
y=ValuesAxis[float](values=[4.56]),
t=ValuesAxis[AwareDatetime](values=[datetime.now(tz=timezone.utc)])
)
t=ValuesAxis[AwareDatetime](values=[datetime(2024, 8, 1, tzinfo=timezone.utc)]),
),
),
ranges={
"temperature": NdArray(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0])
"temperature": NdArrayFloat(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0])
}
)

Expand All @@ -77,7 +77,7 @@ Will print
},
"t": {
"values": [
"2023-09-14T11:54:02.151493Z"
"2024-08-01T00:00:00Z"
]
}
}
Expand Down Expand Up @@ -140,8 +140,7 @@ This library is used to build an OGC Environmental Data Retrieval (EDR) API, ser
## TODOs
Help is wanted in the following areas to fully implement the CovJSON spec:
* The polygon based domain types are not supported.
* The `Trajectory` and `Section` domain type are not supported.
* The `NdArray` only supports `float` data.
* The `Section` domain type is not supported.
* Not all requirements in the spec relating different fields are implemented.

## License
Expand Down
6 changes: 3 additions & 3 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from covjson_pydantic.domain import Domain
from covjson_pydantic.domain import DomainType
from covjson_pydantic.domain import ValuesAxis
from covjson_pydantic.ndarray import NdArray
from covjson_pydantic.ndarray import NdArrayFloat
from pydantic import AwareDatetime

c = Coverage(
Expand All @@ -15,10 +15,10 @@
axes=Axes(
x=ValuesAxis[float](values=[1.23]),
y=ValuesAxis[float](values=[4.56]),
t=ValuesAxis[AwareDatetime](values=[datetime.now(tz=timezone.utc)]),
t=ValuesAxis[AwareDatetime](values=[datetime(2024, 8, 1, tzinfo=timezone.utc)]),
),
),
ranges={"temperature": NdArray(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0])},
ranges={"temperature": NdArrayFloat(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0])},
)

print(c.model_dump_json(exclude_none=True, indent=4))
22 changes: 22 additions & 0 deletions performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import timeit
from pathlib import Path

filename = Path(__file__).parent.resolve() / "tests" / "test_data" / "coverage-json.json"

setup = f"""
import json
from covjson_pydantic.coverage import Coverage

file = "{filename}"
# Put JSON in default unindented format
with open(file, "r") as f:
data = json.load(f)
json_string = json.dumps(data, separators=(",", ":"))
cj = Coverage.model_validate_json(json_string)
"""

# This can be used to quickly check performance. The first call checks JSON to Python conversion
# The second call checks Python to JSON conversion
# Consider generating a larger CoverageJSON file
print(timeit.timeit("Coverage.model_validate_json(json_string)", setup, number=1000))
print(timeit.timeit("cj.model_dump_json(exclude_none=True)", setup, number=1000))
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ classifiers = [
"Topic :: Scientific/Engineering :: GIS",
"Typing :: Typed",
]
version = "0.4.0"
version = "0.5.0"
dependencies = ["pydantic>=2.3,<3"]

[project.optional-dependencies]
Expand Down
18 changes: 15 additions & 3 deletions src/covjson_pydantic/coverage.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
import sys

if sys.version_info < (3, 9):
from typing_extensions import Annotated

Check warning on line 4 in src/covjson_pydantic/coverage.py

View check run for this annotation

Codecov / codecov/patch

src/covjson_pydantic/coverage.py#L4

Added line #L4 was not covered by tests
else:
from typing import Annotated

from typing import Dict
from typing import List
from typing import Literal
from typing import Optional
from typing import Union

from pydantic import AnyUrl
from pydantic import Field

from .base_models import CovJsonBaseModel
from .domain import Domain
from .domain import DomainType
from .ndarray import NdArray
from .ndarray import TiledNdArray
from .ndarray import NdArrayFloat
from .ndarray import NdArrayInt
from .ndarray import NdArrayStr
from .ndarray import TiledNdArrayFloat
from .parameter import Parameter
from .parameter import ParameterGroup
from .reference_system import ReferenceSystemConnectionObject

NdArrayTypes = Annotated[Union[NdArrayFloat, NdArrayInt, NdArrayStr], Field(discriminator="dataType")]


class Coverage(CovJsonBaseModel, extra="allow"):
id: Optional[str] = None
type: Literal["Coverage"] = "Coverage"
domain: Domain
parameters: Optional[Dict[str, Parameter]] = None
parameterGroups: Optional[List[ParameterGroup]] = None # noqa: N815
ranges: Dict[str, Union[NdArray, TiledNdArray, AnyUrl]]
ranges: Dict[str, Union[NdArrayTypes, TiledNdArrayFloat, AnyUrl]]


class CoverageCollection(CovJsonBaseModel, extra="allow"):
Expand Down
38 changes: 28 additions & 10 deletions src/covjson_pydantic/ndarray.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import math
from enum import Enum
from typing import List
from typing import Literal
from typing import Optional
Expand All @@ -9,17 +8,20 @@
from .base_models import CovJsonBaseModel


# TODO: Support for integers and strings
class DataType(str, Enum):
float = "float"


class NdArray(CovJsonBaseModel, extra="allow"):
type: Literal["NdArray"] = "NdArray"
dataType: DataType = DataType.float # noqa: N815
dataType: str # Kept here to ensure order of output in JSON # noqa: N815
axisNames: Optional[List[str]] = None # noqa: N815
shape: Optional[List[int]] = None
values: List[Optional[float]]

@model_validator(mode="before")
@classmethod
def validate_is_sub_class(cls, values):
if cls is NdArray:
raise TypeError(
"NdArray cannot be instantiated directly, please use a NdArrayFloat, NdArrayInt or NdArrayStr"
)
return values

@model_validator(mode="after")
def check_field_dependencies(self):
Expand All @@ -43,15 +45,31 @@ def check_field_dependencies(self):
return self


class NdArrayFloat(NdArray):
dataType: Literal["float"] = "float" # noqa: N815
values: List[Optional[float]]


class NdArrayInt(NdArray):
dataType: Literal["integer"] = "integer" # noqa: N815
values: List[Optional[int]]


class NdArrayStr(NdArray):
dataType: Literal["string"] = "string" # noqa: N815
values: List[Optional[str]]


class TileSet(CovJsonBaseModel):
tileShape: List[Optional[int]] # noqa: N815
urlTemplate: str # noqa: N815


# TODO: Validation of field dependencies
class TiledNdArray(CovJsonBaseModel, extra="allow"):
# TODO: Support string and integer type TiledNdArray
class TiledNdArrayFloat(CovJsonBaseModel, extra="allow"):
type: Literal["TiledNdArray"] = "TiledNdArray"
dataType: DataType = DataType.float # noqa: N815
dataType: Literal["float"] = "float" # noqa: N815
axisNames: List[str] # noqa: N815
shape: List[int]
tileSets: List[TileSet] # noqa: N815
44 changes: 40 additions & 4 deletions tests/test_coverage.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
import sys
from io import StringIO
from pathlib import Path

import pytest
Expand All @@ -7,7 +9,10 @@
from covjson_pydantic.domain import Axes
from covjson_pydantic.domain import Domain
from covjson_pydantic.ndarray import NdArray
from covjson_pydantic.ndarray import TiledNdArray
from covjson_pydantic.ndarray import NdArrayFloat
from covjson_pydantic.ndarray import NdArrayInt
from covjson_pydantic.ndarray import NdArrayStr
from covjson_pydantic.ndarray import TiledNdArrayFloat
from covjson_pydantic.parameter import Parameter
from covjson_pydantic.parameter import ParameterGroup
from covjson_pydantic.reference_system import ReferenceSystem
Expand All @@ -18,7 +23,9 @@
("spec-axes.json", Axes),
("str-axes.json", Axes),
("coverage-json.json", Coverage),
("coverage-mixed-type-ndarray.json", Coverage),
("doc-example-coverage.json", Coverage),
("example_py.json", Coverage),
("spec-vertical-profile-coverage.json", Coverage),
("spec-trajectory-coverage.json", Coverage),
("doc-example-coverage-collection.json", CoverageCollection),
Expand All @@ -32,9 +39,11 @@
("spec-domain-multipoint-series.json", Domain),
("spec-domain-multipoint.json", Domain),
("spec-domain-trajectory.json", Domain),
("ndarray-float.json", NdArray),
("spec-ndarray.json", NdArray),
("spec-tiled-ndarray.json", TiledNdArray),
("ndarray-float.json", NdArrayFloat),
("ndarray-string.json", NdArrayStr),
("ndarray-integer.json", NdArrayInt),
("spec-ndarray.json", NdArrayFloat),
("spec-tiled-ndarray.json", TiledNdArrayFloat),
("continuous-data-parameter.json", Parameter),
("categorical-data-parameter.json", Parameter),
("spec-parametergroup.json", ParameterGroup),
Expand Down Expand Up @@ -65,6 +74,12 @@ def test_happy_cases(file_name, object_type):
("point-series-domain-no-t.json", Domain, r"A 'PointSeries' must have a 't'-axis."),
("mixed-type-axes.json", Axes, r"Input should be a valid number"),
("mixed-type-axes-2.json", Axes, r"Input should be a valid string"),
("mixed-type-ndarray-1.json", NdArrayFloat, r"Input should be a valid number"),
("mixed-type-ndarray-1.json", NdArrayStr, r"Input should be 'string'"),
("mixed-type-ndarray-2.json", NdArrayFloat, r"Input should be a valid number"),
("mixed-type-ndarray-2.json", NdArrayStr, r"Input should be 'string'"),
("mixed-type-ndarray-3.json", NdArrayInt, r"Input should be a valid integer"),
("mixed-type-ndarray-3.json", NdArrayFloat, r"Input should be 'float'"),
]


Expand All @@ -78,3 +93,24 @@ def test_error_cases(file_name, object_type, error_message):

with pytest.raises(ValidationError, match=error_message):
object_type.model_validate_json(json_string)


def test_ndarray_directly():
with pytest.raises(TypeError, match="NdArray cannot be instantiated directly"):
NdArray(axisNames=["x", "y", "t"], shape=[1, 1, 1], values=[42.0])


def test_example_py():
file = Path(__file__).parent.parent.resolve() / "example.py"

with open(file, "r") as f:
code = f.read()

old_stdout = sys.stdout
sys.stdout = my_stdout = StringIO()
exec(code)
sys.stdout = old_stdout

file = Path(__file__).parent.resolve() / "test_data" / "example_py.json"
with open(file, "r") as f:
assert my_stdout.getvalue() == f.read()
Loading