lamin_cli 1.2.0__tar.gz → 1.4.0__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.4.0/.github/workflows/build.yml +82 -0
  2. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/.gitignore +1 -0
  3. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/.pre-commit-config.yaml +1 -1
  4. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/PKG-INFO +1 -1
  5. lamin_cli-1.4.0/lamin_cli/__init__.py +3 -0
  6. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/__main__.py +78 -27
  7. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_load.py +3 -1
  8. lamin_cli-1.4.0/lamin_cli/_save.py +239 -0
  9. lamin_cli-1.4.0/lamin_cli/compute/__init__.py +0 -0
  10. lamin_cli-1.4.0/lamin_cli/compute/modal.py +175 -0
  11. lamin_cli-1.4.0/noxfile.py +13 -0
  12. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_multi_process.py +1 -1
  13. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_files.py +22 -4
  14. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_notebooks.py +116 -22
  15. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_r_code.py +2 -2
  16. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_scripts.py +13 -7
  17. lamin_cli-1.4.0/tests/modal/test_modal.py +19 -0
  18. lamin_cli-1.4.0/tests/notebooks/with-title-and-initialized-consecutive.ipynb +55 -0
  19. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track-and-finish.py +0 -4
  20. lamin_cli-1.2.0/lamin_cli/__init__.py +0 -3
  21. lamin_cli-1.2.0/lamin_cli/_save.py +0 -158
  22. lamin_cli-1.2.0/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -191
  23. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/.github/workflows/doc-changes.yml +0 -0
  24. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/LICENSE +0 -0
  25. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/README.md +0 -0
  26. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_cache.py +0 -0
  27. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_migration.py +0 -0
  28. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_settings.py +0 -0
  29. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/pyproject.toml +0 -0
  30. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/conftest.py +0 -0
  31. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_cli.py +0 -0
  32. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_load.py +0 -0
  33. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_migrate.py +0 -0
  34. {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_parse_uid_from_code.py +0 -0
  35. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/notebooks/not-initialized.ipynb +0 -0
  36. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
  37. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/merely-import-lamindb.py +0 -0
  38. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track-and-finish-sync-git.py +0 -0
  39. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track-with-params.py +0 -0
  40. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track.R +0 -0
  41. {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track.qmd +0 -0
@@ -0,0 +1,82 @@
1
+ name: build
2
+
3
+ on:
4
+ push:
5
+ branches: [release]
6
+ pull_request:
7
+
8
+ jobs:
9
+ pre-filter:
10
+ runs-on: ubuntu-latest
11
+ outputs:
12
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - uses: dorny/paths-filter@v3
19
+ id: changes
20
+ if: github.event_name != 'push'
21
+ with:
22
+ filters: |
23
+ modal:
24
+ - 'lamin_cli/compute/modal.py'
25
+ - 'tests/modal/**'
26
+
27
+ - id: set-matrix
28
+ shell: bash
29
+ run: |
30
+ BASE_GROUPS=$(jq -n -c '[]')
31
+
32
+ if [[ "${{ github.event_name }}" == "push" || "${{ steps.changes.outputs.modal }}" == "true" ]]; then
33
+ # Run everything on push or when modal paths change
34
+ MATRIX=$(jq -n -c --argjson groups "$BASE_GROUPS" '{group: ($groups + ["modal"])}')
35
+ else
36
+ # Otherwise only run base groups
37
+ MATRIX=$(jq -n -c --argjson groups "$BASE_GROUPS" '{group: $groups}')
38
+ fi
39
+
40
+ # Output as single line for GitHub Actions
41
+ echo "matrix=$(echo "$MATRIX" | jq -c .)" >> $GITHUB_OUTPUT
42
+
43
+ # Pretty print for debugging
44
+ echo "Generated matrix:"
45
+ echo "$MATRIX" | jq .
46
+
47
+ test:
48
+ needs: pre-filter
49
+ runs-on: ubuntu-latest
50
+ env:
51
+ LAMIN_API_KEY: ${{ secrets.LAMIN_API_KEY_TESTUSER1 }}
52
+ strategy:
53
+ fail-fast: false
54
+ matrix: ${{fromJson(needs.pre-filter.outputs.matrix)}}
55
+ timeout-minutes: 20
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+ with:
59
+ submodules: recursive
60
+ fetch-depth: 0
61
+
62
+ - uses: actions/setup-python@v5
63
+ with:
64
+ python-version: 3.12
65
+
66
+ - name: cache pre-commit
67
+ uses: actions/cache@v4
68
+ with:
69
+ path: ~/.cache/pre-commit
70
+ key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
71
+
72
+ - run: pip install git+https://github.com/laminlabs/laminci
73
+
74
+ - run: uv pip install --system modal pytest
75
+
76
+ - run: modal token set --token-id ${{ secrets.MODAL_DEV_TOKEN_ID }} --token-secret ${{ secrets.MODAL_DEV_TOKEN_SECRET }}
77
+
78
+ - run: nox -s setup
79
+
80
+ - run: lamin login
81
+
82
+ - run: pytest tests/modal
@@ -1,4 +1,5 @@
1
1
  # LaminDB
2
+ modal_mount_dir/
2
3
  lamindb_docs/
3
4
  _build
4
5
  mydata/
@@ -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.2.0
3
+ Version: 1.4.0
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.0"
@@ -2,13 +2,22 @@ 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
 
14
+ from lamindb_setup._init_instance import (
15
+ DOC_DB,
16
+ DOC_INSTANCE_NAME,
17
+ DOC_MODULES,
18
+ DOC_STORAGE_ARG,
19
+ )
20
+
12
21
  if TYPE_CHECKING:
13
22
  from collections.abc import Mapping
14
23
 
@@ -41,12 +50,7 @@ else:
41
50
  "lamin": [
42
51
  {
43
52
  "name": "Connect to an instance",
44
- "commands": [
45
- "connect",
46
- "disconnect",
47
- "info",
48
- "init",
49
- ],
53
+ "commands": ["connect", "disconnect", "info", "init", "run"],
50
54
  },
51
55
  {
52
56
  "name": "Read & write data",
@@ -155,24 +159,20 @@ def schema_to_modules_callback(ctx, param, value):
155
159
 
156
160
  # fmt: off
157
161
  @main.command()
158
- @click.option("--storage", type=str, help="Local directory, s3://bucket_name, gs://bucket_name.")
159
- @click.option("--db", type=str, default=None, help="Postgres database connection URL, do not pass for SQLite.")
160
- @click.option("--modules", type=str, default=None, help="Comma-separated string of schema modules.")
161
- @click.option("--name", type=str, default=None, help="The instance name.")
162
- @click.option("--schema", type=str, default=None, help="[DEPRECATED] Use --modules instead.", callback=schema_to_modules_callback)
162
+ @click.option("--storage", type=str, default = ".", help=DOC_STORAGE_ARG)
163
+ @click.option("--name", type=str, default=None, help=DOC_INSTANCE_NAME)
164
+ @click.option("--db", type=str, default=None, help=DOC_DB)
165
+ @click.option("--modules", type=str, default=None, help=DOC_MODULES)
163
166
  # fmt: on
164
167
  def init(
165
168
  storage: str,
169
+ name: str | None,
166
170
  db: str | None,
167
171
  modules: str | None,
168
- name: str | None,
169
- schema: str | None,
170
172
  ):
171
173
  """Init an instance."""
172
174
  from lamindb_setup._init_instance import init as init_
173
175
 
174
- modules = modules if modules is not None else schema
175
-
176
176
  return init_(storage=storage, db=db, modules=modules, name=name)
177
177
 
178
178
 
@@ -188,6 +188,8 @@ def connect(instance: str):
188
188
  `lamin connect` switches
189
189
  {attr}`~lamindb.setup.core.SetupSettings.auto_connect` to `True` so that you
190
190
  auto-connect in a Python session upon importing `lamindb`.
191
+
192
+ For manually connecting in a Python session, use {func}`~lamindb.connect`.
191
193
  """
192
194
  from lamindb_setup import connect as connect_
193
195
  from lamindb_setup import settings as settings_
@@ -307,30 +309,79 @@ def get(entity: str, uid: str | None = None, key: str | None = None):
307
309
 
308
310
 
309
311
  @main.command()
310
- @click.argument("filepath", 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("--registry", type=str, default=None)
314
- def save(filepath: str, key: str, description: 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):
315
319
  """Save a file or folder.
316
320
 
317
- Defaults to saving `.py` and `.ipynb` as {class}`~lamindb.Transform` and
318
- other file types and folders as {class}`~lamindb.Artifact`.
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
+ ```
319
326
 
320
- You can save a `.py` or `.ipynb` file as an {class}`~lamindb.Artifact` by
321
- passing `--registry artifact`.
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
329
+ an {class}`~lamindb.Artifact` by passing `--registry artifact`.
322
330
  """
323
- from lamin_cli._save import save_from_filepath_cli
331
+ from lamin_cli._save import save_from_path_cli
324
332
 
325
- if save_from_filepath_cli(filepath, key, description, registry) is not None:
333
+ if save_from_path_cli(path, key, description, stem_uid, project, registry) is not None:
326
334
  sys.exit(1)
327
335
 
328
336
 
337
+ @main.command()
338
+ @click.argument("filepath", type=str)
339
+ @click.option("--project", type=str, default=None, help="A valid project name or uid. When running on Modal, creates an app with the same name.", required=True)
340
+ @click.option("--image-url", type=str, default=None, help="A URL to the base docker image to use.")
341
+ @click.option("--packages", type=str, default="lamindb", help="A comma-separated list of additional packages to install.")
342
+ @click.option("--cpu", type=float, default=None, help="Configuration for the CPU.")
343
+ @click.option("--gpu", type=str, default=None, help="The type of GPU to use (only compatible with cuda images).")
344
+ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gpu: str | None):
345
+ """Run a compute job in the cloud.
346
+
347
+ This is an EXPERIMENTAL feature that enables to run a script on Modal.
348
+
349
+ Example: Given a valid project name "my_project".
350
+
351
+ ```
352
+ lamin run my_script.py --project my_project
353
+ ```
354
+ """
355
+ from lamin_cli.compute.modal import Runner
356
+
357
+ default_mount_dir = Path('./modal_mount_dir')
358
+ if not default_mount_dir.is_dir():
359
+ default_mount_dir.mkdir(parents=True, exist_ok=True)
360
+
361
+ shutil.copy(filepath, default_mount_dir)
362
+
363
+ filepath_in_mount_dir = default_mount_dir / Path(filepath).name
364
+
365
+ package_list = []
366
+ if packages:
367
+ package_list = [package.strip() for package in packages.split(',')]
368
+
369
+ runner = Runner(
370
+ local_mount_dir=default_mount_dir,
371
+ app_name=project,
372
+ packages=package_list,
373
+ image_url=image_url,
374
+ cpu=cpu,
375
+ gpu=gpu
376
+ )
377
+
378
+ runner.run(filepath_in_mount_dir)
379
+
380
+
329
381
  main.add_command(settings)
330
382
  main.add_command(cache)
331
383
  main.add_command(migrate)
332
384
 
333
-
334
385
  # https://stackoverflow.com/questions/57810659/automatically-generate-all-help-documentation-for-click-commands
335
386
  # https://claude.ai/chat/73c28487-bec3-4073-8110-50d1a2dd6b84
336
387
  def _generate_help():
@@ -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'")
File without changes