Skip to content

Commit c6705bc

Browse files
committed
Add pydantic model for deployment config
1 parent 2557ae2 commit c6705bc

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed

pyglider/_config_components.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from typing import Optional
2+
from pydantic import BaseModel, HttpUrl
3+
4+
class Metadata(BaseModel):
5+
acknowledgement: str
6+
comment: str
7+
contributor_name: str
8+
contributor_role: str
9+
creator_email: str
10+
creator_name: str
11+
creator_url: HttpUrl
12+
deployment_id: str
13+
deployment_name: str
14+
deployment_start: str
15+
deployment_end: str
16+
format_version: str
17+
glider_name: str
18+
glider_serial: str
19+
glider_model: str
20+
glider_instrument_name: str
21+
glider_wmo: str
22+
institution: str
23+
keywords: str
24+
keywords_vocabulary: str
25+
license: str
26+
metadata_link: HttpUrl
27+
Metadata_Conventions: str
28+
naming_authority: str
29+
platform_type: str
30+
processing_level: str
31+
project: str
32+
project_url: HttpUrl
33+
publisher_email: str
34+
publisher_name: str
35+
publisher_url: HttpUrl
36+
references: str
37+
sea_name: str
38+
source: str
39+
standard_name_vocabulary: str
40+
summary: str
41+
transmission_system: str
42+
wmo_id: str
43+
44+
class Device(BaseModel):
45+
make: str
46+
model: str
47+
serial: str
48+
long_name: Optional[str] = None
49+
make_model: Optional[str] = None
50+
factory_calibrated: Optional[str] = None
51+
calibration_date: Optional[str] = None
52+
calibration_report: Optional[str] = None
53+
comment: Optional[str] = None
54+
55+
56+
class GliderDevices(BaseModel):
57+
pressure: Device
58+
ctd: Device
59+
optics: Device
60+
oxygen: Device
61+
62+
63+
class NetCDFVariable(BaseModel):
64+
source: str
65+
long_name: Optional[str] = None
66+
standard_name: Optional[str] = None
67+
units: Optional[str] = None
68+
axis: Optional[str] = None
69+
coordinates: Optional[str] = None
70+
conversion: Optional[str] = None
71+
comment: Optional[str] = None
72+
observation_type: Optional[str] = None
73+
platform: Optional[str] = None
74+
reference: Optional[str] = None
75+
valid_max: Optional[float] = None
76+
valid_min: Optional[float] = None
77+
coordinate_reference_frame: Optional[str] = None
78+
instrument: Optional[str] = None
79+
accuracy: Optional[float] = None
80+
precision: Optional[float] = None
81+
resolution: Optional[float] = None
82+
positive: Optional[str] = None
83+
reference_datum: Optional[str] = None
84+
coarsen: Optional[int] = None
85+
86+
87+
class NetCDFVariables(BaseModel):
88+
timebase: Optional[NetCDFVariable] = None #! Is this required? `example-slocum`` doesn't have it
89+
time: NetCDFVariable
90+
latitude: NetCDFVariable
91+
longitude: NetCDFVariable
92+
heading: NetCDFVariable
93+
pitch: NetCDFVariable
94+
roll: NetCDFVariable
95+
conductivity: NetCDFVariable
96+
temperature: NetCDFVariable
97+
pressure: NetCDFVariable
98+
chlorophyll: NetCDFVariable
99+
cdom: NetCDFVariable
100+
backscatter_700: NetCDFVariable
101+
oxygen_concentration: NetCDFVariable
102+
temperature_oxygen: Optional[NetCDFVariable] = None #! Is this required? `example-slocum`` doesn't have it
103+
104+
105+
class ProfileVariable(BaseModel):
106+
comment: str
107+
long_name: str
108+
valid_max: Optional[float] = None
109+
valid_min: Optional[float] = None
110+
observation_type: Optional[str] = None
111+
platform: Optional[str] = None
112+
standard_name: Optional[str] = None
113+
units: Optional[str] = None
114+
calendar: Optional[str] = None
115+
type: Optional[str] = None
116+
calibration_date: Optional[str] = None
117+
calibration_report: Optional[str] = None
118+
factory_calibrated: Optional[str] = None
119+
make_model: Optional[str] = None
120+
serial_number: Optional[str] = None
121+
122+
123+
class ProfileVariables(BaseModel):
124+
profile_id: ProfileVariable
125+
profile_time: ProfileVariable
126+
profile_time_start: ProfileVariable
127+
profile_time_end: ProfileVariable
128+
profile_lat: ProfileVariable
129+
profile_lon: ProfileVariable
130+
u: ProfileVariable
131+
v: ProfileVariable
132+
lon_uv: ProfileVariable
133+
lat_uv: ProfileVariable
134+
time_uv: ProfileVariable
135+
instrument_ctd: ProfileVariable

