cimsi 0.7.8.dev1__py3-none-any.whl
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.
- cimsi-0.7.8.dev1.dist-info/METADATA +80 -0
- cimsi-0.7.8.dev1.dist-info/RECORD +95 -0
- cimsi-0.7.8.dev1.dist-info/WHEEL +5 -0
- cimsi-0.7.8.dev1.dist-info/entry_points.txt +3 -0
- cimsi-0.7.8.dev1.dist-info/top_level.txt +1 -0
- imsi/__init__.py +16 -0
- imsi/_version.py +34 -0
- imsi/cli/__init__.py +0 -0
- imsi/cli/cli_snapshot_state.sh +175 -0
- imsi/cli/core_cli.py +307 -0
- imsi/cli/core_tracking.py +169 -0
- imsi/cli/entry.py +59 -0
- imsi/cli/lazy.py +104 -0
- imsi/cli/post_install.py +16 -0
- imsi/cli/sectioned_group.py +26 -0
- imsi/config_manager/.gitignore +5 -0
- imsi/config_manager/__init__.py +0 -0
- imsi/config_manager/config_manager.py +516 -0
- imsi/config_manager/databases.py +107 -0
- imsi/config_manager/schema/__init__.py +0 -0
- imsi/config_manager/schema/compiler.py +8 -0
- imsi/config_manager/schema/components.py +87 -0
- imsi/config_manager/schema/experiment.py +34 -0
- imsi/config_manager/schema/machine.py +156 -0
- imsi/config_manager/schema/model.py +21 -0
- imsi/config_manager/schema/post_processing.py +12 -0
- imsi/config_manager/schema/sequencing.py +26 -0
- imsi/config_manager/schema/setup_params.py +7 -0
- imsi/config_manager/schema/types.py +7 -0
- imsi/config_manager/schema/utilities.py +9 -0
- imsi/config_manager/tests.py +50 -0
- imsi/imsi.site.rc +4 -0
- imsi/scheduler_interface/__init__.py +0 -0
- imsi/scheduler_interface/scheduler_tools.py +96 -0
- imsi/scheduler_interface/schedulers.py +177 -0
- imsi/sequencer_interface/__init__.py +0 -0
- imsi/sequencer_interface/iss_cap.py +215 -0
- imsi/sequencer_interface/maestro_cap.py +1242 -0
- imsi/sequencer_interface/maestro_status.py +201 -0
- imsi/sequencer_interface/sequencers.py +65 -0
- imsi/shell_interface/__init__.py +6 -0
- imsi/shell_interface/config_hooks_collection.py +139 -0
- imsi/shell_interface/config_hooks_collection_config.yaml +8 -0
- imsi/shell_interface/config_hooks_manager.py +89 -0
- imsi/shell_interface/parse_dbs_example.py +43 -0
- imsi/shell_interface/shell_comp_environment.py +171 -0
- imsi/shell_interface/shell_config_parameters.py +109 -0
- imsi/shell_interface/shell_diag_parameters.py +35 -0
- imsi/shell_interface/shell_inputs_outputs.py +189 -0
- imsi/shell_interface/shell_interface_manager.py +203 -0
- imsi/shell_interface/shell_interface_utilities.py +36 -0
- imsi/shell_interface/shell_timing_vars.py +72 -0
- imsi/tools/__init__.py +0 -0
- imsi/tools/disk_tools/__init__.py +0 -0
- imsi/tools/disk_tools/disk_tools.py +72 -0
- imsi/tools/disk_tools/disk_tools_cli.py +26 -0
- imsi/tools/ensemble/README.md +129 -0
- imsi/tools/ensemble/__init__.py +0 -0
- imsi/tools/ensemble/all_supported_exp.yaml +49 -0
- imsi/tools/ensemble/config/example_table.csv +3 -0
- imsi/tools/ensemble/config/example_table.yaml +18 -0
- imsi/tools/ensemble/config.py +53 -0
- imsi/tools/ensemble/config.yaml +11 -0
- imsi/tools/ensemble/ensemble_cli.py +105 -0
- imsi/tools/ensemble/ensemble_manager.py +231 -0
- imsi/tools/ensemble/table_utils/__init__.py +0 -0
- imsi/tools/ensemble/table_utils/data_model.py +113 -0
- imsi/tools/ensemble/table_utils/table_model.py +202 -0
- imsi/tools/ensemble/table_utils/table_utils.py +157 -0
- imsi/tools/list/__init__.py +0 -0
- imsi/tools/list/list_cli.py +114 -0
- imsi/tools/list/list_manager.py +119 -0
- imsi/tools/menu/menu_cli.py +51 -0
- imsi/tools/menu/menu_helpers.py +99 -0
- imsi/tools/simple_sequencer/__init__.py +0 -0
- imsi/tools/simple_sequencer/iss.py +872 -0
- imsi/tools/simple_sequencer/iss_cli.py +203 -0
- imsi/tools/simple_sequencer/iss_globals.py +148 -0
- imsi/tools/time_manager/__init__.py +0 -0
- imsi/tools/time_manager/cftime_utils.py +300 -0
- imsi/tools/time_manager/chunk_manager.py +359 -0
- imsi/tools/time_manager/time_manager.py +354 -0
- imsi/tools/time_manager/timer_cli.py +136 -0
- imsi/tools/validate/validate_cli.py +146 -0
- imsi/user_interface/__init__.py +0 -0
- imsi/user_interface/setup_manager.py +288 -0
- imsi/user_interface/ui_manager.py +296 -0
- imsi/user_interface/ui_utils.py +113 -0
- imsi/utils/__init__.py +0 -0
- imsi/utils/dict_tools.py +366 -0
- imsi/utils/general.py +138 -0
- imsi/utils/git_tools.py +190 -0
- imsi/utils/multiple_inheritance_test.py +86 -0
- imsi/utils/nml_tools.py +200 -0
- imsi/utils/repo_query_status.sh +29 -0
imsi/cli/core_cli.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
|
|
2
|
+
import click
|
|
3
|
+
from functools import wraps
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommandWithPassthroughEOO(click.Command):
|
|
11
|
+
def format_usage(self, ctx, formatter):
|
|
12
|
+
formatter.write_usage(ctx.command.name, "[OPTIONS] -- [PASSTHROUGH_ARGS]")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def passthrough_eoo_delimiter(ctx, param, value):
|
|
16
|
+
# enforce requirement that end of options delimiter '--'
|
|
17
|
+
# is entered before trailing args
|
|
18
|
+
if value:
|
|
19
|
+
try:
|
|
20
|
+
delim_index = sys.argv.index('--')
|
|
21
|
+
first_arg_index = sys.argv.index(value[0])
|
|
22
|
+
if delim_index > first_arg_index:
|
|
23
|
+
raise click.UsageError("Use '--' before passthrough arguments.")
|
|
24
|
+
except ValueError:
|
|
25
|
+
raise click.UsageError("Use '--' before the trailing arguments.")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def force_dirs(
|
|
30
|
+
path: Path = Path("src"),
|
|
31
|
+
):
|
|
32
|
+
user_in_path = path.exists()
|
|
33
|
+
wrk_dir_env_set = os.getenv("WRK_DIR") is not None
|
|
34
|
+
|
|
35
|
+
if user_in_path and wrk_dir_env_set and Path.cwd().resolve() == Path(os.getenv("WRK_DIR")).resolve():
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if wrk_dir_env_set and user_in_path:
|
|
39
|
+
warnings.warn(
|
|
40
|
+
f"\n\n**WARNING**: Both WRK_DIR and {path} directory found. Defaulting to CWD {path} at {Path('.').resolve()}\n"
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if wrk_dir_env_set and not Path(os.getenv("WRK_DIR"), path).exists():
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"⚠️ $WRK_DIR = {os.getenv('WRK_DIR')} is not a valid imsi directory. Please check the path and try again."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if not any([user_in_path, wrk_dir_env_set]):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"⚠️ {path} directory not found! Possibly because:\n"
|
|
52
|
+
"1. You are not currently in your setup directory or one hasn't been created.\n"
|
|
53
|
+
" or \n"
|
|
54
|
+
"2. The environment variable WRK_DIR is not set to the correct directory."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def log_cli(func=None, logger_name='cli'):
|
|
59
|
+
from imsi.utils.git_tools import is_repo_clean, git_add_commit
|
|
60
|
+
import imsi.cli.core_tracking as ct
|
|
61
|
+
|
|
62
|
+
# decorator for logging imsi cli click commands
|
|
63
|
+
def decorator_func(func):
|
|
64
|
+
# the actual decorator
|
|
65
|
+
@wraps(func)
|
|
66
|
+
def wrapper_func(*args, **kwargs):
|
|
67
|
+
# the actual function being wrapped
|
|
68
|
+
|
|
69
|
+
track = True
|
|
70
|
+
|
|
71
|
+
# hack - required to make sure that:
|
|
72
|
+
# - this logging isn't possible for all imsi functions (eg setup)
|
|
73
|
+
# - logs aren't written to files when imsi cli commands are invoked
|
|
74
|
+
# from the wrong location
|
|
75
|
+
force_dirs() # success -> pwd == work dir
|
|
76
|
+
path = Path.cwd()
|
|
77
|
+
|
|
78
|
+
if args:
|
|
79
|
+
if isinstance(args[0], click.core.Context):
|
|
80
|
+
# get the cli func name from the context rather than
|
|
81
|
+
# func.__name__ (because of how click invokes func names)
|
|
82
|
+
ctx = args[0]
|
|
83
|
+
func_name = ctx.info_name
|
|
84
|
+
else:
|
|
85
|
+
# FIXME TODO fallback
|
|
86
|
+
func_name = func.__name__
|
|
87
|
+
|
|
88
|
+
# init log
|
|
89
|
+
imsi_logger = ct.get_imsi_logger(logger_name, path)
|
|
90
|
+
ct.imsi_log_prelude(func_name, imsi_logger)
|
|
91
|
+
|
|
92
|
+
if track:
|
|
93
|
+
config_dir = path / 'config'
|
|
94
|
+
ct.imsi_state_snapshot(path, logger=imsi_logger)
|
|
95
|
+
|
|
96
|
+
# force a clean repo for /config
|
|
97
|
+
if logger_name == 'cli':
|
|
98
|
+
clean_config, _ = is_repo_clean(config_dir)
|
|
99
|
+
if not clean_config:
|
|
100
|
+
# always force the config dir to be clean
|
|
101
|
+
msg = f"IMSI pre-run commit cli:{func_name}"
|
|
102
|
+
git_add_commit(msg=msg, path=config_dir)
|
|
103
|
+
imsi_logger.info(msg)
|
|
104
|
+
|
|
105
|
+
# invoke the wrapped function
|
|
106
|
+
try:
|
|
107
|
+
result = func(*args, **kwargs)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
imsi_logger.error(f'ERROR {func_name} {type(e).__name__}')
|
|
110
|
+
raise e
|
|
111
|
+
ct.imsi_log_postlude(func_name, imsi_logger)
|
|
112
|
+
return result
|
|
113
|
+
return wrapper_func
|
|
114
|
+
if func:
|
|
115
|
+
# hack to handle decorator without kwargs (style)
|
|
116
|
+
return decorator_func(func)
|
|
117
|
+
return decorator_func
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@click.command(short_help="Set up a run directory and obtain model source code")
|
|
121
|
+
@click.option(
|
|
122
|
+
"--runid",
|
|
123
|
+
default=None,
|
|
124
|
+
required=True,
|
|
125
|
+
help='Unique short string, without "_" or special chars.',
|
|
126
|
+
)
|
|
127
|
+
@click.option(
|
|
128
|
+
"--repo",
|
|
129
|
+
default="git@gitlab.science.gc.ca:CanESM/CanESM5.git",
|
|
130
|
+
required=True,
|
|
131
|
+
help="Git repository URL or file path.",
|
|
132
|
+
)
|
|
133
|
+
@click.option("--ver", default=None, help="Version of the code to clone.")
|
|
134
|
+
@click.option("--exp", default="cmip6-piControl", help="Experiment name.")
|
|
135
|
+
@click.option("--model", default=None, help="Model name.")
|
|
136
|
+
@click.option(
|
|
137
|
+
"--fetch_method",
|
|
138
|
+
default="clone", type=click.Choice(["clone", "clone-full", "link", "copy"]),
|
|
139
|
+
help="Fetch method for source code.",
|
|
140
|
+
show_default=True
|
|
141
|
+
)
|
|
142
|
+
@click.option("--seq", default=None, help='Sequencer to use, like "iss" or "maestro".')
|
|
143
|
+
@click.option("--machine", default=None, help="Machine to use.")
|
|
144
|
+
@click.option("--flow", default=None, help="Workflow to use.")
|
|
145
|
+
@click.option("--postproc", default=None, help="Postprocessing profile to use.")
|
|
146
|
+
@click.pass_context
|
|
147
|
+
def setup(ctx, **kwargs):
|
|
148
|
+
"""Create a run directory, obtain the model source code, and extract all required model configuration files.
|
|
149
|
+
|
|
150
|
+
https://imsi.readthedocs.io/en/latest/usage.html#setting-up-a-run
|
|
151
|
+
"""
|
|
152
|
+
from imsi.user_interface.setup_manager import (
|
|
153
|
+
setup_run,
|
|
154
|
+
ValidatedSetupOptions,
|
|
155
|
+
InvalidSetupConfig,
|
|
156
|
+
)
|
|
157
|
+
import imsi.cli.core_tracking as ct
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
setup_args = ValidatedSetupOptions(**kwargs)
|
|
161
|
+
except InvalidSetupConfig as e:
|
|
162
|
+
click.echo(e)
|
|
163
|
+
raise e
|
|
164
|
+
|
|
165
|
+
setup_run(setup_args, force=ctx.obj["FORCE"])
|
|
166
|
+
|
|
167
|
+
ct.log_setup(sys.argv, setup_args)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@click.command(help="Log the imsi state.",
|
|
171
|
+
epilog="Note: this is nominally an imsi utility function.")
|
|
172
|
+
@click.option('-m', '--msg', default=None, required=False,
|
|
173
|
+
help='Message to include in the log.')
|
|
174
|
+
@click.option('-p', '--path', type=click.Path(exists=True), default='.',
|
|
175
|
+
required=True,
|
|
176
|
+
help='Path to run folder.')
|
|
177
|
+
def log_state(msg, path):
|
|
178
|
+
import imsi.cli.core_tracking as ct
|
|
179
|
+
|
|
180
|
+
path = Path(path).resolve()
|
|
181
|
+
logger = ct.get_imsi_logger('runtime', path)
|
|
182
|
+
ct.imsi_log_prelude('log-state', logger)
|
|
183
|
+
ct.imsi_state_snapshot(path, logger=logger)
|
|
184
|
+
if msg is not None:
|
|
185
|
+
logger.info(f'MESSAGE: {msg}')
|
|
186
|
+
ct.imsi_log_postlude('log-state', logger)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@click.command()
|
|
190
|
+
@click.pass_context
|
|
191
|
+
@log_cli
|
|
192
|
+
def config(ctx):
|
|
193
|
+
"""Configure a simulation with updated settings from on-disk repo."""
|
|
194
|
+
import imsi.user_interface.ui_manager as uim
|
|
195
|
+
|
|
196
|
+
force_dirs(Path("src"))
|
|
197
|
+
uim.validate_version_reqs()
|
|
198
|
+
uim.update_config_from_state(force=ctx.obj["FORCE"])
|
|
199
|
+
click.echo("IMSI Config")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@click.command(
|
|
203
|
+
short_help="Reload the imsi configuration from the on-disk repo"
|
|
204
|
+
)
|
|
205
|
+
@click.pass_context
|
|
206
|
+
@log_cli
|
|
207
|
+
def reload(ctx):
|
|
208
|
+
"""Reload the imsi configuration from the on-disk repo config files and update the simulation configuration"""
|
|
209
|
+
import imsi.user_interface.ui_manager as uim
|
|
210
|
+
|
|
211
|
+
force_dirs(Path("src"))
|
|
212
|
+
uim.validate_version_reqs()
|
|
213
|
+
uim.reload_config_from_source(force=ctx.obj["FORCE"])
|
|
214
|
+
click.echo("IMSI Update")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@click.command(short_help="Set an imsi selection in the configuration")
|
|
218
|
+
@click.option("-f", "--file", help="Name of a configuration file containing imsi selections")
|
|
219
|
+
@click.option(
|
|
220
|
+
"-s",
|
|
221
|
+
"--selections",
|
|
222
|
+
metavar="KEY=VALUE",
|
|
223
|
+
multiple=True,
|
|
224
|
+
help="A series of KEY=VALUE selection pairs.",
|
|
225
|
+
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"-o",
|
|
228
|
+
"--options",
|
|
229
|
+
metavar="KEY=VALUE",
|
|
230
|
+
multiple=True,
|
|
231
|
+
help="A series of KEY=VALUE option pairs.",
|
|
232
|
+
)
|
|
233
|
+
@click.pass_context
|
|
234
|
+
@log_cli
|
|
235
|
+
def set(ctx, file, selections, options):
|
|
236
|
+
import imsi.user_interface.ui_manager as uim
|
|
237
|
+
|
|
238
|
+
force_dirs(Path("src"))
|
|
239
|
+
uim.validate_version_reqs()
|
|
240
|
+
if any([file, selections, options]):
|
|
241
|
+
uim.set_selections(file, selections, options, force=ctx.obj["FORCE"])
|
|
242
|
+
else:
|
|
243
|
+
click.echo(
|
|
244
|
+
"Error: Must provide at least one of --file, --selections, or --options",
|
|
245
|
+
err=True,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@click.command(
|
|
250
|
+
cls=CommandWithPassthroughEOO,
|
|
251
|
+
context_settings=dict(ignore_unknown_options=True,),
|
|
252
|
+
short_help="Compile model components.",
|
|
253
|
+
help="Compile model components.",
|
|
254
|
+
epilog="The script 'imsi-tmp-compile.sh' will be executed."
|
|
255
|
+
)
|
|
256
|
+
@click.option("--script-help", is_flag=True, help="Display the help message of the script.")
|
|
257
|
+
@click.argument("args", nargs=-1, callback=passthrough_eoo_delimiter)
|
|
258
|
+
@click.pass_context
|
|
259
|
+
@log_cli
|
|
260
|
+
def build(ctx, script_help, args):
|
|
261
|
+
import imsi.user_interface.ui_manager as uim
|
|
262
|
+
|
|
263
|
+
force_dirs(Path("src"))
|
|
264
|
+
uim.validate_version_reqs()
|
|
265
|
+
args = ['-h'] if script_help else args
|
|
266
|
+
click.echo("IMSI Build")
|
|
267
|
+
uim.compile_model_execs(args)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@click.command(help="Submit a simulation to run")
|
|
271
|
+
@log_cli
|
|
272
|
+
def submit():
|
|
273
|
+
import imsi.user_interface.ui_manager as uim
|
|
274
|
+
|
|
275
|
+
force_dirs(Path("src"))
|
|
276
|
+
uim.validate_version_reqs()
|
|
277
|
+
click.echo("IMSI submit")
|
|
278
|
+
uim.submit_run()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@click.command(
|
|
282
|
+
cls=CommandWithPassthroughEOO,
|
|
283
|
+
context_settings=dict(ignore_unknown_options=True,),
|
|
284
|
+
short_help="Save the model restart files.",
|
|
285
|
+
help="Save the model restart files.",
|
|
286
|
+
epilog="The script 'save_restart_files.sh' will be executed."
|
|
287
|
+
)
|
|
288
|
+
@click.option("--script-help", is_flag=True, help="Display the help message of the script.")
|
|
289
|
+
@click.argument("args", nargs=-1, callback=passthrough_eoo_delimiter)
|
|
290
|
+
@log_cli
|
|
291
|
+
def save_restarts(script_help, args):
|
|
292
|
+
import imsi.user_interface.ui_manager as uim
|
|
293
|
+
|
|
294
|
+
force_dirs(Path("src"))
|
|
295
|
+
uim.validate_version_reqs()
|
|
296
|
+
args = ['-h'] if script_help else args
|
|
297
|
+
click.echo("IMSI save restarts")
|
|
298
|
+
uim.save_restarts(args)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@click.command(help="Get sequencer status information.")
|
|
302
|
+
def status():
|
|
303
|
+
import imsi.user_interface.ui_manager as uim
|
|
304
|
+
|
|
305
|
+
force_dirs(Path("src"))
|
|
306
|
+
uim.validate_version_reqs()
|
|
307
|
+
uim.get_sequencer_status()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import imsi
|
|
8
|
+
from imsi.utils.general import get_active_venv
|
|
9
|
+
from imsi.user_interface.setup_manager import ValidatedSetupOptions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
IMSI_STATEFUL_FOLDERS = ['src', 'config']
|
|
13
|
+
|
|
14
|
+
_logger_config = {
|
|
15
|
+
'setup': {
|
|
16
|
+
'logger_name': 'imsi-setup',
|
|
17
|
+
'filename': '.imsi-setup.log',
|
|
18
|
+
'hidden': True,
|
|
19
|
+
'level': logging.DEBUG
|
|
20
|
+
},
|
|
21
|
+
'cli': {
|
|
22
|
+
'logger_name': 'imsi-cli',
|
|
23
|
+
'filename': '.imsi-cli.log',
|
|
24
|
+
'hidden': True,
|
|
25
|
+
'level': logging.INFO
|
|
26
|
+
},
|
|
27
|
+
'runtime': {
|
|
28
|
+
'logger_name': 'imsi-runtime',
|
|
29
|
+
'filename': '.imsi-cli.log',
|
|
30
|
+
'hidden': True,
|
|
31
|
+
'level': logging.INFO
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_logger(logger_name: str=None, path: Path=None, filename: str=None,
|
|
37
|
+
hidden=True, level=logging.INFO):
|
|
38
|
+
# Return a logging.Logger with the name `logger_name` set at logging
|
|
39
|
+
# level `level`. The log file will be written to the full path
|
|
40
|
+
# constructed from `path / filename`. The `filename` must end with
|
|
41
|
+
# extension `.log`. The `hidden=True` flag ensures that the filename
|
|
42
|
+
# begins with a period (`.`) (conversely, ensures it is not included
|
|
43
|
+
# if `hidden=False`).
|
|
44
|
+
|
|
45
|
+
path = Path(path)
|
|
46
|
+
|
|
47
|
+
if not path.exists():
|
|
48
|
+
raise FileNotFoundError(f"ERROR: can't write log file to path {path}")
|
|
49
|
+
if not filename.endswith('.log'):
|
|
50
|
+
raise ValueError('filename must end with .log')
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(logger_name)
|
|
53
|
+
if logger.hasHandlers():
|
|
54
|
+
logger.handlers.clear()
|
|
55
|
+
|
|
56
|
+
if hidden:
|
|
57
|
+
basename = f".{filename}" if not filename.startswith('.') else filename
|
|
58
|
+
else:
|
|
59
|
+
basename = filename.lstrip('.')
|
|
60
|
+
|
|
61
|
+
formatter = logging.Formatter(
|
|
62
|
+
fmt='%(asctime)s - %(thread)d - %(name)s - %(levelname)s - %(message)s',
|
|
63
|
+
datefmt='%Y-%m-%dT%H:%M:%S%z'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# instantiate log config
|
|
67
|
+
fh = logging.FileHandler(str(path / basename))
|
|
68
|
+
fh.setFormatter(formatter)
|
|
69
|
+
logger.addHandler(fh)
|
|
70
|
+
logger.setLevel(level)
|
|
71
|
+
|
|
72
|
+
return logger
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_imsi_logger(name, path):
|
|
76
|
+
"""Return an imsi logger of name `name` written to the log file `path`."""
|
|
77
|
+
logger_settings = _logger_config[name]
|
|
78
|
+
logger_settings['path'] = path
|
|
79
|
+
return _get_logger(**logger_settings)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def log_setup(cli_args, setup_params: ValidatedSetupOptions, with_src_status=True):
|
|
83
|
+
"""Log setup command and information to the setup log file."""
|
|
84
|
+
setup_path = Path(setup_params.runid).resolve()
|
|
85
|
+
|
|
86
|
+
logger = get_imsi_logger('setup', setup_path)
|
|
87
|
+
logger.info(f"🚀 IMSI setup for {setup_params.runid} 🚀")
|
|
88
|
+
logger.info('setup_params: {}'.format(' '.join(f"{k}={v}" for k, v in setup_params.model_dump().items())))
|
|
89
|
+
|
|
90
|
+
if with_src_status:
|
|
91
|
+
# capture the status of the git repo under /src
|
|
92
|
+
s = files("imsi").joinpath("utils/repo_query_status.sh")
|
|
93
|
+
src_path = setup_path / 'src'
|
|
94
|
+
try:
|
|
95
|
+
proc = subprocess.run([str(s)], capture_output=True, cwd=src_path)
|
|
96
|
+
proc.check_returncode()
|
|
97
|
+
except subprocess.CalledProcessError as e:
|
|
98
|
+
src_msg = f'Could not determine status under {src_path}'
|
|
99
|
+
else:
|
|
100
|
+
src_msg = f'src: {proc.stdout.decode().strip()}'
|
|
101
|
+
|
|
102
|
+
logger.info(src_msg)
|
|
103
|
+
|
|
104
|
+
# DEV: keep this as the last setup log entry (newline)
|
|
105
|
+
input_args = ' '.join(sys.argv)
|
|
106
|
+
logger.info(f"Setup command used:\n{input_args}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def imsi_log_prelude(func_name, logger):
|
|
110
|
+
"""Log header information (imsi metadata) to the `logger` related to
|
|
111
|
+
running `func_name`. Logging is set to logging.INFO.
|
|
112
|
+
"""
|
|
113
|
+
imsi_meta = f"imsi {imsi.__version__} {imsi.__path__[0]}"
|
|
114
|
+
imsi_venv = get_active_venv()
|
|
115
|
+
logger.info(f'INVOKING {func_name}')
|
|
116
|
+
logger.info(' '.join(sys.argv))
|
|
117
|
+
logger.info(imsi_meta)
|
|
118
|
+
logger.info(f'VIRTUAL_ENV {imsi_venv}')
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def imsi_log_postlude(func_name, logger):
|
|
122
|
+
"""Log footer information related to running `func_name`.
|
|
123
|
+
Logging level is set to logging.INFO.
|
|
124
|
+
"""
|
|
125
|
+
logger.info(f'COMPLETED {func_name}')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def imsi_state_snapshot(path, folders=None, logger=None):
|
|
129
|
+
"""Take a snapshot of the imsi state and return the state hash.
|
|
130
|
+
|
|
131
|
+
This runs the snapshot tool (cli_snapshot_state.sh) for the `folders`
|
|
132
|
+
specified under the `path`.
|
|
133
|
+
|
|
134
|
+
Parameters:
|
|
135
|
+
path : path to an imsi run folder
|
|
136
|
+
folders : folder names (relative to `path`) on which to take the state
|
|
137
|
+
snapshot. These folders must be git repos. Default list is
|
|
138
|
+
['config', 'src'].
|
|
139
|
+
logger : instance of a Logger.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
the hash of the imsi state folder
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
if folders is None:
|
|
146
|
+
folders = IMSI_STATEFUL_FOLDERS
|
|
147
|
+
elif isinstance(folders, str):
|
|
148
|
+
folders = [folders]
|
|
149
|
+
|
|
150
|
+
tracker_script = Path(__file__).parent / 'cli_snapshot_state.sh'
|
|
151
|
+
folder_args = [l for s in [['-p', p] for p in folders] for l in s]
|
|
152
|
+
io_args = ['-i', path, '-o', path]
|
|
153
|
+
|
|
154
|
+
if not tracker_script.exists():
|
|
155
|
+
raise FileNotFoundError(f'ERROR missing snapshot tool: {tracker_script}')
|
|
156
|
+
try:
|
|
157
|
+
proc = subprocess.run([str(tracker_script)] + folder_args + io_args, capture_output=True)
|
|
158
|
+
proc.check_returncode()
|
|
159
|
+
except subprocess.CalledProcessError as e:
|
|
160
|
+
if logger:
|
|
161
|
+
logger.error(f'state snapshot failed')
|
|
162
|
+
logger.error(f'HALTING')
|
|
163
|
+
raise ChildProcessError("Failed call: {cmd}\n{err}".format(cmd=' '.join(proc.args), err=proc.stdout.decode())) from e
|
|
164
|
+
|
|
165
|
+
state_sha = proc.stdout.decode().strip()
|
|
166
|
+
if logger:
|
|
167
|
+
logger.info(f'IMSI state:{state_sha}')
|
|
168
|
+
|
|
169
|
+
return state_sha
|
imsi/cli/entry.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
imsi CLI
|
|
3
|
+
--------
|
|
4
|
+
|
|
5
|
+
The entry-point console script that interfaces all users commands to imsi.
|
|
6
|
+
|
|
7
|
+
imsi has several categories of sub-commands. As this module develops further,
|
|
8
|
+
the sub-groups are implemented in the relevant downstream modules.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from imsi.cli.sectioned_group import SectionedGroup
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group(
|
|
19
|
+
cls=SectionedGroup,
|
|
20
|
+
invoke_without_command=True,
|
|
21
|
+
context_settings=CONTEXT_SETTINGS,
|
|
22
|
+
)
|
|
23
|
+
@click.version_option()
|
|
24
|
+
@click.option("-f", "--force", is_flag=True, help="Force the operation")
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def cli(ctx, force):
|
|
27
|
+
"""IMSI CLI — manage configs, builds, runs, and tools. Add the -h or --help flag to any command for more information."""
|
|
28
|
+
ctx.ensure_object(dict)
|
|
29
|
+
ctx.obj["FORCE"] = force
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
cli.add_lazy_command("imsi.cli.core_cli.setup")
|
|
33
|
+
cli.add_lazy_command("imsi.cli.core_cli.config", short_help="Configure a simulation with updated settings from on-disk repo")
|
|
34
|
+
cli.add_lazy_command(
|
|
35
|
+
"imsi.cli.core_cli.save_restarts",
|
|
36
|
+
name="save-restarts",
|
|
37
|
+
short_help="Save restarts into the local run database & RUNPATH. Add -h for more information.",
|
|
38
|
+
context_settings={"ignore_unknown_options": True},
|
|
39
|
+
add_help_option=False, # Disable automatic help flag,
|
|
40
|
+
)
|
|
41
|
+
cli.add_lazy_command(
|
|
42
|
+
"imsi.cli.core_cli.build",
|
|
43
|
+
short_help="Compile model components. Add -h for more information.",
|
|
44
|
+
context_settings={"ignore_unknown_options": True},
|
|
45
|
+
add_help_option=False, # Disable automatic help flag
|
|
46
|
+
)
|
|
47
|
+
cli.add_lazy_command("imsi.cli.core_cli.submit")
|
|
48
|
+
cli.add_lazy_command("imsi.cli.core_cli.status")
|
|
49
|
+
cli.add_lazy_command("imsi.tools.disk_tools.disk_tools_cli.clean")
|
|
50
|
+
cli.add_lazy_command("imsi.cli.core_cli.reload")
|
|
51
|
+
cli.add_lazy_command("imsi.cli.core_cli.set")
|
|
52
|
+
cli.add_lazy_command("imsi.tools.list.list_cli.list")
|
|
53
|
+
cli.add_lazy_command("imsi.cli.core_cli.log_state", name="log-state")
|
|
54
|
+
|
|
55
|
+
cli.add_lazy_command("imsi.tools.ensemble.ensemble_cli.ensemble")
|
|
56
|
+
cli.add_lazy_command("imsi.tools.time_manager.timer_cli.chunk_manager", name="chunk-manager")
|
|
57
|
+
cli.add_lazy_command("imsi.tools.simple_sequencer.iss_cli.iss")
|
|
58
|
+
cli.add_lazy_command("imsi.tools.menu.menu_cli.setup_menu", name="setup-menu", short_help="Interactive menu to explore IMSI configurations.")
|
|
59
|
+
cli.add_lazy_command("imsi.tools.validate.validate_cli.validate", name="validate", short_help="Validate imsi configuration files.")
|
imsi/cli/lazy.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LazyCommand(click.Command):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
import_path,
|
|
9
|
+
name=None,
|
|
10
|
+
short_help="",
|
|
11
|
+
context_settings=None,
|
|
12
|
+
add_help_option=False,
|
|
13
|
+
):
|
|
14
|
+
self._import_path = import_path
|
|
15
|
+
self._real_command = None
|
|
16
|
+
self._short_help = short_help
|
|
17
|
+
super().__init__(
|
|
18
|
+
name or import_path.split(".")[-1],
|
|
19
|
+
short_help=short_help,
|
|
20
|
+
context_settings=context_settings or {},
|
|
21
|
+
add_help_option=add_help_option,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _load_command(self):
|
|
25
|
+
if self._real_command is None:
|
|
26
|
+
module_path, func_name = self._import_path.rsplit(".", 1)
|
|
27
|
+
module = importlib.import_module(module_path)
|
|
28
|
+
self._real_command = getattr(module, func_name)
|
|
29
|
+
return self._real_command
|
|
30
|
+
|
|
31
|
+
def invoke(self, ctx):
|
|
32
|
+
return self._load_command().invoke(ctx)
|
|
33
|
+
|
|
34
|
+
def get_help(self, ctx):
|
|
35
|
+
return self._load_command().get_help(ctx)
|
|
36
|
+
|
|
37
|
+
def get_params(self, ctx):
|
|
38
|
+
return self._load_command().get_params(ctx)
|
|
39
|
+
|
|
40
|
+
def format_help(self, ctx, formatter):
|
|
41
|
+
return self._load_command().format_help(ctx, formatter)
|
|
42
|
+
|
|
43
|
+
def format_usage(self, ctx, formatter):
|
|
44
|
+
return self._load_command().format_usage(ctx, formatter)
|
|
45
|
+
|
|
46
|
+
def get_short_help_str(self, limit=45):
|
|
47
|
+
return self._short_help or self._load_command().get_short_help_str(limit)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LazyGroup(click.Group):
|
|
51
|
+
def __init__(self, *args, **kwargs):
|
|
52
|
+
self._lazy_commands = {}
|
|
53
|
+
super().__init__(*args, **kwargs)
|
|
54
|
+
|
|
55
|
+
def add_lazy_command(
|
|
56
|
+
self,
|
|
57
|
+
import_path: str,
|
|
58
|
+
name: str | None = None,
|
|
59
|
+
short_help: str = "",
|
|
60
|
+
**lazy_kwargs,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Register a lazy‑loaded command or group.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
import_path : str
|
|
68
|
+
Dotted import path to a click.Command or click.Group object.
|
|
69
|
+
name : str, optional
|
|
70
|
+
Override the command name shown in the CLI.
|
|
71
|
+
short_help : str, optional
|
|
72
|
+
One‑liner shown in the parent help without importing the module.
|
|
73
|
+
**lazy_kwargs
|
|
74
|
+
Extra keyword args forwarded to LazyCommand
|
|
75
|
+
(e.g. context_settings, add_help_option).
|
|
76
|
+
"""
|
|
77
|
+
name = name or import_path.split(".")[-1]
|
|
78
|
+
self._lazy_commands[name] = (import_path, short_help, lazy_kwargs)
|
|
79
|
+
|
|
80
|
+
def get_command(self, ctx, cmd_name):
|
|
81
|
+
if cmd_name in self._lazy_commands:
|
|
82
|
+
import_path, short_help, extra = self._lazy_commands[cmd_name]
|
|
83
|
+
module_path, func_name = import_path.rsplit(".", 1)
|
|
84
|
+
module = importlib.import_module(module_path)
|
|
85
|
+
real_cmd = getattr(module, func_name)
|
|
86
|
+
|
|
87
|
+
# If it's a Group, return it as‑is (no wrapper needed)
|
|
88
|
+
if isinstance(real_cmd, click.Group):
|
|
89
|
+
return real_cmd
|
|
90
|
+
|
|
91
|
+
# Else wrap it, forwarding all extra kwargs
|
|
92
|
+
return LazyCommand(
|
|
93
|
+
import_path,
|
|
94
|
+
name=cmd_name,
|
|
95
|
+
short_help=short_help,
|
|
96
|
+
**extra,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return super().get_command(ctx, cmd_name)
|
|
100
|
+
|
|
101
|
+
def list_commands(self, ctx):
|
|
102
|
+
cmds = list(self.commands.keys())
|
|
103
|
+
lazies = [name for name in self._lazy_commands.keys() if name not in self.commands]
|
|
104
|
+
return cmds + lazies
|
imsi/cli/post_install.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def add_site_repos_to_rc(path_to_site_repos):
|
|
6
|
+
path_to_rc = files("imsi").joinpath("imsi.site.rc")
|
|
7
|
+
with open(path_to_rc, "a") as f:
|
|
8
|
+
# append IMSI_DEFAULT_CONFIG_REPOS=path_to_site_repos
|
|
9
|
+
f.write(f"\nIMSI_DEFAULT_CONFIG_REPOS={path_to_site_repos}\n")
|
|
10
|
+
click.echo(f"Added IMSI_DEFAULT_CONFIG_REPOS={path_to_site_repos} to {path_to_rc}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command()
|
|
14
|
+
@click.option('--path-to-site-repos', help='Location on disk where site-repos are stored. Will add value to the imsi.site.rc file. Not meant for regular imsi users, only for system install.', required=True)
|
|
15
|
+
def post_install(path_to_site_repos):
|
|
16
|
+
add_site_repos_to_rc(path_to_site_repos)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from imsi.cli.lazy import LazyGroup
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SectionedGroup(LazyGroup):
|
|
6
|
+
"""Like LazyGroup, but prints commands and sub‑groups in separate sections in the help message."""
|
|
7
|
+
|
|
8
|
+
def format_commands(self, ctx, formatter):
|
|
9
|
+
commands, groups = [], []
|
|
10
|
+
|
|
11
|
+
for name in self.list_commands(ctx):
|
|
12
|
+
cmd = self.get_command(ctx, name)
|
|
13
|
+
if cmd is None or cmd.hidden:
|
|
14
|
+
continue
|
|
15
|
+
row = (name, cmd.get_short_help_str())
|
|
16
|
+
# check if in a group or a command
|
|
17
|
+
(groups if isinstance(cmd, click.Group) else commands).append(row)
|
|
18
|
+
|
|
19
|
+
if commands:
|
|
20
|
+
with formatter.section("Commands"):
|
|
21
|
+
formatter.write_dl(commands)
|
|
22
|
+
|
|
23
|
+
if groups:
|
|
24
|
+
# Blank line between sections is automatic
|
|
25
|
+
with formatter.section("Command Groups"):
|
|
26
|
+
formatter.write_dl(groups)
|
|
File without changes
|