lamin_cli 1.3.0__tar.gz → 1.4.1__tar.gz
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-1.3.0 → lamin_cli-1.4.1}/.pre-commit-config.yaml +1 -1
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/PKG-INFO +1 -1
- lamin_cli-1.4.1/lamin_cli/__init__.py +3 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/__main__.py +20 -14
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_load.py +3 -1
- lamin_cli-1.4.1/lamin_cli/_save.py +239 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/conftest.py +4 -4
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_migrate.py +3 -3
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_files.py +21 -3
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_notebooks.py +114 -18
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_r_code.py +1 -1
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_scripts.py +13 -7
- lamin_cli-1.4.1/tests/notebooks/with-title-and-initialized-consecutive.ipynb +55 -0
- lamin_cli-1.3.0/lamin_cli/__init__.py +0 -3
- lamin_cli-1.3.0/lamin_cli/_save.py +0 -185
- lamin_cli-1.3.0/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -191
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/.github/workflows/build.yml +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/.github/workflows/doc-changes.yml +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/.gitignore +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/LICENSE +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/README.md +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_cache.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_migration.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_settings.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/compute/__init__.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/compute/modal.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/noxfile.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/pyproject.toml +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_cli.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_load.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_multi_process.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_parse_uid_from_code.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/modal/test_modal.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/notebooks/not-initialized.ipynb +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/merely-import-lamindb.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track-and-finish-sync-git.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track-and-finish.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track-with-params.py +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track.R +0 -0
- {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track.qmd +0 -0
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import os
|
|
5
|
+
import shutil
|
|
5
6
|
import sys
|
|
6
7
|
import warnings
|
|
7
8
|
from collections import OrderedDict
|
|
8
9
|
from functools import wraps
|
|
9
10
|
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
from pathlib import Path
|
|
10
12
|
from typing import TYPE_CHECKING
|
|
11
13
|
|
|
12
14
|
from lamindb_setup._init_instance import (
|
|
@@ -307,21 +309,28 @@ def get(entity: str, uid: str | None = None, key: str | None = None):
|
|
|
307
309
|
|
|
308
310
|
|
|
309
311
|
@main.command()
|
|
310
|
-
@click.argument("path", type=
|
|
311
|
-
@click.option("--key", type=str, default=None)
|
|
312
|
-
@click.option("--description", type=str, default=None)
|
|
313
|
-
@click.option("--stem-uid", type=str, default=None)
|
|
314
|
-
@click.option("--
|
|
315
|
-
|
|
312
|
+
@click.argument("path", type=str)
|
|
313
|
+
@click.option("--key", type=str, default=None, help="The key of the artifact or transform.")
|
|
314
|
+
@click.option("--description", type=str, default=None, help="A description of the artifact or transform.")
|
|
315
|
+
@click.option("--stem-uid", type=str, default=None, help="The stem uid of the artifact or transform.")
|
|
316
|
+
@click.option("--project", type=str, default=None, help="A valid project name or uid.")
|
|
317
|
+
@click.option("--registry", type=str, default=None, help="Either 'artifact' or 'transform'. If not passed, chooses based on path suffix.")
|
|
318
|
+
def save(path: str, key: str, description: str, stem_uid: str, project: str, registry: str):
|
|
316
319
|
"""Save a file or folder.
|
|
317
320
|
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
Example: Given a valid project name "my_project".
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
lamin save my_table.csv --key my_tables/my_table.csv --project my_project
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Note: Defaults to saving `.py`, `.ipynb`, `.R`, `.Rmd`, and `.qmd` as {class}`~lamindb.Transform` and
|
|
328
|
+
other file types and folders as {class}`~lamindb.Artifact`. You can enforce saving a file as
|
|
320
329
|
an {class}`~lamindb.Artifact` by passing `--registry artifact`.
|
|
321
330
|
"""
|
|
322
|
-
from lamin_cli._save import
|
|
331
|
+
from lamin_cli._save import save_from_path_cli
|
|
323
332
|
|
|
324
|
-
if
|
|
333
|
+
if save_from_path_cli(path, key, description, stem_uid, project, registry) is not None:
|
|
325
334
|
sys.exit(1)
|
|
326
335
|
|
|
327
336
|
|
|
@@ -343,9 +352,6 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
|
|
|
343
352
|
lamin run my_script.py --project my_project
|
|
344
353
|
```
|
|
345
354
|
"""
|
|
346
|
-
import shutil
|
|
347
|
-
from pathlib import Path
|
|
348
|
-
|
|
349
355
|
from lamin_cli.compute.modal import Runner
|
|
350
356
|
|
|
351
357
|
default_mount_dir = Path('./modal_mount_dir')
|
|
@@ -354,7 +360,7 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
|
|
|
354
360
|
|
|
355
361
|
shutil.copy(filepath, default_mount_dir)
|
|
356
362
|
|
|
357
|
-
filepath_in_mount_dir =
|
|
363
|
+
filepath_in_mount_dir = default_mount_dir / Path(filepath).name
|
|
358
364
|
|
|
359
365
|
package_list = []
|
|
360
366
|
if packages:
|
|
@@ -78,7 +78,9 @@ def load(
|
|
|
78
78
|
)
|
|
79
79
|
if bump_revision:
|
|
80
80
|
uid = transform.uid
|
|
81
|
-
if
|
|
81
|
+
if (
|
|
82
|
+
uid in new_content
|
|
83
|
+
): # this only hits if it has the full uid, not for the stem uid
|
|
82
84
|
new_uid = f"{uid[:-4]}{increment_base62(uid[-4:])}"
|
|
83
85
|
new_content = new_content.replace(uid, new_uid)
|
|
84
86
|
logger.important(f"updated uid: {uid} → {new_uid}")
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from lamin_utils import logger
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_uid_from_code(content: str, suffix: str) -> str | None:
|
|
15
|
+
if suffix == ".py":
|
|
16
|
+
track_pattern = re.compile(
|
|
17
|
+
r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{12,16})\1'
|
|
18
|
+
)
|
|
19
|
+
uid_pattern = re.compile(r'\.context\.uid\s*=\s*["\']([^"\']+)["\']')
|
|
20
|
+
elif suffix == ".ipynb":
|
|
21
|
+
track_pattern = re.compile(
|
|
22
|
+
r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{12,16})(?:\\"|\')'
|
|
23
|
+
)
|
|
24
|
+
# backward compat
|
|
25
|
+
uid_pattern = re.compile(r'\.context\.uid\s*=\s*\\["\']([^"\']+)\\["\']')
|
|
26
|
+
elif suffix in {".R", ".qmd", ".Rmd"}:
|
|
27
|
+
track_pattern = re.compile(
|
|
28
|
+
r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{12,16})\1'
|
|
29
|
+
)
|
|
30
|
+
uid_pattern = None
|
|
31
|
+
else:
|
|
32
|
+
raise SystemExit(
|
|
33
|
+
"Only .py, .ipynb, .R, .qmd, .Rmd files are supported for saving"
|
|
34
|
+
" transforms."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Search for matches in the entire file content
|
|
38
|
+
uid_match = track_pattern.search(content)
|
|
39
|
+
group_index = 1 if suffix == ".ipynb" else 2
|
|
40
|
+
uid = uid_match.group(group_index) if uid_match else None
|
|
41
|
+
|
|
42
|
+
if uid_pattern is not None and uid is None:
|
|
43
|
+
uid_match = uid_pattern.search(content)
|
|
44
|
+
uid = uid_match.group(1) if uid_match else None
|
|
45
|
+
|
|
46
|
+
return uid
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def save_from_path_cli(
|
|
50
|
+
path: Path | str,
|
|
51
|
+
key: str | None,
|
|
52
|
+
description: str | None,
|
|
53
|
+
stem_uid: str | None,
|
|
54
|
+
project: str | None,
|
|
55
|
+
registry: str | None,
|
|
56
|
+
) -> str | None:
|
|
57
|
+
import lamindb_setup as ln_setup
|
|
58
|
+
from lamindb_setup.core.upath import LocalPathClasses, UPath, create_path
|
|
59
|
+
|
|
60
|
+
# this will be gone once we get rid of lamin load or enable loading multiple
|
|
61
|
+
# instances sequentially
|
|
62
|
+
auto_connect_state = ln_setup.settings.auto_connect
|
|
63
|
+
ln_setup.settings.auto_connect = True
|
|
64
|
+
|
|
65
|
+
import lamindb as ln
|
|
66
|
+
|
|
67
|
+
if not ln.setup.core.django.IS_SETUP:
|
|
68
|
+
sys.exit(-1)
|
|
69
|
+
from lamindb._finish import save_context_core
|
|
70
|
+
|
|
71
|
+
ln_setup.settings.auto_connect = auto_connect_state
|
|
72
|
+
|
|
73
|
+
# this allows to have the correct treatment of credentials in case of cloud paths
|
|
74
|
+
path = create_path(path)
|
|
75
|
+
# isinstance is needed to cast the type of path to UPath
|
|
76
|
+
# to avoid mypy erors
|
|
77
|
+
assert isinstance(path, UPath)
|
|
78
|
+
if not path.exists():
|
|
79
|
+
raise click.BadParameter(f"Path {path} does not exist", param_hint="path")
|
|
80
|
+
|
|
81
|
+
if registry is None:
|
|
82
|
+
suffixes_transform = {
|
|
83
|
+
"py": {".py", ".ipynb"},
|
|
84
|
+
"R": {".R", ".qmd", ".Rmd"},
|
|
85
|
+
}
|
|
86
|
+
registry = (
|
|
87
|
+
"transform"
|
|
88
|
+
if path.suffix in suffixes_transform["py"].union(suffixes_transform["R"])
|
|
89
|
+
else "artifact"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if project is not None:
|
|
93
|
+
project_record = ln.Project.filter(
|
|
94
|
+
ln.Q(name=project) | ln.Q(uid=project)
|
|
95
|
+
).one_or_none()
|
|
96
|
+
if project_record is None:
|
|
97
|
+
raise ln.errors.InvalidArgument(
|
|
98
|
+
f"Project '{project}' not found, either create it with `ln.Project(name='...').save()` or fix typos."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
is_cloud_path = not isinstance(path, LocalPathClasses)
|
|
102
|
+
|
|
103
|
+
if registry == "artifact":
|
|
104
|
+
ln.settings.creation.artifact_silence_missing_run_warning = True
|
|
105
|
+
revises = None
|
|
106
|
+
if stem_uid is not None:
|
|
107
|
+
revises = (
|
|
108
|
+
ln.Artifact.filter(uid__startswith=stem_uid)
|
|
109
|
+
.order_by("-created_at")
|
|
110
|
+
.first()
|
|
111
|
+
)
|
|
112
|
+
if revises is None:
|
|
113
|
+
raise ln.errors.InvalidArgument("The stem uid is not found.")
|
|
114
|
+
|
|
115
|
+
if is_cloud_path:
|
|
116
|
+
if key is not None:
|
|
117
|
+
logger.error("Do not pass --key for cloud paths")
|
|
118
|
+
return "key-with-cloud-path"
|
|
119
|
+
elif key is None and description is None:
|
|
120
|
+
logger.error("Please pass a key or description via --key or --description")
|
|
121
|
+
return "missing-key-or-description"
|
|
122
|
+
|
|
123
|
+
artifact = ln.Artifact(
|
|
124
|
+
path, key=key, description=description, revises=revises
|
|
125
|
+
).save()
|
|
126
|
+
logger.important(f"saved: {artifact}")
|
|
127
|
+
logger.important(f"storage path: {artifact.path}")
|
|
128
|
+
if ln_setup.settings.storage.type == "s3":
|
|
129
|
+
logger.important(f"storage url: {artifact.path.to_url()}")
|
|
130
|
+
if project is not None:
|
|
131
|
+
artifact.projects.add(project_record)
|
|
132
|
+
logger.important(f"labeled with project: {project_record.name}")
|
|
133
|
+
if ln_setup.settings.instance.is_remote:
|
|
134
|
+
slug = ln_setup.settings.instance.slug
|
|
135
|
+
logger.important(f"go to: https://lamin.ai/{slug}/artifact/{artifact.uid}")
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
if registry == "transform":
|
|
139
|
+
if is_cloud_path:
|
|
140
|
+
logger.error("Can not register a transform from a cloud path")
|
|
141
|
+
return "transform-with-cloud-path"
|
|
142
|
+
|
|
143
|
+
if path.suffix in {".qmd", ".Rmd"}:
|
|
144
|
+
html_file_exists = path.with_suffix(".html").exists()
|
|
145
|
+
nb_html_file_exists = path.with_suffix(".nb.html").exists()
|
|
146
|
+
|
|
147
|
+
if not html_file_exists and not nb_html_file_exists:
|
|
148
|
+
logger.error(
|
|
149
|
+
f"Please export your {path.suffix} file as an html file here"
|
|
150
|
+
f" {path.with_suffix('.html')}"
|
|
151
|
+
)
|
|
152
|
+
return "export-qmd-Rmd-as-html"
|
|
153
|
+
elif html_file_exists and nb_html_file_exists:
|
|
154
|
+
logger.error(
|
|
155
|
+
f"Please delete one of\n - {path.with_suffix('.html')}\n -"
|
|
156
|
+
f" {path.with_suffix('.nb.html')}"
|
|
157
|
+
)
|
|
158
|
+
return "delete-html-or-nb-html"
|
|
159
|
+
|
|
160
|
+
with path.open() as file:
|
|
161
|
+
content = file.read()
|
|
162
|
+
uid = parse_uid_from_code(content, path.suffix)
|
|
163
|
+
|
|
164
|
+
if uid is not None:
|
|
165
|
+
logger.important(f"mapped '{path.name}' on uid '{uid}'")
|
|
166
|
+
if len(uid) == 16:
|
|
167
|
+
# is full uid
|
|
168
|
+
transform = ln.Transform.filter(uid=uid).one_or_none()
|
|
169
|
+
else:
|
|
170
|
+
# is stem uid
|
|
171
|
+
if stem_uid is not None:
|
|
172
|
+
assert stem_uid == uid, (
|
|
173
|
+
"passed stem uid and parsed stem uid do not match"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
stem_uid = uid
|
|
177
|
+
transform = (
|
|
178
|
+
ln.Transform.filter(uid__startswith=uid)
|
|
179
|
+
.order_by("-created_at")
|
|
180
|
+
.first()
|
|
181
|
+
)
|
|
182
|
+
if transform is None:
|
|
183
|
+
uid = f"{stem_uid}0000"
|
|
184
|
+
else:
|
|
185
|
+
# TODO: account for folders and hash equivalence as we do in ln.track()
|
|
186
|
+
transform = ln.Transform.filter(key=path.name, is_latest=True).one_or_none()
|
|
187
|
+
revises = None
|
|
188
|
+
if stem_uid is not None:
|
|
189
|
+
revises = (
|
|
190
|
+
ln.Transform.filter(uid__startswith=stem_uid)
|
|
191
|
+
.order_by("-created_at")
|
|
192
|
+
.first()
|
|
193
|
+
)
|
|
194
|
+
if revises is None:
|
|
195
|
+
raise ln.errors.InvalidArgument("The stem uid is not found.")
|
|
196
|
+
if transform is None:
|
|
197
|
+
if path.suffix == ".ipynb":
|
|
198
|
+
from nbproject.dev import read_notebook
|
|
199
|
+
from nbproject.dev._meta_live import get_title
|
|
200
|
+
|
|
201
|
+
nb = read_notebook(path)
|
|
202
|
+
description = get_title(nb)
|
|
203
|
+
else:
|
|
204
|
+
description = None
|
|
205
|
+
transform = ln.Transform(
|
|
206
|
+
uid=uid,
|
|
207
|
+
description=description,
|
|
208
|
+
key=path.name,
|
|
209
|
+
type="script" if path.suffix in {".R", ".py"} else "notebook",
|
|
210
|
+
revises=revises,
|
|
211
|
+
).save()
|
|
212
|
+
logger.important(f"created Transform('{transform.uid}')")
|
|
213
|
+
if project is not None:
|
|
214
|
+
transform.projects.add(project_record)
|
|
215
|
+
logger.important(f"labeled with project: {project_record.name}")
|
|
216
|
+
# latest run of this transform by user
|
|
217
|
+
run = ln.Run.filter(transform=transform).order_by("-started_at").first()
|
|
218
|
+
if run is not None and run.created_by.id != ln_setup.settings.user.id:
|
|
219
|
+
response = input(
|
|
220
|
+
"You are trying to save a transform created by another user: Source"
|
|
221
|
+
" and report files will be tagged with *your* user id. Proceed?"
|
|
222
|
+
" (y/n)"
|
|
223
|
+
)
|
|
224
|
+
if response != "y":
|
|
225
|
+
return "aborted-save-notebook-created-by-different-user"
|
|
226
|
+
if run is None and transform.key.endswith(".ipynb"):
|
|
227
|
+
run = ln.Run(transform=transform).save()
|
|
228
|
+
logger.important(
|
|
229
|
+
f"found no run, creating Run('{run.uid}') to display the html"
|
|
230
|
+
)
|
|
231
|
+
return_code = save_context_core(
|
|
232
|
+
run=run,
|
|
233
|
+
transform=transform,
|
|
234
|
+
filepath=path,
|
|
235
|
+
from_cli=True,
|
|
236
|
+
)
|
|
237
|
+
return return_code
|
|
238
|
+
else:
|
|
239
|
+
raise SystemExit("Allowed values for '--registry' are: 'artifact', 'transform'")
|
|
@@ -7,13 +7,13 @@ from lamin_utils import logger
|
|
|
7
7
|
|
|
8
8
|
def pytest_sessionstart(session: pytest.Session):
|
|
9
9
|
ln.setup.init(
|
|
10
|
-
storage="./
|
|
11
|
-
name="
|
|
10
|
+
storage="./default_storage_cli",
|
|
11
|
+
name="lamin-cli-unit-tests",
|
|
12
12
|
)
|
|
13
13
|
ln.setup.settings.auto_connect = True
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def pytest_sessionfinish(session: pytest.Session):
|
|
17
17
|
logger.set_verbosity(1)
|
|
18
|
-
shutil.rmtree("./
|
|
19
|
-
ln.setup.delete("
|
|
18
|
+
shutil.rmtree("./default_storage_cli")
|
|
19
|
+
ln.setup.delete("lamin-cli-unit-tests", force=True)
|
|
@@ -5,9 +5,9 @@ import os
|
|
|
5
5
|
# import lamindb_setup as ln_setup
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def test_migrate_create():
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# def test_migrate_create():
|
|
9
|
+
# exit_status = os.system("lamin migrate create")
|
|
10
|
+
# assert exit_status == 0
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def test_migrate_deploy():
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
+
import lamindb as ln
|
|
4
5
|
import lamindb_setup as ln_setup
|
|
5
6
|
|
|
6
7
|
test_file = Path(__file__).parent.parent.parent.resolve() / ".gitignore"
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
def
|
|
10
|
+
def test_save_local_file():
|
|
10
11
|
filepath = test_file
|
|
11
12
|
|
|
12
13
|
# neither key nor description
|
|
@@ -23,10 +24,10 @@ def test_save_file():
|
|
|
23
24
|
)
|
|
24
25
|
assert result.returncode == 1
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
ln.Project(name="test_project").save()
|
|
27
28
|
|
|
28
29
|
result = subprocess.run(
|
|
29
|
-
f"lamin save {filepath} --key mytest",
|
|
30
|
+
f"lamin save {filepath} --key mytest --project test_project",
|
|
30
31
|
shell=True,
|
|
31
32
|
capture_output=True,
|
|
32
33
|
)
|
|
@@ -34,6 +35,7 @@ def test_save_file():
|
|
|
34
35
|
print(result.stderr.decode())
|
|
35
36
|
assert "key='mytest'" in result.stdout.decode()
|
|
36
37
|
assert "storage path:" in result.stdout.decode()
|
|
38
|
+
assert "labeled with project: test_project" in result.stdout.decode()
|
|
37
39
|
assert result.returncode == 0
|
|
38
40
|
|
|
39
41
|
# test passing the registry and saving the same file
|
|
@@ -62,3 +64,19 @@ def test_save_file():
|
|
|
62
64
|
in result.stderr.decode()
|
|
63
65
|
)
|
|
64
66
|
assert result.returncode == 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_save_cloud_file():
|
|
70
|
+
# should be no key for cloud paths
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
"lamin save s3://cellxgene-data-public/cell-census/2024-07-01/h5ads/fe1a73ab-a203-45fd-84e9-0f7fd19efcbd.h5ad --key wrongkey.h5ad",
|
|
73
|
+
shell=True,
|
|
74
|
+
check=False,
|
|
75
|
+
)
|
|
76
|
+
assert result.returncode == 1
|
|
77
|
+
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
"lamin save s3://cellxgene-data-public/cell-census/2024-07-01/h5ads/fe1a73ab-a203-45fd-84e9-0f7fd19efcbd.h5ad",
|
|
80
|
+
shell=True,
|
|
81
|
+
check=True,
|
|
82
|
+
)
|
|
@@ -54,7 +54,7 @@ def test_save_non_consecutive():
|
|
|
54
54
|
assert process.returncode == 0
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def
|
|
57
|
+
def test_save_consecutive_user_passes_uid():
|
|
58
58
|
notebook_path = Path(
|
|
59
59
|
f"{notebook_dir}with-title-and-initialized-consecutive.ipynb"
|
|
60
60
|
).resolve()
|
|
@@ -73,14 +73,47 @@ def test_save_consecutive():
|
|
|
73
73
|
capture_output=True,
|
|
74
74
|
env=env,
|
|
75
75
|
)
|
|
76
|
-
assert result.returncode ==
|
|
77
|
-
assert "
|
|
76
|
+
assert result.returncode == 0
|
|
77
|
+
assert "created Transform('hlsFXswrJjtt0000')" in result.stdout.decode()
|
|
78
|
+
assert "found no run, creating" in result.stdout.decode()
|
|
78
79
|
|
|
79
80
|
# now, let's re-run this notebook so that `ln.track()` is actually run
|
|
81
|
+
# this mimics the interactive execution in an editor
|
|
82
|
+
with pytest.raises(CellExecutionError) as error:
|
|
83
|
+
nbproject_test.execute_notebooks(notebook_path)
|
|
84
|
+
assert (
|
|
85
|
+
're-running notebook with already-saved source code, please update the `uid` argument in `track()` to "hlsFXswrJjtt0001"'
|
|
86
|
+
in error.exconly()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# because nbconvert is recognized as non-interactive execution, the execution works out
|
|
90
|
+
result = subprocess.run(
|
|
91
|
+
f"jupyter nbconvert --to notebook --inplace --execute {notebook_path}",
|
|
92
|
+
shell=True,
|
|
93
|
+
capture_output=True,
|
|
94
|
+
env=env,
|
|
95
|
+
)
|
|
96
|
+
assert result.returncode == 0
|
|
97
|
+
assert (
|
|
98
|
+
"loaded Transform('hlsFXswrJjtt0000'), started new Run"
|
|
99
|
+
in notebook_path.read_text()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# now let's simulate the interactive use case and use nbproject_test again
|
|
103
|
+
# use the stem uid instead and it will bump the version
|
|
104
|
+
notebook_path.write_text(
|
|
105
|
+
notebook_path.read_text().replace("hlsFXswrJjtt0000", "hlsFXswrJjtt")
|
|
106
|
+
)
|
|
107
|
+
assert r"ln.track(\"hlsFXswrJjtt\")" in notebook_path.read_text()
|
|
108
|
+
|
|
80
109
|
nbproject_test.execute_notebooks(notebook_path, print_outputs=True)
|
|
110
|
+
assert (
|
|
111
|
+
"created Transform('hlsFXswrJjtt0001'), started new"
|
|
112
|
+
in notebook_path.read_text()
|
|
113
|
+
)
|
|
81
114
|
|
|
82
|
-
# now, there is a transform record, but we're missing all artifacts
|
|
83
|
-
transform = ln.Transform.filter(uid="
|
|
115
|
+
# now, there is a transform record, but we're missing all artifacts because ln.finish() wasn't called
|
|
116
|
+
transform = ln.Transform.filter(uid="hlsFXswrJjtt0001").one_or_none()
|
|
84
117
|
assert transform is not None
|
|
85
118
|
assert transform.latest_run.report is None
|
|
86
119
|
assert transform.source_code is None
|
|
@@ -98,7 +131,7 @@ def test_save_consecutive():
|
|
|
98
131
|
assert result.returncode == 0
|
|
99
132
|
|
|
100
133
|
# now, we have the associated artifacts
|
|
101
|
-
transform = ln.Transform.filter(uid="
|
|
134
|
+
transform = ln.Transform.filter(uid="hlsFXswrJjtt0001").one_or_none()
|
|
102
135
|
assert transform is not None
|
|
103
136
|
assert (
|
|
104
137
|
transform.source_code
|
|
@@ -109,13 +142,14 @@ def test_save_consecutive():
|
|
|
109
142
|
import lamindb as ln
|
|
110
143
|
|
|
111
144
|
# %%
|
|
112
|
-
|
|
145
|
+
# pass full uid, will be updated to stem uid during tests
|
|
146
|
+
ln.track("hlsFXswrJjtt")
|
|
113
147
|
|
|
114
148
|
# %%
|
|
115
149
|
print("my consecutive cell")
|
|
116
150
|
"""
|
|
117
151
|
)
|
|
118
|
-
assert transform.hash == "
|
|
152
|
+
assert transform.hash == "OQ8V3hVrEJcxK0aOOwpl4g"
|
|
119
153
|
# below is the test that we can use if store the run repot as `.ipynb`
|
|
120
154
|
# and not as html as we do right now
|
|
121
155
|
assert transform.latest_run.report.suffix == ".html"
|
|
@@ -137,12 +171,6 @@ print("my consecutive cell")
|
|
|
137
171
|
nb.cells.append(new_cell) # duplicate last cell
|
|
138
172
|
write_notebook(nb, notebook_path)
|
|
139
173
|
|
|
140
|
-
# attempt re-running - it fails
|
|
141
|
-
with pytest.raises(CellExecutionError) as error:
|
|
142
|
-
nbproject_test.execute_notebooks(notebook_path, print_outputs=True)
|
|
143
|
-
# print(error.exconly())
|
|
144
|
-
assert "UpdateContext" in error.exconly()
|
|
145
|
-
|
|
146
174
|
# attempt re-saving - it works but the user needs to confirm overwriting
|
|
147
175
|
# source code and run report
|
|
148
176
|
process = subprocess.Popen(
|
|
@@ -159,16 +187,16 @@ print("my consecutive cell")
|
|
|
159
187
|
assert "You are about to overwrite an existing report" in stdout
|
|
160
188
|
assert process.returncode == 0
|
|
161
189
|
# the source code is overwritten with the edits, reflected in a new hash
|
|
162
|
-
transform = ln.Transform.get("
|
|
190
|
+
transform = ln.Transform.get("hlsFXswrJjtt0001")
|
|
163
191
|
assert transform.latest_run.report.path.exists()
|
|
164
192
|
assert transform.latest_run.report.path == transform.latest_run.report.path
|
|
165
|
-
assert transform.hash == "
|
|
193
|
+
assert transform.hash == "Ya5xuSTL7HVbE8MRogXiAQ"
|
|
166
194
|
assert transform.latest_run.environment.path.exists()
|
|
167
195
|
|
|
168
196
|
# get the the source code via command line
|
|
169
197
|
result = subprocess.run(
|
|
170
198
|
"yes | lamin load"
|
|
171
|
-
f" https://lamin.ai/{ln.setup.settings.user.handle}/
|
|
199
|
+
f" https://lamin.ai/{ln.setup.settings.user.handle}/lamin-cli-unit-tests/transform/hlsFXswrJjtt0001",
|
|
172
200
|
shell=True,
|
|
173
201
|
capture_output=True,
|
|
174
202
|
)
|
|
@@ -185,6 +213,74 @@ print("my consecutive cell")
|
|
|
185
213
|
os.system(f"cp {notebook_path} {new_path}")
|
|
186
214
|
|
|
187
215
|
# upon re-running it, the notebook name is updated
|
|
188
|
-
|
|
216
|
+
result = subprocess.run(
|
|
217
|
+
f"jupyter nbconvert --to notebook --inplace --execute {new_path}",
|
|
218
|
+
shell=True,
|
|
219
|
+
capture_output=True,
|
|
220
|
+
env=env,
|
|
221
|
+
)
|
|
222
|
+
print(result.stdout.decode())
|
|
223
|
+
assert result.returncode == 0
|
|
189
224
|
transform = ln.Transform.get("hlsFXswrJjtt0001")
|
|
190
225
|
assert "new_name.ipynb" in transform.key
|
|
226
|
+
|
|
227
|
+
# edit the notebook
|
|
228
|
+
nb = read_notebook(new_path)
|
|
229
|
+
nb.cells[-1]["source"] = ["ln.finish()"]
|
|
230
|
+
write_notebook(nb, new_path)
|
|
231
|
+
|
|
232
|
+
# run omitting the `--inplace` flag
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
f"jupyter nbconvert --to notebook --execute {new_path}",
|
|
235
|
+
shell=True,
|
|
236
|
+
capture_output=True,
|
|
237
|
+
)
|
|
238
|
+
print(result.stdout.decode())
|
|
239
|
+
assert (
|
|
240
|
+
"Please execute notebook 'nbconvert' by passing option '--inplace'."
|
|
241
|
+
in result.stderr.decode()
|
|
242
|
+
)
|
|
243
|
+
assert result.returncode == 1
|
|
244
|
+
|
|
245
|
+
result = subprocess.run(
|
|
246
|
+
f"jupyter nbconvert --to notebook --inplace --execute {new_path}",
|
|
247
|
+
shell=True,
|
|
248
|
+
capture_output=True,
|
|
249
|
+
)
|
|
250
|
+
assert result.returncode == 0
|
|
251
|
+
transform = ln.Transform.get("hlsFXswrJjtt")
|
|
252
|
+
assert "new_name.ipynb" in transform.key
|
|
253
|
+
assert (
|
|
254
|
+
transform.source_code
|
|
255
|
+
== """# %% [markdown]
|
|
256
|
+
#
|
|
257
|
+
|
|
258
|
+
# %%
|
|
259
|
+
import lamindb as ln
|
|
260
|
+
|
|
261
|
+
# %%
|
|
262
|
+
# pass full uid, will be updated to stem uid during tests
|
|
263
|
+
ln.track("hlsFXswrJjtt")
|
|
264
|
+
|
|
265
|
+
# %%
|
|
266
|
+
print("my consecutive cell")
|
|
267
|
+
|
|
268
|
+
# %%
|
|
269
|
+
ln.finish()
|
|
270
|
+
"""
|
|
271
|
+
)
|
|
272
|
+
assert transform.latest_run.report is None
|
|
273
|
+
assert transform.latest_run.environment.path.exists()
|
|
274
|
+
result = subprocess.run(
|
|
275
|
+
f"lamin save {new_path}",
|
|
276
|
+
shell=True,
|
|
277
|
+
capture_output=True,
|
|
278
|
+
)
|
|
279
|
+
assert result.returncode == 0
|
|
280
|
+
assert transform.latest_run.report.path.exists()
|
|
281
|
+
assert (
|
|
282
|
+
"to save the notebook html, run: lamin save"
|
|
283
|
+
in transform.latest_run.report.path.read_text()
|
|
284
|
+
)
|
|
285
|
+
# for manual inspection
|
|
286
|
+
# os.system(f"cp {transform.latest_run.report.path} ./my_report.html")
|
|
@@ -55,7 +55,7 @@ def test_run_save_cache():
|
|
|
55
55
|
# print(result.stdout.decode())
|
|
56
56
|
# print(result.stderr.decode())
|
|
57
57
|
assert result.returncode == 1
|
|
58
|
-
assert "Please export your" in result.
|
|
58
|
+
assert "Please export your" in result.stdout.decode()
|
|
59
59
|
|
|
60
60
|
filepath.with_suffix(".html").write_text("dummy html")
|
|
61
61
|
|
|
@@ -13,15 +13,18 @@ def test_save_without_uid():
|
|
|
13
13
|
env["LAMIN_TESTING"] = "true"
|
|
14
14
|
filepath = scripts_dir / "run-track-and-finish.py"
|
|
15
15
|
|
|
16
|
+
ln.Project(name="test_project").save()
|
|
17
|
+
|
|
16
18
|
# attempt to save the script without it yet being run
|
|
17
19
|
result = subprocess.run(
|
|
18
|
-
f"lamin save {filepath}",
|
|
20
|
+
f"lamin save {filepath} --project test_project",
|
|
19
21
|
shell=True,
|
|
20
22
|
capture_output=True,
|
|
21
23
|
)
|
|
22
24
|
# print(result.stdout.decode())
|
|
23
25
|
assert result.returncode == 0
|
|
24
26
|
assert "created Transform" in result.stdout.decode()
|
|
27
|
+
assert "labeled with project: test_project" in result.stdout.decode()
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
def test_run_save_cache_with_git_and_uid():
|
|
@@ -35,9 +38,12 @@ def test_run_save_cache_with_git_and_uid():
|
|
|
35
38
|
shell=True,
|
|
36
39
|
capture_output=True,
|
|
37
40
|
)
|
|
38
|
-
|
|
39
|
-
assert
|
|
40
|
-
|
|
41
|
+
assert result.returncode == 0
|
|
42
|
+
assert (
|
|
43
|
+
"mapped 'run-track-and-finish-sync-git.py' on uid 'm5uCHTTpJnjQ0000'"
|
|
44
|
+
in result.stdout.decode()
|
|
45
|
+
)
|
|
46
|
+
assert "created Transform('m5uCHTTpJnjQ0000')" in result.stdout.decode()
|
|
41
47
|
|
|
42
48
|
# run the script
|
|
43
49
|
result = subprocess.run(
|
|
@@ -48,7 +54,7 @@ def test_run_save_cache_with_git_and_uid():
|
|
|
48
54
|
print(result.stdout.decode())
|
|
49
55
|
print(result.stderr.decode())
|
|
50
56
|
assert result.returncode == 0
|
|
51
|
-
assert "
|
|
57
|
+
assert "loaded Transform" in result.stdout.decode()
|
|
52
58
|
assert "m5uCHTTp" in result.stdout.decode()
|
|
53
59
|
assert "started new Run" in result.stdout.decode()
|
|
54
60
|
|
|
@@ -178,10 +184,10 @@ if __name__ == "__main__":
|
|
|
178
184
|
assert result.returncode == 1
|
|
179
185
|
assert "already works on this draft" in result.stderr.decode()
|
|
180
186
|
|
|
181
|
-
# try to get the
|
|
187
|
+
# try to get the source code via command line
|
|
182
188
|
result = subprocess.run(
|
|
183
189
|
"yes | lamin load"
|
|
184
|
-
f" https://lamin.ai/{settings.user.handle}/
|
|
190
|
+
f" https://lamin.ai/{settings.user.handle}/lamin-cli-unit-tests/transform/m5uCHTTpJnjQ0000",
|
|
185
191
|
shell=True,
|
|
186
192
|
capture_output=True,
|
|
187
193
|
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "c4ea5bc1",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": [
|
|
8
|
+
"# My test notebook (consecutive)"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"cell_type": "code",
|
|
13
|
+
"execution_count": null,
|
|
14
|
+
"id": "3ac1c25a",
|
|
15
|
+
"metadata": {},
|
|
16
|
+
"outputs": [],
|
|
17
|
+
"source": [
|
|
18
|
+
"import lamindb as ln"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"cell_type": "code",
|
|
23
|
+
"execution_count": null,
|
|
24
|
+
"id": "44bfcdec",
|
|
25
|
+
"metadata": {},
|
|
26
|
+
"outputs": [],
|
|
27
|
+
"source": [
|
|
28
|
+
"# pass full uid, will be updated to stem uid during tests\n",
|
|
29
|
+
"ln.track(\"hlsFXswrJjtt0000\")"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"cell_type": "code",
|
|
34
|
+
"execution_count": null,
|
|
35
|
+
"id": "c2c32e5a",
|
|
36
|
+
"metadata": {},
|
|
37
|
+
"outputs": [],
|
|
38
|
+
"source": [
|
|
39
|
+
"print(\"my consecutive cell\")"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"metadata": {
|
|
44
|
+
"jupytext": {
|
|
45
|
+
"cell_metadata_filter": "-all",
|
|
46
|
+
"main_language": "python",
|
|
47
|
+
"notebook_metadata_filter": "-all"
|
|
48
|
+
},
|
|
49
|
+
"language_info": {
|
|
50
|
+
"name": "python"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"nbformat": 4,
|
|
54
|
+
"nbformat_minor": 5
|
|
55
|
+
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from lamin_utils import logger
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def parse_uid_from_code(content: str, suffix: str) -> str | None:
|
|
11
|
-
if suffix == ".py":
|
|
12
|
-
track_pattern = re.compile(
|
|
13
|
-
r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{16})\1'
|
|
14
|
-
)
|
|
15
|
-
uid_pattern = re.compile(r'\.context\.uid\s*=\s*["\']([^"\']+)["\']')
|
|
16
|
-
elif suffix == ".ipynb":
|
|
17
|
-
track_pattern = re.compile(
|
|
18
|
-
r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{16})(?:\\"|\')'
|
|
19
|
-
)
|
|
20
|
-
# backward compat
|
|
21
|
-
uid_pattern = re.compile(r'\.context\.uid\s*=\s*\\["\']([^"\']+)\\["\']')
|
|
22
|
-
elif suffix in {".R", ".qmd", ".Rmd"}:
|
|
23
|
-
track_pattern = re.compile(
|
|
24
|
-
r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{16})\1'
|
|
25
|
-
)
|
|
26
|
-
uid_pattern = None
|
|
27
|
-
else:
|
|
28
|
-
raise SystemExit(
|
|
29
|
-
"Only .py, .ipynb, .R, .qmd, .Rmd files are supported for saving"
|
|
30
|
-
" transforms."
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
# Search for matches in the entire file content
|
|
34
|
-
uid_match = track_pattern.search(content)
|
|
35
|
-
group_index = 1 if suffix == ".ipynb" else 2
|
|
36
|
-
uid = uid_match.group(group_index) if uid_match else None
|
|
37
|
-
|
|
38
|
-
if uid_pattern is not None and uid is None:
|
|
39
|
-
uid_match = uid_pattern.search(content)
|
|
40
|
-
uid = uid_match.group(1) if uid_match else None
|
|
41
|
-
|
|
42
|
-
return uid
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def save_from_filepath_cli(
|
|
46
|
-
filepath: str | Path,
|
|
47
|
-
key: str | None,
|
|
48
|
-
description: str | None,
|
|
49
|
-
stem_uid: str | None,
|
|
50
|
-
registry: str | None,
|
|
51
|
-
) -> str | None:
|
|
52
|
-
import lamindb_setup as ln_setup
|
|
53
|
-
|
|
54
|
-
if not isinstance(filepath, Path):
|
|
55
|
-
filepath = Path(filepath)
|
|
56
|
-
|
|
57
|
-
# this will be gone once we get rid of lamin load or enable loading multiple
|
|
58
|
-
# instances sequentially
|
|
59
|
-
auto_connect_state = ln_setup.settings.auto_connect
|
|
60
|
-
ln_setup.settings.auto_connect = True
|
|
61
|
-
|
|
62
|
-
import lamindb as ln
|
|
63
|
-
|
|
64
|
-
if not ln.setup.core.django.IS_SETUP:
|
|
65
|
-
sys.exit(-1)
|
|
66
|
-
from lamindb._finish import save_context_core
|
|
67
|
-
|
|
68
|
-
ln_setup.settings.auto_connect = auto_connect_state
|
|
69
|
-
|
|
70
|
-
suffixes_transform = {
|
|
71
|
-
"py": {".py", ".ipynb"},
|
|
72
|
-
"R": {".R", ".qmd", ".Rmd"},
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if filepath.suffix in {".qmd", ".Rmd"}:
|
|
76
|
-
if not (
|
|
77
|
-
filepath.with_suffix(".html").exists()
|
|
78
|
-
or filepath.with_suffix(".nb.html").exists()
|
|
79
|
-
):
|
|
80
|
-
raise SystemExit(
|
|
81
|
-
f"Please export your {filepath.suffix} file as an html file here"
|
|
82
|
-
f" {filepath.with_suffix('.html')}"
|
|
83
|
-
)
|
|
84
|
-
if (
|
|
85
|
-
filepath.with_suffix(".html").exists()
|
|
86
|
-
and filepath.with_suffix(".nb.html").exists()
|
|
87
|
-
):
|
|
88
|
-
raise SystemExit(
|
|
89
|
-
f"Please delete one of\n - {filepath.with_suffix('.html')}\n -"
|
|
90
|
-
f" {filepath.with_suffix('.nb.html')}"
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
if registry is None:
|
|
94
|
-
registry = (
|
|
95
|
-
"transform"
|
|
96
|
-
if filepath.suffix
|
|
97
|
-
in suffixes_transform["py"].union(suffixes_transform["R"])
|
|
98
|
-
else "artifact"
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
if registry == "artifact":
|
|
102
|
-
ln.settings.creation.artifact_silence_missing_run_warning = True
|
|
103
|
-
revises = None
|
|
104
|
-
if stem_uid is not None:
|
|
105
|
-
revises = (
|
|
106
|
-
ln.Artifact.filter(uid__startswith=stem_uid)
|
|
107
|
-
.order_by("-created_at")
|
|
108
|
-
.first()
|
|
109
|
-
)
|
|
110
|
-
if revises is None:
|
|
111
|
-
raise ln.errors.InvalidArgument("The stem uid is not found.")
|
|
112
|
-
elif key is None and description is None:
|
|
113
|
-
logger.error("Please pass a key or description via --key or --description")
|
|
114
|
-
return "missing-key-or-description"
|
|
115
|
-
artifact = ln.Artifact(
|
|
116
|
-
filepath, key=key, description=description, revises=revises
|
|
117
|
-
).save()
|
|
118
|
-
logger.important(f"saved: {artifact}")
|
|
119
|
-
logger.important(f"storage path: {artifact.path}")
|
|
120
|
-
if ln_setup.settings.storage.type == "s3":
|
|
121
|
-
logger.important(f"storage url: {artifact.path.to_url()}")
|
|
122
|
-
if ln_setup.settings.instance.is_remote:
|
|
123
|
-
slug = ln_setup.settings.instance.slug
|
|
124
|
-
logger.important(f"go to: https://lamin.ai/{slug}/artifact/{artifact.uid}")
|
|
125
|
-
return None
|
|
126
|
-
elif registry == "transform":
|
|
127
|
-
with open(filepath) as file:
|
|
128
|
-
content = file.read()
|
|
129
|
-
uid = parse_uid_from_code(content, filepath.suffix)
|
|
130
|
-
if uid is not None:
|
|
131
|
-
logger.important(f"mapped '{filepath}' on uid '{uid}'")
|
|
132
|
-
transform = ln.Transform.filter(uid=uid).one_or_none()
|
|
133
|
-
if transform is None:
|
|
134
|
-
logger.error(
|
|
135
|
-
f"Did not find uid '{uid}'"
|
|
136
|
-
" in Transform registry. Did you run `ln.track()`?"
|
|
137
|
-
)
|
|
138
|
-
return "not-tracked-in-transform-registry"
|
|
139
|
-
else:
|
|
140
|
-
revises = None
|
|
141
|
-
if stem_uid is not None:
|
|
142
|
-
revises = (
|
|
143
|
-
ln.Transform.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
|
-
# TODO: build in the logic that queries for relative file paths
|
|
150
|
-
# we have in Context; add tests for multiple versions
|
|
151
|
-
transform = ln.Transform.filter(
|
|
152
|
-
key=filepath.name, is_latest=True
|
|
153
|
-
).one_or_none()
|
|
154
|
-
if transform is None:
|
|
155
|
-
transform = ln.Transform(
|
|
156
|
-
description=filepath.name,
|
|
157
|
-
key=filepath.name,
|
|
158
|
-
type="script" if filepath.suffix in {".R", ".py"} else "notebook",
|
|
159
|
-
revises=revises,
|
|
160
|
-
).save()
|
|
161
|
-
logger.important(f"created Transform('{transform.uid}')")
|
|
162
|
-
# latest run of this transform by user
|
|
163
|
-
run = ln.Run.filter(transform=transform).order_by("-started_at").first()
|
|
164
|
-
if run is not None and run.created_by.id != ln_setup.settings.user.id:
|
|
165
|
-
response = input(
|
|
166
|
-
"You are trying to save a transform created by another user: Source"
|
|
167
|
-
" and report files will be tagged with *your* user id. Proceed?"
|
|
168
|
-
" (y/n)"
|
|
169
|
-
)
|
|
170
|
-
if response != "y":
|
|
171
|
-
return "aborted-save-notebook-created-by-different-user"
|
|
172
|
-
if run is None and transform.key.endswith(".ipynb"):
|
|
173
|
-
run = ln.Run(transform=transform).save()
|
|
174
|
-
logger.important(
|
|
175
|
-
f"found no run, creating Run('{run.uid}') to display the html"
|
|
176
|
-
)
|
|
177
|
-
return_code = save_context_core(
|
|
178
|
-
run=run,
|
|
179
|
-
transform=transform,
|
|
180
|
-
filepath=filepath,
|
|
181
|
-
from_cli=True,
|
|
182
|
-
)
|
|
183
|
-
return return_code
|
|
184
|
-
else:
|
|
185
|
-
raise SystemExit("Allowed values for '--registry' are: 'artifact', 'transform'")
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"cells": [
|
|
3
|
-
{
|
|
4
|
-
"cell_type": "markdown",
|
|
5
|
-
"metadata": {},
|
|
6
|
-
"source": [
|
|
7
|
-
"# My test notebook (consecutive)"
|
|
8
|
-
]
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"cell_type": "code",
|
|
12
|
-
"execution_count": 1,
|
|
13
|
-
"metadata": {
|
|
14
|
-
"execution": {
|
|
15
|
-
"iopub.execute_input": "2024-04-30T02:45:28.590279Z",
|
|
16
|
-
"iopub.status.busy": "2024-04-30T02:45:28.590128Z",
|
|
17
|
-
"iopub.status.idle": "2024-04-30T02:45:29.921019Z",
|
|
18
|
-
"shell.execute_reply": "2024-04-30T02:45:29.920739Z"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"outputs": [
|
|
22
|
-
{
|
|
23
|
-
"name": "stdout",
|
|
24
|
-
"output_type": "stream",
|
|
25
|
-
"text": [
|
|
26
|
-
"💡 connected lamindb: testuser1/laminci-unit-tests\n"
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"source": [
|
|
31
|
-
"import lamindb as ln"
|
|
32
|
-
]
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"cell_type": "code",
|
|
36
|
-
"execution_count": 2,
|
|
37
|
-
"metadata": {
|
|
38
|
-
"execution": {
|
|
39
|
-
"iopub.execute_input": "2024-04-30T02:45:29.923093Z",
|
|
40
|
-
"iopub.status.busy": "2024-04-30T02:45:29.922808Z",
|
|
41
|
-
"iopub.status.idle": "2024-04-30T02:45:32.189053Z",
|
|
42
|
-
"shell.execute_reply": "2024-04-30T02:45:32.188627Z"
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
"outputs": [
|
|
46
|
-
{
|
|
47
|
-
"name": "stdout",
|
|
48
|
-
"output_type": "stream",
|
|
49
|
-
"text": [
|
|
50
|
-
"💡 notebook imports: lamindb==0.71.0\n"
|
|
51
|
-
]
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"name": "stdout",
|
|
55
|
-
"output_type": "stream",
|
|
56
|
-
"text": [
|
|
57
|
-
"💡 saved: Transform(uid='hlsFXswrJjtt5zKv', name='My test notebook (consecutive)', key='with-title-and-initialized-consecutive', version='1', type='notebook', updated_at=2024-04-30 02:45:31 UTC, created_by_id=1)\n"
|
|
58
|
-
]
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"name": "stdout",
|
|
62
|
-
"output_type": "stream",
|
|
63
|
-
"text": [
|
|
64
|
-
"💡 saved: Run(uid='oX11QZyYk6dhucek9jNg', transform_id=1, created_by_id=1)\n"
|
|
65
|
-
]
|
|
66
|
-
}
|
|
67
|
-
],
|
|
68
|
-
"source": [
|
|
69
|
-
"ln.track(\"hlsFXswrJjtt0000\")"
|
|
70
|
-
]
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
"cell_type": "code",
|
|
74
|
-
"execution_count": 3,
|
|
75
|
-
"metadata": {
|
|
76
|
-
"execution": {
|
|
77
|
-
"iopub.execute_input": "2024-04-30T02:45:32.191144Z",
|
|
78
|
-
"iopub.status.busy": "2024-04-30T02:45:32.191016Z",
|
|
79
|
-
"iopub.status.idle": "2024-04-30T02:45:32.193262Z",
|
|
80
|
-
"shell.execute_reply": "2024-04-30T02:45:32.192907Z"
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
"outputs": [
|
|
84
|
-
{
|
|
85
|
-
"name": "stdout",
|
|
86
|
-
"output_type": "stream",
|
|
87
|
-
"text": [
|
|
88
|
-
"my consecutive cell\n"
|
|
89
|
-
]
|
|
90
|
-
}
|
|
91
|
-
],
|
|
92
|
-
"source": [
|
|
93
|
-
"print(\"my consecutive cell\")"
|
|
94
|
-
]
|
|
95
|
-
}
|
|
96
|
-
],
|
|
97
|
-
"metadata": {
|
|
98
|
-
"kernelspec": {
|
|
99
|
-
"display_name": "py39",
|
|
100
|
-
"language": "python",
|
|
101
|
-
"name": "python3"
|
|
102
|
-
},
|
|
103
|
-
"language_info": {
|
|
104
|
-
"codemirror_mode": {
|
|
105
|
-
"name": "ipython",
|
|
106
|
-
"version": 3
|
|
107
|
-
},
|
|
108
|
-
"file_extension": ".py",
|
|
109
|
-
"mimetype": "text/x-python",
|
|
110
|
-
"name": "python",
|
|
111
|
-
"nbconvert_exporter": "python",
|
|
112
|
-
"pygments_lexer": "ipython3",
|
|
113
|
-
"version": "3.10.13"
|
|
114
|
-
},
|
|
115
|
-
"widgets": {
|
|
116
|
-
"application/vnd.jupyter.widget-state+json": {
|
|
117
|
-
"state": {
|
|
118
|
-
"31729cb39d074e8fb483120775f53731": {
|
|
119
|
-
"model_module": "ipylab",
|
|
120
|
-
"model_module_version": "^1.0.0",
|
|
121
|
-
"model_name": "SessionManagerModel",
|
|
122
|
-
"state": {
|
|
123
|
-
"_model_module": "ipylab",
|
|
124
|
-
"_model_module_version": "^1.0.0",
|
|
125
|
-
"_model_name": "SessionManagerModel",
|
|
126
|
-
"_view_count": null,
|
|
127
|
-
"_view_module": null,
|
|
128
|
-
"_view_module_version": "",
|
|
129
|
-
"_view_name": null,
|
|
130
|
-
"current_session": {},
|
|
131
|
-
"sessions": []
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
"6939d8a17fbb46b8877f987b20b56757": {
|
|
135
|
-
"model_module": "ipylab",
|
|
136
|
-
"model_module_version": "^1.0.0",
|
|
137
|
-
"model_name": "CommandRegistryModel",
|
|
138
|
-
"state": {
|
|
139
|
-
"_command_list": [],
|
|
140
|
-
"_commands": [],
|
|
141
|
-
"_model_module": "ipylab",
|
|
142
|
-
"_model_module_version": "^1.0.0",
|
|
143
|
-
"_model_name": "CommandRegistryModel",
|
|
144
|
-
"_view_count": null,
|
|
145
|
-
"_view_module": null,
|
|
146
|
-
"_view_module_version": "",
|
|
147
|
-
"_view_name": null
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
"82c3ccb5d1114b968e80e1605d528742": {
|
|
151
|
-
"model_module": "ipylab",
|
|
152
|
-
"model_module_version": "^1.0.0",
|
|
153
|
-
"model_name": "JupyterFrontEndModel",
|
|
154
|
-
"state": {
|
|
155
|
-
"_model_module": "ipylab",
|
|
156
|
-
"_model_module_version": "^1.0.0",
|
|
157
|
-
"_model_name": "JupyterFrontEndModel",
|
|
158
|
-
"_view_count": null,
|
|
159
|
-
"_view_module": null,
|
|
160
|
-
"_view_module_version": "",
|
|
161
|
-
"_view_name": null,
|
|
162
|
-
"commands": "IPY_MODEL_6939d8a17fbb46b8877f987b20b56757",
|
|
163
|
-
"sessions": "IPY_MODEL_31729cb39d074e8fb483120775f53731",
|
|
164
|
-
"shell": "IPY_MODEL_9f239e52e3794df9b2c06052072cad24",
|
|
165
|
-
"version": ""
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
"9f239e52e3794df9b2c06052072cad24": {
|
|
169
|
-
"model_module": "ipylab",
|
|
170
|
-
"model_module_version": "^1.0.0",
|
|
171
|
-
"model_name": "ShellModel",
|
|
172
|
-
"state": {
|
|
173
|
-
"_model_module": "ipylab",
|
|
174
|
-
"_model_module_version": "^1.0.0",
|
|
175
|
-
"_model_name": "ShellModel",
|
|
176
|
-
"_view_count": null,
|
|
177
|
-
"_view_module": null,
|
|
178
|
-
"_view_module_version": "",
|
|
179
|
-
"_view_name": null,
|
|
180
|
-
"_widgets": []
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
"version_major": 2,
|
|
185
|
-
"version_minor": 0
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
"nbformat": 4,
|
|
190
|
-
"nbformat_minor": 2
|
|
191
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|