emso-metadata-harmonizer 1.1.0.dev0__tar.gz → 1.1.0.dev1__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.
Files changed (30) hide show
  1. {emso_metadata_harmonizer-1.1.0.dev0/src/emso_metadata_harmonizer.egg-info → emso_metadata_harmonizer-1.1.0.dev1}/PKG-INFO +1 -1
  2. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/__init__.py +1 -1
  3. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/waterframe.py +171 -116
  4. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1/src/emso_metadata_harmonizer.egg-info}/PKG-INFO +1 -1
  5. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/LICENSE +0 -0
  6. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/README.md +0 -0
  7. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/pypi.md +0 -0
  8. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/pyproject.toml +0 -0
  9. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/setup.cfg +0 -0
  10. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/dataset_generator.py +0 -0
  11. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/erddap/__init__.py +0 -0
  12. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/erddap/datasets_xml.py +0 -0
  13. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/erddap/erddap.py +0 -0
  14. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/erddap_config.py +0 -0
  15. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/__init__.py +0 -0
  16. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/constants.py +0 -0
  17. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/dataset.py +0 -0
  18. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/emso.py +0 -0
  19. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/metadata_templates.py +0 -0
  20. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/resource_manager.py +0 -0
  21. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/tests.py +0 -0
  22. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/utils.py +0 -0
  23. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/vocabularies.py +0 -0
  24. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/metadata/xmlutils.py +0 -0
  25. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer/report.py +0 -0
  26. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer.egg-info/SOURCES.txt +0 -0
  27. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer.egg-info/dependency_links.txt +0 -0
  28. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer.egg-info/requires.txt +0 -0
  29. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/src/emso_metadata_harmonizer.egg-info/top_level.txt +0 -0
  30. {emso_metadata_harmonizer-1.1.0.dev0 → emso_metadata_harmonizer-1.1.0.dev1}/tests/test_metadata_harmonizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emso-metadata-harmonizer
3
- Version: 1.1.0.dev0
3
+ Version: 1.1.0.dev1
4
4
  Summary: Toolbox for creating/assessing EMSO-compliant NetCDF datasets and integrate them into ERDDAP services
5
5
  Author-email: Enoc Martínez <enoc.martinez@upc.edu>
6
6
  Maintainer-email: Enoc Martínez <enoc.martinez@upc.edu>
@@ -6,4 +6,4 @@ from .dataset_generator import generate_dataset
6
6
  from .metadata.waterframe import WaterFrame
7
7
  from .metadata import setup_log
8
8
 
9
- __version__ = "1.1.0dev0"
9
+ __version__ = "1.1.0dev1"
@@ -12,7 +12,7 @@ created: 6/6/24
12
12
  import datetime
13
13
  import logging
14
14
  import os.path
15
- from typing import List, Tuple
15
+ from typing import List, Tuple, Dict, Any
16
16
  from .utils import assert_type
17
17
  import numpy as np
18
18
  import pandas as pd
@@ -20,13 +20,13 @@ import netCDF4 as nc
20
20
  import xarray as xr
21
21
  import rich
22
22
  import warnings
23
+ import requests
24
+
23
25
  from .metadata_templates import time_valid_names, depth_valid_names, latitude_valid_names, longitude_valid_names, sensor_id_valid_names, platform_id_valid_names, is_coordinate
24
26
  from . import init_emso_metadata
25
27
  from .constants import iso_time_format
26
28
  from .metadata_templates import dimension_metadata, quality_control_metadata
27
29
  from .utils import LoggerSuperclass, CYN
28
- import requests
29
-
30
30
  from .vocabularies import Keyword
31
31
 
32
32
  try:
@@ -1259,127 +1259,182 @@ def collect_sensor_metadata(metadata:dict, df: pd.DataFrame) -> dict:
1259
1259
  def collect_platform_metadata(metadata:dict, df: pd.DataFrame) -> dict:
1260
1260
  return __collect_metadata(metadata, df, "platform")
1261
1261
 
1262
+ def operational_tests(wf: WaterFrame, quiet=False, verbose=False) -> Tuple[bool, dict]:
1263
+ return OperationalTester(quiet=quiet, verbose=verbose).validate(wf)
1262
1264
 
1263
1265
 
