lamin_cli 1.6.0__py2.py3-none-any.whl → 1.7.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
@@ -1,3 +1,3 @@
1
1
  """Lamin CLI."""
2
2
 
3
- __version__ = "1.6.0"
3
+ __version__ = "1.7.0"
lamin_cli/__main__.py CHANGED
@@ -19,6 +19,8 @@ from lamindb_setup._init_instance import (
19
19
  DOC_STORAGE_ARG,
20
20
  )
21
21
 
22
+ from .urls import decompose_url
23
+
22
24
  if TYPE_CHECKING:
23
25
  from collections.abc import Mapping
24
26
 
@@ -114,8 +116,9 @@ def main():
114
116
 
115
117
  @main.command()
116
118
  @click.argument("user", type=str, default=None, required=False)
117
- @click.option("--key", type=str, default=None, help="The legacy API key.")
119
+ @click.option("--key", type=str, default=None, hidden=True, help="The legacy API key.")
118
120
  def login(user: str, key: str | None):
121
+ # note that the docstring needs to be synced with ln.setup.login()
119
122
  """Log into LaminHub.
120
123
 
121
124
  `lamin login` prompts for your API key unless you set it via environment variable `LAMIN_API_KEY`.
@@ -123,24 +126,12 @@ def login(user: str, key: str | None):
123
126
  You can create your API key in your account settings on LaminHub (top right corner).
124
127
 
125
128
  After authenticating once, you can re-authenticate and switch between accounts via `lamin login myhandle`.
129
+
130
+ See also: Login in a Python session via {func}`~lamindb.setup.login`.
126
131
  """
127
132
  from lamindb_setup._setup_user import login as login_
128
133
 
129
- if user is None:
130
- if "LAMIN_API_KEY" in os.environ:
131
- api_key = os.environ["LAMIN_API_KEY"]
132
- else:
133
- api_key = input("Your API key: ")
134
- else:
135
- api_key = None
136
-
137
- if key is not None:
138
- click.echo(
139
- "--key is deprecated and will be removed in the future, "
140
- "use `lamin login` and enter your API key."
141
- )
142
-
143
- return login_(user, key=key, api_key=api_key)
134
+ return login_(user, key=key)
144
135
 
145
136
 
146
137
  @main.command()
@@ -175,7 +166,10 @@ def init(
175
166
  db: str | None,
176
167
  modules: str | None,
177
168
  ):
178
- """Init an instance."""
169
+ """Init a LaminDB instance.
170
+
171
+ See also: Init in a Python session via {func}`~lamindb.setup.init`.
172
+ """
179
173
  from lamindb_setup._init_instance import init as init_
180
174
 
181
175
  return init_(storage=storage, db=db, modules=modules, name=name)
