-
Notifications
You must be signed in to change notification settings - Fork 22
697.workflow checkout tool #736
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
base: main
Are you sure you want to change the base?
Changes from all commits
0dc8622
099342f
40d43a5
440a040
3249384
7c96c60
125b5e8
b217d5e
0694409
13ca73c
70171a6
c558027
60e73cc
38accb6
06750f2
ecb3ed3
32d5906
881366e
8bef3ee
7d1eb4e
516cf5d
95e25d8
e3c2452
7b1f816
c48728b
817bf7a
09afc2d
a29645a
560093c
10ce009
61be15c
04e8985
9daa391
14e87e9
7af0425
c235bb4
5ee7f19
007696d
9ab07ca
2d26bce
1af0992
14c0d20
ef0377b
779b6ba
6ce4930
0bd5f60
e542b60
b1f4664
fd672d3
98fafcb
7ad2290
f62089f
361f65f
a5e9314
d59b3b6
2411f6e
37bc158
b6f8d65
63c92a8
e1459ef
cf09e3d
ce50e1f
c2886ca
2164715
6923876
c7cbbfe
db193b9
96a1f7a
3bca7db
87b326f
1293aa8
4a36008
866b9d4
c374c69
57452a3
341c7d8
6928357
f4b7a7d
6725ce8
169f048
1f84993
56b4741
8997960
8a80800
637e6ee
318a426
a1e96c1
322fb44
78101e8
560936d
9c23e71
053c01a
a95469e
6122a2c
8c5a8c2
ea0d90f
8ea6c34
67c1544
c619a25
ca4c2e2
4d19b2f
3ee11a4
da10b19
6507c01
1a0e830
964e31f
c348ceb
390d74d
3edada1
8b94b3f
338893c
b1766d3
dfe8c38
e7e9f5f
9aab444
a8eed8a
b1b5142
7cffb66
0230fd9
1e29e4e
be774dc
7cf7523
4a94f60
f5bece4
375f833
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| """ | ||
| CLI Tests for fre workflow * | ||
|
|
||
| Tests the command-line-interface commands for each tool in the fre workflow suite. | ||
| - successful invocation of fre workflow $tool | ||
| - successful invocation of fre workflow $tool --help | ||
| - expected failure for fre workflow $tool --optionDne (failure for undefined click option) | ||
| """ | ||
| from pathlib import Path | ||
| from click.testing import CliRunner | ||
| from fre import fre | ||
|
|
||
| runner = CliRunner() | ||
|
|
||
| #-- fre workflow | ||
| def test_cli_fre_workflow(): | ||
| ''' fre workflow ''' | ||
| result = runner.invoke(fre.fre, args=["workflow"]) | ||
| assert result.exit_code == 2 | ||
|
|
||
| def test_cli_fre_workflow_help(): | ||
| ''' fre workflow --help ''' | ||
| result = runner.invoke(fre.fre, args=["workflow", "--help"]) | ||
| assert result.exit_code == 0 | ||
|
|
||
| def test_cli_fre_workflow_opt_dne(): | ||
| ''' fre workflow optionDNE ''' | ||
| result = runner.invoke(fre.fre, args=["workflow", "optionDNE"]) | ||
| assert result.exit_code == 2 | ||
|
|
||
| #-- fre workflow checkout | ||
| def test_cli_fre_workflow_checkout(): | ||
| ''' fre workflow checkout''' | ||
| result = runner.invoke(fre.fre, args=["workflow", "checkout"]) | ||
| assert result.exit_code == 2 | ||
|
|
||
| def test_cli_fre_workflow_checkout_help(): | ||
| ''' fre workflow checkout --help ''' | ||
| result = runner.invoke(fre.fre, args=["workflow", "checkout", "--help"]) | ||
| assert result.exit_code == 0 | ||
|
|
||
| def test_cli_fre_workflow_checkout_opt_dne(): | ||
| ''' fre workflow checkout optionDNE ''' | ||
| result = runner.invoke(fre.fre, args=["workflow", "checkout", "optionDNE"]) | ||
| assert result.exit_code == 2 | ||
|
|
||
| def test_cli_fre_workflow_checkout_target_dir_set(tmp_path): | ||
| """ | ||
| Test checkout in target directory if --target-dir is explicitly set. | ||
| """ | ||
| experiment = "c96L65_am5f7b12r1_amip_TESTING" | ||
| result = runner.invoke(fre.fre, args=["workflow", "checkout", | ||
| "--yamlfile", "fre/workflow/tests/AM5_example/am5.yaml", | ||
| "--experiment", experiment, | ||
| "--application", "pp", | ||
| "--target-dir", tmp_path]) | ||
| assert result.exit_code == 0 | ||
| assert Path(f"{tmp_path}/cylc-src/{experiment}").exists() | ||
|
|
||
| def test_cli_fre_workflow_checkout_default_dir(monkeypatch, tmp_path): | ||
| """ | ||
| Test workflow repository is cloned in the default location | ||
| if --target-dir is not set; default = ~./fre-workflows | ||
| """ | ||
| # Create and set a mock HOME | ||
| fake_home = f"{tmp_path}/fake_home" | ||
| Path(fake_home).mkdir(parents=True,exist_ok=True) | ||
| monkeypatch.setenv("HOME", f"{tmp_path}/fake_home") | ||
|
|
||
| experiment = "c96L65_am5f7b12r1_amip_TESTING" | ||
| result = runner.invoke(fre.fre, args=["workflow", "checkout", | ||
| "-y", "fre/workflow/tests/AM5_example/am5.yaml", | ||
| "-e", experiment, | ||
| "-a", "pp"]) | ||
| assert result.exit_code == 0 | ||
| assert Path(f"{fake_home}/.fre-workflows/cylc-src/{experiment}").exists() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # FRE workflow | ||
|
|
||
| The`fre workflow` toolset allows users to clone, install, and run a cylc workflow. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am left wondering "what exactly is a workflow". Could we explicitly define that here? |
||
|
|
||
| The workflow repository and version are specified in `the setting.yaml`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. settings.yaml? or setting.yaml?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also, do you plan to link to a yaml? Or is there a section in the docs that explains what the settings.yaml is? |
||
|
|
||
| ## Quickstart | ||
| From the top-level dircetory of the fre-cli repository: | ||
| ``` | ||
| # Checkout/clone the post-processing workflow repository | ||
| fre workflow checkout -y fre/workflow/tests/AM5_example/am5.yaml -e c96L65_am5f7b12r1_amip_TESTING --application pp | ||
| ``` | ||
|
|
||
| ## Subtools | ||
| - `fre workflow checkout [options]` | ||
| - Purpose: Clone the specified workflow repository from the settings.yaml, associated with the application passed. | ||
| - Options: | ||
| - `-y, --yamlfile [model yaml] (str; required)` | ||
| - `-e, --experiment [experiment name] (str; required)` | ||
| - `-a, --application [ run | pp ] (str; required)` | ||
| - `--target-dir [target location where workflow will be cloned] (str; optional; default is ~/.fre-workflows` | ||
| - `--force-checkout (bool; optional)` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| """ Workflow checkout """ | ||
| import os | ||
| import subprocess | ||
| import filecmp | ||
| from pathlib import Path | ||
| import logging | ||
| import shutil | ||
| from datetime import datetime | ||
| from typing import Optional | ||
| import json | ||
| from jsonschema import validate, SchemaError, ValidationError | ||
|
|
||
| import fre.yamltools.combine_yamls_script as cy | ||
| from fre.app.helpers import change_directory | ||
|
|
||
| fre_logger = logging.getLogger(__name__) | ||
|
|
||
| ######VALIDATE##### | ||
| def validate_yaml(yamlfile: dict, application: str): | ||
| """ | ||
| Validate the format of the yaml file against the | ||
| schema.json held in [gfdl_msd_schemas](https://github.com/NOAA-GFDL/gfdl_msd_schemas). | ||
|
|
||
| :param yamlfile: Dictionary containing the combined model, | ||
| settings, pp, and analysis yaml content | ||
| :type yamlfile: dict | ||
| :param application: type of workflow to check out/clone | ||
| :type application: string | ||
| :raises ValueError: | ||
| - invalid gfdl_msd_schema path | ||
| - invalid combined yaml | ||
| - miscellaneous error in validation | ||
| """ | ||
| schema_dir = Path(__file__).resolve().parents[1] | ||
| schema_path = os.path.join(schema_dir, 'gfdl_msd_schemas', 'FRE', f'fre_{application}.json') | ||
| fre_logger.info("Using yaml schema '%s'", schema_path) | ||
| # Load the json schema: .load() (vs .loads()) reads and parses the json in one) | ||
| try: | ||
| with open(schema_path,'r', encoding='utf-8') as s: | ||
| schema = json.load(s) | ||
| except: | ||
| fre_logger.error("Schema '%s' is not valid. Contact the FRE team.", schema_path) | ||
| raise | ||
|
|
||
| # Validate yaml | ||
| # If the yaml is not valid, the schema validation will raise errors and exit | ||
| try: | ||
| validate(instance = yamlfile,schema=schema) | ||
| fre_logger.info(" ** COMBINED YAML VALID ** ") | ||
| except SchemaError as exc: | ||
| raise ValueError(f"Schema '{schema_path}' is not valid. Contact the FRE team.") from exc | ||
| except ValidationError as exc: | ||
| raise ValueError("Combined yaml is not valid. Please fix the errors and try again.") from exc | ||
| except Exception as exc: | ||
| raise ValueError("Miscellaneous error from validation. Please try to find the error and try again.") from exc | ||
|
|
||
| def workflow_checkout(target_dir: str = None, yamlfile: str = None, experiment: str = None, | ||
| application: str = None, force_checkout: Optional[bool] = False): | ||
| """ | ||
| Create a directory and clone the workflow template files from a specified repository. | ||
|
|
||
| :param yamlfile: Model yaml configuration file | ||
| :type yamlfile: str | ||
| :param experiment: One of the experiment names listed in the model yaml file. | ||
| Note: the command "fre list exps -y [model_yamlfile]" can be used to | ||
| list the available experiment names | ||
| :type experiment: str | ||
| :param application: String used to specify the type of workflow to be cloned. | ||
| Ex.: run, postprocess | ||
| :type application: str | ||
| :param target_dir: Target location to create the cylc-src/<workflow> directory in | ||
| :type target_dir: str | ||
| :param force_checkout: If the workflow directory exists, move it to an archived location | ||
| (~/.fre-workflows/archived) and re-clone the workflow repository | ||
| :type force_checkout: bool | ||
| :raises OSError: if the checkout script cannot be created | ||
| :raises ValueError: | ||
| - if the repository and/or tag was not defined | ||
| - if the target directory does not exist or cannot be found | ||
| - if tag or branch does not match the git clone branch arg | ||
| """ | ||
| # Used in consolidate_yamls function for now | ||
| platform = None | ||
| target = None | ||
|
|
||
| # Set the default target directory location | ||
| if target_dir is None: | ||
| target_dir = os.path.expanduser("~/.fre-workflows") | ||
|
|
||
| if application in ["run", "pp"]: | ||
| fre_logger.info(" ** Configuring the resolved YAML for the %s **", application) | ||
| yaml = cy.consolidate_yamls(yamlfile=yamlfile, | ||
| experiment=experiment, | ||
| platform=platform, | ||
| target=target, | ||
| use=application, | ||
| output="config.yaml") | ||
|
|
||
| validate_yaml(yamlfile = yaml, application = application) | ||
|
|
||
| # Reset application for pp to make it discoverable in yaml config | ||
| if application == "pp": | ||
| application = "postprocess" | ||
|
|
||
| workflow_info = yaml.get(application).get("workflow") | ||
|
|
||
| yaml_filepath = f"{Path.cwd()}/config.yaml" | ||
| repo = workflow_info.get("repository") | ||
| tag = workflow_info.get("version") | ||
| fre_logger.info("Defined tag ==> '%s'", tag) | ||
|
|
||
| if None in [repo, tag]: | ||
| raise ValueError(f"One of these are None: repo / tag = {repo} / {tag}") | ||
|
|
||
| fre_logger.info("(%s):(%s) check out for %s ==> REQUESTED", repo, tag, application) | ||
|
|
||
| # Create src_dir if it does not exist | ||
| if not Path(target_dir).exists(): | ||
| Path(target_dir).mkdir(parents=True, exist_ok=True) | ||
|
|
||
| # Define cylc-src directory | ||
| src_dir = f"{target_dir}/cylc-src" | ||
|
singhd789 marked this conversation as resolved.
|
||
| # workflow name | ||
| workflow_name = experiment | ||
|
|
||
| # create workflow in cylc-src | ||
| try: | ||
| Path(src_dir).mkdir(parents=True, exist_ok=True) | ||
| except Exception as exc: | ||
| raise OSError( | ||
| f"(checkoutScript) directory {src_dir} wasn't able to be created. exit!") from exc | ||
|
|
||
| if Path(f"{src_dir}/{workflow_name}").is_dir(): | ||
| fre_logger.info(" *** PREVIOUS CHECKOUT FOUND: %s/%s *** ", src_dir, workflow_name) | ||
| if force_checkout: | ||
| # Create archived workflows location | ||
| archived = f"{target_dir}/archived_workflows" | ||
| Path(archived).mkdir(parents=True, exist_ok=True) | ||
|
|
||
| # Move previous workflow to archived location | ||
| fre_logger.warning(" *** Moving previous checkout to %s ***", archived) | ||
| shutil.move(f"{src_dir}/{workflow_name}", archived) | ||
|
|
||
| # Rename previous workflow | ||
| move_timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") | ||
| os.rename(f"{archived}/{workflow_name}", f"{archived}/{workflow_name}_{move_timestamp}") | ||
|
|
||
| # # Keeping this here in case we want to switch force_checkout to removing instead of moving | ||
| # fre_logger.warning(" *** REMOVING %s/%s *** ", src_dir, workflow_name) | ||
| # shutil.rmtree(f"{src_dir}/{workflow_name}") | ||
| else: | ||
| with change_directory(f"{src_dir}/{workflow_name}"): | ||
| ## Compare previous workflow directory | ||
| # capture the branch and tag | ||
| # if either match git_clone_branch_arg, then success. otherwise, fail. | ||
| current_tag = subprocess.run(["git","describe","--tags"], | ||
| capture_output = True, | ||
| text = True, check = True).stdout.strip() | ||
| current_branch = subprocess.run(["git", "branch", "--show-current"], | ||
| capture_output = True, | ||
| text = True, check = True).stdout.strip() | ||
|
|
||
| if tag in (current_tag, current_branch): | ||
| fre_logger.info("Checkout exists ('%s/%s'), and matches '%s'", src_dir, workflow_name, tag) | ||
| else: | ||
| fre_logger.error( | ||
| "ERROR: Checkout exists ('%s/%s') and does not match '%s'", src_dir, workflow_name, tag) | ||
| fre_logger.error( | ||
| "ERROR: Current branch: '%s', Current tag-describe: '%s'", current_branch, current_tag) | ||
| raise ValueError('Neither tag nor branch matches the git clone branch arg') | ||
|
|
||
|
|
||
| ## Compare content of current and previous configured, resolved yamls | ||
| if filecmp.cmp(yaml_filepath, "config.yaml", shallow=False): | ||
| fre_logger.info("Resolved yaml already exists and did not change.") | ||
| else: | ||
| fre_logger.error("") | ||
| fre_logger.error("ERROR: Checkout and resolved yaml already exist but resolved yaml files " | ||
| "are not identical!") | ||
| fre_logger.error("For troubleshooting:") | ||
| fre_logger.error(" - Current resolved yaml: %s", yaml_filepath) | ||
| fre_logger.error(" - Previous resolved yaml: %s", f"{src_dir}/{workflow_name}/config.yaml") | ||
| fre_logger.error("Try:") | ||
| fre_logger.error(" - resolving yaml differences if nothing else has changed") | ||
| fre_logger.error(f" - removing the {target_dir}/cylc-src/{workflow_name} folder and " | ||
| "re-running the command") | ||
| fre_logger.error(" - pass the --force-checkout option to archive the workflow (move to " | ||
| "~/.fre-workflows/archived/) and clone a new workflow.") | ||
| return | ||
| # raise ValueError("Resolve yaml differences or pass --force-checkout to archive the workflow" | ||
| # "(moved to ~/.fre-workflows/archived) and clone a new workflow.") | ||
|
|
||
| if not Path(f"{src_dir}/{workflow_name}").is_dir(): | ||
| fre_logger.info("Workflow does not exist; will create now") | ||
| clone_output = subprocess.run( ["git", "clone","--recursive", | ||
| f"--branch={tag}", | ||
| repo, f"{src_dir}/{workflow_name}"], | ||
| capture_output = True, text = True, check = True) | ||
| fre_logger.debug(clone_output) | ||
| fre_logger.info("(%s):(%s) check out ==> SUCCESSFUL", repo, tag) | ||
|
|
||
| ## Move combined yaml to cylc-src location | ||
| current_dir = Path.cwd() | ||
| shutil.move(Path(f"{current_dir}/config.yaml"), f"{src_dir}/{workflow_name}") | ||
| fre_logger.info("Combined yaml file moved to %s/%s", src_dir, workflow_name) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| ''' fre workflow click interface for fre workflow subcommands''' | ||
| import os | ||
| import click | ||
| import logging | ||
| fre_logger = logging.getLogger(__name__) | ||
|
|
||
| #fre tools | ||
| from . import checkout_script | ||
| #from . import install_script | ||
| #from . import run_script | ||
|
|
||
| @click.group(help=click.style(" - workflow subcommands", fg=(57,139,210))) | ||
| def workflow_cli(): | ||
| ''' entry point to fre workflow click commands ''' | ||
|
|
||
| @workflow_cli.command() | ||
| @click.option("-y", "--yamlfile", type=str, | ||
| help="Model yaml file", | ||
|
laurenchilutti marked this conversation as resolved.
|
||
| required=True) | ||
| @click.option("-e", "--experiment", type=str, | ||
| help="Experiment name", | ||
| required=True) | ||
| @click.option("-a", "--application", | ||
|
singhd789 marked this conversation as resolved.
|
||
| type=click.Choice(['run', 'pp']), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about leaving off the enumeration here, to let 'fre workflow checkout' use the workflow name in the yaml?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you Dana. Great question, and I see what you mean. I think the full "fre workflows checkout" call has to accept at least experiment name and "application"/"workflow". Let's take the following resolved yaml: And let's assume we have a model yaml that resolves to the above. fre workflow checkout -y model.yaml -e c96L65_am5f7b12r1_amip_TESTING --application=postprocess That will checkout out the postprocessing workflow definition, to ~/.fre-workflows/c96L65_am5f7b12r1_amip_TESTING/postprocessing (I think). Then for the run case: fre workflow checkout -y model.yaml -e c96L65_am5f7b12r1_amip_TESTING --application=run will checkout to ~/.fre-workflows/c96L65_am5f7b12r1_amip_TESTING/run That gives us the flexibility to "discover" other workflows, and install and run them too. fre workflow checkout -y model.yaml -e c96L65_am5f7b12r1_amip_TESTING --application=download_data
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think maybe we could checkout to these locations instead, which would reduce the volume. The checked out location could instead be the workflow definition name as seen by git: fre workflow checkout -y model.yaml -e c96L65_am5f7b12r1_amip_TESTING --application=postprocess would checkout to ~/.fre-workflows/fre-workflows fre workflow checkout -y model.yaml -e c96L65_am5f7b12r1_amip_TESTING --application=run would checkout to ~/.fre-workflows/esm-run and fre workflow checkout -y model.yaml -e c96L65_am5f7b12r1_amip_TESTING --application=download_data would check out to ~/.fre-workflows/download_data
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AH, I see what you're saying now. We would still have Regarding where the workflow will be checked out, I suppose that makes sense since only one checked out version will exist unless
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Excellent. I think I'm following you. Right- "fre-workflows" as we have it is actually (one of) the postprocessing workflow definitions, so in the future it should be renamed accordingly, as it's somewhat misleadingly named right now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok looping back to this real quick, thinking about this more, I think we should still include the experiment name in the workflow checkout location in the case that different experiments are being checked out. If someone is doing |
||
| help="Type of workflow to check out/clone", | ||
| required=True) | ||
| @click.option("--target-dir", | ||
| type=str, | ||
| help=f"""Target directory for the workflow to be cloned into. | ||
| If not defined, a default location of ~/.fre-workflows | ||
| will be used""") | ||
| @click.option("--force-checkout", | ||
| is_flag=True, | ||
| default=False, | ||
| help="If the checkout already, exists, remove and clone the desired repo again.") | ||
| def checkout(target_dir, yamlfile, experiment, application, force_checkout): | ||
| """ | ||
| Checkout/clone the workflow repository. | ||
| """ | ||
| checkout_script.workflow_checkout(target_dir, yamlfile, experiment, application, force_checkout) | ||
Uh oh!
There was an error while loading. Please reload this page.