1264
- def operational_tests(wf: WaterFrame, quiet=False) -> Tuple[bool, dict]:
1266
+ class OperationalTester:
1265
1267
  """
1266
- Ensures that the current WaterFrame is operationally sound. The following tests are preformed:
1267
- 1. All variables have the variable_type attribute with a valid value
1268
- 2. Ensure that we have coordinates with the following names: time, depth, latitude, longitude, sensor_id, platform_id
1269
- 3. sensor_id and platform_id values are resolvable identifiers of sensors and platforms metadata variables
1268
+ Ensures that a WaterFrame is operationally sound by checking variable types,
1269
+ mandatory coordinates, P01 parameter codes, standard names, and sensor/platform IDs.
1270
1270
  """
1271
- errors = []
1272
- warnings = []
1273
- infos = []
1274
- __valid_coordinates = ["time", "depth", "latitude", "longitude", "sensor_id", "platform_id", "precise_latitude", "precise_longitude", "time_end"]
1275
- __mandatory_coords = ["time", "depth", "latitude", "longitude", "sensor_id", "platform_id"]
1276
- __valid_variable_types = ["environmental", "biological", "technical", "coordinate", "quality_control", "sensor", "platform"]
1277
-
1278
- for varname, meta in wf.vocabulary.items():
1279
- if "variable_type" not in meta.keys():
1280
- errors.append(f"variable '{varname}' does not have the mandatory variable_type attribute")
1281
- elif meta["variable_type"] not in __valid_variable_types:
1282
- errors.append(f"variable '{varname}' does not have a valid variable_type attribute: '{meta['variable_type']}'")
1283
- elif meta["variable_type"] == "coordinate" and varname not in __valid_coordinates:
1284
- errors.append(f"not a valid coordinate name: '{varname}'")
1285
-
1286
- # check mandatory coordinates
1287
- for man in __mandatory_coords:
1288
- if man not in wf.data.keys():
1289
- errors.append(f"Mandatory coordinate '{man}' not found in sensors metadata")
1290
-
1291
-
1292
- # check unique P01 codes across variables
1293
- p01 = {}
1294
- for varname, meta in wf.vocabulary.items():
1295
- if "sdn_parameter_uri" in meta.keys():
1296
- code = meta["sdn_parameter_uri"]
1297
-
1298
- if code not in p01.keys():
1299
- p01[code] = []
1300
-
1301
- if code: # avoid nulls
1302
- p01[code].append(varname)
1303
-
1304
- for key, values in p01.items():
1305
- if len(values) > 1:
1306
- errors.append(f"P01 code shared across variables {values} ({key})")
1307
-
1308
- # check unique standard_names across variables
1309
- std_names = {}
1310
- for varname, meta in wf.vocabulary.items():
1311
- if "standard_name" in meta.keys():
1312
- code = meta["standard_name"]
1313
-
1314
- if code not in std_names.keys():
1315
- std_names[code] = []
1316
-
1317
- if code: # avoid nulls
1318
- std_names[code].append(varname)
1319
-
1320
- for key, values in std_names.items():
1321
- if len(values) > 1:
1322
- errors.append(f"standard_name attributes shared across variables {values} ({key})")
1323
-
1324
- # Check that sensor_id values are resolvable
1325
-
1326
- if "sensor_id" in wf.data.keys():
1327
- sensor_ids = wf.data["sensor_id"].unique().astype(str)
1328
- for sensor_id in sensor_ids:
1329
- if sensor_id not in wf.sensors.keys():
1330
- errors.append(f"sensor_id not found in sensors metadata: '{sensor_id}'")
1331
-
1332
- # Check that sensor_id values are resolvable
1333
- if "platform_id" in wf.data.keys():
1334
- platform_ids = wf.data["platform_id"].unique().astype(str)
1335
- for platform_id in platform_ids:
1336
- if platform_id not in wf.platforms.keys():
1337
- errors.append(f"not a valid platform_id: '{platform_id}'")
1338
-
1339
- if len(errors) > 0:
1340
- print(wf)
1341
-
1342
- success = True
1343
- if len(errors) > 0:
1344
- success = False
1345
-
1346
- if not quiet:
1347
- rich.print("[cyan]=========== Running Operational tests ===========")
1348
- rich.print(f"ERRORS: {len(errors)}")
1349
- for e in errors:
1350
- rich.print(f"[red] ERROR: {e}[/red]")
1351
-
1352
- rich.print(f"WARNINGS: {len(warnings)}")
1353
- for w in warnings:
1354
- rich.print(f"[yellow]{w}")
1355
-
1356
- rich.print(f"INFO: {len(warnings)}")
1357
- for i in infos:
1358
- rich.print(f"[cyan]{i}")
1359
-
1360
- if success:
1361
- rich.print(f"✅ the NetCDF file is operationally sound!")
1362
- else:
1363
- rich.print(f"❌ the NetCDF file is not operationally valid")
1364
1271
 