@@ -186,15 +180,13 @@ def init(
186
180
  @click.argument("instance", type=str)
187
181
  # fmt: on
188
182
  def connect(instance: str):
189
- """Connect to an instance.
183
+ """Configure default instance for connections.
190
184
 
191
- Pass a slug (`account/name`) or URL (`https://lamin.ai/account/name`).
185
+ Python/R sessions and CLI commands will then auto-connect to the configured instance.
192
186
 
193
- `lamin connect` switches
194
- {attr}`~lamindb.setup.core.SetupSettings.auto_connect` to `True` so that you
195
- auto-connect in a Python session upon importing `lamindb`.
187
+ Pass a slug (`account/name`) or URL (`https://lamin.ai/account/name`).
196
188
 
197
- Alternatively, you can connect in a Python session via {func}`~lamindb.connect`.
189
+ See also: Connect in a Python session via {func}`~lamindb.connect`.
198
190
  """
199
191
  from lamindb_setup._connect_instance import _connect_cli
200
192
  return _connect_cli(instance)
@@ -202,13 +194,13 @@ def connect(instance: str):
202
194
 
203
195
  @main.command()
204
196
  def disconnect():
205
- """Disconnect from an instance.
197
+ """Clear default instance configuration.
206
198
 
207
- Is the opposite of connecting to an instance.
199
+ See also: Disconnect in a Python session via {func}`~lamindb.setup.disconnect`.
208
200
  """
209
- from lamindb_setup import close as close_
201
+ from lamindb_setup import disconnect as disconnect_
210
202
 
211
- return close_()
203
+ return disconnect_()
212
204
 
213
205
 
214
206
  # fmt: off
@@ -277,7 +269,10 @@ def switch(branch: str | None = None, space: str | None = None):
277
269
  @main.command()
278
270
  @click.option("--schema", is_flag=True, help="View database schema.")
279
271
  def info(schema: bool):
280
- """Show info about the environment, instance, branch, space, and user."""
272
+ """Show info about the environment, instance, branch, space, and user.
273
+
274
+ See also: Print the instance settings in a Python session via {func}`~lamindb.setup.settings`.
275
+ """
281
276
  if schema:
282
277
  from lamindb_setup._schema import view
283
278
 
@@ -300,14 +295,21 @@ def info(schema: bool):
300
295
  def delete(entity: str, name: str | None = None, uid: str | None = None, slug: str | None = None, force: bool = False):
301
296
  """Delete an entity.
302
297
 
303
- Currently supported: `branch`, `artifact`, and `instance`.
298
+ Currently supported: `branch`, `artifact`, `transform`, `collection`, and `instance`. For example:
304
299
 
305
300
  ```
306
- lamin delete instance --slug account/name
301
+ lamin delete https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5
307
302
  lamin delete branch --name my_branch
303
+ lamin delete instance --slug account/name
308
304
  ```
309
305
  """
310
- from lamindb_setup._delete import delete
306
+ from lamindb_setup import connect, delete
307
+
308
+ # TODO: refactor to abstract getting and deleting across entities
309
+ if entity.startswith("https://") and "lamin" in entity:
310
+ url = entity
311
+ instance, entity, uid = decompose_url(url)
312
+ connect(instance)
311
313
 
312
314
  if entity == "branch":
313
315
  assert name is not None, "You have to pass a name for deleting a branch."
@@ -319,6 +321,16 @@ def delete(entity: str, name: str | None = None, uid: str | None = None, slug: s
319
321
  from lamindb import Artifact
320
322
 
321
323
  Artifact.get(uid).delete()
324
+ elif entity == "transform":
325
+ assert uid is not None, "You have to pass a uid for deleting an transform."
326
+ from lamindb import Transform
327
+
328
+ Transform.get(uid).delete()
329
+ elif entity == "collection":
330
+ assert uid is not None, "You have to pass a uid for deleting an collection."
331
+ from lamindb import Collection
332
+
333
+ Collection.get(uid).delete()
322
334
  elif entity == "instance":
323
335
  return delete(slug, force=force)
324
336
  else: # backwars compatibility
@@ -387,10 +399,14 @@ def _describe(entity: str = "artifact", uid: str | None = None, key: str | None
387
399
  def describe(entity: str = "artifact", uid: str | None = None, key: str | None = None):
388
400
  """Describe an artifact.
389
401
 
402
+ Examples:
403
+
390
404
  ```
391
405
  lamin describe --key example_datasets/mini_immuno/dataset1.h5ad
392
406
  lamin describe https://lamin.ai/laminlabs/lamin-site-assets/artifact/6sofuDVvTANB0f48
393
407
  ```
408
+
409
+ See also: Describe an artifact in a Python session via {func}`~lamindb.Artifact.describe`.
394
410
  """
395
411
  _describe(entity=entity, uid=uid, key=key)
396
412
 
@@ -402,7 +418,7 @@ def describe(entity: str = "artifact", uid: str | None = None, key: str | None =
402
418
  def get(entity: str = "artifact", uid: str | None = None, key: str | None = None):
403
419
  """Query metadata about an entity.
404
420
 
405
- Currently still equivalent to `lamin describe`.
421
+ Currently equivalent to `lamin describe`.
406
422
  """
407
423
  logger.warning("please use `lamin describe` instead of `lamin get` to describe")
408
424
  _describe(entity=entity, uid=uid, key=key)
@@ -420,7 +436,7 @@ def get(entity: str = "artifact", uid: str | None = None, key: str | None = None
420
436
  def save(path: str, key: str, description: str, stem_uid: str, project: str, space: str, branch: str, registry: str):
421
437
  """Save a file or folder.
422
438
 
423
- Example: Given a valid project name "my_project".
439
+ Example: Given a valid project name "my_project",
424
440
 
425
441
  ```
426
442
  lamin save my_table.csv --key my_tables/my_table.csv --project my_project
@@ -448,7 +464,7 @@ def save(path: str, key: str, description: str, stem_uid: str, project: str, spa
448
464
  def annotate(key: str, uid: str, project: str, registry: str, features: tuple):
449
465
  """Annotate an artifact or a transform.
450
466
 
451
- You can annotate with projects and valid features & values.
467
+ You can annotate with projects and valid features & values. For example,
452
468
 
453
469
  ```
454
470
  lamin annotate --key raw/sample.fastq --project "My Project"
@@ -511,7 +527,7 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
511
527
 
512
528
  This is an EXPERIMENTAL feature that enables to run a script on Modal.
513
529
 
514
- Example: Given a valid project name "my_project".
530
+ Example: Given a valid project name "my_project",
515
531
 
516
532
  ```
517
533
  lamin run my_script.py --project my_project
lamin_cli/_load.py CHANGED
@@ -6,15 +6,8 @@ from pathlib import Path
6
6
 
7
7
  from lamin_utils import logger
8
8
 
9
-
10
- def decompose_url(url: str) -> tuple[str, str, str]:
11
- assert any(keyword in url for keyword in ["transform", "artifact", "collection"])
12
- for entity in ["transform", "artifact", "collection"]:
13
- if entity in url:
14
- break
15
- uid = url.split(f"{entity}/")[1]
16
- instance_slug = "/".join(url.split("/")[3:5])
17
- return instance_slug, entity, uid
9
+ from ._save import parse_title_r_notebook
10
+ from .urls import decompose_url
18
11
 
19
12
 
20
13
  def load(
@@ -55,27 +48,21 @@ def load(
55
48
  else:
56
49
  new_content = transform.source_code
57
50
  else: # R notebook
58
- # Pattern to match title only within YAML header section
59
- title_pattern = r'^---\n.*?title:\s*"([^"]*)".*?---'
60
- title_match = re.search(
61
- title_pattern, transform.source_code, flags=re.DOTALL | re.MULTILINE
62
- )
63
51
  new_content = transform.source_code
64
- if title_match:
65
- current_title = title_match.group(1)
66
- if current_title != transform.description:
67
- pattern = r'^(---\n.*?title:\s*)"([^"]*)"(.*?---)'
68
- replacement = f'\\1"{transform.description}"\\3'
69
- new_content = re.sub(
70
- pattern,
71
- replacement,
72
- new_content,
73
- flags=re.DOTALL | re.MULTILINE,
74
- )
75
- logger.important(
76
- f"updated title to match description: {current_title}"
77
- f" {transform.description}"
78
- )
52
+ current_title = parse_title_r_notebook(new_content)
53
+ if current_title is not None and current_title != transform.description:
54
+ pattern = r'^(---\n.*?title:\s*)"([^"]*)"(.*?---)'
55
+ replacement = f'\\1"{transform.description}"\\3'
56
+ new_content = re.sub(
57
+ pattern,
58
+ replacement,
59
+ new_content,
60
+ flags=re.DOTALL | re.MULTILINE,
61
+ )
62
+ logger.important(
63
+ f"updated title to match description: {current_title} →"
64
+ f" {transform.description}"
65
+ )
79
66
  if bump_revision:
80
67
  uid = transform.uid
81
68
  if (
lamin_cli/_migration.py CHANGED
@@ -23,11 +23,13 @@ def create():
23
23
 
24
24
 
25
25
  @migrate.command("deploy")
26
- def deploy():
26
+ @click.option("--package-name", type=str, default=None)
27
+ @click.option("--number", type=str, default=None)
28
+ def deploy(package_name: str | None = None, number: str | None = None):
27
29
  """Deploy migrations."""
28
30
  from lamindb_setup._migrate import migrate
29
31
 
30
- return migrate.deploy()
32
+ return migrate.deploy(package_name=package_name, number=number)
31
33
 
32
34
 
33
35
  @migrate.command("squash")
lamin_cli/_save.py CHANGED
@@ -61,6 +61,16 @@ def parse_uid_from_code(content: str, suffix: str) -> str | None:
61
61
  return uid
62
62
 
63
63
 
64
+ def parse_title_r_notebook(content: str) -> str | None:
65
+ # Pattern to match title only within YAML header section
66
+ title_pattern = r'^---\n.*?title:\s*"([^"]*)".*?---'
67
+ title_match = re.search(title_pattern, content, flags=re.DOTALL | re.MULTILINE)
68
+ if title_match:
69
+ return title_match.group(1)
70
+ else:
71
+ return None
72
+
73
+
64
74
  def save_from_path_cli(
65
75
  path: Path | str,
66
76
  key: str | None,
@@ -187,8 +197,7 @@ def save_from_path_cli(
187
197
  )
188
198
  return "delete-html-or-nb-html"
189
199
 
190
- with path.open() as file:
191
- content = file.read()
200
+ content = path.read_text()
192
201
  uid = parse_uid_from_code(content, path.suffix)
193
202
 
194
203
  if uid is not None:
@@ -217,10 +226,18 @@ def save_from_path_cli(
217
226
  transform = ln.Transform.filter(key=path.name, is_latest=True).one_or_none()
218
227
  if transform is not None and transform.hash is not None:
219
228
  if transform.hash == transform_hash:
220
- logger.important(
221
- f"found existing Transform('{uid}') with matching hash"
222
- )
223
- return None
229
+ if transform.type != "notebook":
230
+ return None
231
+ if os.getenv("LAMIN_TESTING") == "true":
232
+ response = "y"
233
+ else:
234
+ response = input(
235
+ f"Found an existing Transform('{transform.uid}') "
236
+ "with matching source code hash.\n"
237
+ "Do you want to update it? (y/n) "
238
+ )
239
+ if response != "y":
240
+ return None
224
241
  else:
225
242
  # we need to create a new version
226
243
  stem_uid = transform.uid[:12]
@@ -241,6 +258,8 @@ def save_from_path_cli(
241
258
 
242
259
  nb = read_notebook(path)
243
260
  description = get_title(nb)
261
+ elif path.suffix in {".qmd", ".Rmd"}:
262
+ description = parse_title_r_notebook(content)
244
263
  else:
245
264
  description = None
246
265
  transform = ln.Transform(
@@ -262,17 +281,17 @@ def save_from_path_cli(
262
281
  # latest run of this transform by user
263
282
  run = ln.Run.filter(transform=transform).order_by("-started_at").first()
264
283
  if run is not None and run.created_by.id != ln_setup.settings.user.id:
265
- if os.environ.get("LAMIN_TESTING") == "true":
284
+ if os.getenv("LAMIN_TESTING") == "true":
266
285
  response = "y"
267
286
  else:
268
287
  response = input(
269
288
  "You are trying to save a transform created by another user: Source"
270
289
  " and report files will be tagged with *your* user id. Proceed?"
271
- " (y/n)"
290
+ " (y/n) "
272
291
  )
273
292
  if response != "y":
274
293
  return "aborted-save-notebook-created-by-different-user"
275
- if run is None and transform.key.endswith(".ipynb"):
294
+ if run is None and transform.type == "notebook":
276
295
  run = ln.Run(transform=transform).save()
277
296
  logger.important(
278
297
  f"found no run, creating Run('{run.uid}') to display the html"
lamin_cli/_settings.py CHANGED
@@ -40,3 +40,23 @@ def set(setting: str, value: bool):
40
40
  settings_.auto_connect = value
41
41
  if setting == "private-django-api":
42
42
  settings_.private_django_api = value
43
+
44
+
45
+ @settings.command("get")
46
+ @click.argument(
47
+ "setting",
48
+ type=click.Choice(
49
+ ["auto-connect", "private-django-api", "space", "branch"], case_sensitive=False
50
+ ),
51
+ )
52
+ def get(setting: str):
53
+ """Get a setting."""
54
+ from lamindb_setup import settings as settings_
55
+
56
+ if setting == "branch":
57
+ _, value = settings_._read_branch_idlike_name()
58
+ elif setting == "space":
59
+ _, value = settings_._read_space_idlike_name()
60
+ else:
61
+ value = getattr(settings_, setting.replace("-", "_"))
62
+ click.echo(value)
lamin_cli/urls.py ADDED
@@ -0,0 +1,8 @@
1
+ def decompose_url(url: str) -> tuple[str, str, str]:
2
+ assert any(keyword in url for keyword in ["transform", "artifact", "collection"])
3
+ for entity in ["transform", "artifact", "collection"]:
4
+ if entity in url:
5
+ break
6
+ uid = url.split(f"{entity}/")[1]
7
+ instance_slug = "/".join(url.split("/")[3:5])
8
+ return instance_slug, entity, uid
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lamin_cli
3
- Version: 1.6.0
3
+ Version: 1.7.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,16 @@
1
+ lamin_cli/__init__.py,sha256=pYo6D4PqHL3amAAxIhCrkK0o08xnL02H3DTM3whSlEQ,40
2
+ lamin_cli/__main__.py,sha256=eMfRcYrgxeb7f16tW8rDhCjUVzeNIwz6UUIU0qZ2Wuo,19750
3
+ lamin_cli/_annotate.py,sha256=ZD76__K-mQt7UpYqyM1I2lKCs-DraTmnkjsByIHmD-g,1839
4
+ lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
5
+ lamin_cli/_load.py,sha256=OgTGqZQtoJ0GYOhuJihz-izt0F4jM4U_nSX_vhPoukg,7698
6
+ lamin_cli/_migration.py,sha256=XUl_L9_3pTTk5jJoBiqbzf0Bd2LdKKtHa1zPZ4Rla5c,1267
7
+ lamin_cli/_save.py,sha256=4-pPCNxNZO62pYgRuI8oJMraTexWewwJQEOeNgocuyU,11831
8
+ lamin_cli/_settings.py,sha256=xG5e_zWk9N8glcodeyE0Kap79tl6T7yp4MWW1pBLOmk,1684
9
+ lamin_cli/urls.py,sha256=gc72s4SpaAQA8J50CtCIWlr49DWOSL_a6OM9lXfPouM,367
10
+ lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
12
+ lamin_cli-1.7.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
13
+ lamin_cli-1.7.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
14
+ lamin_cli-1.7.0.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
15
+ lamin_cli-1.7.0.dist-info/METADATA,sha256=RIe8cPwox_IIgQA9VnfiWofW9BDYN7fVsrA0DhVUTYY,337
16
+ lamin_cli-1.7.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- lamin_cli/__init__.py,sha256=RMGZmTcFDyOsY_o5u0yTfy5XRyzFXCN8Ylh6drfn6iU,40
2
- lamin_cli/__main__.py,sha256=CeFFK696hYVY19dNHbUeZ8-OVByVFsNiVybkDnD_Ras,18936
3
- lamin_cli/_annotate.py,sha256=ZD76__K-mQt7UpYqyM1I2lKCs-DraTmnkjsByIHmD-g,1839
4
- lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
5
- lamin_cli/_load.py,sha256=QqiFxGYmvFz2RjhvwKO5r1fmPWMZ5Ai4y1pJytdYKak,8301
6
- lamin_cli/_migration.py,sha256=xQi6mwnpBzY5wcv1-TJhveD7a3XJIlpiYx6Z3AJ1NF0,1063
7
- lamin_cli/_save.py,sha256=fC_tD65x3wufXjPyN36Xhl5gSuBj8FsD3G1JRNHmJrw,11021
8
- lamin_cli/_settings.py,sha256=O2tecCf5EIZu98ima4DTJujo4KuywckOLgw8c-Ke3dY,1142
9
- lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
11
- lamin_cli-1.6.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
12
- lamin_cli-1.6.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
13
- lamin_cli-1.6.0.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
14
- lamin_cli-1.6.0.dist-info/METADATA,sha256=jePbx3ZZwBQIyU_IsSOQk1e3DoVzHBG4VaPEQTDTGz8,337
15
- lamin_cli-1.6.0.dist-info/RECORD,,