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/__init__.py +27 -27
- lamin_cli/__main__.py +826 -625
- lamin_cli/_annotate.py +47 -47
- lamin_cli/_cache.py +49 -41
- lamin_cli/_context.py +76 -76
- lamin_cli/_delete.py +85 -44
- lamin_cli/_io.py +147 -144
- lamin_cli/_load.py +203 -203
- lamin_cli/_migration.py +50 -50
- lamin_cli/_save.py +350 -325
- lamin_cli/_settings.py +154 -96
- lamin_cli/clone/_clone_verification.py +56 -56
- lamin_cli/clone/create_sqlite_clone_and_import_db.py +53 -51
- lamin_cli/compute/modal.py +174 -175
- lamin_cli/urls.py +10 -8
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info}/METADATA +3 -2
- lamin_cli-1.12.1.dist-info/RECORD +22 -0
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info}/WHEEL +1 -1
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info/licenses}/LICENSE +201 -201
- lamin_cli-1.11.0.dist-info/RECORD +0 -22
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info}/entry_points.txt +0 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
)
|