lamin_cli 1.11.0__py2.py3-none-any.whl → 1.12.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/_load.py CHANGED
@@ -1,203 +1,203 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- import shutil
5
- from pathlib import Path
6
-
7
- from lamin_utils import logger
8
-
9
- from ._context import get_current_run_file
10
- from ._save import infer_registry_from_path, parse_title_r_notebook
11
- from .urls import decompose_url
12
-
13
-
14
- def load(
15
- entity: str | None = None,
16
- uid: str | None = None,
17
- key: str | None = None,
18
- with_env: bool = False,
19
- ):
20
- """Load artifact, collection, or transform from LaminDB.
21
-
22
- Args:
23
- entity: URL containing 'lamin', or 'artifact', 'collection', or 'transform'
24
- uid: Unique identifier (prefix matching supported)
25
- key: Key identifier
26
- with_env: If True, also load environment requirements file for transforms
27
-
28
- Returns:
29
- Path to loaded transform, or None for artifacts/collections
30
- """
31
- import lamindb_setup as ln_setup
32
-
33
- if entity is None:
34
- if key is None:
35
- raise SystemExit("Either entity or key has to be provided.")
36
- else:
37
- entity = infer_registry_from_path(key)
38
-
39
- if entity.startswith("https://") and "lamin" in entity:
40
- url = entity
41
- instance, entity, uid = decompose_url(url)
42
- elif entity not in {"artifact", "transform", "collection"}:
43
- raise SystemExit(
44
- "Entity has to be a laminhub URL or 'artifact', 'collection', or 'transform'"
45
- )
46
- else:
47
- instance = ln_setup.settings.instance.slug
48
-
49
- ln_setup.connect(instance)
50
- import lamindb as ln
51
-
52
- current_run = None
53
- if get_current_run_file().exists():
54
- current_run = ln.Run.get(uid=get_current_run_file().read_text().strip())
55
-
56
- def script_to_notebook(
57
- transform: ln.Transform, notebook_path: Path, bump_revision: bool = False
58
- ) -> None:
59
- import jupytext
60
- from lamin_utils._base62 import increment_base62
61
-
62
- if notebook_path.suffix == ".ipynb":
63
- # below is backward compat
64
- if "# # transform.name" in transform.source_code:
65
- new_content = transform.source_code.replace(
66
- "# # transform.name", f"# # {transform.description}"
67
- )
68
- elif transform.source_code.startswith("# %% [markdown]"):
69
- source_code_split = transform.source_code.split("\n")
70
- if source_code_split[1] == "#":
71
- source_code_split[1] = f"# # {transform.description}"
72
- new_content = "\n".join(source_code_split)
73
- else:
74
- new_content = transform.source_code
75
- else: # R notebook
76
- new_content = transform.source_code
77
- current_title = parse_title_r_notebook(new_content)
78
- if current_title is not None and current_title != transform.description:
79
- pattern = r'^(---\n.*?title:\s*)"([^"]*)"(.*?---)'
80
- replacement = f'\\1"{transform.description}"\\3'
81
- new_content = re.sub(
82
- pattern,
83
- replacement,
84
- new_content,
85
- flags=re.DOTALL | re.MULTILINE,
86
- )
87
- logger.important(
88
- f"updated title to match description: {current_title} →"
89
- f" {transform.description}"
90
- )
91
- if bump_revision:
92
- uid = transform.uid
93
- if (
94
- uid in new_content
95
- ): # this only hits if it has the full uid, not for the stem uid
96
- new_uid = f"{uid[:-4]}{increment_base62(uid[-4:])}"
97
- new_content = new_content.replace(uid, new_uid)
98
- logger.important(f"updated uid: {uid} → {new_uid}")
99
- if notebook_path.suffix == ".ipynb":
100
- notebook = jupytext.reads(new_content, fmt="py:percent")
101
- jupytext.write(notebook, notebook_path)
102
- else:
103
- notebook_path.write_text(new_content)
104
-
105
- query_by_uid = uid is not None
106
-
107
- match entity:
108
- case "transform":
109
- if query_by_uid:
110
- # we don't use .get here because DoesNotExist is hard to catch
111
- # due to private django API
112
- # here full uid is not expected anymore as before
113
- # via ln.Transform.objects.get(uid=uid)
114
- transforms = ln.Transform.objects.filter(uid__startswith=uid)
115
- else:
116
- # if below, we take is_latest=True as the criterion, we might get draft notebooks
117
- # hence, we use source_code__isnull=False and order by created_at instead
118
- transforms = ln.Transform.objects.filter(
119
- key=key, source_code__isnull=False
120
- )
121
-
122
- if (n_transforms := len(transforms)) == 0:
123
- err_msg = f"uid {uid}" if query_by_uid else f"key={key} and source_code"
124
- raise SystemExit(f"Transform with {err_msg} does not exist.")
125
-
126
- if n_transforms > 1:
127
- transforms = transforms.order_by("-created_at")
128
- transform = transforms.first()
129
-
130
- target_path = Path(transform.key)
131
- if ln_setup.settings.dev_dir is not None:
132
- target_path = ln_setup.settings.dev_dir / target_path
133
- if len(target_path.parents) > 1:
134
- target_path.parent.mkdir(parents=True, exist_ok=True)
135
- if target_path.exists():
136
- response = input(f"! {target_path} exists: replace? (y/n)")
137
- if response != "y":
138
- raise SystemExit("Aborted.")
139
-
140
- if transform.source_code is not None:
141
- if target_path.suffix in (".ipynb", ".Rmd", ".qmd"):
142
- script_to_notebook(transform, target_path, bump_revision=True)
143
- else:
144
- target_path.write_text(transform.source_code)
145
- else:
146
- raise SystemExit("No source code available for this transform.")
147
-
148
- logger.important(f"{transform.type} is here: {target_path}")
149
-
150
- if with_env:
151
- ln.settings.track_run_inputs = False
152
- if (
153
- transform.latest_run is not None
154
- and transform.latest_run.environment is not None
155
- ):
156
- filepath_env_cache = transform.latest_run.environment.cache()
157
- target_env_filename = (
158
- target_path.parent / f"{target_path.stem}__requirements.txt"
159
- )
160
- shutil.move(filepath_env_cache, target_env_filename)
161
- logger.important(f"environment is here: {target_env_filename}")
162
- else:
163
- logger.warning(
164
- "latest transform run with environment doesn't exist"
165
- )
166
-
167
- return target_path
168
- case "artifact" | "collection":
169
- ln.settings.track_run_inputs = False
170
-
171
- EntityClass = ln.Artifact if entity == "artifact" else ln.Collection
172
-
173
- # we don't use .get here because DoesNotExist is hard to catch due to private django API
174
- # we use `.objects` here because we don't want to exclude kind = __lamindb_run__ artifacts
175
- if query_by_uid:
176
- entities = EntityClass.objects.filter(uid__startswith=uid)
177
- else:
178
- entities = EntityClass.objects.filter(key=key)
179
-
180
- if (n_entities := len(entities)) == 0:
181
- err_msg = f"uid={uid}" if query_by_uid else f"key={key}"
182
- raise SystemExit(
183
- f"{entity.capitalize()} with {err_msg} does not exist."
184
- )
185
-
186
- if n_entities > 1:
187
- entities = entities.order_by("-created_at")
188
-
189
- entity_obj = entities.first()
190
- cache_path = entity_obj.cache(is_run_input=current_run)
191
-
192
- # collection gives us a list of paths
193
- if isinstance(cache_path, list):
194
- logger.important(f"{entity} paths ({len(cache_path)} files):")
195
- for i, path in enumerate(cache_path):
196
- if i < 5 or i >= len(cache_path) - 5:
197
- logger.important(f" [{i + 1}/{len(cache_path)}] {path}")
198
- elif i == 5:
199
- logger.important(f" ... {len(cache_path) - 10} more files ...")
200
- else:
201
- logger.important(f"{entity} is here: {cache_path}")
202
- case _:
203
- raise AssertionError(f"unknown entity {entity}")
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ from lamin_utils import logger
8
+
9
+ from ._context import get_current_run_file
10
+ from ._save import infer_registry_from_path, parse_title_r_notebook
11
+ from .urls import decompose_url
12
+
13
+
14
+ def load(
15
+ entity: str | None = None,
16
+ uid: str | None = None,
17
+ key: str | None = None,
18
+ with_env: bool = False,
19
+ ):
20
+ """Load artifact, collection, or transform from LaminDB.
21
+
22
+ Args:
23
+ entity: URL containing 'lamin', or 'artifact', 'collection', or 'transform'
24
+ uid: Unique identifier (prefix matching supported)
25
+ key: Key identifier
26
+ with_env: If True, also load environment requirements file for transforms
27
+
28
+ Returns:
29
+ Path to loaded transform, or None for artifacts/collections
30
+ """
31
+ import lamindb_setup as ln_setup
32
+
33
+ if entity is None:
34
+ if key is None:
35
+ raise SystemExit("Either entity or key has to be provided.")
36
+ else:
37
+ entity = infer_registry_from_path(key)
38
+
39
+ if entity.startswith("https://") and "lamin" in entity:
40
+ url = entity
41
+ instance, entity, uid = decompose_url(url)
42
+ elif entity not in {"artifact", "transform", "collection"}:
43
+ raise SystemExit(
44
+ "Entity has to be a laminhub URL or 'artifact', 'collection', or 'transform'"
45
+ )
46
+ else:
47
+ instance = ln_setup.settings.instance.slug
48
+
49
+ ln_setup.connect(instance)
50
+ import lamindb as ln
51
+
52
+ current_run = None
53
+ if get_current_run_file().exists():
54
+ current_run = ln.Run.get(uid=get_current_run_file().read_text().strip())
55
+
56
+ def script_to_notebook(
57
+ transform: ln.Transform, notebook_path: Path, bump_revision: bool = False
58
+ ) -> None:
59
+ import jupytext
60
+ from lamin_utils._base62 import increment_base62
61
+
62
+ if notebook_path.suffix == ".ipynb":
63
+ # below is backward compat
64
+ if "# # transform.name" in transform.source_code:
65
+ new_content = transform.source_code.replace(
66
+ "# # transform.name", f"# # {transform.description}"
67
+ )
68
+ elif transform.source_code.startswith("# %% [markdown]"):
69
+ source_code_split = transform.source_code.split("\n")
70
+ if source_code_split[1] == "#":
71
+ source_code_split[1] = f"# # {transform.description}"
72
+ new_content = "\n".join(source_code_split)
73
+ else:
74
+ new_content = transform.source_code
75
+ else: # R notebook
76
+ new_content = transform.source_code
77
+ current_title = parse_title_r_notebook(new_content)
78
+ if current_title is not None and current_title != transform.description:
79
+ pattern = r'^(---\n.*?title:\s*)"([^"]*)"(.*?---)'
80
+ replacement = f'\\1"{transform.description}"\\3'
81
+ new_content = re.sub(
82
+ pattern,
83
+ replacement,
84
+ new_content,
85
+ flags=re.DOTALL | re.MULTILINE,
86
+ )
87
+ logger.important(
88
+ f"updated title to match description: {current_title} →"
89
+ f" {transform.description}"
90
+ )
91
+ if bump_revision:
92
+ uid = transform.uid
93
+ if (
94
+ uid in new_content
95
+ ): # this only hits if it has the full uid, not for the stem uid
96
+ new_uid = f"{uid[:-4]}{increment_base62(uid[-4:])}"
97
+ new_content = new_content.replace(uid, new_uid)
98
+ logger.important(f"updated uid: {uid} → {new_uid}")
99
+ if notebook_path.suffix == ".ipynb":
100
+ notebook = jupytext.reads(new_content, fmt="py:percent")
101
+ jupytext.write(notebook, notebook_path)
102
+ else:
103
+ notebook_path.write_text(new_content)
104
+
105
+ query_by_uid = uid is not None
106
+
107
+ match entity:
108
+ case "transform":
109
+ if query_by_uid:
110
+ # we don't use .get here because DoesNotExist is hard to catch
111
+ # due to private django API
112
+ # here full uid is not expected anymore as before
113
+ # via ln.Transform.objects.get(uid=uid)
114
+ transforms = ln.Transform.objects.filter(uid__startswith=uid)
115
+ else:
116
+ # if below, we take is_latest=True as the criterion, we might get draft notebooks
117
+ # hence, we use source_code__isnull=False and order by created_at instead
118
+ transforms = ln.Transform.objects.filter(
119
+ key=key, source_code__isnull=False
120
+ )
121
+
122
+ if (n_transforms := len(transforms)) == 0:
123
+ err_msg = f"uid {uid}" if query_by_uid else f"key={key} and source_code"
124
+ raise SystemExit(f"Transform with {err_msg} does not exist.")
125
+
126
+ if n_transforms > 1:
127
+ transforms = transforms.order_by("-created_at")
128
+ transform = transforms.first()
129
+
130
+ target_path = Path(transform.key)
131
+ if ln_setup.settings.dev_dir is not None:
132
+ target_path = ln_setup.settings.dev_dir / target_path
133
+ if len(target_path.parents) > 1:
134
+ target_path.parent.mkdir(parents=True, exist_ok=True)
135
+ if target_path.exists():
136
+ response = input(f"! {target_path} exists: replace? (y/n)")
137
+ if response != "y":
138
+ raise SystemExit("Aborted.")
139
+
140
+ if transform.source_code is not None:
141
+ if target_path.suffix in (".ipynb", ".Rmd", ".qmd"):
142
+ script_to_notebook(transform, target_path, bump_revision=True)
143
+ else:
144
+ target_path.write_text(transform.source_code)
145
+ else:
146
+ raise SystemExit("No source code available for this transform.")
147
+
148
+ logger.important(f"{transform.type} is here: {target_path}")
149
+
150
+ if with_env:
151
+ ln.settings.track_run_inputs = False
152
+ if (
153
+ transform.latest_run is not None
154
+ and transform.latest_run.environment is not None
155
+ ):
156
+ filepath_env_cache = transform.latest_run.environment.cache()
157
+ target_env_filename = (
158
+ target_path.parent / f"{target_path.stem}__requirements.txt"
159
+ )
160
+ shutil.move(filepath_env_cache, target_env_filename)
161
+ logger.important(f"environment is here: {target_env_filename}")
162
+ else:
163
+ logger.warning(
164
+ "latest transform run with environment doesn't exist"
165
+ )
166
+
167
+ return target_path
168
+ case "artifact" | "collection":
169
+ ln.settings.track_run_inputs = False
170
+
171
+ EntityClass = ln.Artifact if entity == "artifact" else ln.Collection
172
+
173
+ # we don't use .get here because DoesNotExist is hard to catch due to private django API
174
+ # we use `.objects` here because we don't want to exclude kind = __lamindb_run__ artifacts
175
+ if query_by_uid:
176
+ entities = EntityClass.objects.filter(uid__startswith=uid)
177
+ else:
178
+ entities = EntityClass.objects.filter(key=key)
179
+
180
+ if (n_entities := len(entities)) == 0:
181
+ err_msg = f"uid={uid}" if query_by_uid else f"key={key}"
182
+ raise SystemExit(
183
+ f"{entity.capitalize()} with {err_msg} does not exist."
184
+ )
185
+
186
+ if n_entities > 1:
187
+ entities = entities.order_by("-created_at")
188
+
189
+ entity_obj = entities.first()
190
+ cache_path = entity_obj.cache(is_run_input=current_run)
191
+
192
+ # collection gives us a list of paths
193
+ if isinstance(cache_path, list):
194
+ logger.important(f"{entity} paths ({len(cache_path)} files):")
195
+ for i, path in enumerate(cache_path):
196
+ if i < 5 or i >= len(cache_path) - 5:
197
+ logger.important(f" [{i + 1}/{len(cache_path)}] {path}")
198
+ elif i == 5:
199
+ logger.important(f" ... {len(cache_path) - 10} more files ...")
200
+ else:
201
+ logger.important(f"{entity} is here: {cache_path}")
202
+ case _:
203
+ raise AssertionError(f"unknown entity {entity}")
lamin_cli/_migration.py CHANGED
@@ -1,50 +1,50 @@
1
- from __future__ import annotations
2
-
3
- import os
4
-
5
- if os.environ.get("NO_RICH"):
6
- import click as click
7
- else:
8
- import rich_click as click
9
-
10
-
11
- @click.group()
12
- def migrate():
13
- """Manage database schema migrations."""
14
-
15
-
16
- @migrate.command("create")
17
- def create():
18
- """Create a new migration."""
19
- from lamindb_setup._migrate import migrate
20
-
21
- return migrate.create()
22
-
23
-
24
- @migrate.command("deploy")
25
- @click.option("--package-name", type=str, default=None)
26
- @click.option("--number", type=str, default=None)
27
- def deploy(package_name: str | None = None, number: str | None = None):
28
- """Deploy migrations."""
29
- from lamindb_setup._migrate import migrate
30
-
31
- return migrate.deploy(package_name=package_name, number=number)
32
-
33
-
34
- @migrate.command("squash")
35
- @click.option("--package-name", type=str, default=None)
36
- @click.option("--end-number", type=str, default=None)
37
- @click.option("--start-number", type=str, default=None)
38
- def squash(
39
- package_name: str | None,
40
- end_number: str | None,
41
- start_number: str | None,
42
- ):
43
- """Squash migrations."""
44
- from lamindb_setup._migrate import migrate
45
-
46
- return migrate.squash(
47
- package_name=package_name,
48
- migration_nr=end_number,
49
- start_migration_nr=start_number,
50
- )
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ if os.environ.get("NO_RICH"):
6
+ import click as click
7
+ else:
8
+ import rich_click as click
9
+
10
+
11
+ @click.group()
12
+ def migrate():
13
+ """Manage database schema migrations."""
14
+
15
+
16
+ @migrate.command("create")
17
+ def create():
18
+ """Create a new migration."""
19
+ from lamindb_setup._migrate import migrate
20
+
21
+ return migrate.create()
22
+
23
+
24
+ @migrate.command("deploy")
25
+ @click.option("--package-name", type=str, default=None)
26
+ @click.option("--number", type=str, default=None)
27
+ def deploy(package_name: str | None = None, number: str | None = None):
28
+ """Deploy migrations."""
29
+ from lamindb_setup._migrate import migrate
30
+
31
+ return migrate.deploy(package_name=package_name, number=number)
32
+
33
+
34
+ @migrate.command("squash")
35
+ @click.option("--package-name", type=str, default=None)
36
+ @click.option("--end-number", type=str, default=None)
37
+ @click.option("--start-number", type=str, default=None)
38
+ def squash(
39
+ package_name: str | None,
40
+ end_number: str | None,
41
+ start_number: str | None,
42
+ ):
43
+ """Squash migrations."""
44
+ from lamindb_setup._migrate import migrate
45
+
46
+ return migrate.squash(
47
+ package_name=package_name,
48
+ migration_nr=end_number,
49
+ start_migration_nr=start_number,
50
+ )