lamin_cli 1.3.0__py2.py3-none-any.whl → 1.4.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 +1 -1
- lamin_cli/__main__.py +20 -14
- lamin_cli/_load.py +3 -1
- lamin_cli/_save.py +120 -66
- {lamin_cli-1.3.0.dist-info → lamin_cli-1.4.0.dist-info}/METADATA +1 -1
- lamin_cli-1.4.0.dist-info/RECORD +14 -0
- lamin_cli-1.3.0.dist-info/RECORD +0 -14
- {lamin_cli-1.3.0.dist-info → lamin_cli-1.4.0.dist-info}/LICENSE +0 -0
- {lamin_cli-1.3.0.dist-info → lamin_cli-1.4.0.dist-info}/WHEEL +0 -0
- {lamin_cli-1.3.0.dist-info → lamin_cli-1.4.0.dist-info}/entry_points.txt +0 -0
lamin_cli/__init__.py
CHANGED
lamin_cli/__main__.py
CHANGED
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import os
|
|
5
|
+
import shutil
|
|
5
6
|
import sys
|
|
6
7
|
import warnings
|
|
7
8
|
from collections import OrderedDict
|
|
8
9
|
from functools import wraps
|
|
9
10
|
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
from pathlib import Path
|
|
10
12
|
from typing import TYPE_CHECKING
|
|
11
13
|
|
|
12
14
|
from lamindb_setup._init_instance import (
|
|
@@ -307,21 +309,28 @@ def get(entity: str, uid: str | None = None, key: str | None = None):
|
|
|
307
309
|
|
|
308
310
|
|
|
309
311
|
@main.command()
|
|
310
|
-
@click.argument("path", type=
|
|
311
|
-
@click.option("--key", type=str, default=None)
|
|
312
|
-
@click.option("--description", type=str, default=None)
|
|
313
|
-
@click.option("--stem-uid", type=str, default=None)
|
|
314
|
-
@click.option("--
|
|
315
|
-
|
|
312
|
+
@click.argument("path", type=str)
|
|
313
|
+
@click.option("--key", type=str, default=None, help="The key of the artifact or transform.")
|
|
314
|
+
@click.option("--description", type=str, default=None, help="A description of the artifact or transform.")
|
|
315
|
+
@click.option("--stem-uid", type=str, default=None, help="The stem uid of the artifact or transform.")
|
|
316
|
+
@click.option("--project", type=str, default=None, help="A valid project name or uid.")
|
|
317
|
+
@click.option("--registry", type=str, default=None, help="Either 'artifact' or 'transform'. If not passed, chooses based on path suffix.")
|
|
318
|
+
def save(path: str, key: str, description: str, stem_uid: str, project: str, registry: str):
|
|
316
319
|
"""Save a file or folder.
|
|
317
320
|
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
Example: Given a valid project name "my_project".
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
lamin save my_table.csv --key my_tables/my_table.csv --project my_project
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Note: Defaults to saving `.py`, `.ipynb`, `.R`, `.Rmd`, and `.qmd` as {class}`~lamindb.Transform` and
|
|
328
|
+
other file types and folders as {class}`~lamindb.Artifact`. You can enforce saving a file as
|
|
320
329
|
an {class}`~lamindb.Artifact` by passing `--registry artifact`.
|
|
321
330
|
"""
|
|
322
|
-
from lamin_cli._save import
|
|
331
|
+
from lamin_cli._save import save_from_path_cli
|
|
323
332
|
|
|
324
|
-
if
|
|
333
|
+
if save_from_path_cli(path, key, description, stem_uid, project, registry) is not None:
|
|
325
334
|
sys.exit(1)
|
|
326
335
|
|
|
327
336
|
|
|
@@ -343,9 +352,6 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
|
|
|
343
352
|
lamin run my_script.py --project my_project
|
|
344
353
|
```
|
|
345
354
|
"""
|
|
346
|
-
import shutil
|
|
347
|
-
from pathlib import Path
|
|
348
|
-
|
|
349
355
|
from lamin_cli.compute.modal import Runner
|
|
350
356
|
|
|
351
357
|
default_mount_dir = Path('./modal_mount_dir')
|
|
@@ -354,7 +360,7 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
|
|
|
354
360
|
|
|
355
361
|
shutil.copy(filepath, default_mount_dir)
|
|
356
362
|
|
|
357
|
-
filepath_in_mount_dir =
|
|
363
|
+
filepath_in_mount_dir = default_mount_dir / Path(filepath).name
|
|
358
364
|
|
|
359
365
|
package_list = []
|
|
360
366
|
if packages:
|
lamin_cli/_load.py
CHANGED
|
@@ -78,7 +78,9 @@ def load(
|
|
|
78
78
|
)
|
|
79
79
|
if bump_revision:
|
|
80
80
|
uid = transform.uid
|
|
81
|
-
if
|
|
81
|
+
if (
|
|
82
|
+
uid in new_content
|
|
83
|
+
): # this only hits if it has the full uid, not for the stem uid
|
|
82
84
|
new_uid = f"{uid[:-4]}{increment_base62(uid[-4:])}"
|
|
83
85
|
new_content = new_content.replace(uid, new_uid)
|
|
84
86
|
logger.important(f"updated uid: {uid} → {new_uid}")
|
lamin_cli/_save.py
CHANGED
|
@@ -2,26 +2,30 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import sys
|
|
5
|
-
from
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
+
import click
|
|
7
8
|
from lamin_utils import logger
|
|
8
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
def parse_uid_from_code(content: str, suffix: str) -> str | None:
|
|
11
15
|
if suffix == ".py":
|
|
12
16
|
track_pattern = re.compile(
|
|
13
|
-
r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{16})\1'
|
|
17
|
+
r'ln\.track\(\s*(?:transform\s*=\s*)?(["\'])([a-zA-Z0-9]{12,16})\1'
|
|
14
18
|
)
|
|
15
19
|
uid_pattern = re.compile(r'\.context\.uid\s*=\s*["\']([^"\']+)["\']')
|
|
16
20
|
elif suffix == ".ipynb":
|
|
17
21
|
track_pattern = re.compile(
|
|
18
|
-
r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{16})(?:\\"|\')'
|
|
22
|
+
r'ln\.track\(\s*(?:transform\s*=\s*)?(?:\\"|\')([a-zA-Z0-9]{12,16})(?:\\"|\')'
|
|
19
23
|
)
|
|
20
24
|
# backward compat
|
|
21
25
|
uid_pattern = re.compile(r'\.context\.uid\s*=\s*\\["\']([^"\']+)\\["\']')
|
|
22
26
|
elif suffix in {".R", ".qmd", ".Rmd"}:
|
|
23
27
|
track_pattern = re.compile(
|
|
24
|
-
r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{16})\1'
|
|
28
|
+
r'track\(\s*(?:transform\s*=\s*)?([\'"])([a-zA-Z0-9]{12,16})\1'
|
|
25
29
|
)
|
|
26
30
|
uid_pattern = None
|
|
27
31
|
else:
|
|
@@ -42,17 +46,16 @@ def parse_uid_from_code(content: str, suffix: str) -> str | None:
|
|
|
42
46
|
return uid
|
|
43
47
|
|
|
44
48
|
|
|
45
|
-
def
|
|
46
|
-
|
|
49
|
+
def save_from_path_cli(
|
|
50
|
+
path: Path | str,
|
|
47
51
|
key: str | None,
|
|
48
52
|
description: str | None,
|
|
49
53
|
stem_uid: str | None,
|
|
54
|
+
project: str | None,
|
|
50
55
|
registry: str | None,
|
|
51
56
|
) -> str | None:
|
|
52
57
|
import lamindb_setup as ln_setup
|
|
53
|
-
|
|
54
|
-
if not isinstance(filepath, Path):
|
|
55
|
-
filepath = Path(filepath)
|
|
58
|
+
from lamindb_setup.core.upath import LocalPathClasses, UPath, create_path
|
|
56
59
|
|
|
57
60
|
# this will be gone once we get rid of lamin load or enable loading multiple
|
|
58
61
|
# instances sequentially
|
|
@@ -67,37 +70,36 @@ def save_from_filepath_cli(
|
|
|
67
70
|
|
|
68
71
|
ln_setup.settings.auto_connect = auto_connect_state
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
filepath.with_suffix(".html").exists()
|
|
78
|
-
or filepath.with_suffix(".nb.html").exists()
|
|
79
|
-
):
|
|
80
|
-
raise SystemExit(
|
|
81
|
-
f"Please export your {filepath.suffix} file as an html file here"
|
|
82
|
-
f" {filepath.with_suffix('.html')}"
|
|
83
|
-
)
|
|
84
|
-
if (
|
|
85
|
-
filepath.with_suffix(".html").exists()
|
|
86
|
-
and filepath.with_suffix(".nb.html").exists()
|
|
87
|
-
):
|
|
88
|
-
raise SystemExit(
|
|
89
|
-
f"Please delete one of\n - {filepath.with_suffix('.html')}\n -"
|
|
90
|
-
f" {filepath.with_suffix('.nb.html')}"
|
|
91
|
-
)
|
|
73
|
+
# this allows to have the correct treatment of credentials in case of cloud paths
|
|
74
|
+
path = create_path(path)
|
|
75
|
+
# isinstance is needed to cast the type of path to UPath
|
|
76
|
+
# to avoid mypy erors
|
|
77
|
+
assert isinstance(path, UPath)
|
|
78
|
+
if not path.exists():
|
|
79
|
+
raise click.BadParameter(f"Path {path} does not exist", param_hint="path")
|
|
92
80
|
|
|
93
81
|
if registry is None:
|
|
82
|
+
suffixes_transform = {
|
|
83
|
+
"py": {".py", ".ipynb"},
|
|
84
|
+
"R": {".R", ".qmd", ".Rmd"},
|
|
85
|
+
}
|
|
94
86
|
registry = (
|
|
95
87
|
"transform"
|
|
96
|
-
if
|
|
97
|
-
in suffixes_transform["py"].union(suffixes_transform["R"])
|
|
88
|
+
if path.suffix in suffixes_transform["py"].union(suffixes_transform["R"])
|
|
98
89
|
else "artifact"
|
|
99
90
|
)
|
|
100
91
|
|
|
92
|
+
if project is not None:
|
|
93
|
+
project_record = ln.Project.filter(
|
|
94
|
+
ln.Q(name=project) | ln.Q(uid=project)
|
|
95
|
+
).one_or_none()
|
|
96
|
+
if project_record is None:
|
|
97
|
+
raise ln.errors.InvalidArgument(
|
|
98
|
+
f"Project '{project}' not found, either create it with `ln.Project(name='...').save()` or fix typos."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
is_cloud_path = not isinstance(path, LocalPathClasses)
|
|
102
|
+
|
|
101
103
|
if registry == "artifact":
|
|
102
104
|
ln.settings.creation.artifact_silence_missing_run_warning = True
|
|
103
105
|
revises = None
|
|
@@ -109,56 +111,108 @@ def save_from_filepath_cli(
|
|
|
109
111
|
)
|
|
110
112
|
if revises is None:
|
|
111
113
|
raise ln.errors.InvalidArgument("The stem uid is not found.")
|
|
114
|
+
|
|
115
|
+
if is_cloud_path:
|
|
116
|
+
if key is not None:
|
|
117
|
+
logger.error("Do not pass --key for cloud paths")
|
|
118
|
+
return "key-with-cloud-path"
|
|
112
119
|
elif key is None and description is None:
|
|
113
120
|
logger.error("Please pass a key or description via --key or --description")
|
|
114
121
|
return "missing-key-or-description"
|
|
122
|
+
|
|
115
123
|
artifact = ln.Artifact(
|
|
116
|
-
|
|
124
|
+
path, key=key, description=description, revises=revises
|
|
117
125
|
).save()
|
|
118
126
|
logger.important(f"saved: {artifact}")
|
|
119
127
|
logger.important(f"storage path: {artifact.path}")
|
|
120
128
|
if ln_setup.settings.storage.type == "s3":
|
|
121
129
|
logger.important(f"storage url: {artifact.path.to_url()}")
|
|
130
|
+
if project is not None:
|
|
131
|
+
artifact.projects.add(project_record)
|
|
132
|
+
logger.important(f"labeled with project: {project_record.name}")
|
|
122
133
|
if ln_setup.settings.instance.is_remote:
|
|
123
134
|
slug = ln_setup.settings.instance.slug
|
|
124
135
|
logger.important(f"go to: https://lamin.ai/{slug}/artifact/{artifact.uid}")
|
|
125
136
|
return None
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
|
|
138
|
+
if registry == "transform":
|
|
139
|
+
if is_cloud_path:
|
|
140
|
+
logger.error("Can not register a transform from a cloud path")
|
|
141
|
+
return "transform-with-cloud-path"
|
|
142
|
+
|
|
143
|
+
if path.suffix in {".qmd", ".Rmd"}:
|
|
144
|
+
html_file_exists = path.with_suffix(".html").exists()
|
|
145
|
+
nb_html_file_exists = path.with_suffix(".nb.html").exists()
|
|
146
|
+
|
|
147
|
+
if not html_file_exists and not nb_html_file_exists:
|
|
134
148
|
logger.error(
|
|
135
|
-
f"
|
|
136
|
-
"
|
|
149
|
+
f"Please export your {path.suffix} file as an html file here"
|
|
150
|
+
f" {path.with_suffix('.html')}"
|
|
137
151
|
)
|
|
138
|
-
return "
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
return "export-qmd-Rmd-as-html"
|
|
153
|
+
elif html_file_exists and nb_html_file_exists:
|
|
154
|
+
logger.error(
|
|
155
|
+
f"Please delete one of\n - {path.with_suffix('.html')}\n -"
|
|
156
|
+
f" {path.with_suffix('.nb.html')}"
|
|
157
|
+
)
|
|
158
|
+
return "delete-html-or-nb-html"
|
|
159
|
+
|
|
160
|
+
with path.open() as file:
|
|
161
|
+
content = file.read()
|
|
162
|
+
uid = parse_uid_from_code(content, path.suffix)
|
|
163
|
+
|
|
164
|
+
if uid is not None:
|
|
165
|
+
logger.important(f"mapped '{path.name}' on uid '{uid}'")
|
|
166
|
+
if len(uid) == 16:
|
|
167
|
+
# is full uid
|
|
168
|
+
transform = ln.Transform.filter(uid=uid).one_or_none()
|
|
169
|
+
else:
|
|
170
|
+
# is stem uid
|
|
171
|
+
if stem_uid is not None:
|
|
172
|
+
assert stem_uid == uid, (
|
|
173
|
+
"passed stem uid and parsed stem uid do not match"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
stem_uid = uid
|
|
177
|
+
transform = (
|
|
178
|
+
ln.Transform.filter(uid__startswith=uid)
|
|
144
179
|
.order_by("-created_at")
|
|
145
180
|
.first()
|
|
146
181
|
)
|
|
147
|
-
if
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
transform = ln.Transform.filter(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
if transform is None:
|
|
183
|
+
uid = f"{stem_uid}0000"
|
|
184
|
+
else:
|
|
185
|
+
# TODO: account for folders and hash equivalence as we do in ln.track()
|
|
186
|
+
transform = ln.Transform.filter(key=path.name, is_latest=True).one_or_none()
|
|
187
|
+
revises = None
|
|
188
|
+
if stem_uid is not None:
|
|
189
|
+
revises = (
|
|
190
|
+
ln.Transform.filter(uid__startswith=stem_uid)
|
|
191
|
+
.order_by("-created_at")
|
|
192
|
+
.first()
|
|
193
|
+
)
|
|
194
|
+
if revises is None:
|
|
195
|
+
raise ln.errors.InvalidArgument("The stem uid is not found.")
|
|
196
|
+
if transform is None:
|
|
197
|
+
if path.suffix == ".ipynb":
|
|
198
|
+
from nbproject.dev import read_notebook
|
|
199
|
+
from nbproject.dev._meta_live import get_title
|
|
200
|
+
|
|
201
|
+
nb = read_notebook(path)
|
|
202
|
+
description = get_title(nb)
|
|
203
|
+
else:
|
|
204
|
+
description = None
|
|
205
|
+
transform = ln.Transform(
|
|
206
|
+
uid=uid,
|
|
207
|
+
description=description,
|
|
208
|
+
key=path.name,
|
|
209
|
+
type="script" if path.suffix in {".R", ".py"} else "notebook",
|
|
210
|
+
revises=revises,
|
|
211
|
+
).save()
|
|
212
|
+
logger.important(f"created Transform('{transform.uid}')")
|
|
213
|
+
if project is not None:
|
|
214
|
+
transform.projects.add(project_record)
|
|
215
|
+
logger.important(f"labeled with project: {project_record.name}")
|
|
162
216
|
# latest run of this transform by user
|
|
163
217
|
run = ln.Run.filter(transform=transform).order_by("-started_at").first()
|
|
164
218
|
if run is not None and run.created_by.id != ln_setup.settings.user.id:
|
|
@@ -177,7 +231,7 @@ def save_from_filepath_cli(
|
|
|
177
231
|
return_code = save_context_core(
|
|
178
232
|
run=run,
|
|
179
233
|
transform=transform,
|
|
180
|
-
filepath=
|
|
234
|
+
filepath=path,
|
|
181
235
|
from_cli=True,
|
|
182
236
|
)
|
|
183
237
|
return return_code
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
lamin_cli/__init__.py,sha256=YeP3WxwX7_OcORoNOBGWEDB-_0yt4bX9J3ib92jScow,40
|
|
2
|
+
lamin_cli/__main__.py,sha256=1GdJbaTQQmy_xPu-6QWglewBVkXdlGe37PbzSWPKCBM,12813
|
|
3
|
+
lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
|
|
4
|
+
lamin_cli/_load.py,sha256=ow9IPQog_mPvjoGj1Bn08l4AG1YyRNnEyBpQvQkfLZs,8182
|
|
5
|
+
lamin_cli/_migration.py,sha256=xQi6mwnpBzY5wcv1-TJhveD7a3XJIlpiYx6Z3AJ1NF0,1063
|
|
6
|
+
lamin_cli/_save.py,sha256=LvxEIXKgUPvpwhjgRyxi1MTDRcL6rlAMbMQLfPtfDhs,9047
|
|
7
|
+
lamin_cli/_settings.py,sha256=O2tecCf5EIZu98ima4DTJujo4KuywckOLgw8c-Ke3dY,1142
|
|
8
|
+
lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
|
|
10
|
+
lamin_cli-1.4.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
|
|
11
|
+
lamin_cli-1.4.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
+
lamin_cli-1.4.0.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
|
|
13
|
+
lamin_cli-1.4.0.dist-info/METADATA,sha256=GZKhv9Y-CgSgG32doVUMA2a42IZtjKyo0F-8YwgptCQ,337
|
|
14
|
+
lamin_cli-1.4.0.dist-info/RECORD,,
|
lamin_cli-1.3.0.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
lamin_cli/__init__.py,sha256=M6lfPWq9Iw7_uUcLoHjPp1SVuvhsIU0NkQQDt-EYpIc,40
|
|
2
|
-
lamin_cli/__main__.py,sha256=2CZdjlQK0nHrMyDpNvKn7iQ34Owlbwhl4mmavc5yXgc,12392
|
|
3
|
-
lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
|
|
4
|
-
lamin_cli/_load.py,sha256=lMhV9AMkybjvj4VChJE_v7IMy6qGqisFlo40BJUibsA,8087
|
|
5
|
-
lamin_cli/_migration.py,sha256=xQi6mwnpBzY5wcv1-TJhveD7a3XJIlpiYx6Z3AJ1NF0,1063
|
|
6
|
-
lamin_cli/_save.py,sha256=JQQxby2PP6SfUb1FZhnwNnp0fkBqTj-UnbGql4oPt8g,6948
|
|
7
|
-
lamin_cli/_settings.py,sha256=O2tecCf5EIZu98ima4DTJujo4KuywckOLgw8c-Ke3dY,1142
|
|
8
|
-
lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
|
|
10
|
-
lamin_cli-1.3.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
|
|
11
|
-
lamin_cli-1.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
-
lamin_cli-1.3.0.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
|
|
13
|
-
lamin_cli-1.3.0.dist-info/METADATA,sha256=m6_U_cQGfZ-2HjU-u_1frMM-Q6ATjQCObBkiebqNzMs,337
|
|
14
|
-
lamin_cli-1.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|