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.
Files changed (41) hide show
  1. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/.pre-commit-config.yaml +1 -1
  2. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/PKG-INFO +1 -1
  3. lamin_cli-1.4.1/lamin_cli/__init__.py +3 -0
  4. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/__main__.py +20 -14
  5. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_load.py +3 -1
  6. lamin_cli-1.4.1/lamin_cli/_save.py +239 -0
  7. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/conftest.py +4 -4
  8. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_migrate.py +3 -3
  9. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_files.py +21 -3
  10. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_notebooks.py +114 -18
  11. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_r_code.py +1 -1
  12. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_save_scripts.py +13 -7
  13. lamin_cli-1.4.1/tests/notebooks/with-title-and-initialized-consecutive.ipynb +55 -0
  14. lamin_cli-1.3.0/lamin_cli/__init__.py +0 -3
  15. lamin_cli-1.3.0/lamin_cli/_save.py +0 -185
  16. lamin_cli-1.3.0/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -191
  17. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/.github/workflows/build.yml +0 -0
  18. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/.github/workflows/doc-changes.yml +0 -0
  19. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/.gitignore +0 -0
  20. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/LICENSE +0 -0
  21. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/README.md +0 -0
  22. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_cache.py +0 -0
  23. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_migration.py +0 -0
  24. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/_settings.py +0 -0
  25. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/compute/__init__.py +0 -0
  26. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/lamin_cli/compute/modal.py +0 -0
  27. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/noxfile.py +0 -0
  28. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/pyproject.toml +0 -0
  29. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_cli.py +0 -0
  30. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_load.py +0 -0
  31. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_multi_process.py +0 -0
  32. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/core/test_parse_uid_from_code.py +0 -0
  33. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/modal/test_modal.py +0 -0
  34. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/notebooks/not-initialized.ipynb +0 -0
  35. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
  36. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/merely-import-lamindb.py +0 -0
  37. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track-and-finish-sync-git.py +0 -0
  38. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track-and-finish.py +0 -0
  39. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track-with-params.py +0 -0
  40. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track.R +0 -0
  41. {lamin_cli-1.3.0 → lamin_cli-1.4.1}/tests/scripts/run-track.qmd +0 -0
@@ -31,7 +31,7 @@ repos:
31
31
  tests
32
32
  )
33
33
  - repo: https://github.com/pre-commit/mirrors-mypy
34
- rev: v1.14.1
34
+ rev: v1.15.0
35
35
  hooks:
36
36
  - id: mypy
37
37
  exclude: |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lamin_cli
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: Lamin CLI.
5
5
  Author-email: Lamin Labs <open-source@lamin.ai>
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,3 @@
1
+ """Lamin CLI."""
2
+
3
+ __version__ = "1.4.1"
@@ -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=click.Path(exists=True, dir_okay=True, file_okay=True))
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("--registry", type=str, default=None)
315
- def save(path: str, key: str, description: str, stem_uid: str, registry: str):
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
- Defaults to saving `.py`, `.ipynb`, `.R`, `.Rmd`, and `.qmd` as {class}`~lamindb.Transform` and
319
- other file types and folders as {class}`~lamindb.Artifact`. You can save a `.py` or `.ipynb` file as
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 save_from_filepath_cli
331
+ from lamin_cli._save import save_from_path_cli
323
332
 
324
- if save_from_filepath_cli(path, key, description, stem_uid, registry) is not None:
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 = Path(default_mount_dir) / Path(filepath).name
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 uid in new_content:
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="./default_storage_ci",
11
- name="laminci-unit-tests",
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("./default_storage_ci")
19
- ln.setup.delete("laminci-unit-tests", force=True)
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
- exit_status = os.system("lamin migrate create")
10
- assert exit_status == 0
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 test_save_file():
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
- print(ln_setup.settings.instance.slug)
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 test_save_consecutive():
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 == 1
77
- assert "Did not find uid 'hlsFXswrJjtt0000'" in result.stdout.decode()
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="hlsFXswrJjtt0000").one_or_none()
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="hlsFXswrJjtt0000").one_or_none()
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
- ln.track("hlsFXswrJjtt0000")
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 == "ik5Dilxs2RmwOGydohFolQ"
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("hlsFXswrJjtt0000")
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 == "Jv0_TrZfzM-0erbp1FGdrQ"
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}/laminci-unit-tests/transform/hlsFXswrJjtt0000",
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
- nbproject_test.execute_notebooks(new_path, print_outputs=True)
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.stderr.decode()
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
- # print(result.stdout.decode())
39
- assert result.returncode == 1
40
- assert "Did you run `ln.track()`?" in result.stdout.decode()
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 "created Transform" in result.stdout.decode()
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 the source code via command line
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}/laminci-unit-tests/transform/m5uCHTTpJnjQ0000",
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,3 +0,0 @@
1
- """Lamin CLI."""
2
-
3
- __version__ = "1.3.0"
@@ -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