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

UW-506 sfc_climo_gen driver #410

Merged
merged 40 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1eba70a
Remove "user" block from fv3.jsonschema
maddenp Feb 14, 2024
db16daa
Add sfc_climo_gen.jsonschema
maddenp Feb 14, 2024
832cd03
Work on schema
maddenp Feb 14, 2024
f298375
Add sfc_climo_gen.py
maddenp Feb 14, 2024
be93ade
Initial unit test
maddenp Feb 14, 2024
07b1b0b
Work on schemas
maddenp Feb 14, 2024
4b88a77
Work on schemas
maddenp Feb 14, 2024
831e7d1
Add comments
maddenp Feb 14, 2024
fc97527
Add CLI integration and API module
maddenp Feb 14, 2024
7b67068
Add API doc stub for sfc_climo_gen
maddenp Feb 14, 2024
074b12b
Add namelist task
maddenp Feb 14, 2024
2b8138d
Add runscript task
maddenp Feb 14, 2024
aded7fb
WIP
maddenp Feb 14, 2024
f65c578
WIP
maddenp Feb 14, 2024
22b18ac
WIP
maddenp Feb 14, 2024
e562e88
WIP
maddenp Feb 14, 2024
2fc27c7
WIP
maddenp Feb 14, 2024
a6000c6
Unit tests @ 98%
maddenp Feb 14, 2024
44643ce
Work on tests
maddenp Feb 14, 2024
e679fd4
Unit tests @ 99%
maddenp Feb 15, 2024
4d32efd
Work on tests
maddenp Feb 15, 2024
f3bb78e
Unit tests @ 100%
maddenp Feb 15, 2024
0efe946
More unit tests
maddenp Feb 15, 2024
a73b276
Work on schema tests
maddenp Feb 15, 2024
c81999c
Doc tweaks
maddenp Feb 15, 2024
7a2c184
Rename test functions
maddenp Feb 15, 2024
d4ae354
Convert str->Path in CLI
maddenp Feb 15, 2024
fa48c9a
Add and use format script
maddenp Feb 15, 2024
68e304c
Reorg resources & update schema use
maddenp Feb 15, 2024
5bdc69f
Work on schema validation
maddenp Feb 15, 2024
191d6df
Work on schema validation
maddenp Feb 15, 2024
89b4cfc
Work on schema validation
maddenp Feb 15, 2024
16fdeeb
Work on schema validation
maddenp Feb 15, 2024
1fe77d0
Work on schema validation
maddenp Feb 15, 2024
199ccd7
Unit tests @ 100%
maddenp Feb 15, 2024
393b4f7
Add __init__.py files in resources subdirectories
maddenp Feb 15, 2024
04fab2d
Add module docstrings to uwtools.api.*
maddenp Feb 15, 2024
f9fa2cf
Add module docstring
maddenp Feb 15, 2024
4d2e3a0
Simplify some docstrings
maddenp Feb 15, 2024
ef2eb53
Tweak task descriptions
maddenp Feb 21, 2024
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
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
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved

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
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
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
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
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
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
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",
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
"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.
"""
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
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.
"""
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
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.
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved

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
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved


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,
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
)


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)
maddenp-noaa marked this conversation as resolved.
Show resolved Hide resolved
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
Loading