lamin_cli 0.17.6__tar.gz → 0.17.8__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 (31) hide show
  1. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/PKG-INFO +1 -1
  2. lamin_cli-0.17.8/lamin_cli/__init__.py +3 -0
  3. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/lamin_cli/__main__.py +111 -94
  4. lamin_cli-0.17.8/lamin_cli/_load.py +110 -0
  5. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/lamin_cli/_migration.py +1 -1
  6. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/lamin_cli/_save.py +20 -17
  7. lamin_cli-0.17.8/lamin_cli/_settings.py +41 -0
  8. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/notebooks/with-title-and-initialized-consecutive.ipynb +1 -2
  9. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +1 -1
  10. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/scripts/run-track-and-finish-sync-git.py +2 -2
  11. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/scripts/run-track-and-finish.py +3 -2
  12. lamin_cli-0.17.8/tests/scripts/run-track-with-params.py +21 -0
  13. lamin_cli-0.17.6/tests/test_get.py → lamin_cli-0.17.8/tests/test_load.py +13 -9
  14. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/test_migrate.py +4 -2
  15. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/test_save_notebooks.py +7 -8
  16. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/test_save_scripts.py +89 -12
  17. lamin_cli-0.17.6/lamin_cli/__init__.py +0 -3
  18. lamin_cli-0.17.6/lamin_cli/_get.py +0 -82
  19. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/.github/workflows/doc-changes.yml +0 -0
  20. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/.gitignore +0 -0
  21. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/.pre-commit-config.yaml +0 -0
  22. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/LICENSE +0 -0
  23. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/README.md +0 -0
  24. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/lamin_cli/_cache.py +0 -0
  25. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/pyproject.toml +0 -0
  26. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/conftest.py +0 -0
  27. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/notebooks/not-initialized.ipynb +0 -0
  28. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/scripts/merely-import-lamindb.py +0 -0
  29. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/test_cli.py +0 -0
  30. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/test_multi_process.py +0 -0
  31. {lamin_cli-0.17.6 → lamin_cli-0.17.8}/tests/test_save_files.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lamin_cli
3
- Version: 0.17.6
3
+ Version: 0.17.8
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__ = "0.17.8"
@@ -35,26 +35,28 @@ else:
35
35
  COMMAND_GROUPS = {
36
36
  "lamin": [
37
37
  {
38
- "name": "Main commands",
38
+ "name": "Connect to an instance",
39
39
  "commands": [
40
- "login",
41
- "init",
42
- "load",
40
+ "connect",
41
+ "disconnect",
43
42
  "info",
44
- "delete",
43
+ "init",
45
44
  ],
46
45
  },
47
46
  {
48
- "name": "Data commands",
49
- "commands": ["get", "save"],
47
+ "name": "Read & write data",
48
+ "commands": ["load", "save", "get", "delete"],
50
49
  },
51
50
  {
52
- "name": "Configuration commands",
53
- "commands": ["cache", "set"],
51
+ "name": "Configure",
52
+ "commands": ["cache", "settings", "migrate"],
54
53
  },
55
54
  {
56
- "name": "Schema migration",
57
- "commands": ["migrate"],
55
+ "name": "Auth",
56
+ "commands": [
57
+ "login",
58
+ "logout",
59
+ ],
58
60
  },
59
61
  ]
60
62
  }
@@ -63,7 +65,7 @@ else:
63
65
  @click.rich_config(
64
66
  help_config=click.RichHelpConfiguration(
65
67
  command_groups=COMMAND_GROUPS,
66
- style_commands_table_column_width_ratio=(1, 13),
68
+ style_commands_table_column_width_ratio=(1, 10),
67
69
  )
68
70
  )
69
71
  @click.group()
@@ -77,6 +79,7 @@ else:
77
79
  from click import Command, Context
78
80
  from lamindb_setup._silence_loggers import silence_loggers
79
81
 
82
+ from lamin_cli._settings import settings
80
83
  from lamin_cli._cache import cache
81
84
  from lamin_cli._migration import migrate
82
85
 
@@ -96,44 +99,34 @@ def main():
96
99
  @main.command()
97
100
  @click.argument("user", type=str, default=None, required=False)
98
101
  @click.option("--key", type=str, default=None, help="The API key.")
99
- @click.option("--logout", is_flag=True, help="Logout instead of logging in.")
100
- def login(user: str, key: Optional[str], logout: bool = False):
102
+ def login(user: str, key: Optional[str]):
101
103
  """Log into LaminHub.
102
104
 
103
- Upon logging in the first time, you need to pass your API key via:
104
-
105
- ```
106
- lamin login myemail@acme.com --key YOUR_API_KEY
107
- ```
108
-
109
- You'll find your API key on LaminHub in the top right corner under "Settings".
110
-
111
- After this, you can either use `lamin login myhandle` or `lamin login myemail@acme.com`
105
+ `lamin login` prompts for your API key unless you set it via environment variable `LAMIN_API_KEY`.
112
106
 
113
- You can also call this without arguments:
114
-
115
- ```
116
- lamin login
117
- ```
107
+ You find your API key in your account settings on LaminHub (top right corner).
118
108
 
119
- You will be prompted for your Beta API key unless you set an environment variable `LAMIN_API_KEY`.
109
+ After authenticating once, you can re-authenticate and switch between accounts via `lamin login myhandle`.
120
110
  """
121
- if logout:
122
- from lamindb_setup._setup_user import logout as logout_func
111
+ from lamindb_setup._setup_user import login as login_
123
112
 
