lamin_cli 1.10.0__py2.py3-none-any.whl → 1.11.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lamin_cli/__init__.py CHANGED
@@ -6,7 +6,7 @@ The interface is defined in `__main__.py`.
6
6
  The root API here is used by LaminR to replicate the CLI functionality.
7
7
  """
8
8
 
9
- __version__ = "1.10.0"
9
+ __version__ = "1.11.0"
10
10
 
11
11
  from lamindb_setup import disconnect, logout
12
12
  from lamindb_setup._connect_instance import _connect_cli as connect
lamin_cli/__main__.py CHANGED
@@ -42,6 +42,10 @@ COMMAND_GROUPS = {
42
42
  "name": "Load, save, create & delete data",
43
43
  "commands": ["load", "save", "create", "delete"],
44
44
  },
45
+ {
46
+ "name": "Tracking within shell scripts",
47
+ "commands": ["track", "finish"],
48
+ },
45
49
  {
46
50
  "name": "Describe, annotate & list data",
47
51
  "commands": ["describe", "annotate", "list"],
@@ -436,6 +440,41 @@ def save(path: str, key: str, description: str, stem_uid: str, project: str, spa
436
440
  if save_(path=path, key=key, description=description, stem_uid=stem_uid, project=project, space=space, branch=branch, registry=registry) is not None:
437
441
  sys.exit(1)
438
442
 
443
+ @main.command()
444
+ def track():
445
+ """Start tracking a run of a shell script.
446
+
447
+ This command works like {func}`~lamindb.track()` in a Python session. Here is an example script:
448
+
449
+ ```
450
+ # my_script.sh
451
+ set -e # exit on error
452
+ lamin track # initiate a tracked shell script run
453
+ lamin load --key raw/file1.txt
454
+ # do something
455
+ lamin save processed_file1.txt --key processed/file1.txt
456
+ lamin finish # mark the shell script run as finished
457
+ ```
458
+
459
+ If you run that script, it will track the run of the script, and save the input and output artifacts:
460
+
461
+ ```
462
+ sh my_script.sh
463
+ ```
464
+ """
465
+ from lamin_cli._context import track as track_
466
+ return track_()
467
+
468
+
469
+ @main.command()
470
+ def finish():
471
+ """Finish the current tracked run of a shell script.
472
+
473
+ This command works like {func}`~lamindb.finish()` in a Python session.
474
+ """
475
+ from lamin_cli._context import finish as finish_
476
+ return finish_()
477
+
439
478
 
440
479
  @main.command()
441
480
  @click.argument("entity", type=str, default=None, required=False)
lamin_cli/_context.py ADDED
@@ -0,0 +1,76 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from lamin_utils import logger
7
+ from lamindb_setup.core._settings_store import settings_dir
8
+
9
+
10
+ def get_current_run_file() -> Path:
11
+ """Get the path to the file storing the current run UID."""
12
+ return settings_dir / "current_shell_run.txt"
13
+
14
+
15
+ def is_interactive_shell() -> bool:
16
+ """Check if running in an interactive terminal."""
17
+ return sys.stdin.isatty() and sys.stdout.isatty() and os.isatty(0)
18
+
19
+
20
+ def get_script_filename() -> Path:
21
+ """Try to get the filename of the calling shell script."""
22
+ import psutil
23
+
24
+ parent = psutil.Process(os.getppid())
25
+ cmdline = parent.cmdline()
26
+
27
+ # For shells like bash, sh, zsh
28
+ if parent.name() in ["bash", "sh", "zsh", "dash"]:
29
+ # cmdline is typically: ['/bin/bash', 'script.sh', ...]
30
+ if len(cmdline) > 1 and not cmdline[1].startswith("-"):
31
+ return Path(cmdline[1])
32
+ raise click.ClickException(
33
+ "Cannot determine script filename. Please run in an interactive shell."
34
+ )
35
+
36
+
37
+ def track():
38
+ import lamindb as ln
39
+
40
+ if not ln.setup.settings._instance_exists:
41
+ raise click.ClickException(
42
+ "Not connected to an instance. Please run: lamin connect account/name"
43
+ )
44
+ path = get_script_filename()
45
+ source_code = path.read_text()
46
+ transform = ln.Transform(
47
+ key=path.name, source_code=source_code, type="script"
48
+ ).save()
49
+ run = ln.Run(transform=transform).save()
50
+ current_run_file = get_current_run_file()
51
+ current_run_file.parent.mkdir(parents=True, exist_ok=True)
52
+ current_run_file.write_text(run.uid)
53
+ logger.important(f"started tracking shell run: {run.uid}")
54
+
55
+
56
+ def finish():
57
+ from datetime import datetime, timezone
58
+
59
+ import lamindb as ln
60
+
61
+ if not ln.setup.settings._instance_exists:
62
+ raise click.ClickException(
63
+ "Not connected to an instance. Please run: lamin connect account/name"
64
+ )
65
+
66
+ current_run_file = get_current_run_file()
67
+ if not current_run_file.exists():
68
+ raise click.ClickException(
69
+ "No active run to finish. Please run `lamin track` first."
70
+ )
71
+ run = ln.Run.get(uid=current_run_file.read_text().strip())
72
+ run._status_code = 0
73
+ run.finished_at = datetime.now(timezone.utc)
74
+ run.save()
75
+ current_run_file.unlink()
76
+ logger.important(f"finished tracking shell run: {run.uid}")
lamin_cli/_io.py CHANGED
@@ -30,7 +30,7 @@ def io():
30
30
  @click.option("--track/--no-track", is_flag=True, help="Whether to track snapshot generation.", default=True)
31
31
  # fmt: on
32
32
  def snapshot(upload: bool, track: bool) -> None:
33
- """Create and optionally upload a SQLite snapshot of the current instance."""
33
+ """Create a SQLite snapshot of the connected instance."""
34
34
  if not ln_setup.settings._instance_exists:
35
35
  raise click.ClickException(
36
36
  "Not connected to an instance. Please run: lamin connect account/name"
@@ -86,8 +86,8 @@ def snapshot(upload: bool, track: bool) -> None:
86
86
  for table, (orig, clone) in mismatches.items()]
87
87
  )
88
88
  raise click.ClickException(error_msg)
89
- except json.JSONDecodeError:
90
- error_msg = f"Clone verification failed:\n{result.stderr}"
89
+ except (json.JSONDecodeError, AttributeError, ValueError, TypeError):
90
+ raise click.ClickException(f"Clone verification failed:\n{result.stderr}") from None
91
91
 
92
92
 
93
93
  ln_setup.connect(f"{instance_owner}/{instance_name}", use_root_db_user=True)
lamin_cli/_load.py CHANGED
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
 
7
7
  from lamin_utils import logger
8
8
 
9
+ from ._context import get_current_run_file
9
10
  from ._save import infer_registry_from_path, parse_title_r_notebook
10
11
  from .urls import decompose_url
11
12
 
@@ -48,6 +49,10 @@ def load(
48
49
  ln_setup.connect(instance)
49
50
  import lamindb as ln
50
51
 
52
+ current_run = None
53
+ if get_current_run_file().exists():
54
+ current_run = ln.Run.get(uid=get_current_run_file().read_text().strip())
55
+
51
56
  def script_to_notebook(
52
57
  transform: ln.Transform, notebook_path: Path, bump_revision: bool = False
53
58
  ) -> None:
@@ -182,7 +187,7 @@ def load(
182
187
  entities = entities.order_by("-created_at")
183
188
 
184
189
  entity_obj = entities.first()
185
- cache_path = entity_obj.cache()
190
+ cache_path = entity_obj.cache(is_run_input=current_run)
186
191
 
187
192
  # collection gives us a list of paths
188
193
  if isinstance(cache_path, list):
lamin_cli/_save.py CHANGED
@@ -9,17 +9,23 @@ import lamindb_setup as ln_setup
9
9
  from lamin_utils import logger
10
10
  from lamindb_setup.core.hashing import hash_file
11
11
 
12
+ from lamin_cli._context import get_current_run_file
13
+
12
14
 
13
15
  def infer_registry_from_path(path: Path | str) -> str:
14
16
  suffixes_transform = {
15
17
  "py": {".py", ".ipynb"},
16
18
  "R": {".R", ".qmd", ".Rmd"},
19
+ "sh": {".sh"},
17
20
  }
18
21
  if isinstance(path, str):
19
22
  path = Path(path)
20
23
  registry = (
21
24
  "transform"
22
- if path.suffix in suffixes_transform["py"].union(suffixes_transform["R"])
25
+ if path.suffix
26
+ in suffixes_transform["py"]
27
+ .union(suffixes_transform["R"])
28
+ .union(suffixes_transform["sh"])
23
29
  else "artifact"
24
30
  )
25
31
  return registry
@@ -42,9 +48,11 @@ def parse_uid_from_code(content: str, suffix: str) -> str | None:
42
48
  r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{12,16})\1'
43
49
  )
44
50
  uid_pattern = None
51
+ elif suffix == ".sh":
52
+ return None
45
53
  else:
46
54
  raise SystemExit(
47
- "Only .py, .ipynb, .R, .qmd, .Rmd files are supported for saving"
55
+ "Only .py, .ipynb, .R, .qmd, .Rmd, .sh files are supported for saving"
48
56
  " transforms."
49
57
  )
50
58
 
@@ -82,8 +90,13 @@ def save(
82
90
  ) -> str | None:
83
91
  import lamindb as ln
84
92
  from lamindb._finish import save_context_core
93
+ from lamindb_setup.core._settings_store import settings_dir
85
94
  from lamindb_setup.core.upath import LocalPathClasses, UPath, create_path
86
95
 
96
+ current_run = None
97
+ if get_current_run_file().exists():
98
+ current_run = ln.Run.get(uid=get_current_run_file().read_text().strip())
99
+
87
100
  # this allows to have the correct treatment of credentials in case of cloud paths
88
101
  ppath = create_path(path)
89
102
  # isinstance is needed to cast the type of path to UPath
@@ -149,6 +162,7 @@ def save(
149
162
  revises=revises,
150
163
  branch=branch_record,
151
164
  space=space_record,
165
+ run=current_run,
152
166
  ).save()
153
167
  logger.important(f"saved: {artifact}")
154
168
  logger.important(f"storage path: {artifact.path}")
@@ -219,7 +233,7 @@ def save(
219
233
  if transform is None:
220
234
  uid = f"{stem_uid}0000"
221
235
  else:
222
- transform_hash, _ = hash_file(ppath)
236
+ _, transform_hash, _ = hash_file(ppath)
223
237
  transform = ln.Transform.filter(hash=transform_hash).first()
224
238
  if transform is not None and transform.hash is not None:
225
239
  if transform.hash == transform_hash:
@@ -268,7 +282,7 @@ def save(
268
282
  uid=uid,
269
283
  description=description,
270
284
  key=key,
271
- type="script" if ppath.suffix in {".R", ".py"} else "notebook",
285
+ type="script" if ppath.suffix in {".R", ".py", ".sh"} else "notebook",
272
286
  revises=revises,
273
287
  )
274
288
  if space is not None:
@@ -1,10 +1,9 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: lamin_cli
3
- Version: 1.10.0
3
+ Version: 1.11.0
4
4
  Summary: Lamin CLI.
5
5
  Author-email: Lamin Labs <open-source@lamin.ai>
6
6
  Description-Content-Type: text/markdown
7
- License-File: LICENSE
8
7
  Requires-Dist: rich-click>=1.7
9
8
  Project-URL: Home, https://github.com/laminlabs/lamin-cli
10
9
 
@@ -1,12 +1,13 @@
1
- lamin_cli/__init__.py,sha256=EEqwDNhBI_f1OthJACbb7XrDCxzXxoPuIkCP0wbdWdk,605
2
- lamin_cli/__main__.py,sha256=zGMlXpDgKZpfQqIsedPnk0S8-jy0NEFcF4amtyIOHE0,19721
1
+ lamin_cli/__init__.py,sha256=bd6la8_wxlClB7o8h3FNAjQ6z8HPcZ4QqxvVp4Cvycg,605
2
+ lamin_cli/__main__.py,sha256=DEifHuxyyffXHVpBjn9-LVpVUsuSnRhicxTfsuZ0YUw,20791
3
3
  lamin_cli/_annotate.py,sha256=ZD76__K-mQt7UpYqyM1I2lKCs-DraTmnkjsByIHmD-g,1839
4
4
  lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
5
+ lamin_cli/_context.py,sha256=JLhv98isasX_M_o6tIDkeBWcNH8ryRrl3nk2rmwsuD4,2392
5
6
  lamin_cli/_delete.py,sha256=1wp8LQdiWHJznRrm4abxEhApB9derBWEm25ufrzjvIg,1531
6
- lamin_cli/_io.py,sha256=v103aEmMPegQQJGUfsrAB5rzdLN4NqkN07ZxhglTv7o,4989
7
- lamin_cli/_load.py,sha256=OqkOJe2Afq4ZrN5abM1ueqaDDvm1psdnQmzhXTTWe1I,8233
7
+ lamin_cli/_io.py,sha256=DboDoecZN5OLCHe5vJzOLuwztW8hxbdRffWj1kmJ9WI,5036
8
+ lamin_cli/_load.py,sha256=v7PCB7sYqGs6jF186aN191lTV2Mr5jdayHKSmdgsw_k,8445
8
9
  lamin_cli/_migration.py,sha256=w-TLYC2q6sqcfNtIUewqRW_69_xy_71X3eSV7Ud58jk,1239
9
- lamin_cli/_save.py,sha256=bysKSkivcvELW1srOoc772udOzREEnFRVPUClBJ9Qac,12084
10
+ lamin_cli/_save.py,sha256=8dM4blz6caKJtbqND-6xJXUMlGIvo2FwvSu1CDcBPig,12517
10
11
  lamin_cli/_settings.py,sha256=nqVM8FO9kjL0SXYqP8x-Xfww6Qth4I86HKKeTirsgc4,2657
11
12
  lamin_cli/urls.py,sha256=gc72s4SpaAQA8J50CtCIWlr49DWOSL_a6OM9lXfPouM,367
12
13
  lamin_cli/clone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -14,8 +15,8 @@ lamin_cli/clone/_clone_verification.py,sha256=8hRFZt_1Z0i2wMqKBkrioq01RJEjwHy9cY
14
15
  lamin_cli/clone/create_sqlite_clone_and_import_db.py,sha256=yv2mLkHdRUao_IkZO_4Swu5w4Nieh1gMrBwPbyn4c1o,1544
15
16
  lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
17
- lamin_cli-1.10.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
18
- lamin_cli-1.10.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
19
- lamin_cli-1.10.0.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
20
- lamin_cli-1.10.0.dist-info/METADATA,sha256=QIppUnCd0P8oMGAcvCSREQo5De9ifM_uomSTgfWdH6k,360
21
- lamin_cli-1.10.0.dist-info/RECORD,,
18
+ lamin_cli-1.11.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
19
+ lamin_cli-1.11.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
20
+ lamin_cli-1.11.0.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
21
+ lamin_cli-1.11.0.dist-info/METADATA,sha256=4TK6hWuABuBxr1PYF_iBpm7KP8xNH7g4LjMWUkHgDKE,338
22
+ lamin_cli-1.11.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 3.12.0
2
+ Generator: flit 3.10.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any