Skip to content

Commit

Permalink
UW-506 sfc_climo_gen driver (#410)
Browse files Browse the repository at this point in the history
  • Loading branch information
maddenp-noaa authored Feb 21, 2024
1 parent ea8e7e8 commit b89a788
Show file tree
Hide file tree
Showing 51 changed files with 1,496 additions and 687 deletions.
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ env: package
conda create -y -n $(call spec,buildnum,-) $(CHANNELS) $(call spec,build,=)

format:
@echo "=> Running formatters"
black src
isort src
cd src && docformatter . || test $$? -eq 3
for a in $$(find src -type f -name "*.jsonschema"); do b=$$(jq -S . $$a) && echo "$$b" >$$a || exit 1; done
@./format

lint:
recipe/run_test.sh lint
Expand Down
1 change: 1 addition & 0 deletions docs/sections/user_guide/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ API
fv3
logging
rocoto
sfc_climo_gen
template
5 changes: 5 additions & 0 deletions docs/sections/user_guide/api/sfc_climo_gen.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
``uwtools.api.sfc_climo_gen``
=============================

.. automodule:: uwtools.api.sfc_climo_gen
:members:
2 changes: 1 addition & 1 deletion docs/sections/user_guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ User Guide
installation
cli/index
api/index
uw_yaml/index
yaml/index
File renamed without changes.
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions format
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash -eu

echo "=> Running black"
black src

echo "=> Running isort"
isort -q src

echo "=> Running docformatter"
(cd src && docformatter . || test $$? -eq 3)

echo "=> Running jq"
for a in $(find src -type f -name "*.jsonschema"); do
b=$(jq -S . $a) && echo "$b" >$a || (echo " in $a"; false)
done
1 change: 0 additions & 1 deletion recipe/run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ unittest() {
}

test "${CONDA_BUILD:-}" = 1 && cd ../test_files || cd $(dirname $0)/../src
msg Running in $PWD
if [[ -n "${1:-}" ]]; then
# Run single specified code-quality tool.
$1
Expand Down
1 change: 1 addition & 0 deletions src/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ warn_return_any = true
[tool.pylint.messages_control]
disable = [
"consider-using-f-string",
"duplicate-code",
"invalid-name",
"missing-module-docstring",
"too-few-public-methods",
Expand Down
3 changes: 3 additions & 0 deletions src/uwtools/api/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
API access to uwtools configuration management tools.
"""
import os
from pathlib import Path
from typing import List, Optional, Union
Expand Down
5 changes: 4 additions & 1 deletion src/uwtools/api/fv3.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
API access to the uwtools FV3 driver.
"""
import datetime as dt
from pathlib import Path
from typing import Dict
Expand All @@ -21,7 +24,7 @@ def execute(
Otherwise, the forecast will be run directly on the current system.
:param task: The task to execute
:param config_file: Path to UW YAML config file
:param config_file: Path to YAML config file
:param cycle: The cycle to run
:param batch: Submit run to the batch system
:param dry_run: Do not run forecast, just report what would have been done
Expand Down
3 changes: 3 additions & 0 deletions src/uwtools/api/logging.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
API access to uwtools logging logic.
"""
import logging

from uwtools.logging import setup_logging as _setup_logging
Expand Down
3 changes: 3 additions & 0 deletions src/uwtools/api/rocoto.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
API access to uwtools Rocoto support.
"""
from pathlib import Path
from typing import Optional, Union

Expand Down
42 changes: 42 additions & 0 deletions src/uwtools/api/sfc_climo_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
API access to the uwtools sfc_climo_gen driver.
"""
from pathlib import Path
from typing import Dict

import iotaa

from uwtools.drivers.sfc_climo_gen import SfcClimoGen


def execute(
task: str,
config_file: Path,
batch: bool = False,
dry_run: bool = False,
) -> bool:
"""
Execute an sfc_climo_gen task.
If ``batch`` is specified, a runscript will be written and submitted to the batch system.
Otherwise, the forecast will be run directly on the current system.
:param task: The task to execute
:param config_file: Path to YAML config file
:param batch: Submit run to the batch system
:param dry_run: Do not run forecast, just report what would have been done
:return: True if task completes without raising an exception
"""
obj = SfcClimoGen(config_file=config_file, batch=batch, dry_run=dry_run)
getattr(obj, task)()
return True


def tasks() -> Dict[str, str]:
"""
Returns a mapping from task names to their one-line descriptions.
"""
return {
task: getattr(SfcClimoGen, task).__doc__.strip().split("\n")[0]
for task in iotaa.tasknames(SfcClimoGen)
}
3 changes: 3 additions & 0 deletions src/uwtools/api/template.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
API access to uwtools templating logic.
"""
from pathlib import Path
from typing import Dict, Optional, Union

Expand Down
69 changes: 63 additions & 6 deletions src/uwtools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import uwtools.api.config
import uwtools.api.fv3
import uwtools.api.rocoto
import uwtools.api.sfc_climo_gen
import uwtools.api.template
import uwtools.config.jinja2
import uwtools.rocoto
Expand Down Expand Up @@ -51,6 +52,7 @@ def main() -> None:
STR.config: _dispatch_config,
STR.fv3: _dispatch_fv3,
STR.rocoto: _dispatch_rocoto,
STR.sfcclimogen: _dispatch_sfc_climo_gen,
STR.template: _dispatch_template,
}
sys.exit(0 if modes[args[STR.mode]](args) else 1)
Expand Down Expand Up @@ -327,6 +329,58 @@ def _dispatch_rocoto_validate(args: Args) -> bool:
return uwtools.api.rocoto.validate(xml_file=args[STR.infile])


# Mode sfc_climo_gen


def _add_subparser_sfc_climo_gen(subparsers: Subparsers) -> ModeChecks:
"""
Subparser for mode: sfc_climo_gen
:param subparsers: Parent parser's subparsers, to add this subparser to.
"""
parser = _add_subparser(subparsers, STR.sfcclimogen, "Execute sfc_climo_gen tasks")
_basic_setup(parser)
subparsers = _add_subparsers(parser, STR.action, STR.task.upper())
return {
task: _add_subparser_sfc_climo_gen_task(subparsers, task, helpmsg)
for task, helpmsg in uwtools.api.sfc_climo_gen.tasks().items()
}


def _add_subparser_sfc_climo_gen_task(
subparsers: Subparsers, task: str, helpmsg: str
) -> ActionChecks:
"""
Subparser for mode: sfc_climo_gen <task>
:param subparsers: Parent parser's subparsers, to add this subparser to.
:param task: The task to add a subparser for.
:param helpmsg: Help message for task.
"""
parser = _add_subparser(subparsers, task, helpmsg.rstrip("."))
required = parser.add_argument_group(TITLE_REQ_ARG)
_add_arg_config_file(required)
optional = _basic_setup(parser)
_add_arg_batch(optional)
_add_arg_dry_run(optional)
checks = _add_args_verbosity(optional)
return checks


def _dispatch_sfc_climo_gen(args: Args) -> bool:
"""
Dispatch logic for sfc_climo_gen mode.
:param args: Parsed command-line args.
"""
return uwtools.api.sfc_climo_gen.execute(
task=args[STR.action],
config_file=args[STR.cfgfile],
batch=args[STR.batch],
dry_run=args[STR.dryrun],
)


# Mode template


Expand Down Expand Up @@ -442,7 +496,7 @@ def _add_arg_config_file(group: Group) -> None:
help="Path to config file",
metavar="PATH",
required=True,
type=str,
type=Path,
)


Expand Down Expand Up @@ -491,7 +545,7 @@ def _add_arg_file_path(group: Group, switch: str, helpmsg: str, required: bool =
help=helpmsg,
metavar="PATH",
required=required,
type=str,
type=Path,
)


Expand All @@ -502,7 +556,7 @@ def _add_arg_input_file(group: Group, required: bool = False) -> None:
help="Path to input file (defaults to stdin)",
metavar="PATH",
required=required,
type=str,
type=Path,
)