124
- return logout_func()
113
+ if user is None:
114
+ if "LAMIN_API_KEY" in os.environ:
115
+ api_key = os.environ["LAMIN_API_KEY"]
116
+ else:
117
+ api_key = input("Your API key: ")
125
118
  else:
126
- from lamindb_setup._setup_user import login
119
+ api_key = None
127
120
 
128
- if user is None:
129
- if "LAMIN_API_KEY" in os.environ:
130
- api_key = os.environ["LAMIN_API_KEY"]
131
- else:
132
- api_key = input("Your API key: ")
133
- else:
134
- api_key = None
121
+ return login_(user, key=key, api_key=api_key)
135
122
 
136
- return login(user, key=key, api_key=api_key)
123
+
124
+ @main.command()
125
+ def logout():
126
+ """Log out of LaminHub."""
127
+ from lamindb_setup import logout as logout_
128
+
129
+ return logout_()
137
130
 
138
131
 
139
132
  # fmt: off
@@ -144,7 +137,7 @@ def login(user: str, key: Optional[str], logout: bool = False):
144
137
  @click.option("--name", type=str, default=None, help="The instance name.")
145
138
  # fmt: on
146
139
  def init(storage: str, db: Optional[str], schema: Optional[str], name: Optional[str]):
147
- """Init a LaminDB instance."""
140
+ """Init an instance."""
148
141
  from lamindb_setup._init_instance import init as init_
149
142
 
150
143
  return init_(storage=storage, db=db, schema=schema, name=name)
