cloud-submit 0.1__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.
cloud_submit/config.py ADDED
@@ -0,0 +1,164 @@
1
+ import os
2
+ import contextlib
3
+
4
+ from .execution.utils import write_json
5
+ from .execution.config import ConfigError
6
+ from .envs.local.environment_handler import LocalEnv
7
+ from .images import BaseImage, ExecutionImage
8
+
9
+
10
+ def _dictify(items, kind):
11
+ if items is None:
12
+ items = []
13
+ item_dict = {}
14
+ for item in items:
15
+ if item.name in item_dict:
16
+ raise ConfigError(f'Duplicate {kind} name {item.name}.')
17
+ if kind == 'image' and item.parent is not None:
18
+ if item.parent not in item_dict:
19
+ raise ConfigError(
20
+ f'Parent image of {item.name} must be '
21
+ 'listed before {item.name}.'
22
+ )
23
+ if isinstance(item_dict[item.parent], ExecutionImage):
24
+ raise ConfigError(
25
+ f'Image {item.parent} cannot be the parent of {item.name} '
26
+ 'since it is an execution image.'
27
+ )
28
+ item_dict[item.name] = item
29
+ return item_dict
30
+
31
+
32
+ class Config:
33
+ def __init__(
34
+ self,
35
+ project_name,
36
+ user_name,
37
+ project_root=None,
38
+ images=None,
39
+ artifacts=None,
40
+ pipelines=None,
41
+ environments=None,
42
+ build_default=None,
43
+ run_default=None,
44
+ ):
45
+ self.project_name = project_name
46
+ self.user_name = user_name
47
+ self.project_root = project_root
48
+ self.environments = _dictify(environments, 'environment')
49
+ self.images = _dictify(images, 'image')
50
+ self.artifacts = _dictify(artifacts, 'artifact')
51
+ self.pipelines = _dictify(pipelines, 'pipeline')
52
+ self.build_default = build_default
53
+ self.run_default = run_default
54
+
55
+ if self.project_root is None:
56
+ self.project_root = os.getcwd()
57
+ self.project_root = os.path.abspath(self.project_root)
58
+
59
+ # Create local environment if no environments are given.
60
+ if not self.environments:
61
+ self.environments['local'] = LocalEnv(
62
+ name='local',
63
+ project=self.project_name,
64
+ user=self.user_name,
65
+ )
66
+
67
+ # Set default environment to first item in the list if not given.
68
+ firstenv = next(iter(self.environments.keys()))
69
+ if self.build_default is None:
70
+ self.build_default = firstenv
71
+ if self.run_default is None:
72
+ self.run_default = firstenv
73
+
74
+ # check if default environments exist
75
+ if self.build_default not in self.environments:
76
+ raise ConfigError(
77
+ f'Default build environment {repr(self.build_default)} '
78
+ 'not found in environment list.'
79
+ )
80
+ if self.run_default not in self.environments:
81
+ raise ConfigError(
82
+ f'Default run environment {repr(self.run_default)} '
83
+ 'not found in environment list.'
84
+ )
85
+
86
+ # check pipelines
87
+ for pipeline_name, pipeline in self.pipelines.items():
88
+ for step in pipeline.steps:
89
+ if step.image not in self.images:
90
+ raise ConfigError(
91
+ f'Step {repr(step.name)} in pipeline '
92
+ f'{repr(pipeline_name)} requires unknown image '
93
+ f'{repr(step.image)}.'
94
+ )
95
+ image = self.images[step.image]
96
+ if not isinstance(image, ExecutionImage):
97
+ raise ConfigError(
98
+ f'Step {repr(step.name)} in pipeline '
99
+ f'{repr(pipeline_name)} requires image '
100
+ f'{repr(step.image)}, which is not an execution image.'
101
+ )
102
+ for a in step.inputs.values():
103
+ if a.artifact_name not in self.artifacts:
104
+ raise ConfigError(
105
+ f'Step {repr(step.name)} in pipeline '
106
+ f'{repr(pipeline_name)} uses undeclared '
107
+ f'input artifact {repr(a.artifact_name)} '
108
+ )
109
+ for a in step.outputs.values():
110
+ if a.artifact_name not in self.artifacts:
111
+ raise ConfigError(
112
+ f'Step {repr(step.name)} in pipeline '
113
+ f'{repr(pipeline_name)} uses undeclared '
114
+ f'output artifact {repr(a.artifact_name)} '
115
+ )
116
+ for a in step.temporaries.values():
117
+ if a.artifact_name not in self.artifacts:
118
+ raise ConfigError(
119
+ f'Step {repr(step.name)} in pipeline '
120
+ f'{repr(pipeline_name)} uses undeclared '
121
+ f'temporary artifact {repr(a.artifact_name)} '
122
+ )
123
+
124
+ def export_pipelines(self, path):
125
+ obj = {}
126
+ for name, pipeline in self.pipelines.items():
127
+ obj[name] = [step.to_dict() for step in pipeline.steps]
128
+ write_json(obj, path)
129
+
130
+ def export_artifacts(self, path):
131
+ obj = {'artifacts': [a.to_dict() for a in self.artifacts.values()]}
132
+ write_json(obj, path)
133
+
134
+ def _get_env_handler(self, env, purpose):
135
+ if env is None:
136
+ if purpose == 'build':
137
+ env = self.build_default
138
+ elif purpose == 'run':
139
+ env = self.run_default
140
+ else:
141
+ raise ValueError(f'Invalid `purpose` argument {repr(purpose)}.')
142
+ try:
143
+ return self.environments[env]
144
+ except KeyError:
145
+ raise ValueError(f'Unknown environment {env}.')
146
+
147
+ def get_build_env(self, env=None):
148
+ return self._get_env_handler(env, 'build')
149
+
150
+ def get_run_env(self, env=None):
151
+ return self._get_env_handler(env, 'run')
152
+
153
+ def get_image_ancestry(self, image_name):
154
+ images = []
155
+ while image_name is not None:
156
+ image = self.images[image_name]
157
+ images.append(image_name)
158
+ image_name = image.parent
159
+
160
+ return [self.images[name] for name in images[::-1]]
161
+
162
+ def in_project_root(self):
163
+ return contextlib.chdir(self.project_root)
164
+
@@ -0,0 +1,375 @@
1
+ import os
2
+ import shutil
3
+ import datetime as dt
4
+
5
+ from .utils import parse_image_ref
6
+ from .execution.utils import ensure_path, CloudSubmitError
7
+ from .images import ExecutionImage, BaseImage
8
+ from .execution.config import to_utc
9
+
10
+ class Controller:
11
+ def __init__(self, config):
12
+ self._config = config
13
+
14
+ def _install_execution_modules(self, path):
15
+ sourcedir = os.path.dirname(__file__)
16
+ shutil.copyfile(
17
+ os.path.join(sourcedir, 'execution', 'config.py'),
18
+ os.path.join(path, 'config.py'),
19
+ )
20
+ shutil.copyfile(
21
+ os.path.join(sourcedir, 'execution', 'base_execution_handler.py'),
22
+ os.path.join(path, 'base_execution_handler.py'),
23
+ )
24
+ shutil.copyfile(
25
+ os.path.join(sourcedir, 'execution', 'utils.py'),
26
+ os.path.join(path, 'utils.py'),
27
+ )
28
+ shutil.copyfile(
29
+ os.path.join(sourcedir, 'execution', 'execute.py'),
30
+ os.path.join(path, 'execute.py'),
31
+ )
32
+ self._config.export_pipelines(os.path.join(path, 'pipelines.json'))
33
+ self._config.export_artifacts(os.path.join(path, 'artifacts.json'))
34
+
35
+ def _build_image(self, image, build_id, env, rebuild=False):
36
+ if not rebuild:
37
+ ref = env.get_image_ref(image)
38
+ if ref is not None:
39
+ print(f'Using existing build of {image.name}: {ref}')
40
+ return ref
41
+
42
+ is_execution_image = isinstance(image, ExecutionImage)
43
+ path = os.path.join('temp', 'build', image.name)
44
+ if is_execution_image:
45
+ path = os.path.join(path, env.name)
46
+ ensure_path(path, clear=True)
47
+ shutil.copytree('src', os.path.join(path, 'src'))
48
+ csub_path = os.path.join(path, 'src', 'csub')
49
+ ensure_path(csub_path, clear=True)
50
+ self._install_execution_modules(csub_path)
51
+ env.install_execution_handler(csub_path)
52
+ else:
53
+ ensure_path(path, clear=True)
54
+
55
+ parent_ref = None
56
+ if image.parent is not None:
57
+ parent_image = self._config.images.get(image.parent)
58
+ if parent_image is None:
59
+ raise CloudSubmitError(
60
+ f'Could not find declaration for image {image.parent}.')
61
+ parent_ref = env.get_image_ref(parent_image)
62
+ if parent_ref is None:
63
+ raise CloudSubmitError(
64
+ f'Could not find reference for image {image.parent}.')
65
+ env.pull_base_image(parent_ref)
66
+ image.setup_builddir(path, parent_ref)
67
+
68
+ print(f'Building image {image.name}.')
69
+ ref = env.build_image(path, image, build_id)
70
+ if ref is None:
71
+ raise CloudSubmitError(
72
+ 'build_image method of environment handler did not '
73
+ 'return a valid image reference.'
74
+ )
75
+ env.save_image_ref(image, ref)
76
+
77
+ def build(self, images, build_id=None, env=None):
78
+ now = dt.datetime.now(tz=dt.UTC)
79
+ env_handler = self._config.get_build_env(env)
80
+ build_id = env_handler.generate_build_id(now, build_id)
81
+
82
+ images = set(images)
83
+ for image in images:
84
+ if image not in self._config.images:
85
+ raise CloudSubmitError(
86
+ f'No definition found for image {image}.'
87
+ )
88
+ all_images = set(
89
+ i.name for image in images
90
+ for i in self._config.get_image_ancestry(image)
91
+ )
92
+
93
+ with self._config.in_project_root():
94
+ for image_name, image in self._config.images.items():
95
+ if image_name not in all_images:
96
+ continue
97
+ rebuild = image_name in images
98
+ self._build_image(
99
+ image, build_id, env_handler, rebuild=rebuild)
100
+
101
+ def list_image_refs(self, images=None, ids=None, env=None, remote=False):
102
+ env_handler = self._config.get_build_env(env)
103
+ if images is None:
104
+ images = list(self._config.images.keys())
105
+ else:
106
+ images = set(images)
107
+ images = [
108
+ i for i in self._config.images.keys()
109
+ if i in images
110
+ ]
111
+ if ids is not None:
112
+ ids = set(ids)
113
+
114
+ results = []
115
+ for image in images:
116
+ image = self._config.images[image]
117
+ repo_name = env_handler.get_image_repo_name(image)
118
+ if remote:
119
+ tags = env_handler.list_remote_image_tags(repo_name)
120
+ else:
121
+ tags = env_handler.list_local_image_tags(repo_name)
122
+ if ids is not None:
123
+ tags = [t for t in tags if t in ids]
124
+ for tag in tags:
125
+ results.append(':'.join([repo_name, tag]))
126
+ return results
127
+
128
+ def list_images(self, images=None, env=None):
129
+ env_handler = self._config.get_build_env(env)
130
+ if images is not None:
131
+ images = set(images)
132
+ result = []
133
+ with self._config.in_project_root():
134
+ for i in self._config.images.values():
135
+ if images is not None and i.name not in images:
136
+ continue
137
+ if isinstance(i, BaseImage):
138
+ tpe = 'base'
139
+ elif isinstance(i, ExecutionImage):
140
+ tpe = 'execution'
141
+ else:
142
+ raise ValueError('Unknown image type.')
143
+ ref = env_handler.get_image_ref(i)
144
+ result.append((i.name, tpe, ref))
145
+ return result
146
+
147
+ def remove_image_refs(self, refs, remote=False, env=None):
148
+ env_handler = self._config.get_build_env(env)
149
+ if remote:
150
+ env_handler.remove_remote_image_refs(refs)
151
+ else:
152
+ env_handler.remove_local_image_refs(refs)
153
+
154
+ def set_image(self, image_name, ref, env=None):
155
+ env_handler = self._config.get_build_env(env)
156
+ image = self._config.images.get(image_name)
157
+ if image is None:
158
+ raise CloudSubmitError(f'Unknown image name {image_name}.')
159
+ with self._config.in_project_root():
160
+ env_handler.save_image_ref(image, ref)
161
+
162
+ def unset_image(self, image_name, env=None):
163
+ env_handler = self._config.get_build_env(env)
164
+ image = self._config.images.get(image_name)
165
+ if image is None:
166
+ raise CloudSubmitError(f'Unknown image name {image_name}.')
167
+ with self._config.in_project_root():
168
+ env_handler.clear_image_ref(image)
169
+
170
+ def list_artifacts(
171
+ self,
172
+ artifact_names=None,
173
+ run_ids=None,
174
+ remote=False,
175
+ env=None,
176
+ ):
177
+ if artifact_names is None:
178
+ artifact_names = list(self._config.artifacts.keys())
179
+ artifacts = []
180
+ for name in artifact_names:
181
+ try:
182
+ artifact = self._config.artifacts[name]
183
+ except KeyError:
184
+ raise CloudSubmitError(f'Unknown artifact name: {name}.')
185
+ artifacts.append(artifact.copy())
186
+
187
+ env_handler = self._config.get_run_env(env)
188
+ with self._config.in_project_root():
189
+ if remote:
190
+ runs = env_handler.list_remote_artifacts(
191
+ artifacts, run_ids=run_ids)
192
+ else:
193
+ runs = env_handler.list_local_artifacts(
194
+ artifacts, run_ids=run_ids)
195
+
196
+ return [a.name for a in artifacts], runs
197
+
198
+ def remove_artifacts(
199
+ self,
200
+ artifact_names,
201
+ run_ids,
202
+ remote=False,
203
+ env=None,
204
+ ):
205
+ env_handler = self._config.get_run_env(env)
206
+ artifacts = [
207
+ self._config.artifacts[name].copy() for name in artifact_names]
208
+ with self._config.in_project_root():
209
+ if remote:
210
+ env_handler.remove_remote_artifacts(artifacts, run_ids)
211
+ else:
212
+ env_handler.remove_local_artifacts(artifacts, run_ids)
213
+
214
+ def pull_artifacts(
215
+ self,
216
+ artifact_names,
217
+ run_ids,
218
+ env=None,
219
+ ):
220
+ env_handler = self._config.get_run_env(env)
221
+ artifacts = [
222
+ self._config.artifacts[name].copy() for name in artifact_names]
223
+ with self._config.in_project_root():
224
+ env_handler.remove_local_artifacts(artifacts, run_ids)
225
+ env_handler.pull_artifacts(artifacts, run_ids)
226
+
227
+ def push_artifacts(
228
+ self,
229
+ artifact_names,
230
+ run_ids,
231
+ env=None,
232
+ ):
233
+ env_handler = self._config.get_run_env(env)
234
+ artifacts = [
235
+ self._config.artifacts[name].copy() for name in artifact_names]
236
+ with self._config.in_project_root():
237
+ env_handler.remove_remote_artifacts(artifacts, run_ids)
238
+ env_handler.push_artifacts(artifacts, run_ids)
239
+
240
+ def copy_artifacts(
241
+ self,
242
+ artifact_names,
243
+ from_run_id,
244
+ to_run_id,
245
+ remote=False,
246
+ env=None,
247
+ ):
248
+ if from_run_id == to_run_id:
249
+ raise CloudSubmitError(
250
+ f'Source and destination run ID are identical: {from_run_id}')
251
+ env_handler = self._config.get_run_env(env)
252
+ artifacts = [
253
+ self._config.artifacts[name].copy() for name in artifact_names
254
+ if self._config.artifacts[name].scope == 'run'
255
+ ]
256
+ with self._config.in_project_root():
257
+ if remote:
258
+ env_handler.remove_remote_artifacts(
259
+ artifacts, [[to_run_id]]*len(artifacts))
260
+ env_handler.copy_remote_artifacts(
261
+ artifacts, from_run_id, to_run_id)
262
+ else:
263
+ env_handler.remove_local_artifacts(
264
+ artifacts, [[to_run_id]]*len(artifacts))
265
+ env_handler.copy_local_artifacts(
266
+ artifacts, from_run_id, to_run_id)
267
+
268
+ def move_artifacts(
269
+ self,
270
+ artifact_names,
271
+ from_run_id,
272
+ to_run_id,
273
+ remote=False,
274
+ env=None,
275
+ ):
276
+ if from_run_id == to_run_id:
277
+ raise CloudSubmitError(
278
+ f'Source and destination run ID are identical: {from_run_id}')
279
+ env_handler = self._config.get_run_env(env)
280
+ artifacts = [
281
+ self._config.artifacts[name].copy() for name in artifact_names
282
+ if self._config.artifacts[name].scope == 'run'
283
+ ]
284
+ with self._config.in_project_root():
285
+ if remote:
286
+ env_handler.remove_remote_artifacts(
287
+ artifacts, [[to_run_id]]*len(artifacts))
288
+ env_handler.move_remote_artifacts(
289
+ artifacts, from_run_id, to_run_id)
290
+ else:
291
+ env_handler.remove_local_artifacts(
292
+ artifacts, [[to_run_id]]*len(artifacts))
293
+ env_handler.move_local_artifacts(
294
+ artifacts, from_run_id, to_run_id)
295
+
296
+ def run_pipeline(
297
+ self,
298
+ pipeline,
299
+ steps=None,
300
+ run_id=None,
301
+ timestamp=None,
302
+ env=None,
303
+ build_env=None,
304
+ ):
305
+ try:
306
+ pipeline = self._config.pipelines[pipeline]
307
+ except KeyError:
308
+ raise CloudSubmitError(f'Pipeline not found: {pipeline}')
309
+
310
+ if build_env is None:
311
+ build_env = env
312
+
313
+ now = dt.datetime.now(tz=dt.UTC)
314
+ if isinstance(timestamp, str) and timestamp == 'now':
315
+ timestamp = now
316
+ if timestamp is None:
317
+ timestamp = pipeline.default_submit_timestamp
318
+ if timestamp is None:
319
+ timestamp = now
320
+ timestamp = to_utc(timestamp)
321
+ env_handler = self._config.get_run_env(env)
322
+ run_id = env_handler.generate_run_id(now, run_id)
323
+ if steps is None:
324
+ steps = [step.name for step in pipeline.steps]
325
+ steps = set(steps)
326
+ steps = [step for step in pipeline.steps if step.name in steps]
327
+
328
+ with self._config.in_project_root():
329
+ images = [step.image for step in steps]
330
+ self.build(images, build_id=run_id, env=build_env)
331
+ refs = {}
332
+ overwrite_artifacts = []
333
+ for step in steps:
334
+ image = self._config.images.get(step.image)
335
+ if image is None:
336
+ raise CloudSubmitError(
337
+ f'Cannot find declaration for image {step.image}.')
338
+ ref = env_handler.get_image_ref(image)
339
+ if ref is None:
340
+ raise CloudSubmitError(
341
+ f'Could not find image ref for image {step.image}')
342
+ refs[step.name] = ref
343
+
344
+ for loc in step.outputs.values():
345
+ overwrite_artifacts.append(
346
+ self._config.artifacts[loc.artifact_name].copy())
347
+ for loc in step.temporaries.values():
348
+ overwrite_artifacts.append(
349
+ self._config.artifacts[loc.artifact_name].copy())
350
+
351
+ temp_path = 'temp/run'
352
+ ensure_path(temp_path, clear=True)
353
+ env_handler.run_pipeline(
354
+ pipeline, refs, overwrite_artifacts,
355
+ timestamp, run_id, temp_path,
356
+ )
357
+
358
+ return {
359
+ 'run_id': run_id,
360
+ 'start_timestamp': now,
361
+ }
362
+
363
+ def print_logs(
364
+ self,
365
+ run_id,
366
+ since=dt.timedelta(minutes=1),
367
+ env=None,
368
+ stream=False
369
+ ):
370
+ if isinstance(since, dt.timedelta):
371
+ since = dt.datetime.now(tz=dt.UTC) - since
372
+ env_handler = self._config.get_run_env(env)
373
+ env_handler.print_logs(run_id, since, stream=stream)
374
+
375
+