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.
Files changed (95) hide show
  1. cimsi-0.7.8.dev1.dist-info/METADATA +80 -0
  2. cimsi-0.7.8.dev1.dist-info/RECORD +95 -0
  3. cimsi-0.7.8.dev1.dist-info/WHEEL +5 -0
  4. cimsi-0.7.8.dev1.dist-info/entry_points.txt +3 -0
  5. cimsi-0.7.8.dev1.dist-info/top_level.txt +1 -0
  6. imsi/__init__.py +16 -0
  7. imsi/_version.py +34 -0
  8. imsi/cli/__init__.py +0 -0
  9. imsi/cli/cli_snapshot_state.sh +175 -0
  10. imsi/cli/core_cli.py +307 -0
  11. imsi/cli/core_tracking.py +169 -0
  12. imsi/cli/entry.py +59 -0
  13. imsi/cli/lazy.py +104 -0
  14. imsi/cli/post_install.py +16 -0
  15. imsi/cli/sectioned_group.py +26 -0
  16. imsi/config_manager/.gitignore +5 -0
  17. imsi/config_manager/__init__.py +0 -0
  18. imsi/config_manager/config_manager.py +516 -0
  19. imsi/config_manager/databases.py +107 -0
  20. imsi/config_manager/schema/__init__.py +0 -0
  21. imsi/config_manager/schema/compiler.py +8 -0
  22. imsi/config_manager/schema/components.py +87 -0
  23. imsi/config_manager/schema/experiment.py +34 -0
  24. imsi/config_manager/schema/machine.py +156 -0
  25. imsi/config_manager/schema/model.py +21 -0
  26. imsi/config_manager/schema/post_processing.py +12 -0
  27. imsi/config_manager/schema/sequencing.py +26 -0
  28. imsi/config_manager/schema/setup_params.py +7 -0
  29. imsi/config_manager/schema/types.py +7 -0
  30. imsi/config_manager/schema/utilities.py +9 -0
  31. imsi/config_manager/tests.py +50 -0
  32. imsi/imsi.site.rc +4 -0
  33. imsi/scheduler_interface/__init__.py +0 -0
  34. imsi/scheduler_interface/scheduler_tools.py +96 -0
  35. imsi/scheduler_interface/schedulers.py +177 -0
  36. imsi/sequencer_interface/__init__.py +0 -0
  37. imsi/sequencer_interface/iss_cap.py +215 -0
  38. imsi/sequencer_interface/maestro_cap.py +1242 -0
  39. imsi/sequencer_interface/maestro_status.py +201 -0
  40. imsi/sequencer_interface/sequencers.py +65 -0
  41. imsi/shell_interface/__init__.py +6 -0
  42. imsi/shell_interface/config_hooks_collection.py +139 -0
  43. imsi/shell_interface/config_hooks_collection_config.yaml +8 -0
  44. imsi/shell_interface/config_hooks_manager.py +89 -0
  45. imsi/shell_interface/parse_dbs_example.py +43 -0
  46. imsi/shell_interface/shell_comp_environment.py +171 -0
  47. imsi/shell_interface/shell_config_parameters.py +109 -0
  48. imsi/shell_interface/shell_diag_parameters.py +35 -0
  49. imsi/shell_interface/shell_inputs_outputs.py +189 -0
  50. imsi/shell_interface/shell_interface_manager.py +203 -0
  51. imsi/shell_interface/shell_interface_utilities.py +36 -0
  52. imsi/shell_interface/shell_timing_vars.py +72 -0
  53. imsi/tools/__init__.py +0 -0
  54. imsi/tools/disk_tools/__init__.py +0 -0
  55. imsi/tools/disk_tools/disk_tools.py +72 -0
  56. imsi/tools/disk_tools/disk_tools_cli.py +26 -0
  57. imsi/tools/ensemble/README.md +129 -0
  58. imsi/tools/ensemble/__init__.py +0 -0
  59. imsi/tools/ensemble/all_supported_exp.yaml +49 -0
  60. imsi/tools/ensemble/config/example_table.csv +3 -0
  61. imsi/tools/ensemble/config/example_table.yaml +18 -0
  62. imsi/tools/ensemble/config.py +53 -0
  63. imsi/tools/ensemble/config.yaml +11 -0
  64. imsi/tools/ensemble/ensemble_cli.py +105 -0
  65. imsi/tools/ensemble/ensemble_manager.py +231 -0
  66. imsi/tools/ensemble/table_utils/__init__.py +0 -0
  67. imsi/tools/ensemble/table_utils/data_model.py +113 -0
  68. imsi/tools/ensemble/table_utils/table_model.py +202 -0
  69. imsi/tools/ensemble/table_utils/table_utils.py +157 -0
  70. imsi/tools/list/__init__.py +0 -0
  71. imsi/tools/list/list_cli.py +114 -0
  72. imsi/tools/list/list_manager.py +119 -0
  73. imsi/tools/menu/menu_cli.py +51 -0
  74. imsi/tools/menu/menu_helpers.py +99 -0
  75. imsi/tools/simple_sequencer/__init__.py +0 -0
  76. imsi/tools/simple_sequencer/iss.py +872 -0
  77. imsi/tools/simple_sequencer/iss_cli.py +203 -0
  78. imsi/tools/simple_sequencer/iss_globals.py +148 -0
  79. imsi/tools/time_manager/__init__.py +0 -0
  80. imsi/tools/time_manager/cftime_utils.py +300 -0
  81. imsi/tools/time_manager/chunk_manager.py +359 -0
  82. imsi/tools/time_manager/time_manager.py +354 -0
  83. imsi/tools/time_manager/timer_cli.py +136 -0
  84. imsi/tools/validate/validate_cli.py +146 -0
  85. imsi/user_interface/__init__.py +0 -0
  86. imsi/user_interface/setup_manager.py +288 -0
  87. imsi/user_interface/ui_manager.py +296 -0
  88. imsi/user_interface/ui_utils.py +113 -0
  89. imsi/utils/__init__.py +0 -0
  90. imsi/utils/dict_tools.py +366 -0
  91. imsi/utils/general.py +138 -0
  92. imsi/utils/git_tools.py +190 -0
  93. imsi/utils/multiple_inheritance_test.py +86 -0
  94. imsi/utils/nml_tools.py +200 -0
  95. 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
@@ -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)
@@ -0,0 +1,5 @@
1
+ test-configuration.json
2
+ compilation_template
3
+ computational_environment
4
+ shell_parameters
5
+ flattened_config
File without changes