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.
- lamin_cli-1.4.0/.github/workflows/build.yml +82 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/.gitignore +1 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/.pre-commit-config.yaml +1 -1
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/PKG-INFO +1 -1
- lamin_cli-1.4.0/lamin_cli/__init__.py +3 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/__main__.py +78 -27
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_load.py +3 -1
- lamin_cli-1.4.0/lamin_cli/_save.py +239 -0
- lamin_cli-1.4.0/lamin_cli/compute/__init__.py +0 -0
- lamin_cli-1.4.0/lamin_cli/compute/modal.py +175 -0
- lamin_cli-1.4.0/noxfile.py +13 -0
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_multi_process.py +1 -1
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_files.py +22 -4
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_notebooks.py +116 -22
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_r_code.py +2 -2
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_save_scripts.py +13 -7
- lamin_cli-1.4.0/tests/modal/test_modal.py +19 -0
- lamin_cli-1.4.0/tests/notebooks/with-title-and-initialized-consecutive.ipynb +55 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track-and-finish.py +0 -4
- lamin_cli-1.2.0/lamin_cli/__init__.py +0 -3
- lamin_cli-1.2.0/lamin_cli/_save.py +0 -158
- lamin_cli-1.2.0/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -191
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/.github/workflows/doc-changes.yml +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/LICENSE +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/README.md +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_cache.py +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_migration.py +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/lamin_cli/_settings.py +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/pyproject.toml +0 -0
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/conftest.py +0 -0
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_cli.py +0 -0
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_load.py +0 -0
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_migrate.py +0 -0
- {lamin_cli-1.2.0/tests → lamin_cli-1.4.0/tests/core}/test_parse_uid_from_code.py +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/notebooks/not-initialized.ipynb +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/merely-import-lamindb.py +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track-and-finish-sync-git.py +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track-with-params.py +0 -0
- {lamin_cli-1.2.0 → lamin_cli-1.4.0}/tests/scripts/run-track.R +0 -0
- {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
|
|
@@ -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,
|
|
159
|
-
@click.option("--
|
|
160
|
-
@click.option("--
|
|
161
|
-
@click.option("--
|
|
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("
|
|
311
|
-
@click.option("--key", type=str, default=None)
|
|
312
|
-
@click.option("--description", type=str, default=None)
|
|
313
|
-
@click.option("--
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
|
331
|
+
from lamin_cli._save import save_from_path_cli
|
|
324
332
|
|
|
325
|
-
if
|
|
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
|
|
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
|