lamin_cli 1.11.0__py2.py3-none-any.whl → 1.12.1__py2.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.
lamin_cli/_save.py CHANGED
@@ -1,325 +1,350 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import re
5
- from pathlib import Path
6
-
7
- import click
8
- import lamindb_setup as ln_setup
9
- from lamin_utils import logger
10
- from lamindb_setup.core.hashing import hash_file
11
-
12
- from lamin_cli._context import get_current_run_file
13
-
14
-
15
- def infer_registry_from_path(path: Path | str) -> str:
16
- suffixes_transform = {
17
- "py": {".py", ".ipynb"},
18
- "R": {".R", ".qmd", ".Rmd"},
19
- "sh": {".sh"},
20
- }
21
- if isinstance(path, str):
22
- path = Path(path)
23
- registry = (
24
- "transform"
25
- if path.suffix
26
- in suffixes_transform["py"]
27
- .union(suffixes_transform["R"])
28
- .union(suffixes_transform["sh"])
29
- else "artifact"
30
- )
31
- return registry
32
-
33
-
34
- def parse_uid_from_code(content: str, suffix: str) -> str | None:
35
- if suffix == ".py":
36
- track_pattern = re.compile(
37
- r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{12,16})\1'
38
- )
39
- uid_pattern = re.compile(r'\.context\.uid\s*=\s*["\']([^"\']+)["\']')
40
- elif suffix == ".ipynb":
41
- track_pattern = re.compile(
42
- r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{12,16})(?:\\"|\')'
43
- )
44
- # backward compat
45
- uid_pattern = re.compile(r'\.context\.uid\s*=\s*\\["\']([^"\']+)\\["\']')
46
- elif suffix in {".R", ".qmd", ".Rmd"}:
47
- track_pattern = re.compile(
48
- r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{12,16})\1'
49
- )
50
- uid_pattern = None
51
- elif suffix == ".sh":
52
- return None
53
- else:
54
- raise SystemExit(
55
- "Only .py, .ipynb, .R, .qmd, .Rmd, .sh files are supported for saving"
56
- " transforms."
57
- )
58
-
59
- # Search for matches in the entire file content
60
- uid_match = track_pattern.search(content)
61
- group_index = 1 if suffix == ".ipynb" else 2
62
- uid = uid_match.group(group_index) if uid_match else None
63
-
64
- if uid_pattern is not None and uid is None:
65
- uid_match = uid_pattern.search(content)
66
- uid = uid_match.group(1) if uid_match else None
67
-
68
- return uid
69
-
70
-
71
- def parse_title_r_notebook(content: str) -> str | None:
72
- # Pattern to match title only within YAML header section
73
- title_pattern = r'^---\n.*?title:\s*"([^"]*)".*?---'
74
- title_match = re.search(title_pattern, content, flags=re.DOTALL | re.MULTILINE)
75
- if title_match:
76
- return title_match.group(1)
77
- else:
78
- return None
79
-
80
-
81
- def save(
82
- path: Path | str,
83
- key: str | None = None,
84
- description: str | None = None,
85
- stem_uid: str | None = None,
86
- project: str | None = None,
87
- space: str | None = None,
88
- branch: str | None = None,
89
- registry: str | None = None,
90
- ) -> str | None:
91
- import lamindb as ln
92
- from lamindb._finish import save_context_core
93
- from lamindb_setup.core._settings_store import settings_dir
94
- from lamindb_setup.core.upath import LocalPathClasses, UPath, create_path
95
-
96
- current_run = None
97
- if get_current_run_file().exists():
98
- current_run = ln.Run.get(uid=get_current_run_file().read_text().strip())
99
-
100
- # this allows to have the correct treatment of credentials in case of cloud paths
101
- ppath = create_path(path)
102
- # isinstance is needed to cast the type of path to UPath
103
- # to avoid mypy erors
104
- assert isinstance(ppath, UPath)
105
- if not ppath.exists():
106
- raise click.BadParameter(f"Path {ppath} does not exist", param_hint="path")
107
-
108
- if registry is None:
109
- registry = infer_registry_from_path(ppath)
110
-
111
- if project is not None:
112
- project_record = ln.Project.filter(
113
- ln.Q(name=project) | ln.Q(uid=project)
114
- ).one_or_none()
115
- if project_record is None:
116
- raise ln.errors.InvalidArgument(
117
- f"Project '{project}' not found, either create it with `ln.Project(name='...').save()` or fix typos."
118
- )
119
- space_record = None
120
- if space is not None:
121
- space_record = ln.Space.filter(ln.Q(name=space) | ln.Q(uid=space)).one_or_none()
122
- if space_record is None:
123
- raise ln.errors.InvalidArgument(
124
- f"Space '{space}' not found, either create it on LaminHub or fix typos."
125
- )
126
- branch_record = None
127
- if branch is not None:
128
- branch_record = ln.Branch.filter(
129
- ln.Q(name=branch) | ln.Q(uid=branch)
130
- ).one_or_none()
131
- if branch_record is None:
132
- raise ln.errors.InvalidArgument(
133
- f"Branch '{branch}' not found, either create it with `ln.Branch(name='...').save()` or fix typos."
134
- )
135
-
136
- is_cloud_path = not isinstance(ppath, LocalPathClasses)
137
-
138
- if registry == "artifact":
139
- ln.settings.creation.artifact_silence_missing_run_warning = True
140
- revises = None
141
- if stem_uid is not None:
142
- revises = (
143
- ln.Artifact.filter(uid__startswith=stem_uid)
144
- .order_by("-created_at")
145
- .first()
146
- )
147
- if revises is None:
148
- raise ln.errors.InvalidArgument("The stem uid is not found.")
149
-
150
- if is_cloud_path:
151
- if key is not None:
152
- logger.error("Do not pass --key for cloud paths")
153
- return "key-with-cloud-path"
154
- elif key is None and description is None:
155
- logger.error("Please pass a key or description via --key or --description")
156
- return "missing-key-or-description"
157
-
158
- artifact = ln.Artifact(
159
- ppath,
160
- key=key,
161
- description=description,
162
- revises=revises,
163
- branch=branch_record,
164
- space=space_record,
165
- run=current_run,
166
- ).save()
167
- logger.important(f"saved: {artifact}")
168
- logger.important(f"storage path: {artifact.path}")
169
- if artifact.storage.type == "s3":
170
- logger.important(f"storage url: {artifact.path.to_url()}")
171
- if project is not None:
172
- artifact.projects.add(project_record)
173
- logger.important(f"labeled with project: {project_record.name}")
174
- if ln.setup.settings.instance.is_remote:
175
- slug = ln.setup.settings.instance.slug
176
- ui_url = ln.setup.settings.instance.ui_url
177
- logger.important(f"go to: {ui_url}/{slug}/artifact/{artifact.uid}")
178
- return None
179
-
180
- if registry == "transform":
181
- if key is not None:
182
- logger.warning(
183
- "key is ignored for transforms, the transform key is determined by the filename and the development directory (dev-dir)"
184
- )
185
- if is_cloud_path:
186
- logger.error("Can not register a transform from a cloud path")
187
- return "transform-with-cloud-path"
188
-
189
- if ppath.suffix in {".qmd", ".Rmd"}:
190
- html_file_exists = ppath.with_suffix(".html").exists()
191
- nb_html_file_exists = ppath.with_suffix(".nb.html").exists()
192
-
193
- if not html_file_exists and not nb_html_file_exists:
194
- logger.error(
195
- f"Please export your {ppath.suffix} file as an html file here"
196
- f" {ppath.with_suffix('.html')}"
197
- )
198
- return "export-qmd-Rmd-as-html"
199
- elif html_file_exists and nb_html_file_exists:
200
- logger.error(
201
- f"Please delete one of\n - {ppath.with_suffix('.html')}\n -"
202
- f" {ppath.with_suffix('.nb.html')}"
203
- )
204
- return "delete-html-or-nb-html"
205
-
206
- content = ppath.read_text()
207
- uid = parse_uid_from_code(content, ppath.suffix)
208
-
209
- ppath = ppath.resolve().expanduser()
210
- if ln_setup.settings.dev_dir is not None:
211
- key = ppath.relative_to(ln_setup.settings.dev_dir).as_posix()
212
- else:
213
- key = ppath.name
214
-
215
- if uid is not None:
216
- logger.important(f"mapped '{ppath.name}' on uid '{uid}'")
217
- if len(uid) == 16:
218
- # is full uid
219
- transform = ln.Transform.filter(uid=uid).one_or_none()
220
- else:
221
- # is stem uid
222
- if stem_uid is not None:
223
- assert stem_uid == uid, (
224
- "passed stem uid and parsed stem uid do not match"
225
- )
226
- else:
227
- stem_uid = uid
228
- transform = (
229
- ln.Transform.filter(uid__startswith=uid)
230
- .order_by("-created_at")
231
- .first()
232
- )
233
- if transform is None:
234
- uid = f"{stem_uid}0000"
235
- else:
236
- _, transform_hash, _ = hash_file(ppath)
237
- transform = ln.Transform.filter(hash=transform_hash).first()
238
- if transform is not None and transform.hash is not None:
239
- if transform.hash == transform_hash:
240
- if transform.type != "notebook":
241
- logger.important(f"transform already saved: {transform}")
242
- if transform.key != key:
243
- transform.key = key
244
- logger.important(f"updated key to '{key}'")
245
- transform.save()
246
- return None
247
- if os.getenv("LAMIN_TESTING") == "true":
248
- response = "y"
249
- else:
250
- response = input(
251
- f"Found an existing Transform('{transform.uid}') "
252
- "with matching source code hash.\n"
253
- "Do you want to update it? (y/n) "
254
- )
255
- if response != "y":
256
- return None
257
- else:
258
- # we need to create a new version
259
- stem_uid = transform.uid[:12]
260
- transform = None
261
- revises = None
262
- if stem_uid is not None:
263
- revises = (
264
- ln.Transform.filter(uid__startswith=stem_uid)
265
- .order_by("-created_at")
266
- .first()
267
- )
268
- if revises is None:
269
- raise ln.errors.InvalidArgument("The stem uid is not found.")
270
- if transform is None:
271
- if ppath.suffix == ".ipynb":
272
- from nbproject.dev import read_notebook
273
- from nbproject.dev._meta_live import get_title
274
-
275
- nb = read_notebook(ppath)
276
- description = get_title(nb)
277
- elif ppath.suffix in {".qmd", ".Rmd"}:
278
- description = parse_title_r_notebook(content)
279
- else:
280
- description = None
281
- transform = ln.Transform(
282
- uid=uid,
283
- description=description,
284
- key=key,
285
- type="script" if ppath.suffix in {".R", ".py", ".sh"} else "notebook",
286
- revises=revises,
287
- )
288
- if space is not None:
289
- transform.space = space_record
290
- if branch is not None:
291
- transform.branch = branch_record
292
- transform.save()
293
- logger.important(
294
- f"created Transform('{transform.uid}', key='{transform.key}')"
295
- )
296
- if project is not None:
297
- transform.projects.add(project_record)
298
- logger.important(f"labeled with project: {project_record.name}")
299
- # latest run of this transform by user
300
- run = ln.Run.filter(transform=transform).order_by("-started_at").first()
301
- if run is not None and run.created_by.id != ln.setup.settings.user.id:
302
- if os.getenv("LAMIN_TESTING") == "true":
303
- response = "y"
304
- else:
305
- response = input(
306
- "You are trying to save a transform created by another user: Source"
307
- " and report files will be tagged with *your* user id. Proceed?"
308
- " (y/n) "
309
- )
310
- if response != "y":
311
- return "aborted-save-notebook-created-by-different-user"
312
- if run is None and transform.type == "notebook":
313
- run = ln.Run(transform=transform).save()
314
- logger.important(
315
- f"found no run, creating Run('{run.uid}') to display the html"
316
- )
317
- return_code = save_context_core(
318
- run=run,
319
- transform=transform,
320
- filepath=ppath,
321
- from_cli=True,
322
- )
323
- return return_code
324
- else:
325
- raise SystemExit("Allowed values for '--registry' are: 'artifact', 'transform'")
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import lamindb_setup as ln_setup
9
+ from lamin_utils import logger
10
+ from lamindb_setup.core.hashing import hash_file
11
+
12
+ from lamin_cli._context import get_current_run_file
13
+
14
+
15
+ def infer_registry_from_path(path: Path | str) -> str:
16
+ suffixes_transform = {
17
+ "py": {".py", ".ipynb"},
18
+ "R": {".R", ".qmd", ".Rmd"},
19
+ "sh": {".sh"},
20
+ }
21
+ if isinstance(path, str):
22
+ path = Path(path)
23
+ registry = (
24
+ "transform"
25
+ if path.suffix
26
+ in suffixes_transform["py"]
27
+ .union(suffixes_transform["R"])
28
+ .union(suffixes_transform["sh"])
29
+ else "artifact"
30
+ )
31
+ return registry
32
+
33
+
34
+ def parse_uid_from_code(content: str, suffix: str) -> str | None:
35
+ if suffix == ".py":
36
+ track_pattern = re.compile(
37
+ r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{12,16})\1'
38
+ )
39
+ uid_pattern = re.compile(r'\.context\.uid\s*=\s*["\']([^"\']+)["\']')
40
+ elif suffix == ".ipynb":
41
+ track_pattern = re.compile(
42
+ r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{12,16})(?:\\"|\')'
43
+ )
44
+ # backward compat
45
+ uid_pattern = re.compile(r'\.context\.uid\s*=\s*\\["\']([^"\']+)\\["\']')
46
+ elif suffix in {".R", ".qmd", ".Rmd"}:
47
+ track_pattern = re.compile(
48
+ r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{12,16})\1'
49
+ )
50
+ uid_pattern = None
51
+ elif suffix == ".sh":
52
+ return None
53
+ else:
54
+ raise click.ClickException(
55
+ "Only .py, .ipynb, .R, .qmd, .Rmd, .sh files are supported for saving"
56
+ " transforms."
57
+ )
58
+
59
+ # Search for matches in the entire file content
60
+ uid_match = track_pattern.search(content)
61
+ group_index = 1 if suffix == ".ipynb" else 2
62
+ uid = uid_match.group(group_index) if uid_match else None
63
+
64
+ if uid_pattern is not None and uid is None:
65
+ uid_match = uid_pattern.search(content)
66
+ uid = uid_match.group(1) if uid_match else None
67
+
68
+ return uid
69
+
70
+
71
+ def parse_title_r_notebook(content: str) -> str | None:
72
+ # Pattern to match title only within YAML header section
73
+ title_pattern = r'^---\n.*?title:\s*"([^"]*)".*?---'
74
+ title_match = re.search(title_pattern, content, flags=re.DOTALL | re.MULTILINE)
75
+ if title_match:
76
+ return title_match.group(1)
77
+ else:
78
+ return None
79
+
80
+
81
+ def save(
82
+ path: Path | str,
83
+ key: str | None = None,
84
+ description: str | None = None,
85
+ stem_uid: str | None = None,
86
+ project: str | None = None,
87
+ space: str | None = None,
88
+ branch: str | None = None,
89
+ registry: str | None = None,
90
+ ) -> str | None:
91
+ import lamindb as ln
92
+ from lamindb._finish import save_context_core
93
+ from lamindb_setup.core._settings_store import settings_dir
94
+ from lamindb_setup.core.upath import LocalPathClasses, UPath, create_path
95
+
96
+ current_run = None
97
+ if get_current_run_file().exists():
98
+ current_run = ln.Run.get(uid=get_current_run_file().read_text().strip())
99
+
100
+ # this allows to have the correct treatment of credentials in case of cloud paths
101
+ ppath = create_path(path)
102
+ # isinstance is needed to cast the type of path to UPath
103
+ # to avoid mypy erors
104
+ assert isinstance(ppath, UPath)
105
+ if not ppath.exists():
106
+ raise click.BadParameter(f"Path {ppath} does not exist", param_hint="path")
107
+
108
+ if registry is None:
109
+ registry = infer_registry_from_path(ppath)
110
+
111
+ if project is not None:
112
+ project_record = ln.Project.filter(
113
+ ln.Q(name=project) | ln.Q(uid=project)
114
+ ).one_or_none()
115
+ if project_record is None:
116
+ raise click.ClickException(
117
+ f"Project '{project}' not found, either create it with `ln.Project(name='...').save()` or fix typos."
118
+ )
119
+ space_record = None
120
+ if space is not None:
121
+ space_record = ln.Space.filter(ln.Q(name=space) | ln.Q(uid=space)).one_or_none()
122
+ if space_record is None:
123
+ raise click.ClickException(
124
+ f"Space '{space}' not found, either create it on LaminHub or fix typos."
125
+ )
126
+ branch_record = None
127
+ if branch is not None:
128
+ branch_record = ln.Branch.filter(
129
+ ln.Q(name=branch) | ln.Q(uid=branch)
130
+ ).one_or_none()
131
+ if branch_record is None:
132
+ raise click.ClickException(
133
+ f"Branch '{branch}' not found, either create it with `ln.Branch(name='...').save()` or fix typos."
134
+ )
135
+
136
+ is_cloud_path = not isinstance(ppath, LocalPathClasses)
137
+
138
+ if registry == "artifact":
139
+ ln.settings.creation.artifact_silence_missing_run_warning = True
140
+ revises = None
141
+ if stem_uid is not None:
142
+ revises = (
143
+ ln.Artifact.filter(uid__startswith=stem_uid)
144
+ .order_by("-created_at")
145
+ .first()
146
+ )
147
+ if revises is None:
148
+ raise click.ClickException("The stem uid is not found.")
149
+
150
+ if is_cloud_path:
151
+ if key is not None:
152
+ logger.error("Do not pass --key for cloud paths")
153
+ return "key-with-cloud-path"
154
+ elif key is None and description is None:
155
+ logger.error("Please pass a key or description via --key or --description")
156
+ return "missing-key-or-description"
157
+
158
+ artifact = ln.Artifact(
159
+ ppath,
160
+ key=key,
161
+ description=description,
162
+ revises=revises,
163
+ branch=branch_record,
164
+ space=space_record,
165
+ run=current_run,
166
+ ).save()
167
+ logger.important(f"saved: {artifact}")
168
+ logger.important(f"storage path: {artifact.path}")
169
+ if artifact.storage.type == "s3":
170
+ logger.important(f"storage url: {artifact.path.to_url()}")
171
+ if project is not None:
172
+ artifact.projects.add(project_record)
173
+ logger.important(f"labeled with project: {project_record.name}")
174
+ if ln.setup.settings.instance.is_remote:
175
+ slug = ln.setup.settings.instance.slug
176
+ ui_url = ln.setup.settings.instance.ui_url
177
+ logger.important(f"go to: {ui_url}/{slug}/artifact/{artifact.uid}")
178
+ return None
179
+
180
+ if registry == "transform":
181
+ if key is not None:
182
+ logger.warning(
183
+ "key is ignored for transforms, the transform key is determined by the filename and the development directory (dev-dir)"
184
+ )
185
+ if is_cloud_path:
186
+ logger.error("Can not register a transform from a cloud path")
187
+ return "transform-with-cloud-path"
188
+
189
+ if ppath.suffix in {".qmd", ".Rmd"}:
190
+ html_file_exists = ppath.with_suffix(".html").exists()
191
+ nb_html_file_exists = ppath.with_suffix(".nb.html").exists()
192
+
193
+ if not html_file_exists and not nb_html_file_exists:
194
+ logger.error(
195
+ f"Please export your {ppath.suffix} file as an html file here"
196
+ f" {ppath.with_suffix('.html')}"
197
+ )
198
+ return "export-qmd-Rmd-as-html"
199
+ elif html_file_exists and nb_html_file_exists:
200
+ logger.error(
201
+ f"Please delete one of\n - {ppath.with_suffix('.html')}\n -"
202
+ f" {ppath.with_suffix('.nb.html')}"
203
+ )
204
+ return "delete-html-or-nb-html"
205
+
206
+ content = ppath.read_text()
207
+ uid = parse_uid_from_code(content, ppath.suffix)
208
+
209
+ ppath = ppath.resolve().expanduser()
210
+ if ln_setup.settings.dev_dir is not None:
211
+ key = ppath.relative_to(ln_setup.settings.dev_dir).as_posix()
212
+ else:
213
+ key = ppath.name
214
+
215
+ if uid is not None:
216
+ logger.important(f"mapped '{ppath.name}' on uid '{uid}'")
217
+ if len(uid) == 16:
218
+ # is full uid
219
+ transform = ln.Transform.filter(uid=uid).one_or_none()
220
+ else:
221
+ # is stem uid
222
+ if stem_uid is not None:
223
+ assert stem_uid == uid, (
224
+ "passed stem uid and parsed stem uid do not match"
225
+ )
226
+ else:
227
+ stem_uid = uid
228
+ transform = (
229
+ ln.Transform.filter(uid__startswith=uid)
230
+ .order_by("-created_at")
231
+ .first()
232
+ )
233
+ if transform is None:
234
+ uid = f"{stem_uid}0000"
235
+ else:
236
+ _, transform_hash, _ = hash_file(ppath)
237
+ transform = ln.Transform.filter(hash=transform_hash).first()
238
+ if transform is not None and transform.hash is not None:
239
+ if transform.hash == transform_hash:
240
+ if transform.type != "notebook":
241
+ logger.important(f"transform already saved: {transform}")
242
+ if transform.key != key:
243
+ transform.key = key
244
+ logger.important(f"updated key to '{key}'")
245
+ transform.save()
246
+ return None
247
+ if os.getenv("LAMIN_TESTING") == "true":
248
+ response = "y"
249
+ else:
250
+ response = input(
251
+ f"Found an existing Transform('{transform.uid}') "
252
+ "with matching source code hash.\n"
253
+ "Do you want to update it? (y/n) "
254
+ )
255
+ if response != "y":
256
+ return None
257
+ else:
258
+ # we need to create a new version
259
+ stem_uid = transform.uid[:12]
260
+ transform = None
261
+ revises = None
262
+ if stem_uid is not None:
263
+ revises = (
264
+ ln.Transform.filter(uid__startswith=stem_uid)
265
+ .order_by("-created_at")
266
+ .first()
267
+ )
268
+ if revises is None:
269
+ # check if the transform is on other branches
270
+ revises_other_branches = (
271
+ ln.Transform.filter(uid__startswith=stem_uid, branch=None)
272
+ .order_by("-created_at")
273
+ .first()
274
+ )
275
+ if revises_other_branches is not None:
276
+ if revises_other_branches.branch_id == -1:
277
+ raise click.ClickException(
278
+ "Transform is in the trash, please restore it before running `lamin save`!"
279
+ )
280
+ elif revises_other_branches.branch_id == 0:
281
+ raise click.ClickException(
282
+ "Transform is in the archive, please restore it before running `lamin save`!"
283
+ )
284
+ elif (
285
+ revises_other_branches.branch_id != ln_setup.settings.branch.id
286
+ ):
287
+ raise click.ClickException(
288
+ "Transform is on a different branch"
289
+ f"({revises_other_branches.branch.name}), please switch to the correct branch"
290
+ " before running `lamin save`!"
291
+ )
292
+ raise click.ClickException("The stem uid is not found.")
293
+ if transform is None:
294
+ if ppath.suffix == ".ipynb":
295
+ from nbproject.dev import read_notebook
296
+ from nbproject.dev._meta_live import get_title
297
+
298
+ nb = read_notebook(ppath)
299
+ description = get_title(nb)
300
+ elif ppath.suffix in {".qmd", ".Rmd"}:
301
+ description = parse_title_r_notebook(content)
302
+ else:
303
+ description = None
304
+ transform = ln.Transform(
305
+ uid=uid,
306
+ description=description,
307
+ key=key,
308
+ type="script" if ppath.suffix in {".R", ".py", ".sh"} else "notebook",
309
+ revises=revises,
310
+ )
311
+ if space is not None:
312
+ transform.space = space_record
313
+ if branch is not None:
314
+ transform.branch = branch_record
315
+ transform.save()
316
+ logger.important(
317
+ f"created Transform('{transform.uid}', key='{transform.key}')"
318
+ )
319
+ if project is not None:
320
+ transform.projects.add(project_record)
321
+ logger.important(f"labeled with project: {project_record.name}")
322
+ # latest run of this transform by user
323
+ run = ln.Run.filter(transform=transform).order_by("-started_at").first()
324
+ if run is not None and run.created_by.id != ln.setup.settings.user.id:
325
+ if os.getenv("LAMIN_TESTING") == "true":
326
+ response = "y"
327
+ else:
328
+ response = input(
329
+ "You are trying to save a transform created by another user: Source"
330
+ " and report files will be tagged with *your* user id. Proceed?"
331
+ " (y/n) "
332
+ )
333
+ if response != "y":
334
+ return "aborted-save-notebook-created-by-different-user"
335
+ if run is None and transform.type == "notebook":
336
+ run = ln.Run(transform=transform).save()
337
+ logger.important(
338
+ f"found no run, creating Run('{run.uid}') to display the html"
339
+ )
340
+ return_code = save_context_core(
341
+ run=run,
342
+ transform=transform,
343
+ filepath=ppath,
344
+ from_cli=True,
345
+ )
346
+ return return_code
347
+ else:
348
+ raise click.ClickException(
349
+ "Allowed values for '--registry' are: 'artifact', 'transform'"
350
+ )