lamin_cli 0.14.0__tar.gz → 0.16.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.
Files changed (27) hide show
  1. lamin_cli-0.16.0/.github/workflows/doc-changes.yml +23 -0
  2. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/.pre-commit-config.yaml +1 -1
  3. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/PKG-INFO +1 -1
  4. lamin_cli-0.16.0/lamin_cli/__init__.py +3 -0
  5. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/__main__.py +149 -67
  6. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_get.py +3 -3
  7. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/scripts/run-track-and-finish-sync-git.py +2 -0
  8. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/scripts/run-track-and-finish.py +1 -0
  9. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_migrate.py +1 -1
  10. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_save_notebooks.py +10 -10
  11. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_save_scripts.py +2 -2
  12. lamin_cli-0.14.0/lamin_cli/__init__.py +0 -3
  13. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/.gitignore +0 -0
  14. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/README.md +0 -0
  15. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_cache.py +0 -0
  16. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_migration.py +0 -0
  17. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_save.py +0 -0
  18. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/pyproject.toml +0 -0
  19. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/conftest.py +0 -0
  20. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/notebooks/not-initialized.ipynb +0 -0
  21. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -0
  22. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
  23. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/scripts/merely-import-lamindb.py +0 -0
  24. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_cli.py +0 -0
  25. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_get.py +0 -0
  26. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_multi_process.py +0 -0
  27. {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_save_files.py +0 -0
@@ -0,0 +1,23 @@
1
+ name: doc-changes
2
+
3
+ on:
4
+ pull_request_target:
5
+ branches:
6
+ - main
7
+ types:
8
+ - closed
9
+
10
+ jobs:
11
+ latest-changes:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.11"
18
+ - run: pip install "laminci[doc-changes]@git+https://x-access-token:${{ secrets.LAMIN_BUILD_DOCS }}@github.com/laminlabs/laminci"
19
+ - run: laminci doc-changes
20
+ env:
21
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
22
+ docs_token: ${{ secrets.LAMIN_BUILD_DOCS }}
23
+ changelog_file: lamin-docs/docs/changelog/soon/lamindb.md
@@ -23,7 +23,7 @@ repos:
23
23
  - flake8-typing-imports==1.10.0
24
24
  language_version: python3
25
25
  args:
26
- - --max-line-length=88
26
+ - --max-line-length=120
27
27
  - --ignore=E203,W503,BLK100,TYP001
28
28
  exclude: |
29
29
  (?x)(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lamin_cli
3
- Version: 0.14.0
3
+ Version: 0.16.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,3 @@
1
+ """Lamin CLI."""
2
+
3
+ __version__ = "0.16.0"
@@ -1,16 +1,73 @@
1
1
  from __future__ import annotations
2
2
  import os
3
3
  import sys
4
+ from collections import OrderedDict
5
+ import inspect
4
6
  from importlib.metadata import PackageNotFoundError, version
5
- from typing import Optional
7
+ from typing import Optional, Mapping
6
8
 
7
9
  # https://github.com/ewels/rich-click/issues/19
8
10
  # Otherwise rich-click takes over the formatting.
9
11
  if os.environ.get("NO_RICH"):
10
12
  import click as click
13
+
14
+ class OrderedGroup(click.Group):
15
+ """Overwrites list_commands to return commands in order of definition."""
16
+
17
+ def __init__(
18
+ self,
19
+ name: Optional[str] = None,
20
+ commands: Optional[Mapping[str, click.Command]] = None,
21
+ **kwargs,
22
+ ):
23
+ super(OrderedGroup, self).__init__(name, commands, **kwargs)
24
+ self.commands = commands or OrderedDict()
25
+
26
+ def list_commands(self, ctx: click.Context) -> Mapping[str, click.Command]:
27
+ return self.commands
28
+
29
+ lamin_group_decorator = click.group(cls=OrderedGroup)
30
+
11
31
  else:
12
32
  import rich_click as click
13
33
 
34
+ COMMAND_GROUPS = {
35
+ "lamin": [
36
+ {
37
+ "name": "Main commands",
38
+ "commands": [
39
+ "login",
40
+ "init",
41
+ "load",
42
+ "info",
43
+ "close",
44
+ "delete",
45
+ "logout",
46
+ ],
47
+ },
48
+ {
49
+ "name": "Data commands",
50
+ "commands": ["get", "save"],
51
+ },
52
+ {
53
+ "name": "Configuration commands",
54
+ "commands": ["register", "cache", "set"],
55
+ },
56
+ {
57
+ "name": "Schema commands",
58
+ "commands": ["migrate", "schema"],
59
+ },
60
+ ]
61
+ }
62
+
63
+ lamin_group_decorator = click.rich_config(
64
+ help_config=click.RichHelpConfiguration(
65
+ command_groups=COMMAND_GROUPS,
66
+ style_commands_table_column_width_ratio=(1, 13),
67
+ )
68
+ )(click.group())
69
+
70
+
14
71
  from click import Command, Context
15
72
  from lamindb_setup._silence_loggers import silence_loggers
16
73
 
@@ -23,7 +80,7 @@ except PackageNotFoundError:
23
80
  lamindb_version = "lamindb installation not found"
24
81
 
25
82
 
26
- @click.group()
83
+ @lamin_group_decorator
27
84
  @click.version_option(version=lamindb_version, prog_name="lamindb")
28
85
  def main():
29
86
  """Configure LaminDB and perform simple actions."""
@@ -31,11 +88,17 @@ def main():
31
88
 
32
89
 
33
90
  @main.command()
34
- def info():
35
- """Show user & instance info."""
36
- import lamindb_setup
91
+ @click.argument("user", type=str)
92
+ @click.option("--key", type=str, default=None, help="API key")
93
+ @click.option("--password", type=str, default=None, help="legacy password")
94
+ def login(user: str, key: Optional[str], password: Optional[str]):
95
+ """Login using a user email address or handle.
37
96
 
38
- print(lamindb_setup.settings)
97
+ Examples: `lamin login marge` or `lamin login marge@acme.com`
98
+ """
99
+ from lamindb_setup._setup_user import login
100
+
101
+ return login(user, key=key, password=password)
39
102
 
40
103
 
41
104
  # fmt: off
@@ -52,25 +115,6 @@ def init(storage: str, db: Optional[str], schema: Optional[str], name: Optional[
52
115
  return init_(storage=storage, db=db, schema=schema, name=name)
53
116
 
54
117
 
55
- @main.command()
56
- @click.argument("user", type=str)
57
- @click.option("--key", type=str, default=None, help="API key")
58
- @click.option("--password", type=str, default=None, help="legacy password")
59
- def login(user: str, key: Optional[str], password: Optional[str]):
60
- """Login using an email or user handle."""
61
- from lamindb_setup._setup_user import login
62
-
63
- return login(user, key=key, password=password)
64
-
65
-
66
- @main.command()
67
- def logout():
68
- """Logout."""
69
- from lamindb_setup._setup_user import logout
70
-
71
- return logout()
72
-
73
-
74
118
  # fmt: off
75
119
  @main.command()
76
120
  @click.argument("identifier", type=str, default=None)
@@ -78,12 +122,10 @@ def logout():
78
122
  @click.option("--storage", type=str, default=None, help="Update storage while loading.")
79
123
  # fmt: on
80
124
  def load(identifier: str, db: Optional[str], storage: Optional[str]):
81
- """Auto-connect to a lamindb instance.
82
-
83
- Identifier can be slug (account_handle/instance_name) or url
84
- (https://lamin.ai/account_handle/instance_name).
125
+ """Load an instance for auto-connection.
85
126
 
86
- If the owner is the current user the instance_name suffices.
127
+ `IDENTIFIER` is either a slug (`account/instance`) or a `URL`
128
+ (`https://lamin.ai/account/instance`).
87
129
  """
88
130
  from lamindb_setup import settings, connect
89
131
 
@@ -91,53 +133,52 @@ def load(identifier: str, db: Optional[str], storage: Optional[str]):
91
133
  return connect(slug=identifier, db=db, storage=storage)
92
134
 
93
135
 
94
- # fmt: off
95
136
  @main.command()
96
- @click.argument("instance", type=str, default=None)
97
- @click.option("--force", is_flag=True, default=False, help="Do not ask for confirmation.") # noqa: E501
98
- # fmt: on
99
- def delete(instance: str, force: bool = False):
100
- """Delete instance."""
101
- from lamindb_setup._delete import delete
102
-
103
- return delete(instance, force=force)
104
-
105
-
106
- @main.command(name="set")
107
- @click.argument("setting", type=click.Choice(["auto-connect"], case_sensitive=False))
108
- @click.argument("value", type=click.BOOL)
109
- def set_(setting: str, value: bool):
110
- """Update settings."""
111
- from lamindb_setup import settings
137
+ def info():
138
+ """Show user, settings & instance info."""
139
+ import lamindb_setup
112
140
 
113
- if setting == "auto-connect":
114
- settings.auto_connect = value
141
+ print(lamindb_setup.settings)
115
142
 
116
143
 
117
144
  @main.command()
118
145
  def close():
119
- """Close existing instance."""
146
+ """Close an existing instance.
147
+
148
+ Is the opposite of loading an instance.
149
+ """
120
150
  from lamindb_setup._close import close as close_
121
151
 
122
152
  return close_()
123
153
 
124
154
 
155
+ # fmt: off
125
156
  @main.command()
126
- def register():
127
- """Register an instance on the hub."""
128
- from lamindb_setup._register_instance import register as register_
157
+ @click.argument("instance", type=str, default=None)
158
+ @click.option("--force", is_flag=True, default=False, help="Do not ask for confirmation.") # noqa: E501
159
+ # fmt: on
160
+ def delete(instance: str, force: bool = False):
161
+ """Delete an instance."""
162
+ from lamindb_setup._delete import delete
129
163
 
130
- return register_()
164
+ return delete(instance, force=force)
131
165
 
132
166
 
133
167
  @main.command()
134
- @click.argument("action", type=click.Choice(["view"]))
135
- def schema(action: str):
136
- """View schema."""
137
- from lamindb_setup._schema import view
168
+ def logout():
169
+ """Logout."""
170
+ from lamindb_setup._setup_user import logout
138
171
 
139
- if action == "view":
140
- return view()
172
+ return logout()
173
+
174
+
175
+ @main.command()
176
+ @click.argument("url", type=str)
177
+ def get(url: str):
178
+ """Get an object from a lamin.ai URL."""
179
+ from lamin_cli._get import get
180
+
181
+ return get(url)
141
182
 
142
183
 
143
184
  @main.command()
@@ -155,21 +196,53 @@ def save(filepath: str, key: str, description: str):
155
196
 
156
197
 
157
198
  @main.command()
158
- @click.argument("url", type=str)
159
- def get(url: str):
160
- """Get an object from a lamin.ai URL."""
161
- from lamin_cli._get import get
199
+ def register():
200
+ """Register an instance on the hub."""
201
+ from lamindb_setup._register_instance import register as register_
162
202
 
163
- return get(url)
203
+ return register_()
164
204
 
165
205
 
166
206
  main.add_command(cache)
207
+
208
+
209
+ @main.command(name="set")
210
+ @click.argument(
211
+ "setting",
212
+ type=click.Choice(["auto-connect", "private-django-api"], case_sensitive=False),
213
+ )
214
+ @click.argument("value", type=click.BOOL)
215
+ def set_(setting: str, value: bool):
216
+ """Update settings.
217
+
218
+ - `auto-connect` → {attr}`~lamindb.setup.core.SetupSettings.auto_connect`
219
+ - `private-django-api` → {attr}`~lamindb.setup.core.SetupSettings.private_django_api`
220
+ """
221
+ from lamindb_setup import settings
222
+
223
+ if setting == "auto-connect":
224
+ settings.auto_connect = value
225
+ if setting == "private-django-api":
226
+ settings.private_django_api = value
227
+
228
+
167
229
  main.add_command(migrate)
168
230
 
169
231
 
232
+ @main.command()
233
+ @click.argument("action", type=click.Choice(["view"]))
234
+ def schema(action: str):
235
+ """View schema."""
236
+ from lamindb_setup._schema import view
237
+
238
+ if action == "view":
239
+ return view()
240
+
241
+
170
242
  # https://stackoverflow.com/questions/57810659/automatically-generate-all-help-documentation-for-click-commands
243
+ # https://claude.ai/chat/73c28487-bec3-4073-8110-50d1a2dd6b84
171
244
  def _generate_help():
172
- out: dict[str, str] = {}
245
+ out: dict[str, dict[str, str | None]] = {}
173
246
 
174
247
  def recursive_help(
175
248
  cmd: Command, parent: Optional[Context] = None, name: tuple[str, ...] = ()
@@ -177,7 +250,16 @@ def _generate_help():
177
250
  ctx = click.Context(cmd, info_name=cmd.name, parent=parent)
178
251
  assert cmd.name
179
252
  name = (*name, cmd.name)
180
- out[" ".join(name)] = cmd.get_help(ctx)
253
+ command_name = " ".join(name)
254
+
255
+ docstring = inspect.getdoc(cmd.callback)
256
+ usage = cmd.get_help(ctx).split("\n")[0]
257
+ options = cmd.get_help(ctx).split("Options:")[1]
258
+ out[command_name] = {
259
+ "help": usage + "\n\nOptions:" + options,
260
+ "docstring": docstring,
261
+ }
262
+
181
263
  for sub in getattr(cmd, "commands", {}).values():
182
264
  recursive_help(sub, ctx, name=name)
183
265
 
@@ -32,10 +32,10 @@ def get(url: str):
32
32
 
33
33
  if entity == "transform":
34
34
  transform = ln.Transform.get(uid)
35
- filepath_cache = transform.source_code.cache()
35
+ filepath_cache = transform._source_code_artifact.cache()
36
36
  target_filename = transform.key
37
- if not target_filename.endswith(transform.source_code.suffix):
38
- target_filename += transform.source_code.suffix
37
+ if not target_filename.endswith(transform._source_code_artifact.suffix):
38
+ target_filename += transform._source_code_artifact.suffix
39
39
  filepath_cache.rename(target_filename)
40
40
  logger.success(f"cached source code of transform {uid} as {target_filename}")
41
41
  elif entity == "artifact":
@@ -3,6 +3,8 @@ import lamindb as ln
3
3
  ln.settings.sync_git_repo = "https://github.com/laminlabs/lamin-cli"
4
4
  ln.settings.transform.stem_uid = "m5uCHTTpJnjQ"
5
5
  ln.settings.transform.version = "1"
6
+ ln.settings.transform.name = "My good script"
7
+
6
8
 
7
9
  if __name__ == "__main__":
8
10
  # we're using new_run here to mock the notebook situation
@@ -2,6 +2,7 @@ import lamindb as ln
2
2
 
3
3
  ln.settings.transform.stem_uid = "VFYCIuaw2GsX"
4
4
  ln.settings.transform.version = "1"
5
+ ln.settings.transform.name = "My good script 2"
5
6
 
6
7
  if __name__ == "__main__":
7
8
  # we're using new_run here to mock the notebook situation
@@ -15,7 +15,7 @@ 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 laminlabs/static-test-instance-private-sqlite")
18
+ exit_status = os.system("lamin load testuser1/static-test-instance-private-sqlite")
19
19
  assert exit_status == 0
20
20
  exit_status = os.system("lamin migrate deploy")
21
21
  assert exit_status == 0
@@ -81,8 +81,8 @@ def test_save_consecutive():
81
81
  # now, there is a transform record, but we're missing all artifacts
82
82
  transform = ln.Transform.filter(uid="hlsFXswrJjtt5zKv").one_or_none()
83
83
  assert transform is not None
84
- assert transform.latest_report is None
85
- assert transform.source_code is None
84
+ assert transform.latest_run.report is None
85
+ assert transform._source_code_artifact is None
86
86
  assert transform.latest_run.environment is None
87
87
 
88
88
  # and save again
@@ -97,11 +97,11 @@ def test_save_consecutive():
97
97
  # now, we have the associated artifacts
98
98
  transform = ln.Transform.filter(uid="hlsFXswrJjtt5zKv").one_or_none()
99
99
  assert transform is not None
100
- assert transform.latest_report.path.exists()
101
- assert transform.latest_run.report.path == transform.latest_report.path
102
- assert transform.source_code.hash == "5nc_HMjPvT9n26OWrjq6uQ"
100
+ assert transform.latest_run.report.path.exists()
101
+ assert transform.latest_run.report.path == transform.latest_run.report.path
102
+ assert transform._source_code_artifact.hash == "5nc_HMjPvT9n26OWrjq6uQ"
103
103
  assert transform.latest_run.environment.path.exists()
104
- assert transform.source_code.path.exists()
104
+ assert transform._source_code_artifact.path.exists()
105
105
 
106
106
  # now, assume the user modifies the notebook
107
107
  nb = read_notebook(notebook_path)
@@ -128,11 +128,11 @@ def test_save_consecutive():
128
128
  assert result.returncode == 0
129
129
  # the source code is overwritten with the edits, reflected in a new hash
130
130
  transform = ln.Transform.get("hlsFXswrJjtt5zKv")
131
- assert transform.latest_report.path.exists()
132
- assert transform.latest_run.report.path == transform.latest_report.path
133
- assert transform.source_code.hash == "ocLybD0Hv_L3NhhXgTyQcw"
131
+ assert transform.latest_run.report.path.exists()
132
+ assert transform.latest_run.report.path == transform.latest_run.report.path
133
+ assert transform._source_code_artifact.hash == "ocLybD0Hv_L3NhhXgTyQcw"
134
134
  assert transform.latest_run.environment.path.exists()
135
- assert transform.source_code.path.exists()
135
+ assert transform._source_code_artifact.path.exists()
136
136
 
137
137
  # get the the source code via command line
138
138
  result = subprocess.run(
@@ -37,9 +37,9 @@ def test_run_save_cache():
37
37
  assert "saved: Run" in result.stdout.decode()
38
38
 
39
39
  transform = ln.Transform.get("m5uCHTTpJnjQ")
40
- assert transform.source_code.hash == "-QN2dVdC8T3xWG8vBl-wew"
40
+ assert transform._source_code_artifact.hash == "T1zmmTJyeEpBxjaHcHcZdg"
41
41
  assert transform.latest_run.environment.path.exists()
42
- assert transform.source_code.path.exists()
42
+ assert transform._source_code_artifact.path.exists()
43
43
 
44
44
  # you can rerun the same script
45
45
  result = subprocess.run(
@@ -1,3 +0,0 @@
1
- """Lamin CLI."""
2
-
3
- __version__ = "0.14.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes