lamin_cli 1.3.0__py2.py3-none-any.whl → 1.4.1__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
@@ -1,3 +1,3 @@
1
1
  """Lamin CLI."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.4.1"
lamin_cli/__main__.py CHANGED
@@ -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:
lamin_cli/_load.py CHANGED
@@ -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}")
lamin_cli/_save.py CHANGED
@@ -2,26 +2,30 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  import sys
5
- from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
6
 
7
+ import click
7
8
  from lamin_utils import logger
8
9
 
10
+ if TYPE_CHECKING:
11
+ from pathlib import Path
12
+
9
13
 
10
14
  def parse_uid_from_code(content: str, suffix: str) -> str | None:
11
15
  if suffix == ".py":
12
16
  track_pattern = re.compile(
13
- r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{16})\1'
17
+ r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{12,16})\1'
14
18
  )
15
19
  uid_pattern = re.compile(r'\.context\.uid\s*=\s*["\']([^"\']+)["\']')
16
20
  elif suffix == ".ipynb":
17
21
  track_pattern = re.compile(
18
- r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{16})(?:\\"|\')'
22
+ r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{12,16})(?:\\"|\')'
19
23
  )
20
24
  # backward compat
21
25
  uid_pattern = re.compile(r'\.context\.uid\s*=\s*\\["\']([^"\']+)\\["\']')
22
26
  elif suffix in {".R", ".qmd", ".Rmd"}:
23
27
  track_pattern = re.compile(
24
- r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{16})\1'
28
+ r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{12,16})\1'
25
29
  )
26
30
  uid_pattern = None
27
31
  else:
@@ -42,17 +46,16 @@ def parse_uid_from_code(content: str, suffix: str) -> str | None:
42
46
  return uid
43
47
 
44
48
 
45
- def save_from_filepath_cli(
46
- filepath: str | Path,
49
+ def save_from_path_cli(
50
+ path: Path | str,
47
51
  key: str | None,
48
52
  description: str | None,
49
53
  stem_uid: str | None,
54
+ project: str | None,
50
55
  registry: str | None,
51
56
  ) -> str | None:
52
57
  import lamindb_setup as ln_setup
53
-
54
- if not isinstance(filepath, Path):
55
- filepath = Path(filepath)
58
+ from lamindb_setup.core.upath import LocalPathClasses, UPath, create_path
56
59
 
57
60
  # this will be gone once we get rid of lamin load or enable loading multiple
58
61
  # instances sequentially
@@ -67,37 +70,36 @@ def save_from_filepath_cli(
67
70
 
68
71
  ln_setup.settings.auto_connect = auto_connect_state
69
72
 
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
- )
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")
92
80
 
93
81
  if registry is None:
82
+ suffixes_transform = {
83
+ "py": {".py", ".ipynb"},
84
+ "R": {".R", ".qmd", ".Rmd"},
85
+ }
94
86
  registry = (
95
87
  "transform"
96
- if filepath.suffix
97
- in suffixes_transform["py"].union(suffixes_transform["R"])
88
+ if path.suffix in suffixes_transform["py"].union(suffixes_transform["R"])
98
89
  else "artifact"
99
90
  )
100
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
+
101
103
  if registry == "artifact":
102
104
  ln.settings.creation.artifact_silence_missing_run_warning = True
103
105
  revises = None
@@ -109,56 +111,108 @@ def save_from_filepath_cli(
109
111
  )
110
112
  if revises is None:
111
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"
112
119
  elif key is None and description is None:
113
120
  logger.error("Please pass a key or description via --key or --description")
114
121
  return "missing-key-or-description"
122
+
115
123
  artifact = ln.Artifact(
116
- filepath, key=key, description=description, revises=revises
124
+ path, key=key, description=description, revises=revises
117
125
  ).save()
118
126
  logger.important(f"saved: {artifact}")
119
127
  logger.important(f"storage path: {artifact.path}")
120
128
  if ln_setup.settings.storage.type == "s3":
121
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}")
122
133
  if ln_setup.settings.instance.is_remote:
123
134
  slug = ln_setup.settings.instance.slug
124
135
  logger.important(f"go to: https://lamin.ai/{slug}/artifact/{artifact.uid}")
125
136
  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:
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:
134
148
  logger.error(
135
- f"Did not find uid '{uid}'"
136
- " in Transform registry. Did you run `ln.track()`?"
149
+ f"Please export your {path.suffix} file as an html file here"
150
+ f" {path.with_suffix('.html')}"
137
151
  )
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)
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)
144
179
  .order_by("-created_at")
145
180
  .first()
146
181
  )
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}')")
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}")
162
216
  # latest run of this transform by user
163
217
  run = ln.Run.filter(transform=transform).order_by("-started_at").first()
164
218
  if run is not None and run.created_by.id != ln_setup.settings.user.id:
@@ -177,7 +231,7 @@ def save_from_filepath_cli(
177
231
  return_code = save_context_core(
178
232
  run=run,
179
233
  transform=transform,
180
- filepath=filepath,
234
+ filepath=path,
181
235
  from_cli=True,
182
236
  )
183
237
  return return_code
@@ -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,14 @@
1
+ lamin_cli/__init__.py,sha256=4_vIM7YadBIkILkcPZp7oQ7MWMPbKniL-MvG9Pp3UYE,40
2
+ lamin_cli/__main__.py,sha256=1GdJbaTQQmy_xPu-6QWglewBVkXdlGe37PbzSWPKCBM,12813
3
+ lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
4
+ lamin_cli/_load.py,sha256=ow9IPQog_mPvjoGj1Bn08l4AG1YyRNnEyBpQvQkfLZs,8182
5
+ lamin_cli/_migration.py,sha256=xQi6mwnpBzY5wcv1-TJhveD7a3XJIlpiYx6Z3AJ1NF0,1063
6
+ lamin_cli/_save.py,sha256=LvxEIXKgUPvpwhjgRyxi1MTDRcL6rlAMbMQLfPtfDhs,9047
7
+ lamin_cli/_settings.py,sha256=O2tecCf5EIZu98ima4DTJujo4KuywckOLgw8c-Ke3dY,1142
8
+ lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
10
+ lamin_cli-1.4.1.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
11
+ lamin_cli-1.4.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
+ lamin_cli-1.4.1.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
13
+ lamin_cli-1.4.1.dist-info/METADATA,sha256=dsWrH1faO8QjfAYKW7bNArDHztc7UL7ptaE6p5_NTaw,337
14
+ lamin_cli-1.4.1.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- lamin_cli/__init__.py,sha256=M6lfPWq9Iw7_uUcLoHjPp1SVuvhsIU0NkQQDt-EYpIc,40
2
- lamin_cli/__main__.py,sha256=2CZdjlQK0nHrMyDpNvKn7iQ34Owlbwhl4mmavc5yXgc,12392
3
- lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
4
- lamin_cli/_load.py,sha256=lMhV9AMkybjvj4VChJE_v7IMy6qGqisFlo40BJUibsA,8087
5
- lamin_cli/_migration.py,sha256=xQi6mwnpBzY5wcv1-TJhveD7a3XJIlpiYx6Z3AJ1NF0,1063
6
- lamin_cli/_save.py,sha256=JQQxby2PP6SfUb1FZhnwNnp0fkBqTj-UnbGql4oPt8g,6948
7
- lamin_cli/_settings.py,sha256=O2tecCf5EIZu98ima4DTJujo4KuywckOLgw8c-Ke3dY,1142
8
- lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
10
- lamin_cli-1.3.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
11
- lamin_cli-1.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- lamin_cli-1.3.0.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
13
- lamin_cli-1.3.0.dist-info/METADATA,sha256=m6_U_cQGfZ-2HjU-u_1frMM-Q6ATjQCObBkiebqNzMs,337
14
- lamin_cli-1.3.0.dist-info/RECORD,,