Skip to content

Commit eaefbdf

Browse files
committed
Add pydantic model for deployment config
1 parent f30a801 commit eaefbdf

File tree

3 files changed

+230
-0
lines changed

3 files changed

+230
-0
lines changed

pyglider/_config_components.py

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

pyglider/config.py

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

tests/test_config.py

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

0 commit comments

Comments
 (0)