1365
- rich.print("[cyan]=================================================\n")
1272
+ VALID_COORDINATES = [
1273
+ "time", "depth", "latitude", "longitude", "sensor_id",
1274
+ "platform_id", "precise_latitude", "precise_longitude", "time_end"
1275
+ ]
1276
+ MANDATORY_COORDS = [
1277
+ "time", "depth", "latitude", "longitude", "sensor_id", "platform_id"
1278
+ ]
1279
+ VALID_VARIABLE_TYPES = [
1280
+ "environmental", "biological", "technical", "coordinate",
1281
+ "quality_control", "sensor", "platform"
1282
+ ]
1283
+
1284
+ def __init__(self, quiet: bool = False, verbose: bool = False):
1285
+ """
1286
+ :param quiet: if True, nothing is written to stdout; outcome is reported via logger instead
1287
+ :param verbose: if True, extra information is written to stdout (has no effect if quiet is set)
1288
+ """
1289
+ self.quiet = quiet
1290
+ self.verbose = verbose
1291
+ self.errors = []
1292
+ self.warnings = []
1293
+ self.infos = []
1366
1294
 
1367
- else:
1368
- if success:
1369
- logger.info(f"Operational tests: ✅ the NetCDF file is operationally sound!")
1370
- else:
1371
- logger.info("Operational tests: ❌ the NetCDF file is not operationally valid")
1372
-
1373
- report = {
1374
- "valid": success,
1375
- "report": {
1376
- "errors": errors,
1377
- "warnings": warnings,
1378
- "infos": infos
1295
+ # ------------------------------------------------------------------ #
1296
+ # output helpers #
1297
+ # ------------------------------------------------------------------ #
1298
+ def __print(self, message: str = "", end: str = "\n"):
1299
+ """Prints a message unless the validator is in quiet mode"""
1300
+ if not self.quiet:
1301
+ rich.print(message, end=end)
1302
+
1303
+ def __print_verbose(self, message: str = "", end: str = "\n"):
1304
+ """Prints a message only in verbose mode"""
1305
+ if self.verbose:
1306
+ self.__print(message, end=end)
1307
+
1308
+ def __error(self, error: str, message: str = "", end: str = "\n"):
1309
+ """Registers an error and prints it."""
1310
+ self.errors.append(error)
1311
+ self.__print(message if message else f"[red] ERROR: {error}[/red]", end=end)
1312
+
1313
+ def __warning(self, warning: str, message: str = "", end: str = "\n"):
1314
+ """Registers a warning and prints it."""
1315
+ self.warnings.append(warning)
1316
+ self.__print(message if message else f"[yellow] WARNING: {warning}[/yellow]", end=end)
1317
+
1318
+ def __info(self, info: str, message: str = "", end: str = "\n"):
1319
+ """Registers an info message and prints it."""
1320
+ self.infos.append(info)
1321
+ self.__print(message if message else f"[cyan] INFO: {info}[/cyan]", end=end)
1322
+
1323
+ # ------------------------------------------------------------------ #
1324
+ # public API #
1325
+ # ------------------------------------------------------------------ #
1326
+ def validate(self, wf: WaterFrame) -> Tuple[bool, Dict[str, Any]]:
1327
+ """
1328
+ Validates the operational soundness of a WaterFrame. Previous results are discarded.
1329
+
1330
+ :param wf: WaterFrame to be checked
1331
+ :returns: Tuple (success: bool, report: dict)
1332
+ """
1333
+ self.errors = []
1334
+ self.warnings = []
1335
+ self.infos = []
1336
+
1337
+ self.__validate(wf)
1338
+
1339
+ success = len(self.errors) == 0
1340
+
1341
+ if self.quiet:
1342
+ log = logging.getLogger()
1343
+ if success:
1344
+ log.info("Operational tests: ✅ the NetCDF file is operationally sound!")
1345
+ else:
1346
+ log.info("Operational tests: ❌ the NetCDF file is not operationally valid")
1347
+
1348
+ report = {
1349
+ "valid": success,
1350
+ "report": {
1351
+ "errors": self.errors,
1352
+ "warnings": self.warnings,
1353
+ "infos": self.infos,
1354
+ },
1379
1355
  }
1380
- }
1381
- return success, report
1382
1356
 
1357
+ return success, report
1358
+
1359
+ # ------------------------------------------------------------------ #
1360
+ # internal checks #
1361
+ # ------------------------------------------------------------------ #
1362
+ def __validate(self, wf: WaterFrame):
1363
+ """Runs all operational tests on the given WaterFrame."""
1364
+ self.__print("[cyan]=========== Running Operational tests ===========")
1365
+
1366
+ # 1. Check variable_type attributes
1367
+ for varname, meta in wf.vocabulary.items():
1368
+ if "variable_type" not in meta:
1369
+ self.__error(f"variable '{varname}' does not have the mandatory variable_type attribute")
1370
+ elif meta["variable_type"] not in self.VALID_VARIABLE_TYPES:
1371
+ self.__error(
1372
+ f"variable '{varname}' does not have a valid variable_type attribute: '{meta['variable_type']}'"
1373
+ )
1374
+ elif meta["variable_type"] == "coordinate" and varname not in self.VALID_COORDINATES:
1375
+ self.__error(f"not a valid coordinate name: '{varname}'")
1376
+
1377
+ # 2. Check mandatory coordinates
1378
+ for man in self.MANDATORY_COORDS:
1379
+ if man not in wf.data.keys():
1380
+ self.__error(f"Mandatory coordinate '{man}' not found in sensors metadata")
1381
+
1382
+ # 3. Check unique P01 codes across variables
1383
+ p01 = {}
1384
+ for varname, meta in wf.vocabulary.items():
1385
+ if "sdn_parameter_uri" in meta:
1386
+ code = meta["sdn_parameter_uri"]
1387
+ if code not in p01:
1388
+ p01[code] = []
1389
+ if code: # avoid nulls
1390
+ p01[code].append(varname)
1391
+
1392
+ for key, values in p01.items():
1393
+ if len(values) > 1:
1394
+ self.__error(f"P01 code shared across variables {values} ({key})")
1395
+
1396
+ # 4. Check unique standard_names across variables
1397
+ std_names = {}
1398
+ for varname, meta in wf.vocabulary.items():
1399
+ if "standard_name" in meta:
1400
+ code = meta["standard_name"]
1401
+ if code not in std_names:
1402
+ std_names[code] = []
1403
+ if code: # avoid nulls
1404
+ std_names[code].append(varname)
1405
+
1406
+ for key, values in std_names.items():
1407
+ if len(values) > 1:
1408
+ self.__error(f"standard_name attributes shared across variables {values} ({key})")
1409
+
1410
+ # 5. Check resolvable sensor_id values
1411
+ if "sensor_id" in wf.data.keys():
1412
+ sensor_ids = wf.data["sensor_id"].unique().astype(str)
1413
+ for sensor_id in sensor_ids:
1414
+ if sensor_id not in wf.sensors.keys():
1415
+ self.__error(f"sensor_id not found in sensors metadata: '{sensor_id}'")
1416
+
1417
+ # 6. Check resolvable platform_id values
1418
+ if "platform_id" in wf.data.keys():
1419
+ platform_ids = wf.data["platform_id"].unique().astype(str)
1420
+ for platform_id in platform_ids:
1421
+ if platform_id not in wf.platforms.keys():
1422
+ self.__error(f"not a valid platform_id: '{platform_id}'")
1423
+
1424
+ success = len(self.errors) == 0
1425
+
1426
+ # Print summary section if not quiet
1427
+ if not self.quiet:
1428
+ self.__print_verbose(f"ERRORS: {len(self.errors)}")
1429
+ self.__print_verbose(f"WARNINGS: {len(self.warnings)}")
1430
+ self.__print_verbose(f"INFO: {len(self.infos)}")
1431
+
1432
+ if success:
1433
+ self.__print("✅ the NetCDF file is operationally sound!")
1434
+ else:
1435
+ self.__print("❌ the NetCDF file is not operationally valid")
1436
+
1437
+ self.__print("[cyan]=================================================\n")
1383
1438
 
1384
1439
  class KeywordTester:
1385
1440
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: emso-metadata-harmonizer
3
- Version: 1.1.0.dev0
3
+ Version: 1.1.0.dev1
4
4
  Summary: Toolbox for creating/assessing EMSO-compliant NetCDF datasets and integrate them into ERDDAP services
5
5
  Author-email: Enoc Martínez <enoc.martinez@upc.edu>
6
6
  Maintainer-email: Enoc Martínez <enoc.martinez@upc.edu>