atmos-validation 2.1.2__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/CHANGELOG.md +18 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/PKG-INFO +1 -1
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/attrs_to_meta.py +11 -3
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/measurement_metadata_to_schema.json +1 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/parsers.py +6 -10
- atmos_validation-2.2.0/atmos_validation/convert_ascii/tests/test_attrs_to_meta.py +101 -0
- atmos_validation-2.2.0/atmos_validation/convert_ascii/tests/test_parsers.py +15 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/__init__.py +1 -0
- atmos_validation-2.2.0/atmos_validation/schemas/contractor.py +73 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/metadata.py +3 -2
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_metadata.py +1 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_names.py +1 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_parser.py +9 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_position.py +36 -28
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/main.py +20 -1
- atmos_validation-2.2.0/atmos_validation/validate_ascii/tests/test_contractor_validation.py +137 -0
- atmos_validation-2.2.0/atmos_validation/validate_ascii/tests/test_header_position.py +42 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/utils.py +11 -1
- atmos_validation-2.2.0/atmos_validation/validate_netcdf/tests/test_averaging_period_validation.py +46 -0
- atmos_validation-2.2.0/atmos_validation/validate_netcdf/tests/test_contractor_validation.py +262 -0
- atmos_validation-2.2.0/atmos_validation/validate_netcdf/tests/test_instrument_counts_validation.py +27 -0
- atmos_validation-2.2.0/atmos_validation/validate_netcdf/tests/test_location_validation.py +38 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_metadata_schema_validation.py +1 -1
- atmos_validation-2.2.0/atmos_validation/validate_netcdf/tests/test_total_water_depth_validation.py +43 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validation_settings.py +1 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/file_attributes.py +161 -1
- atmos_validation-2.2.0/atmos_validation/validate_netcdf/validators/tests/__init__.py +0 -0
- atmos_validation-2.2.0/atmos_validation/validate_netcdf/validators/variables/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/docs/ascii_format.md +3 -2
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/docs/conventions.md +14 -11
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/examples/example_ascii_measurement.dat +2 -1
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/pyproject.toml +1 -1
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/workflows/ci-conventional-commit-linter.yml +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/workflows/ci-cov-comment.yml +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/workflows/ci-doctest.yml +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/workflows/ci-lint-pr.yml +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/workflows/ci-test-pr.yml +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/workflows/pypi-publisher.yml +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.github/workflows/release-please.yml +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.gitignore +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/.vscode/settings.json +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/LICENSE +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/README.md +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/SECURITY.md +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/__main__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/df_to_nc.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/main.py +0 -0
- {atmos_validation-2.1.2/atmos_validation/schemas → atmos_validation-2.2.0/atmos_validation/convert_ascii}/tests/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/utils.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/classification_level.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/data_usability_level.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/data_usability_levels.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/dim_constants.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/installation_type.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/installation_types.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/instrument_type.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/instrument_types.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/parameter_config.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/parameter_configs.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/qc_tests.py +0 -0
- {atmos_validation-2.1.2/atmos_validation/validate_netcdf → atmos_validation-2.2.0/atmos_validation/schemas}/tests/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/tests/test_classification_level.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/tests/test_parameter_configs.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/schemas/tests/test_unique_configs.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/date_helper.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_date.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_parameter_info.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_parameter_info_list.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/result_file.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/row_parser.py +0 -0
- {atmos_validation-2.1.2/atmos_validation/validate_netcdf/validators → atmos_validation-2.2.0/atmos_validation/validate_ascii/tests}/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/validate_result.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/external_reference_guard.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/main.py +0 -0
- {atmos_validation-2.1.2/atmos_validation/validate_netcdf/validators/dims → atmos_validation-2.2.0/atmos_validation/validate_netcdf/tests}/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_blacklisted_globals.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_coordinate_values_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_data_usability_validation.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_dimvars_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_external_reference_guard.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_file_attributes.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_final_reports_validation.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_height_depth_validation.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_height_longname_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_installation_types_validation.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_lat_lon_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_main.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_sig_digs.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_spatial_validators.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_time_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_util.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_varattrs_validation.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/tests/test_varinterval_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/utils.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validation_logger.py +0 -0
- {atmos_validation-2.1.2/atmos_validation/validate_netcdf/validators/tests → atmos_validation-2.2.0/atmos_validation/validate_netcdf/validators}/__init__.py +0 -0
- {atmos_validation-2.1.2/atmos_validation/validate_netcdf/validators/variables → atmos_validation-2.2.0/atmos_validation/validate_netcdf/validators/dims}/__init__.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/dims/dims_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/dims/dimvars_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/dims/spatial_validators.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/dims/time_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/root_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/variables/sig_dig_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/variables/varattrs_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/variables/vardims_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/variables/variables_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_netcdf/validators/variables/varinterval_validator.py +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/docs/run.md +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/examples/example_netcdf_measurement.nc +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/examples/hindcast_example/example_hindcast_20160101_20160131_T744.nc +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/examples/hindcast_example/example_hindcast_20160201_20160229_T696.nc +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/examples/hindcast_example/example_hindcast_20160301_20160331_T744.nc +0 -0
- {atmos_validation-2.1.2 → atmos_validation-2.2.0}/uv.lock +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.0](https://github.com/equinor/atmos-validation/compare/v2.1.2...v2.2.0) (2026-11-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Add contractor validation and ascii -> netcdf conversion ([#128](https://github.com/equinor/atmos-validation/issues/128)) ([43769dd](https://github.com/equinor/atmos-validation/commit/43769dd40c3e5c6aa07cfe28f92341fd63c48089))
|
|
9
|
+
* **validate-ascii:** standardize location notation and datum ([#126](https://github.com/equinor/atmos-validation/issues/126)) ([fb2e5e5](https://github.com/equinor/atmos-validation/commit/fb2e5e551fc230ed453e87c58cd984313dc376f2))
|
|
10
|
+
* **validate-netcdf:** validate averaging_period units ([#120](https://github.com/equinor/atmos-validation/issues/120)) ([11d2431](https://github.com/equinor/atmos-validation/commit/11d2431e6c8105a82975def1ed4c80aa6b7dfc9c))
|
|
11
|
+
* **validate-netcdf:** validate instrument_types/instrument_specifications count match ([#119](https://github.com/equinor/atmos-validation/issues/119)) ([47a95b1](https://github.com/equinor/atmos-validation/commit/47a95b1a8e56836bcd2d770db66def5e6864f7e7))
|
|
12
|
+
* **validate-netcdf:** validate location notation and datum ([#125](https://github.com/equinor/atmos-validation/issues/125)) ([6ab4c15](https://github.com/equinor/atmos-validation/commit/6ab4c15a72463fc134dd3714ab1de53880e1bf5f))
|
|
13
|
+
* **validate-netcdf:** validate total_water_depth unit ([#121](https://github.com/equinor/atmos-validation/issues/121)) ([a8a3a98](https://github.com/equinor/atmos-validation/commit/a8a3a984a6f7afc04f1a4aa1b2226cc51316b92d))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Update contractor and division handling ([#129](https://github.com/equinor/atmos-validation/issues/129)) ([a8ce84f](https://github.com/equinor/atmos-validation/commit/a8ce84f3f408b0e6dd39da52fd05c44ad7de343f))
|
|
19
|
+
* **validate-netcdf:** allow "NA" for averaging_period ([#127](https://github.com/equinor/atmos-validation/issues/127)) ([bc64afb](https://github.com/equinor/atmos-validation/commit/bc64afb97f837a19e3afacba9c9a2b68fa546369))
|
|
20
|
+
|
|
3
21
|
## [2.1.2](https://github.com/equinor/atmos-validation/compare/v2.1.1...v2.1.2) (2026-11-09)
|
|
4
22
|
|
|
5
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: atmos_validation
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A python library containing automated data validation tools for the Atmos Data Service
|
|
5
5
|
Project-URL: Repository, https://github.com/equinor/atmos-validation
|
|
6
6
|
Project-URL: Documentation, https://github.com/equinor/atmos-validation/tree/main/docs
|
{atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/convert_ascii/attrs_to_meta.py
RENAMED
|
@@ -21,9 +21,7 @@ def parse_measurement_attrs(file_name: str, attrs_map_path: str):
|
|
|
21
21
|
if line.startswith("%"):
|
|
22
22
|
attrs.update(
|
|
23
23
|
{
|
|
24
|
-
metadata: (
|
|
25
|
-
*map(lambda s: s.strip(), filter(None, line.split(":"))),
|
|
26
|
-
)[-1]
|
|
24
|
+
metadata: line.partition(":")[2].strip()
|
|
27
25
|
for metadata, attr in attrs_map.items()
|
|
28
26
|
if line.replace("%", "")
|
|
29
27
|
.strip()
|
|
@@ -39,6 +37,16 @@ def parse_measurement_attrs(file_name: str, attrs_map_path: str):
|
|
|
39
37
|
attrs["source_file"] = (
|
|
40
38
|
file_name.replace("ø", "oe").replace("æ", "ae").replace("å", "aa")
|
|
41
39
|
) # NetCDF attrs can not have Norwegian letters
|
|
40
|
+
contractor = attrs.pop("contractor", "NA")
|
|
41
|
+
attrs["contractor"] = (
|
|
42
|
+
[c.strip() for c in contractor.split(",")] if contractor.strip() else []
|
|
43
|
+
)
|
|
44
|
+
contractor_division = attrs.pop("contractor_division", "")
|
|
45
|
+
attrs["contractor_division"] = (
|
|
46
|
+
[]
|
|
47
|
+
if not contractor_division.strip()
|
|
48
|
+
else [c.strip() for c in contractor_division.split(",")]
|
|
49
|
+
)
|
|
42
50
|
if "asset" not in attrs:
|
|
43
51
|
attrs["asset"] = "NA"
|
|
44
52
|
if "country" not in attrs:
|
|
@@ -13,22 +13,18 @@ MISSING_HEIGHT = -99999
|
|
|
13
13
|
def parse_location_text(line_content: str) -> Tuple[float, float]:
|
|
14
14
|
"""Parses Location text into a float tuple (lat, lon)
|
|
15
15
|
|
|
16
|
+
Expects the new "<lat>N <lon>E" decimal notation (sign carries the
|
|
17
|
+
hemisphere), e.g. "60.3589N 70.8764E" or "-60.3589N -70.8764E".
|
|
18
|
+
|
|
16
19
|
Args:
|
|
17
20
|
line_content: the raw location text line
|
|
18
21
|
|
|
19
22
|
Returns:
|
|
20
23
|
float numbers tuple (lat, lon)
|
|
21
24
|
"""
|
|
22
|
-
location_text = line_content.split(":", 1)[1].split(",")[0]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
is_east = 1
|
|
26
|
-
is_north = 1
|
|
27
|
-
if "S" in location_text:
|
|
28
|
-
is_north = -1
|
|
29
|
-
if "W" in location_text:
|
|
30
|
-
is_east = -1
|
|
31
|
-
return (float(match[0]) * is_north, float(match[1]) * is_east)
|
|
25
|
+
location_text = line_content.split(":", 1)[1].split(",")[0].strip()
|
|
26
|
+
match = re.findall(r"-?[0-9]+\.[0-9]+", location_text)
|
|
27
|
+
return (float(match[0]), float(match[1]))
|
|
32
28
|
|
|
33
29
|
|
|
34
30
|
def data_frame_parser(header_line: str, data_lines: List[List[str]]) -> pd.DataFrame:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
import xarray as xr
|
|
4
|
+
|
|
5
|
+
from atmos_validation.convert_ascii import attrs_to_meta
|
|
6
|
+
from atmos_validation.convert_ascii.df_to_nc import assign_metadata_to_ds
|
|
7
|
+
from atmos_validation.convert_ascii.utils import store_netcdf
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize(
|
|
11
|
+
("contractor", "division", "expected_contractors", "expected_divisions"),
|
|
12
|
+
[
|
|
13
|
+
("Oceanweather Inc.", "Marine", ["Oceanweather Inc."], ["Marine"]),
|
|
14
|
+
("NA", "NA", ["NA"], ["NA"]),
|
|
15
|
+
("Fugro, RPS", "Fugro GB Ltd., NA", ["Fugro", "RPS"], ["Fugro GB Ltd.", "NA"]),
|
|
16
|
+
(
|
|
17
|
+
"Fugro, RPS",
|
|
18
|
+
"NA, RPS Group Inc.",
|
|
19
|
+
["Fugro", "RPS"],
|
|
20
|
+
["NA", "RPS Group Inc."],
|
|
21
|
+
),
|
|
22
|
+
("Fugro, RPS", "NA, NA", ["Fugro", "RPS"], ["NA", "NA"]),
|
|
23
|
+
("Fugro, RPS", "", ["Fugro", "RPS"], []),
|
|
24
|
+
("Fugro, RPS", None, ["Fugro", "RPS"], []),
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
def test_ascii_contractors_are_converted_to_lists(
|
|
28
|
+
tmp_path,
|
|
29
|
+
monkeypatch,
|
|
30
|
+
contractor,
|
|
31
|
+
division,
|
|
32
|
+
expected_contractors,
|
|
33
|
+
expected_divisions,
|
|
34
|
+
):
|
|
35
|
+
attrs_map = {
|
|
36
|
+
"comments": "Comments",
|
|
37
|
+
"contractor": "Contractor/Data Responsible",
|
|
38
|
+
"contractor_division": "Contractor Division",
|
|
39
|
+
"data_history": "History",
|
|
40
|
+
"final_reports": "Final reports",
|
|
41
|
+
"project_name": "Project",
|
|
42
|
+
"qc_provider": "QC provider",
|
|
43
|
+
"averaging_period": "Averaging period",
|
|
44
|
+
"data_usability": "Data usability",
|
|
45
|
+
"instrument_types": "Instrument types",
|
|
46
|
+
"instrument_specifications": "Instrument specifications",
|
|
47
|
+
"installation_type": "Installation type",
|
|
48
|
+
"country": "Country",
|
|
49
|
+
"location": "Location",
|
|
50
|
+
"mooring_name": "Mooring name",
|
|
51
|
+
"total_water_depth": "Total water depth",
|
|
52
|
+
}
|
|
53
|
+
monkeypatch.setattr(attrs_to_meta, "get_encoding", lambda _: "utf-8")
|
|
54
|
+
monkeypatch.setattr(attrs_to_meta, "load_attrs_map", lambda _: attrs_map)
|
|
55
|
+
source = tmp_path / "measurement.txt"
|
|
56
|
+
source.write_text(
|
|
57
|
+
"\n".join(
|
|
58
|
+
[
|
|
59
|
+
"% Comments: processed",
|
|
60
|
+
f"% Contractor/Data Responsible: {contractor}",
|
|
61
|
+
*(
|
|
62
|
+
[f"% Contractor Division: {division}"]
|
|
63
|
+
if division is not None
|
|
64
|
+
else []
|
|
65
|
+
),
|
|
66
|
+
"% History: Original data",
|
|
67
|
+
"% Final reports: NA",
|
|
68
|
+
"% Project: Example",
|
|
69
|
+
"% QC provider: Example QC",
|
|
70
|
+
"% Averaging period: 10 min",
|
|
71
|
+
"% Data usability: usable",
|
|
72
|
+
"% Instrument types: sensor",
|
|
73
|
+
"% Instrument specifications: specification",
|
|
74
|
+
"% Installation type: fixed",
|
|
75
|
+
"% Country: NO",
|
|
76
|
+
"% Location: North Sea",
|
|
77
|
+
"% Mooring name: Example",
|
|
78
|
+
"% Total water depth: 100 m",
|
|
79
|
+
]
|
|
80
|
+
),
|
|
81
|
+
encoding="utf-8",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
metadata = attrs_to_meta.parse_measurement_attrs(str(source), "unused")
|
|
85
|
+
|
|
86
|
+
assert metadata.contractor == expected_contractors
|
|
87
|
+
assert metadata.contractor_division == expected_divisions
|
|
88
|
+
|
|
89
|
+
ds = xr.Dataset(coords={"Time": [np.datetime64("2026-01-01", "ns")]})
|
|
90
|
+
assign_metadata_to_ds(str(source), ds)
|
|
91
|
+
assert ds.attrs["contractor"] == expected_contractors
|
|
92
|
+
assert ds.attrs["contractor_division"] == expected_divisions
|
|
93
|
+
|
|
94
|
+
path = store_netcdf(str(source), ds)
|
|
95
|
+
with xr.open_dataset(path, engine="h5netcdf") as reopened:
|
|
96
|
+
for key, expected in (
|
|
97
|
+
("contractor", expected_contractors),
|
|
98
|
+
("contractor_division", expected_divisions),
|
|
99
|
+
):
|
|
100
|
+
actual = reopened.attrs[key]
|
|
101
|
+
assert ([actual] if isinstance(actual, str) else list(actual)) == expected
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ..parsers import parse_location_text
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_parse_location_text_north_east():
|
|
5
|
+
"""Tests parsing a north-east location line into signed (lat, lon) floats"""
|
|
6
|
+
lat, lon = parse_location_text("% Location: 60.3589N 70.8764E, WGS84")
|
|
7
|
+
assert lat == 60.3589
|
|
8
|
+
assert lon == 70.8764
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_parse_location_text_south_west():
|
|
12
|
+
"""Tests parsing a south-west location line (negative sign) into signed (lat, lon) floats"""
|
|
13
|
+
lat, lon = parse_location_text("% Location: -60.3589N -70.8764E, WGS84")
|
|
14
|
+
assert lat == -60.3589
|
|
15
|
+
assert lon == -70.8764
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, StringConstraints, field_validator
|
|
4
|
+
|
|
5
|
+
ContractorName = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Contractor(BaseModel, extra="forbid"):
|
|
9
|
+
contractor: ContractorName
|
|
10
|
+
divisions: list[ContractorName] | None = Field(
|
|
11
|
+
default=None,
|
|
12
|
+
description="Allowed divisions. Null or an empty list means no divisions are configured.",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
@field_validator("divisions")
|
|
16
|
+
@classmethod
|
|
17
|
+
def validate_unique_divisions(cls, divisions: list[str] | None) -> list[str] | None:
|
|
18
|
+
if divisions is not None and len(divisions) != len(set(divisions)):
|
|
19
|
+
raise ValueError("Division names must be unique within a contractor")
|
|
20
|
+
return divisions
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Contractors(BaseModel):
|
|
24
|
+
configs: list[Contractor]
|
|
25
|
+
|
|
26
|
+
@field_validator("configs")
|
|
27
|
+
@classmethod
|
|
28
|
+
def validate_unique_contractors(cls, configs: list[Contractor]) -> list[Contractor]:
|
|
29
|
+
names = [entry.contractor for entry in configs]
|
|
30
|
+
if len(names) != len(set(names)):
|
|
31
|
+
raise ValueError("Contractor names must be unique")
|
|
32
|
+
return configs
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_contractor_metadata(
|
|
36
|
+
contractor_names: list[str],
|
|
37
|
+
contractor_divisions: list[str],
|
|
38
|
+
configs: list[Contractor],
|
|
39
|
+
) -> list[str]:
|
|
40
|
+
"""Validate contractor names and their positionally corresponding divisions."""
|
|
41
|
+
configs_by_name = {config.contractor: config for config in configs}
|
|
42
|
+
errors = [
|
|
43
|
+
f'Contractor "{name}" is not in the allowed list.'
|
|
44
|
+
for name in contractor_names
|
|
45
|
+
if name.upper() != "NA" and name not in configs_by_name
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
if not contractor_names:
|
|
49
|
+
return [
|
|
50
|
+
f'Division "{division}" is not allowed without a contractor.'
|
|
51
|
+
for division in contractor_divisions
|
|
52
|
+
if division.upper() != "NA"
|
|
53
|
+
]
|
|
54
|
+
if not contractor_divisions:
|
|
55
|
+
return errors
|
|
56
|
+
if len(contractor_names) != len(contractor_divisions):
|
|
57
|
+
errors.append(
|
|
58
|
+
"Contractor and contractor_division lists must have the same length "
|
|
59
|
+
"when divisions are provided. Use NA for unspecified divisions."
|
|
60
|
+
)
|
|
61
|
+
return errors
|
|
62
|
+
|
|
63
|
+
for name, division in zip(contractor_names, contractor_divisions):
|
|
64
|
+
if division.upper() == "NA":
|
|
65
|
+
continue
|
|
66
|
+
config = configs_by_name.get(name)
|
|
67
|
+
allowed_divisions = (config.divisions or []) if config is not None else []
|
|
68
|
+
if division not in allowed_divisions:
|
|
69
|
+
errors.append(
|
|
70
|
+
f'Division "{division}" is not allowed for contractor "{name}". '
|
|
71
|
+
f"Allowed values: {sorted(allowed_divisions)}"
|
|
72
|
+
)
|
|
73
|
+
return errors
|
|
@@ -23,7 +23,8 @@ class CommonMetadata(BaseModel):
|
|
|
23
23
|
model_config = ConfigDict(use_enum_values=True)
|
|
24
24
|
|
|
25
25
|
comments: Union[List[str], str]
|
|
26
|
-
contractor: str
|
|
26
|
+
contractor: Union[List[str], str] = Field(default_factory=list)
|
|
27
|
+
contractor_division: Union[List[str], str] = Field(default_factory=list)
|
|
27
28
|
classification_level: ClassificationLevel = Field(
|
|
28
29
|
default=ClassificationLevel.INTERNAL
|
|
29
30
|
)
|
|
@@ -66,4 +67,4 @@ class MeasurementMetadata(CommonMetadata):
|
|
|
66
67
|
location: Union[str, List[str]]
|
|
67
68
|
mooring_name: str
|
|
68
69
|
source_file: str
|
|
69
|
-
total_water_depth:
|
|
70
|
+
total_water_depth: str
|
{atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_parser.py
RENAMED
|
@@ -18,6 +18,7 @@ class HeaderParser:
|
|
|
18
18
|
errors: List[str],
|
|
19
19
|
accepted_values: Optional[Set[str]] = None,
|
|
20
20
|
split_by: Optional[str] = None,
|
|
21
|
+
optional: bool = False,
|
|
21
22
|
) -> str:
|
|
22
23
|
real_valued = [
|
|
23
24
|
Headers.LOCATION,
|
|
@@ -50,6 +51,8 @@ class HeaderParser:
|
|
|
50
51
|
# No colon to separate key from value in metadata line
|
|
51
52
|
errors.append(f'Missing Colon in line: "{line}"')
|
|
52
53
|
# Could not find the key
|
|
54
|
+
if optional:
|
|
55
|
+
return ""
|
|
53
56
|
if key in real_valued:
|
|
54
57
|
errors.append(f"Metadata must include a real value for {key}")
|
|
55
58
|
else:
|
|
@@ -90,6 +93,11 @@ class HeaderParser:
|
|
|
90
93
|
errors,
|
|
91
94
|
accepted_values[Headers.INSTALLATION_TYPE],
|
|
92
95
|
)
|
|
96
|
+
contractor_division = self.get_simple_value(
|
|
97
|
+
Headers.CONTRACTOR_DIVISION,
|
|
98
|
+
errors,
|
|
99
|
+
optional=True,
|
|
100
|
+
)
|
|
93
101
|
|
|
94
102
|
parameters = HeaderParameterInfoList(
|
|
95
103
|
self.get_lines_from(
|
|
@@ -106,6 +114,7 @@ class HeaderParser:
|
|
|
106
114
|
data = HeaderMetaData(
|
|
107
115
|
number_of_header_lines=len(self.lines),
|
|
108
116
|
contractor=self.get_simple_value(Headers.CONTRACTOR, errors),
|
|
117
|
+
contractor_division=contractor_division,
|
|
109
118
|
final_report=self.get_simple_value(Headers.FINAL_REPORT, errors),
|
|
110
119
|
qc=self.get_simple_value(Headers.QC, errors),
|
|
111
120
|
duration=HeaderDate(self.get_simple_value(Headers.DURATION, errors)),
|
{atmos_validation-2.1.2 → atmos_validation-2.2.0}/atmos_validation/validate_ascii/header_position.py
RENAMED
|
@@ -1,59 +1,59 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import List
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
ALLOWED_DATUMS = [
|
|
5
|
+
"datum uncertain",
|
|
6
|
+
"WGS84",
|
|
7
|
+
"ED50",
|
|
8
|
+
"ED87",
|
|
9
|
+
"ETRS89",
|
|
10
|
+
"Tokyo",
|
|
11
|
+
"NAD27",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
# Matches "<lat>N <lon>E, <datum>", e.g. "60.3589N 70.8764E, WGS84" or
|
|
15
|
+
# "-60.3589N -70.8764E, WGS84". The sign carries the hemisphere; the N/E
|
|
16
|
+
# letters are fixed and do not flip to S/W.
|
|
17
|
+
LOCATION_PATTERN = re.compile(r"^-?[0-9]+\.[0-9]+N -?[0-9]+\.[0-9]+E,.+$")
|
|
5
18
|
|
|
6
19
|
|
|
7
20
|
class HeaderPosition:
|
|
8
21
|
def __init__(self, position_text: str):
|
|
9
|
-
self.parse_location(
|
|
22
|
+
self.parse_location(position_text)
|
|
10
23
|
|
|
11
24
|
def parse_location(self, location_text: str):
|
|
12
|
-
match = None
|
|
13
25
|
regex_example = (
|
|
14
|
-
"location should match e.g
|
|
15
|
-
"
|
|
16
|
-
)
|
|
17
|
-
pattern = re.compile(
|
|
18
|
-
r"^[0-9]*\.[0-9]+°[NS] [0-9]*\.[0-9]+°[EW],.+$", re.IGNORECASE
|
|
26
|
+
"location should match e.g 60.3589N 70.8764E, WGS84 (north-east) or "
|
|
27
|
+
"-60.3589N -70.8764E, WGS84 (south-west)"
|
|
19
28
|
)
|
|
20
|
-
match =
|
|
29
|
+
match = LOCATION_PATTERN.match(location_text)
|
|
21
30
|
if match is None:
|
|
22
31
|
raise ValueError(
|
|
23
|
-
f"the location string {location_text} should match the regex
|
|
24
|
-
f"valid examples: {regex_example}"
|
|
32
|
+
f"the location string {location_text} should match the regex "
|
|
33
|
+
f"{LOCATION_PATTERN.pattern}. valid examples: {regex_example}"
|
|
25
34
|
)
|
|
26
35
|
try:
|
|
27
|
-
items = location_text.split(",")
|
|
36
|
+
items = location_text.split(",", 1)
|
|
28
37
|
location_items = items[0].split(" ")
|
|
29
38
|
self.location_text = location_text
|
|
30
|
-
self.latitude =
|
|
31
|
-
self.longitude =
|
|
32
|
-
self.datum =
|
|
39
|
+
self.latitude = location_items[0][:-1] # strip trailing "N"
|
|
40
|
+
self.longitude = location_items[1][:-1] # strip trailing "E"
|
|
41
|
+
self.datum = items[1].strip()
|
|
33
42
|
except Exception as e:
|
|
34
43
|
print(e)
|
|
35
44
|
|
|
36
45
|
def get_latitude(self) -> str:
|
|
37
|
-
return self.
|
|
46
|
+
return self.latitude
|
|
38
47
|
|
|
39
48
|
def get_longitude(self) -> str:
|
|
40
|
-
return self.
|
|
41
|
-
|
|
42
|
-
def get_position_value(self, text_in: str, direction: str) -> str:
|
|
43
|
-
index = text_in.find("°")
|
|
44
|
-
value = text_in[0:index]
|
|
45
|
-
if text_in[index + 1 : len(text_in)] == direction:
|
|
46
|
-
return "-" + self.replace_special_character(value)
|
|
47
|
-
return self.replace_special_character(value)
|
|
48
|
-
|
|
49
|
-
def replace_special_character(self, text_in: str) -> str:
|
|
50
|
-
return text_in.replace("�", "°").replace("Â", "°")
|
|
49
|
+
return self.longitude
|
|
51
50
|
|
|
52
51
|
def validate(self) -> List[str]:
|
|
53
52
|
messages = []
|
|
54
53
|
try:
|
|
55
54
|
messages = messages + self.validate_latitude()
|
|
56
55
|
messages = messages + self.validate_longitude()
|
|
56
|
+
messages = messages + self.validate_datum()
|
|
57
57
|
except Exception:
|
|
58
58
|
messages.append("Location could not be parsed")
|
|
59
59
|
return messages
|
|
@@ -64,6 +64,14 @@ class HeaderPosition:
|
|
|
64
64
|
def validate_longitude(self) -> List[str]:
|
|
65
65
|
return self.validate_item(self.get_longitude(), "Longitude", -180, 180)
|
|
66
66
|
|
|
67
|
+
def validate_datum(self) -> List[str]:
|
|
68
|
+
if self.datum not in ALLOWED_DATUMS:
|
|
69
|
+
return [
|
|
70
|
+
f'Datum "{self.datum}" is not in the allowed list. '
|
|
71
|
+
f"Allowed values: {ALLOWED_DATUMS}"
|
|
72
|
+
]
|
|
73
|
+
return []
|
|
74
|
+
|
|
67
75
|
def validate_item(
|
|
68
76
|
self, value: str, position: str, lower: int, upper: int
|
|
69
77
|
) -> List[str]:
|
|
@@ -72,5 +80,5 @@ class HeaderPosition:
|
|
|
72
80
|
messages.append(position + " could not be parsed")
|
|
73
81
|
value_f = float(value)
|
|
74
82
|
if value_f < lower or value_f > upper:
|
|
75
|
-
messages.append(position
|
|
83
|
+
messages.append(f"{position} should be between {lower} and {upper}")
|
|
76
84
|
return messages
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from typing import List, Optional, Tuple
|
|
3
3
|
|
|
4
|
+
from ..schemas import validate_contractor_metadata
|
|
4
5
|
from .header_parser import HeaderMetaData, HeaderParser
|
|
5
6
|
from .row_parser import RowParser
|
|
6
|
-
from .utils import load_parameter_configs
|
|
7
|
+
from .utils import load_contractor_configs, load_parameter_configs
|
|
7
8
|
from .validate_result import ValidateMeasurementResult, ValidateResult
|
|
8
9
|
|
|
9
10
|
DOCSTRING = """
|
|
@@ -51,6 +52,9 @@ def main():
|
|
|
51
52
|
|
|
52
53
|
messages = header_info.validate()
|
|
53
54
|
messages += validate_with_parameter_configs(header_info)
|
|
55
|
+
messages += validate_contractor_header(
|
|
56
|
+
header_info.contractor, header_info.contractor_division
|
|
57
|
+
)
|
|
54
58
|
if messages:
|
|
55
59
|
print(ValidateMeasurementResult(ValidateResult.ERROR, messages))
|
|
56
60
|
sys.exit(1)
|
|
@@ -110,3 +114,18 @@ def validate_with_parameter_configs(header_info: HeaderMetaData) -> List[str]:
|
|
|
110
114
|
f"The unit {unit} is incorrect for base parameter {key}. The unit should be {cfg.units}"
|
|
111
115
|
)
|
|
112
116
|
return messages
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def validate_contractor_header(contractor: str, contractor_division: str) -> List[str]:
|
|
120
|
+
contractor_names = [name.strip() for name in contractor.split(",")]
|
|
121
|
+
raw_divisions = contractor_division.strip()
|
|
122
|
+
divisions = (
|
|
123
|
+
[]
|
|
124
|
+
if not raw_divisions
|
|
125
|
+
else [division.strip() for division in raw_divisions.split(",")]
|
|
126
|
+
)
|
|
127
|
+
return validate_contractor_metadata(
|
|
128
|
+
contractor_names,
|
|
129
|
+
divisions,
|
|
130
|
+
load_contractor_configs(),
|
|
131
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from importlib import import_module
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from atmos_validation.schemas import Contractor
|
|
6
|
+
from atmos_validation.validate_ascii import validate_contractor_header
|
|
7
|
+
from atmos_validation.validate_ascii.header_parser import HeaderParser
|
|
8
|
+
|
|
9
|
+
ascii_main = import_module("atmos_validation.validate_ascii.main")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_ascii_contractor_and_multiple_divisions_are_rejected(monkeypatch):
|
|
13
|
+
monkeypatch.setattr(
|
|
14
|
+
ascii_main,
|
|
15
|
+
"load_contractor_configs",
|
|
16
|
+
lambda: [
|
|
17
|
+
Contractor(
|
|
18
|
+
contractor="Oceanweather Inc.",
|
|
19
|
+
divisions=["Marine", "Offshore", "Wind", "Wave", "Current"],
|
|
20
|
+
)
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
errors = validate_contractor_header(
|
|
25
|
+
"Oceanweather Inc.", "Marine, Offshore, Wind, Wave, Current"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
assert len(errors) == 1
|
|
29
|
+
assert "same length" in errors[0]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_ascii_unknown_contractor_is_rejected(monkeypatch):
|
|
33
|
+
monkeypatch.setattr(
|
|
34
|
+
ascii_main,
|
|
35
|
+
"load_contractor_configs",
|
|
36
|
+
lambda: [Contractor(contractor="Oceanweather Inc.", divisions=["Marine"])],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
errors = validate_contractor_header("Unknown Contractor", "")
|
|
40
|
+
|
|
41
|
+
assert len(errors) == 1
|
|
42
|
+
assert "Unknown Contractor" in errors[0]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("division", ["NA", "Marine"])
|
|
46
|
+
def test_ascii_na_contractor_means_no_contractor(monkeypatch, division):
|
|
47
|
+
monkeypatch.setattr(
|
|
48
|
+
ascii_main,
|
|
49
|
+
"load_contractor_configs",
|
|
50
|
+
lambda: [Contractor(contractor="Oceanweather Inc.", divisions=["Marine"])],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
errors = validate_contractor_header("NA", division)
|
|
54
|
+
|
|
55
|
+
if division == "Marine":
|
|
56
|
+
assert len(errors) == 1
|
|
57
|
+
assert 'Division "Marine"' in errors[0]
|
|
58
|
+
else:
|
|
59
|
+
assert errors == []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.parametrize("configured_divisions", [None, [], ["Marine"]])
|
|
63
|
+
def test_ascii_na_division_is_not_validated_as_a_division(
|
|
64
|
+
monkeypatch, configured_divisions
|
|
65
|
+
):
|
|
66
|
+
monkeypatch.setattr(
|
|
67
|
+
ascii_main,
|
|
68
|
+
"load_contractor_configs",
|
|
69
|
+
lambda: [
|
|
70
|
+
Contractor(contractor="Oceanweather Inc.", divisions=configured_divisions)
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assert validate_contractor_header("Oceanweather Inc.", "NA") == []
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_ascii_division_not_allowed_by_selected_contractor_is_rejected(monkeypatch):
|
|
78
|
+
monkeypatch.setattr(
|
|
79
|
+
ascii_main,
|
|
80
|
+
"load_contractor_configs",
|
|
81
|
+
lambda: [
|
|
82
|
+
Contractor(contractor="Oceanweather Inc.", divisions=["Marine"]),
|
|
83
|
+
Contractor(contractor="Example Contractor", divisions=None),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
errors = validate_contractor_header("Example Contractor", "Marine")
|
|
88
|
+
|
|
89
|
+
assert len(errors) == 1
|
|
90
|
+
assert "Marine" in errors[0]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize(
|
|
94
|
+
("divisions", "error_count"),
|
|
95
|
+
[
|
|
96
|
+
("Marine, Offshore", 0),
|
|
97
|
+
("Marine, NA", 0),
|
|
98
|
+
("NA, Offshore", 0),
|
|
99
|
+
("NA, NA", 0),
|
|
100
|
+
("", 0),
|
|
101
|
+
("Offshore, Marine", 2),
|
|
102
|
+
("Marine", 1),
|
|
103
|
+
("NA", 1),
|
|
104
|
+
("Marine, Offshore, Marine", 1),
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
def test_ascii_divisions_match_contractors_by_position(
|
|
108
|
+
monkeypatch, divisions, error_count
|
|
109
|
+
):
|
|
110
|
+
monkeypatch.setattr(
|
|
111
|
+
ascii_main,
|
|
112
|
+
"load_contractor_configs",
|
|
113
|
+
lambda: [
|
|
114
|
+
Contractor(contractor="Oceanweather Inc.", divisions=["Marine"]),
|
|
115
|
+
Contractor(contractor="Example Contractor", divisions=["Offshore"]),
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
errors = validate_contractor_header(
|
|
120
|
+
"Oceanweather Inc., Example Contractor", divisions
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
assert len(errors) == error_count
|
|
124
|
+
if divisions == "Offshore, Marine":
|
|
125
|
+
assert 'contractor "Oceanweather Inc."' in errors[0]
|
|
126
|
+
assert 'contractor "Example Contractor"' in errors[1]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_contractor_division_header_is_optional():
|
|
130
|
+
errors = []
|
|
131
|
+
|
|
132
|
+
value = HeaderParser([]).get_simple_value(
|
|
133
|
+
"Contractor Division", errors, optional=True
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert value == ""
|
|
137
|
+
assert errors == []
|