Expand Down Expand Up @@ -532,7 +586,7 @@ def _add_arg_output_file(group: Group, required: bool = False) -> None:
help="Path to output file (defaults to stdout)",
metavar="PATH",
required=required,
type=str,
type=Path,
)


Expand Down Expand Up @@ -561,7 +615,7 @@ def _add_arg_schema_file(group: Group) -> None:
help="Path to schema file to use for validation",
metavar="PATH",
required=True,
type=str,
type=Path,
)


Expand All @@ -571,6 +625,7 @@ def _add_arg_supplemental_files(group: Group) -> None:
help="Additional files to supplement primary input",
metavar="PATH",
nargs="*",
type=Path,
)


Expand All @@ -580,7 +635,7 @@ def _add_arg_values_file(group: Group, required: bool = False) -> None:
help="Path to file providing override or interpolation values",
metavar="PATH",
required=required,
type=str,
type=Path,
)


Expand Down Expand Up @@ -742,6 +797,7 @@ def _parse_args(raw_args: List[str]) -> Tuple[Args, Checks]:
STR.config: _add_subparser_config(subparsers),
STR.fv3: _add_subparser_fv3(subparsers),
STR.rocoto: _add_subparser_rocoto(subparsers),
STR.sfcclimogen: _add_subparser_sfc_climo_gen(subparsers),
STR.template: _add_subparser_template(subparsers),
}
return vars(parser.parse_args(raw_args)), checks
Expand Down Expand Up @@ -790,6 +846,7 @@ class STR:
rocoto: str = "rocoto"
run: str = "run"
schemafile: str = "schema_file"
sfcclimogen: str = "sfc_climo_gen"
suppfiles: str = "supplemental_files"
task: str = "task"
tasks: str = "tasks"
Expand Down
2 changes: 0 additions & 2 deletions src/uwtools/config/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,6 @@ def _validate_format_supplemental(

# Import-time code

# pylint: disable=duplicate-code

# The following statements dynamically interpolate values into functions' docstrings, which will not
# work if the docstrings are inlined in the functions. They must remain separate statements to avoid
# hardcoding values into them.
Expand Down
26 changes: 20 additions & 6 deletions src/uwtools/config/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
from typing import List, Optional, Union

import jsonschema
from referencing import Registry, Resource
from referencing.jsonschema import DRAFT202012

from uwtools.config.formats.yaml import YAMLConfig
from uwtools.logging import log
from uwtools.utils.file import resource_path

# Public functions

Expand All @@ -18,7 +21,7 @@ def validate_yaml(
schema_file: Path, config: Union[dict, YAMLConfig, Optional[Path]] = None
) -> bool:
"""
Check whether the given config conforms to the given JSON Schema spec.
Report any errors arising from validation of the given config against the given JSON Schema.
:param schema_file: The JSON Schema file to use for validation.
:param config: The config to validate.
Expand All @@ -27,12 +30,10 @@ def validate_yaml(
with open(schema_file, "r", encoding="utf-8") as f:
schema = json.load(f)
cfgobj = _prep_config(config)
# Collect and report on schema-validation errors.
errors = _validation_errors(cfgobj.data, schema)
log_method = log.error if errors else log.info
log_method(
"%s UW schema-validation error%s found", len(errors), "" if len(errors) == 1 else "s"
)
log_msg = "%s UW schema-validation error%s found"
log_method(log_msg, len(errors), "" if len(errors) == 1 else "s")
for error in errors:
for line in str(error).split("\n"):
log.error(line)
Expand All @@ -57,6 +58,19 @@ def _prep_config(config: Union[dict, YAMLConfig, Optional[Path]]) -> YAMLConfig:
def _validation_errors(config: Union[dict, list], schema: dict) -> List[str]:
"""
Identify schema-validation errors.
:param config: A config to validate.
:param schema: JSON Schema to validate the config against.
:return: Any validation errors.
"""
validator = jsonschema.Draft202012Validator(schema)

# See https://github.com/python-jsonschema/referencing/issues/61 about typing issues.

def retrieve(uri: str) -> Resource:
name = uri.split(":")[-1]
with open(resource_path(f"jsonschema/{name}.jsonschema"), "r", encoding="utf-8") as f:
return Resource(contents=json.load(f), specification=DRAFT202012) # type: ignore

registry = Registry(retrieve=retrieve) # type: ignore
validator = jsonschema.Draft202012Validator(schema, registry=registry)
return list(validator.iter_errors(config))
10 changes: 7 additions & 3 deletions src/uwtools/drivers/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from uwtools.exceptions import UWConfigError
from uwtools.logging import log
from uwtools.scheduler import JobScheduler
from uwtools.utils.file import resource_path


class Driver(ABC):
Expand Down Expand Up @@ -133,13 +134,16 @@ def _validate(self) -> None:
Perform all necessary schema validation.
"""

def _validate_one(self, schema_file: Path) -> None:
def _validate_one(self, schema_name: str) -> None:
"""
Validate the config.
:param schema_file: The schema file to validate the config against.
:param schema_name: Name of uwtools schema to validate the config against.
:raises: UWConfigError if config fails validation.
"""
log.info("Validating config per %s", schema_file)

log.info("Validating config per schema %s", schema_name)
schema_file = resource_path("jsonschema") / f"{schema_name}.jsonschema"
log.debug("Using schema file: %s", schema_file)
if not validator.validate_yaml(config=self._config, schema_file=schema_file):
raise UWConfigError("YAML validation errors")
Loading

0 comments on commit b89a788

Please sign in to comment.