@@ -152,26 +145,32 @@ def init(storage: str, db: Optional[str], schema: Optional[str], name: Optional[
152
145
 
153
146
  # fmt: off
154
147
  @main.command()
155
- @click.argument("instance", type=str, default=None, required=False)
156
- @click.option("--unload", is_flag=True, help="Unload the current instance.")
148
+ @click.argument("instance", type=str)
157
149
  # fmt: on
158
- def load(instance: Optional[str], unload: bool):
159
- """Load an instance for auto-connection.
150
+ def connect(instance: str):
151
+ """Connect to an instance.
152
+
153
+ Pass a slug (`account/name`) or URL (`https://lamin.ai/account/name`).
160
154
 
161
- Pass a slug (`account/name`) or URL
162
- (`https://lamin.ai/account/name`).
155
+ `lamin connect` switches
156
+ {attr}`~lamindb.setup.core.SetupSettings.auto_connect` to `True` so that you
157
+ auto-connect in a Python session upon importing `lamindb`.
163
158
  """
164
- if unload:
165
- from lamindb_setup._close import close as close_
159
+ from lamindb_setup import settings as settings_, connect as connect_
166
160
 
167
- return close_()
168
- else:
169
- if instance is None:
170
- raise click.UsageError("INSTANCE is required when loading an instance.")
171
- from lamindb_setup import settings, connect
161
+ settings_.auto_connect = True
162
+ return connect_(slug=instance)
172
163
 
173
- settings.auto_connect = True
174
- return connect(slug=instance)
164
+
165
+ @main.command()
166
+ def disconnect():
167
+ """Disconnect from an instance.
168
+
169
+ Is the opposite of connecting to an instance.
170
+ """
171
+ from lamindb_setup import close as close_
172
+
173
+ return close_()
175
174
 
176
175
 
177
176
  @main.command()
@@ -181,12 +180,12 @@ def info(schema: bool):
181
180
  if schema:
182
181
  from lamindb_setup._schema import view
183
182
 
184
- print("Open in browser: http://127.0.0.1:8000/schema/")
183
+ click.echo("Open in browser: http://127.0.0.1:8000/schema/")
185
184
  return view()
186
185
  else:
187
- import lamindb_setup
186
+ from lamindb_setup import settings as settings_
188
187
 
189
- print(lamindb_setup.settings)
188
+ click.echo(settings_)
190
189
 
191
190
 
192
191
  # fmt: off
@@ -195,7 +194,10 @@ def info(schema: bool):
195
194
  @click.option("--force", is_flag=True, default=False, help="Do not ask for confirmation.") # noqa: E501
196
195
  # fmt: on
197
196
  def delete(instance: str, force: bool = False):
198
- """Delete an instance."""
197
+ """Delete an entity.
198
+
199
+ Currently only supports instance deletion.
200
+ """
199
201
  from lamindb_setup._delete import delete
200
202
 
201
203
  return delete(instance, force=force)
@@ -208,23 +210,52 @@ def delete(instance: str, force: bool = False):
208
210
  @click.option(
209
211
  "--with-env", is_flag=True, help="Also return the environment for a tranform."
210
212
  )
211
- def get(entity: str, uid: str = None, key: str = None, with_env: bool = False):
212
- """Query an entity.
213
+ def load(entity: str, uid: str = None, key: str = None, with_env: bool = False):
214
+ """Load a file or folder.
213
215
 
214
216
  Pass a URL, `artifact`, or `transform`. For example:
215
217
 
216
218
  ```
217
- lamin get https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5
218
- lamin get artifact --key mydatasets/mytable.parquet
219
- lamin get artifact --uid e2G7k9EVul4JbfsEYAy5
220
- lamin get transform --key analysis.ipynb
221
- lamin get transform --uid Vul4JbfsEYAy5
222
- lamin get transform --uid Vul4JbfsEYAy5 --with-env
219
+ lamin load https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5
220
+ lamin load artifact --key mydatasets/mytable.parquet
221
+ lamin load artifact --uid e2G7k9EVul4JbfsEYAy5
222
+ lamin load transform --key analysis.ipynb
223
+ lamin load transform --uid Vul4JbfsEYAy5
224
+ lamin load transform --uid Vul4JbfsEYAy5 --with-env
223
225
  ```
224
226
  """
225
- from lamin_cli._get import get
227
+ is_slug = entity.count("/") == 1
228
+ if is_slug:
229
+ from lamindb_setup import settings as settings_, connect
230
+
231
+ # can decide whether we want to actually deprecate
232
+ # click.echo(
233
+ # f"! please use: lamin connect {entity}"
234
+ # )
235
+ settings_.auto_connect = True
236
+ return connect(slug=entity)
237
+ else:
238
+ from lamin_cli._load import load as load_
226
239
 
227
- return get(entity, uid=uid, key=key, with_env=with_env)
240
+ return load_(entity, uid=uid, key=key, with_env=with_env)
241
+
242
+
243
+ @main.command()
244
+ @click.argument("entity", type=str)
245
+ @click.option("--uid", help="The uid for the entity.")
246
+ @click.option("--key", help="The key for the entity.")
247
+ @click.option(
248
+ "--with-env", is_flag=True, help="Also return the environment for a tranform."
249
+ )
250
+ def get(entity: str, uid: str = None, key: str = None, with_env: bool = False):
251
+ """Query metadata about an entity.
252
+
253
+ Currently only works for artifact & transform and behaves like `lamin load`.
254
+ """
255
+ from lamin_cli._load import load as load_
256
+
257
+ click.echo(f"! to load a file or folder, please use: lamin load {entity}")
258
+ return load_(entity, uid=uid, key=key, with_env=with_env)
228
259
 
229
260
 
230
261
  @main.command()
@@ -235,36 +266,22 @@ def get(entity: str, uid: str = None, key: str = None, with_env: bool = False):
235
266
  @click.option("--description", type=str, default=None)
236
267
  @click.option("--registry", type=str, default=None)
237
268
  def save(filepath: str, key: str, description: str, registry: str):
238
- """Save file or folder."""
269
+ """Save a file or folder.
270
+
271
+ Defaults to saving `.py` and `.ipynb` as :class:`~lamindb.Transform` and
272
+ other file types and folders as :class:`~lamindb.Artifact`.
273
+
274
+ You can save a `.py` or `.ipynb` file as an :class:`~lamindb.Artifact` by
275
+ passing `--registry artifact`.
276
+ """
239
277
  from lamin_cli._save import save_from_filepath_cli
240
278
 
241
279
  if save_from_filepath_cli(filepath, key, description, registry) is not None:
242
280
  sys.exit(1)
243
281
 
244
282
 
283
+ main.add_command(settings)
245
284
  main.add_command(cache)
246
-
247
-
248
- @main.command(name="set")
249
- @click.argument(
250
- "setting",
251
- type=click.Choice(["auto-connect", "private-django-api"], case_sensitive=False),
252
- )
253
- @click.argument("value", type=click.BOOL)
254
- def set_(setting: str, value: bool):
255
- """Update settings.
256
-
257
- - `auto-connect` → {attr}`~lamindb.setup.core.SetupSettings.auto_connect`
258
- - `private-django-api` → {attr}`~lamindb.setup.core.SetupSettings.private_django_api`
259
- """
260
- from lamindb_setup import settings
261
-
262
- if setting == "auto-connect":
263
- settings.auto_connect = value
264
- if setting == "private-django-api":
265
- settings.private_django_api = value
266
-
267
-
268
285
  main.add_command(migrate)
269
286
 
270
287
 
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+ from typing import Tuple
3
+ from lamin_utils import logger
4
+ from pathlib import Path
5
+
6
+
7
+ def decompose_url(url: str) -> Tuple[str, str, str]:
8
+ assert "transform" in url or "artifact" in url
9
+ for entity in ["transform", "artifact"]:
10
+ if entity in url:
11
+ break
12
+ uid = url.split(f"{entity}/")[1]
13
+ instance_slug = "/".join(url.split("/")[3:5])
14
+ return instance_slug, entity, uid
15
+
16
+
17
+ def load(entity: str, uid: str = None, key: str = None, with_env: bool = False):
18
+ import lamindb_setup as ln_setup
19
+
20
+ if entity.startswith("https://") and "lamin" in entity:
21
+ url = entity
22
+ instance, entity, uid = decompose_url(url)
23
+ elif entity not in {"artifact", "transform"}:
24
+ raise SystemExit("Entity has to be a laminhub URL or 'artifact' or 'transform'")
25
+ else:
26
+ instance = ln_setup.settings.instance.slug
27
+
28
+ ln_setup.connect(instance)
29
+ from lnschema_core import models as ln
30
+
31
+ def script_to_notebook(
32
+ transform: ln.Transform, notebook_path: Path, bump_revision: bool = False
33
+ ) -> None:
34
+ import jupytext
35
+ from lamin_utils._base62 import increment_base62
36
+
37
+ py_content = transform.source_code.replace(
38
+ "# # transform.name", f"# # {transform.name}"
39
+ )
40
+ if bump_revision:
41
+ uid = transform.uid
42
+ new_uid = f"{uid[:-4]}{increment_base62(uid[-4:])}"
43
+ py_content = py_content.replace(uid, new_uid)
44
+ logger.important(f"updated uid: {uid} → {new_uid}")
45
+ notebook = jupytext.reads(py_content, fmt="py:percent")
46
+ jupytext.write(notebook, notebook_path)
47
+
48
+ if entity == "transform":
49
+ transform = (
50
+ ln.Transform.objects.get(uid=uid)
51
+ if uid is not None
52
+ # if below, we take is_latest=True as the criterion, we might get draft notebooks
53
+ # hence, we use source_code__isnull=False and order by created_at instead
54
+ else ln.Transform.objects.filter(key=key, source_code__isnull=False)
55
+ .order_by("-created_at")
56
+ .first()
57
+ )
58
+ target_filename = transform.key
59
+ if Path(target_filename).exists():
60
+ response = input(f"! {target_filename} exists: replace? (y/n)")
61
+ if response != "y":
62
+ raise SystemExit("Aborted.")
63
+ if transform._source_code_artifact_id is not None: # backward compat
64
+ # need lamindb here to have .cache() available
65
+ import lamindb as ln
66
+
67
+ ln.settings.track_run_inputs = False
68
+ filepath_cache = transform._source_code_artifact.cache()
69
+ if not target_filename.endswith(transform._source_code_artifact.suffix):
70
+ target_filename += transform._source_code_artifact.suffix
71
+ filepath_cache.rename(target_filename)
72
+ elif transform.source_code is not None:
73
+ if transform.key.endswith(".ipynb"):
74
+ script_to_notebook(transform, target_filename, bump_revision=True)
75
+ else:
76
+ Path(target_filename).write_text(transform.source_code)
77
+ else:
78
+ raise SystemExit("No source code available for this transform.")
79
+ logger.important(f"{transform.type} is here: {target_filename}")
80
+ if with_env:
81
+ import lamindb as ln
82
+
83
+ ln.settings.track_run_inputs = False
84
+ if (
85
+ transform.latest_run is not None
86
+ and transform.latest_run.environment is not None
87
+ ):
88
+ filepath_env_cache = transform.latest_run.environment.cache()
89
+ target_env_filename = (
90
+ ".".join(target_filename.split(".")[:-1]) + "__requirements.txt"
91
+ )
92
+ filepath_env_cache.rename(target_env_filename)
93
+ logger.important(target_env_filename)
94
+ else:
95
+ logger.warning("latest transform run with environment doesn't exist")
96
+ return target_filename
97
+ elif entity == "artifact":
98
+ import lamindb as ln
99
+
100
+ ln.settings.track_run_inputs = False
101
+ artifact = (
102
+ ln.Artifact.get(uid)
103
+ if uid is not None
104
+ else ln.Artifact.filter(key=key, source_code__isnull=False)
105
+ .order_by("-created_at")
106
+ .first()
107
+ )
108
+ cache_path = artifact.cache()
109
+ logger.important(f"artifact is here: {cache_path}")
110
+ return cache_path
@@ -10,7 +10,7 @@ else:
10
10
 
11
11
  @click.group()
12
12
  def migrate():
13
- """Manage migrations."""
13
+ """Manage metadata schema migrations."""
14
14
 
15
15
 
16
16
  @migrate.command("create")
@@ -6,20 +6,20 @@ from lamin_utils import logger
6
6
  import re
7
7
 
8
8
 
9
- def get_stem_uid_and_version_from_file(
10
- file_path: Path,
9
+ def parse_uid_from_code(
10
+ content: str, suffix: str
11
11
  ) -> tuple[str | None, str | None, str | None]:
12
- # line-by-line matching might be faster, but let's go with this for now
13
- with open(file_path) as file:
14
- content = file.read()
15
-
16
- if file_path.suffix == ".py":
12
+ if suffix == ".py":
13
+ track_pattern = re.compile(r'ln\.track\(\s*(?:uid\s*=\s*)?["\']([^"\']+)["\']')
17
14
  uid_pattern = re.compile(r'\.context\.uid\s*=\s*["\']([^"\']+)["\']')
18
15
  stem_uid_pattern = re.compile(
19
16
  r'\.transform\.stem_uid\s*=\s*["\']([^"\']+)["\']'
20
17
  )
21
18
  version_pattern = re.compile(r'\.transform\.version\s*=\s*["\']([^"\']+)["\']')
22
- elif file_path.suffix == ".ipynb":
19
+ elif suffix == ".ipynb":
20
+ track_pattern = re.compile(
21
+ r'ln\.track\(\s*(?:uid\s*=\s*)?\\["\']([^"\']+)\\["\']'
22
+ )
23
23
  uid_pattern = re.compile(r'\.context\.uid\s*=\s*\\["\']([^"\']+)\\["\']')
24
24
  stem_uid_pattern = re.compile(
25
25
  r'\.transform\.stem_uid\s*=\s*\\["\']([^"\']+)\\["\']'
@@ -31,7 +31,10 @@ def get_stem_uid_and_version_from_file(
31
31
  raise ValueError("Only .py and .ipynb files are supported.")
32
32
 
33
33
  # Search for matches in the entire file content
34
- uid_match = uid_pattern.search(content)
34
+ uid_match = track_pattern.search(content)
35
+ uid = uid_match.group(1) if uid_match else None
36
+ if uid is None:
37
+ uid_match = uid_pattern.search(content)
35
38
  stem_uid_match = stem_uid_pattern.search(content)
36
39
  version_match = version_pattern.search(content)
37
40
 
@@ -42,8 +45,8 @@ def get_stem_uid_and_version_from_file(
42
45
 
43
46
  if uid is None and (stem_uid is None or version is None):
44
47
  raise SystemExit(
45
- "ln.context.uid isn't"
46
- f" set in {file_path}\nCall ln.context.track() and copy/paste the output"
48
+ "Cannot infer transform uid."
49
+ "\nCall `ln.track()` and copy/paste the output"
47
50
  " into the notebook"
48
51
  )
49
52
  return uid, stem_uid, version
@@ -86,20 +89,20 @@ def save_from_filepath_cli(
86
89
  logger.important(f"go to: https://lamin.ai/{slug}/artifact/{artifact.uid}")
87
90
  return None
88
91
  elif registry == "transform":
89
- # consider notebooks & scripts a transform
90
- uid, stem_uid, transform_version = get_stem_uid_and_version_from_file(filepath)
92
+ with open(filepath) as file:
93
+ content = file.read()
94
+ uid, stem_uid, version = parse_uid_from_code(content, filepath.suffix)
95
+ logger.important(f"mapped '{filepath}' on uid '{uid}'")
91
96
  if uid is not None:
92
97
  transform = ln.Transform.filter(uid=uid).one_or_none()
93
98
  if transform is None:
94
99
  logger.error(
95
100
  f"Did not find uid '{uid}'"
96
- " in Transform registry. Did you run ln.context.track()?"
101
+ " in Transform registry. Did you run `ln.track()`?"
97
102
  )
98
103
  return "not-tracked-in-transform-registry"
99
104
  else:
100
- transform = ln.Transform.get(
101
- uid__startswith=stem_uid, version=transform_version
102
- )
105
+ transform = ln.Transform.get(uid__startswith=stem_uid, version=version)
103
106
  # latest run of this transform by user
104
107
  run = ln.Run.filter(transform=transform).order_by("-started_at").first()
105
108
  if run.created_by.id != ln_setup.settings.user.id:
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+ import os
3
+
4
+ if os.environ.get("NO_RICH"):
5
+ import click as click
6
+ else:
7
+ import rich_click as click
8
+
9
+
10
+ @click.group(invoke_without_command=True)
11
+ @click.pass_context
12
+ def settings(ctx):
13
+ """Manage settings.
14
+
15
+ Call without subcommands and options to show settings.
16
+ """
17
+ if ctx.invoked_subcommand is None:
18
+ from lamindb_setup import settings as settings_
19
+
20
+ click.echo("Configure: see `lamin settings --help`")
21
+ click.echo(settings_)
22
+
23
+
24
+ @settings.command("set")
25
+ @click.argument(
26
+ "setting",
27
+ type=click.Choice(["auto-connect", "private-django-api"], case_sensitive=False),
28
+ )
29
+ @click.argument("value", type=click.BOOL)
30
+ def set(setting: str, value: bool):
31
+ """Update settings.
32
+
33
+ - `auto-connect` → {attr}`~lamindb.setup.core.SetupSettings.auto_connect`
34
+ - `private-django-api` → {attr}`~lamindb.setup.core.SetupSettings.private_django_api`
35
+ """
36
+ from lamindb_setup import settings as settings_
37
+
38
+ if setting == "auto-connect":
39
+ settings_.auto_connect = value
40
+ if setting == "private-django-api":
41
+ settings_.private_django_api = value
@@ -66,8 +66,7 @@
66
66
  }
67
67
  ],
68
68
  "source": [
69
- "ln.context.uid = \"hlsFXswrJjtt0000\"\n",
70
- "ln.context.track()"
69
+ "ln.track(\"hlsFXswrJjtt0000\")"
71
70
  ]
72
71
  },
73
72
  {
@@ -67,7 +67,7 @@
67
67
  "source": [
68
68
  "ln.settings.transform.stem_uid = \"HDMGkxN9rgFA\"\n",
69
69
  "ln.settings.transform.version = \"1\"\n",
70
- "ln.context.track()"
70
+ "ln.track()"
71
71
  ]
72
72
  }
73
73
  ],
@@ -1,15 +1,15 @@
1
1
  import lamindb as ln
2
2
 
3
3
  ln.settings.sync_git_repo = "https://github.com/laminlabs/lamin-cli"
4
- ln.context.uid = "m5uCHTTpJnjQ0000"
5
4
  ln.context.name = "My good script"
5
+ ln.track("m5uCHTTpJnjQ0000")
6
6
 
7
7
 
8
8
  if __name__ == "__main__":
9
9
  # we're using new_run here to mock the notebook situation
10
10
  # and cover the look up of an existing run in the tests
11
11
  # new_run = True is trivial
12
- ln.context.track(new_run=False)
12
+ ln.track(new_run=False)
13
13
 
14
14
  print("hello!")
15
15
 
@@ -1,13 +1,14 @@
1
1
  import lamindb as ln
2
2
 
3
- ln.context.uid = "VFYCIuaw2GsX0000"
4
3
  ln.context.name = "My good script 2"
4
+ ln.track("VFYCIuaw2GsX0000")
5
+
5
6
 
6
7
  if __name__ == "__main__":
7
8
  # we're using new_run here to mock the notebook situation
8
9
  # and cover the look up of an existing run in the tests
9
10
  # new_run = True is trivial
10
- ln.context.track(new_run=False)
11
+ ln.track(new_run=False)
11
12
 
12
13
  print("hello!")
13
14
 
@@ -0,0 +1,21 @@
1
+ import argparse
2
+ import lamindb as ln
3
+
4
+ if __name__ == "__main__":
5
+ p = argparse.ArgumentParser()
6
+ p.add_argument("--dataset-key", type=str)
7
+ p.add_argument("--downsample", action="store_true")
8
+ p.add_argument("--learning-rate", type=float)
9
+ args = p.parse_args()
10
+
11
+ params = {
12
+ "dataset_key": args.dataset_key,
13
+ "learning_rate": args.learning_rate,
14
+ "downsample": args.downsample,
15
+ }
16
+
17
+ ln.track("JjRF4mACd9m00000", params=params)
18
+
19
+ # actual code
20
+
21
+ ln.finish()
@@ -1,19 +1,23 @@
1
- from lamin_cli._get import decompose_url
1
+ from lamin_cli._load import decompose_url
2
2
  import subprocess
3
3
 
4
4
 
5
5
  def test_decompose_url():
6
- url = "https://lamin.ai/laminlabs/arrayloader-benchmarks/transform/1GCKs8zLtkc85zKv" # noqa
7
- result = decompose_url(url)
8
- instance_slug, entity, uid = result
9
- assert instance_slug == "laminlabs/arrayloader-benchmarks"
10
- assert entity == "transform"
11
- assert uid == "1GCKs8zLtkc85zKv"
6
+ urls = [
7
+ "https://lamin.ai/laminlabs/arrayloader-benchmarks/transform/1GCKs8zLtkc85zKv", # noqa
8
+ "https://lamin.company.com/laminlabs/arrayloader-benchmarks/transform/1GCKs8zLtkc85zKv", # noqa
9
+ ]
10
+ for url in urls:
11
+ result = decompose_url(url)
12
+ instance_slug, entity, uid = result
13
+ assert instance_slug == "laminlabs/arrayloader-benchmarks"
14
+ assert entity == "transform"
15
+ assert uid == "1GCKs8zLtkc85zKv"
12
16
 
13
17
 
14
18
  def test_get_transform():
15
19
  result = subprocess.run(
16
- "lamin get"
20
+ "lamin load"
17
21
  " 'https://lamin.ai/laminlabs/arrayloader-benchmarks/transform/1GCKs8zLtkc85zKv'"
18
22
  " --with-env", # noqa
19
23
  shell=True,
@@ -24,7 +28,7 @@ def test_get_transform():
24
28
 
25
29
  def test_get_artifact():
26
30
  result = subprocess.run(
27
- "lamin get"
31
+ "lamin load"
28
32
  " 'https://lamin.ai/laminlabs/lamin-site-assets/artifact/e2G7k9EVul4JbfsEYAy5'", # noqa
29
33
  shell=True,
30
34
  capture_output=True,
@@ -15,7 +15,9 @@ def test_migrate_deploy():
15
15
  import lamindb as ln
16
16
 
17
17
  instance_slug = ln.setup.settings.instance.slug
18
- exit_status = os.system("lamin load testuser1/static-test-instance-private-sqlite")
18
+ exit_status = os.system(
19
+ "lamin connect testuser1/static-test-instance-private-sqlite"
20
+ )
19
21
  assert exit_status == 0
20
22
  exit_status = os.system("lamin migrate deploy")
21
23
  assert exit_status == 0
@@ -27,7 +29,7 @@ def test_migrate_deploy():
27
29
  # )
28
30
  # import lamindb
29
31
  # assert instance["lamindb_version"] == lamindb.__version__
30
- exit_status = os.system(f"lamin load {instance_slug}")
32
+ exit_status = os.system(f"lamin connect {instance_slug}")
31
33
  assert exit_status == 0
32
34
 
33
35
 
@@ -22,7 +22,7 @@ def test_save_not_initialized():
22
22
  )
23
23
  assert result.returncode == 1
24
24
  assert (
25
- "Call ln.context.track() and copy/paste the output into the notebook"
25
+ "Call `ln.track()` and copy/paste the output into the notebook"
26
26
  in result.stderr.decode()
27
27
  )
28
28
 
@@ -70,7 +70,7 @@ def test_save_consecutive():
70
70
  transform = ln.Transform.filter(uid="hlsFXswrJjtt0000").one_or_none()
71
71
  assert transform is None
72
72
 
73
- # let's try to save a notebook for which `ln.context.track()` was never run
73
+ # let's try to save a notebook for which `ln.track()` was never run
74
74
  result = subprocess.run(
75
75
  f"lamin save {notebook_path}",
76
76
  shell=True,
@@ -80,7 +80,7 @@ def test_save_consecutive():
80
80
  assert result.returncode == 1
81
81
  assert "Did not find uid 'hlsFXswrJjtt0000'" in result.stdout.decode()
82
82
 
83
- # now, let's re-run this notebook so that ln.context.track() is actually run
83
+ # now, let's re-run this notebook so that `ln.track()` is actually run
84
84
  nbproject_test.execute_notebooks(notebook_path, print_outputs=True)
85
85
 
86
86
  # now, there is a transform record, but we're missing all artifacts
@@ -113,14 +113,13 @@ def test_save_consecutive():
113
113
  import lamindb as ln
114
114
 
115
115
  # %%
116
- ln.context.uid = "hlsFXswrJjtt0000"
117
- ln.context.track()
116
+ ln.track("hlsFXswrJjtt0000")
118
117
 
119
118
  # %%
120
119
  print("my consecutive cell")
121
120
  """
122
121
  )
123
- assert transform.hash == "fHpHnC_pScmOl3ZR8x5cTQ"
122
+ assert transform.hash == "OwVL-0-_gmk8heR3zV7BkA"
124
123
  # below is the test that we can use if store the run repot as `.ipynb`
125
124
  # and not as html as we do right now
126
125
  assert transform.latest_run.report.suffix == ".html"
@@ -168,13 +167,13 @@ print("my consecutive cell")
168
167
  transform = ln.Transform.get("hlsFXswrJjtt0000")
169
168
  assert transform.latest_run.report.path.exists()
170
169
  assert transform.latest_run.report.path == transform.latest_run.report.path
171
- assert transform.hash == "b63gPnGqNWBE0G4G3pOghw"
170
+ assert transform.hash == "BhQpym0JfeypqhVMPlQ0ng"
172
171
  assert transform.latest_run.environment.path.exists()
173
172
  assert transform._source_code_artifact is None
174
173
 
175
174
  # get the the source code via command line
176
175
  result = subprocess.run(
177
- "lamin get"
176
+ "yes | lamin load"
178
177
  f" https://lamin.ai/{ln.setup.settings.user.handle}/laminci-unit-tests/transform/hlsFXswrJjtt0000", # noqa
179
178
  shell=True,
180
179
  capture_output=True,
@@ -21,7 +21,7 @@ def test_run_save_cache():
21
21
  )
22
22
  # print(result.stdout.decode())
23
23
  assert result.returncode == 1
24
- assert "Did you run ln.context.track()?" in result.stdout.decode()
24
+ assert "Did you run `ln.track()`?" in result.stdout.decode()
25
25
 
26
26
  # run the script
27
27
  result = subprocess.run(
@@ -33,11 +33,11 @@ def test_run_save_cache():
33
33
  # print(result.stderr.decode())
34
34
  assert result.returncode == 0
35
35
  assert "created Transform" in result.stdout.decode()
36
- assert "m5uCHTTpJnjQ0000" in result.stdout.decode()
37
- assert "created Run" in result.stdout.decode()
36
+ assert "m5uCHTTp" in result.stdout.decode()
37
+ assert "started new Run" in result.stdout.decode()
38
38
 
39
39
  transform = ln.Transform.get("m5uCHTTpJnjQ")
40
- assert transform.hash == "Cwk0OPOyUH5nzTiU2ISlDQ"
40
+ assert transform.hash == "MoIciBQ0lpVPCKQGofPX6g"
41
41
  assert transform.latest_run.environment.path.exists()
42
42
  assert transform._source_code_artifact is None
43
43
 
@@ -52,8 +52,8 @@ def test_run_save_cache():
52
52
  # print(result.stderr.decode())
53
53
  assert result.returncode == 0
54
54
  assert "loaded Transform" in result.stdout.decode()
55
- assert "m5uCHTTpJnjQ0000" in result.stdout.decode()
56
- assert "loaded Run" in result.stdout.decode()
55
+ assert "m5uCHTTp" in result.stdout.decode()
56
+ assert "started Run" in result.stdout.decode()
57
57
  assert "source code is already saved" in result.stdout.decode()
58
58
 
59
59
  # you can re-save the script
@@ -85,12 +85,13 @@ def test_run_save_cache():
85
85
  assert result.returncode == 1
86
86
  assert "Did not find blob hash" in result.stderr.decode()
87
87
 
88
- # edit the script to remove the git integration
88
+ # edit the script to remove the git integration & ln.finish
89
89
  content = filepath.read_text()
90
90
  content_lines = content.split("\n")
91
91
  content_lines.remove(
92
92
  'ln.settings.sync_git_repo = "https://github.com/laminlabs/lamin-cli"'
93
93
  )
94
+ content_lines.remove(" ln.finish()")
94
95
  content = "\n".join(content_lines)
95
96
  filepath.write_text(content)
96
97
 
@@ -101,14 +102,51 @@ def test_run_save_cache():
101
102
  capture_output=True,
102
103
  env=env,
103
104
  )
104
- print(result.stdout.decode())
105
- print(result.stderr.decode())
105
+ # print(result.stdout.decode())
106
+ # print(result.stderr.decode())
106
107
  assert result.returncode == 1
107
108
  assert "Source code changed, bump revision by setting" in result.stderr.decode()
108
109
 
110
+ # update the uid
111
+ content = filepath.read_text()
112
+ filepath.write_text(content.replace("m5uCHTTpJnjQ0000", "m5uCHTTpJnjQ0001"))
113
+
114
+ # re-run the script that lacks ln.finish(), hence doesn't yet save source code
115
+ result = subprocess.run(
116
+ f"python {filepath}",
117
+ shell=True,
118
+ capture_output=True,
119
+ env=env,
120
+ )
121
+ # print(result.stdout.decode())
122
+ # print(result.stderr.decode())
123
+ assert result.returncode == 0
124
+ assert "created Transform(" in result.stdout.decode()
125
+ assert "started new Run(" in result.stdout.decode()
126
+
127
+ # login a different user
128
+ assert ln.setup.settings.user.handle != "testuser2"
129
+ result = subprocess.run(
130
+ "lamin login testuser2",
131
+ shell=True,
132
+ capture_output=True,
133
+ env=env,
134
+ )
135
+ # re-run the script through the second user
136
+ result = subprocess.run(
137
+ f"python {filepath}",
138
+ shell=True,
139
+ capture_output=True,
140
+ env=env,
141
+ )
142
+ # print(result.stdout.decode())
143
+ # print(result.stderr.decode())
144
+ assert result.returncode == 1
145
+ assert "already works on this draft" in result.stderr.decode()
146
+
109
147
  # try to get the the source code via command line
110
148
  result = subprocess.run(
111
- "lamin get"
149
+ "yes | lamin load"
112
150
  f" https://lamin.ai/{settings.user.handle}/laminci-unit-tests/transform/m5uCHTTpJnjQ0000", # noqa
113
151
  shell=True,
114
152
  capture_output=True,
@@ -117,7 +155,7 @@ def test_run_save_cache():
117
155
  assert result.returncode == 0
118
156
 
119
157
  result = subprocess.run(
120
- f"lamin get transform --key {filepath.name}", # noqa
158
+ f"yes | lamin load transform --key {filepath.name}", # noqa
121
159
  shell=True,
122
160
  capture_output=True,
123
161
  )
@@ -125,9 +163,48 @@ def test_run_save_cache():
125
163
  assert result.returncode == 0
126
164
 
127
165
  result = subprocess.run(
128
- f"lamin get transform --uid m5uCHTTpJnjQ0000", # noqa
166
+ f"yes | lamin load transform --uid m5uCHTTpJnjQ0000", # noqa
129
167
  shell=True,
130
168
  capture_output=True,
131
169
  )
132
170
  print(result.stderr.decode())
133
171
  assert result.returncode == 0
172
+
173
+
174
+ def test_run_save_with_params():
175
+ env = os.environ
176
+ env["LAMIN_TESTING"] = "true"
177
+ filepath = scripts_dir / "run-track-with-params.py"
178
+
179
+ # define params
180
+ ln.Param(name="dataset_key", dtype="str").save()
181
+ ln.Param(name="learning_rate", dtype="float").save()
182
+ ln.Param(name="downsample", dtype="bool").save()
183
+
184
+ # run the script
185
+ result = subprocess.run(
186
+ f"python {filepath} --dataset-key mydata --learning-rate 0.01 --downsample",
187
+ shell=True,
188
+ capture_output=True,
189
+ )
190
+ print(result.stdout.decode())
191
+ print(result.stderr.decode())
192
+ assert result.returncode == 0
193
+ assert "created Transform" in result.stdout.decode()
194
+ assert "JjRF4mAC" in result.stdout.decode()
195
+ assert "started new Run" in result.stdout.decode()
196
+
197
+ # you can re-save the script
198
+ result = subprocess.run(
199
+ f"lamin save {filepath}",
200
+ shell=True,
201
+ capture_output=True,
202
+ env=env,
203
+ )
204
+ print(result.stdout.decode())
205
+ print(result.stderr.decode())
206
+ assert result.returncode == 0
207
+ assert "source code is already saved" in result.stdout.decode()
208
+ assert (
209
+ "run-track-with-params.py' on uid 'JjRF4mACd9m00000'" in result.stdout.decode()
210
+ )
@@ -1,3 +0,0 @@
1
- """Lamin CLI."""
2
-
3
- __version__ = "0.17.6"
@@ -1,82 +0,0 @@
1
- from __future__ import annotations
2
- from typing import Tuple
3
- from lamin_utils import logger
4
- import lamindb_setup as ln_setup
5
- from pathlib import Path
6
-
7
-
8
- def decompose_url(url: str) -> Tuple[str, str, str]:
9
- assert "transform" in url or "artifact" in url
10
- for entity in ["transform", "artifact"]:
11
- if entity in url:
12
- break
13
- uid = url.split(f"{entity}/")[1]
14
- instance_slug = "/".join(url.replace("https://lamin.ai/", "").split("/")[:2])
15
- return instance_slug, entity, uid
16
-
17
-
18
- def get(entity: str, uid: str = None, key: str = None, with_env: bool = False):
19
- if entity.startswith("https://lamin.ai"):
20
- url = entity
21
- instance_slug, entity, uid = decompose_url(url)
22
- elif entity not in {"artifact", "transform"}:
23
- raise ValueError(
24
- "entity has to be a URL starting with https://lamin.ai or 'artifact' or"
25
- " 'transform'"
26
- )
27
- else:
28
- instance_slug = None
29
-
30
- if instance_slug is not None:
31
- auto_connect = ln_setup.settings.auto_connect
32
- # we don't want to auto-connect when importing lamindb
33
- ln_setup.settings.auto_connect = False
34
-
35
- import lamindb as ln
36
- from lamindb._finish import script_to_notebook
37
-
38
- ln_setup.settings.auto_connect = auto_connect
39
- ln.connect(instance_slug)
40
- else:
41
- import lamindb as ln
42
- from lamindb._finish import script_to_notebook
43
-
44
- # below is to silence warnings about missing run inputs
45
- ln.settings.track_run_inputs = False
46
-
47
- if entity == "transform":
48
- transform = (
49
- ln.Transform.get(uid) if uid is not None else ln.Transform.get(key=key)
50
- )
51
- target_filename = transform.key
52
- if transform._source_code_artifact_id is not None:
53
- # backward compat
54
- filepath_cache = transform._source_code_artifact.cache()
55
- if not target_filename.endswith(transform._source_code_artifact.suffix):
56
- target_filename += transform._source_code_artifact.suffix
57
- filepath_cache.rename(target_filename)
58
- elif transform.source_code is not None:
59
- if transform.key.endswith(".ipynb"):
60
- script_to_notebook(transform, target_filename)
61
- else:
62
- Path(target_filename).write_text(transform.source_code)
63
- else:
64
- raise ValueError("No source code available for this transform.")
65
- logger.important(target_filename)
66
- if with_env:
67
- if (
68
- transform.latest_run is not None
69
- and transform.latest_run.environment is not None
70
- ):
71
- filepath_env_cache = transform.latest_run.environment.cache()
72
- target_env_filename = (
73
- ".".join(target_filename.split(".")[:-1]) + "__requirements.txt"
74
- )
75
- filepath_env_cache.rename(target_env_filename)
76
- logger.important(target_env_filename)
77
- else:
78
- logger.warning("latest transform run with environment doesn't exist")
79
- elif entity == "artifact":
80
- artifact = ln.Artifact.get(uid) if uid is not None else ln.Artifact.get(key=key)
81
- cache_path = artifact.cache()
82
- logger.important(cache_path)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes