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

Pydantic model for deployment config #211

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ channels:
- conda-forge
dependencies:
- python>=3.10
- pydantic
- numpy
- pip
- xarray
Expand Down
142 changes: 142 additions & 0 deletions pyglider/_config_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from typing import Optional

from pydantic import BaseModel, HttpUrl


class Metadata(BaseModel):
acknowledgement: str
comment: str
contributor_name: str
contributor_role: str
creator_email: str
creator_name: str
creator_url: HttpUrl
deployment_id: str
deployment_name: str
deployment_start: str
deployment_end: str
format_version: str
glider_name: str
glider_serial: str
glider_model: str
glider_instrument_name: str
glider_wmo: str
institution: str
keywords: str
keywords_vocabulary: str
license: str
metadata_link: HttpUrl
Metadata_Conventions: str
naming_authority: str
platform_type: str
processing_level: str
project: str
project_url: HttpUrl
publisher_email: str
publisher_name: str
publisher_url: HttpUrl
references: str
sea_name: str
source: str
standard_name_vocabulary: str
summary: str
transmission_system: str
wmo_id: str


class Device(BaseModel):
make: str
model: str
serial: str
long_name: Optional[str] = None
make_model: Optional[str] = None
factory_calibrated: Optional[str] = None
calibration_date: Optional[str] = None
calibration_report: Optional[str] = None
comment: Optional[str] = None


class GliderDevices(BaseModel):
pressure: Device
ctd: Device
optics: Device
oxygen: Device


class NetCDFVariable(BaseModel):
source: str
long_name: Optional[str] = None
standard_name: Optional[str] = None
units: Optional[str] = None
axis: Optional[str] = None
coordinates: Optional[str] = None
conversion: Optional[str] = None
comment: Optional[str] = None
observation_type: Optional[str] = None
platform: Optional[str] = None
reference: Optional[str] = None
valid_max: Optional[float] = None
valid_min: Optional[float] = None
coordinate_reference_frame: Optional[str] = None
instrument: Optional[str] = None
accuracy: Optional[float] = None
precision: Optional[float] = None
resolution: Optional[float] = None
positive: Optional[str] = None
reference_datum: Optional[str] = None
coarsen: Optional[int] = None


class NetCDFVariables(BaseModel):
timebase: Optional[NetCDFVariable] = (
None #! Is this required? `example-slocum`` doesn't have it
)
time: NetCDFVariable
latitude: NetCDFVariable
longitude: NetCDFVariable
heading: NetCDFVariable
pitch: NetCDFVariable
roll: NetCDFVariable
conductivity: NetCDFVariable
temperature: NetCDFVariable
pressure: NetCDFVariable
chlorophyll: NetCDFVariable
cdom: NetCDFVariable
backscatter_700: NetCDFVariable
oxygen_concentration: NetCDFVariable
temperature_oxygen: Optional[NetCDFVariable] = (
None #! Is this required? `example-slocum`` doesn't have it
)


class ProfileVariable(BaseModel):
comment: str
long_name: str
valid_max: Optional[float] = None
valid_min: Optional[float] = None
observation_type: Optional[str] = None
platform: Optional[str] = None
standard_name: Optional[str] = None
units: Optional[str] = None
calendar: Optional[str] = None
type: Optional[str] = None
calibration_date: Optional[str] = None
calibration_report: Optional[str] = None
factory_calibrated: Optional[str] = None
make_model: Optional[str] = None
serial_number: Optional[str] = None


class ProfileVariables(BaseModel):
profile_id: ProfileVariable
profile_time: ProfileVariable
profile_time_start: ProfileVariable
profile_time_end: ProfileVariable
profile_lat: ProfileVariable
profile_lon: ProfileVariable
u: ProfileVariable
v: ProfileVariable
lon_uv: ProfileVariable
lat_uv: ProfileVariable
time_uv: ProfileVariable
instrument_ctd: ProfileVariable
33 changes: 33 additions & 0 deletions pyglider/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import yaml
from pydantic import BaseModel

from pyglider._config_components import (
GliderDevices,
Metadata,
NetCDFVariables,
ProfileVariables,
)

__all__ = ['Deployment', 'dump_yaml']


class Deployment(BaseModel):
metadata: Metadata
glider_devices: GliderDevices
netcdf_variables: NetCDFVariables
profile_variables: ProfileVariables

@classmethod
def load_yaml(cls, yaml_str: str) -> 'Deployment':
"""Load a yaml string into a Deployment model."""
return _generic_load_yaml(yaml_str, cls)


def dump_yaml(model: BaseModel) -> str:
"""Dump a pydantic model to a yaml string."""
return yaml.safe_dump(model.model_dump(), default_flow_style=False)


def _generic_load_yaml(data: str, model: BaseModel) -> BaseModel:
"""Load a yaml string into a pydantic model."""
return model.model_validate(yaml.safe_load(data))
55 changes: 55 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from pathlib import Path
from typing import Callable

import pytest
import yaml

from pyglider.config import Deployment

library_dir = Path(__file__).parent.parent.absolute()
example_dir = library_dir / 'tests/example-data/'

VALID_CONFIG_PATHS = [
example_dir / 'example-slocum/deploymentRealtime.yml',
example_dir / 'example-seaexplorer/deploymentRealtime.yml',
# TODO: Add other valid example configs?
]


@pytest.fixture(params=VALID_CONFIG_PATHS)
def valid_config(request):
return request.param.read_text()


def test_valid_config(valid_config):
"""Checks all valid configs can be loaded."""
Deployment.load_yaml(valid_config)


def patch_config(yaml_str: str, f: Callable) -> str:
"""Patch a yaml string with a function.
Function should do an in place operation on a dictionary/list.
"""
d = yaml.safe_load(yaml_str)
f(d)
return yaml.safe_dump(d, default_flow_style=False)


@pytest.mark.parametrize(
'input_, expected, f',
[
(
'a: 1\n' 'b: 2\n',
'a: 1\n' 'b: 3\n',
lambda d: d.update({'b': 3}),
),
],
)
def test_patch_config(input_, expected, f):
assert patch_config(input_, f) == expected


# TODO: Stress test the model by taking existing configs and modifying them in breaking ways


def test_incorrect_date_format(): ...
Loading