pyglider/config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import List, Optional, Dict, Union
2+
from pydantic import BaseModel, HttpUrl, Field
3+
from pyglider._config_components import Metadata, GliderDevices, NetCDFVariables, ProfileVariables
4+
import yaml
5+
6+
__all__ = ['Deployment', 'dump_yaml']
7+
8+
class Deployment(BaseModel):
9+
metadata: Metadata
10+
glider_devices: GliderDevices
11+
netcdf_variables: NetCDFVariables
12+
profile_variables: ProfileVariables
13+
14+
@classmethod
15+
def load_yaml(cls, yaml_str: str) -> "Deployment":
16+
"""Load a yaml string into a Deployment model."""
17+
return _generic_load_yaml(yaml_str, cls)
18+
19+
20+
def dump_yaml(model: BaseModel) -> str:
21+
"""Dump a pydantic model to a yaml string."""
22+
return yaml.safe_dump(model.model_dump(), default_flow_style=False)
23+
24+
25+
def _generic_load_yaml(data: str, model: BaseModel) -> BaseModel:
26+
"""Load a yaml string into a pydantic model."""
27+
return model.model_validate(yaml.safe_load(data))
28+

tests/test_config.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from typing import Callable
2+
import yaml
3+
import pytest
4+
from pyglider.config import Deployment
5+
from pathlib import Path
6+
7+
library_dir = Path(__file__).parent.parent.absolute()
8+
example_dir = library_dir / 'tests/example-data/'
9+
10+
VALID_CONFIG_PATHS = [
11+
example_dir / "example-slocum/deploymentRealtime.yml",
12+
example_dir / "example-seaexplorer/deploymentRealtime.yml",
13+
# TODO: Add other valid example configs?
14+
]
15+
16+
@pytest.fixture(params=VALID_CONFIG_PATHS)
17+
def valid_config(request):
18+
return request.param.read_text()
19+
20+
def test_valid_config(valid_config):
21+
"""Checks all valid configs can be loaded."""
22+
Deployment.load_yaml(valid_config)
23+
24+
25+
def patch_config(yaml_str: str, f: Callable) -> str:
26+
"""Patch a yaml string with a function.
27+
Function should do an in place operation on a dictionary/list.
28+
"""
29+
d = yaml.safe_load(yaml_str)
30+
f(d)
31+
return yaml.safe_dump(d, default_flow_style=False)
32+
33+
@pytest.mark.parametrize("input_, expected, f", [
34+
(
35+
'a: 1\n'
36+
'b: 2\n',
37+
'a: 1\n'
38+
'b: 3\n',
39+
lambda d: d.update({'b': 3}),
40+
),
41+
])
42+
def test_patch_config(input_, expected, f):
43+
assert patch_config(input_, f) == expected
44+
45+
# TODO: Stress test the model by taking existing configs and modifying them in breaking ways
46+
47+
def test_incorrect_date_format():
48+
...

0 commit comments

Comments
 (0)