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.
- lamin_cli-0.16.0/.github/workflows/doc-changes.yml +23 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/.pre-commit-config.yaml +1 -1
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/PKG-INFO +1 -1
- lamin_cli-0.16.0/lamin_cli/__init__.py +3 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/__main__.py +149 -67
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_get.py +3 -3
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/scripts/run-track-and-finish-sync-git.py +2 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/scripts/run-track-and-finish.py +1 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_migrate.py +1 -1
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_save_notebooks.py +10 -10
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_save_scripts.py +2 -2
- lamin_cli-0.14.0/lamin_cli/__init__.py +0 -3
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/.gitignore +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/README.md +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_cache.py +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_migration.py +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/lamin_cli/_save.py +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/pyproject.toml +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/conftest.py +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/notebooks/not-initialized.ipynb +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/scripts/merely-import-lamindb.py +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_cli.py +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_get.py +0 -0
- {lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/test_multi_process.py +0 -0
- {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
|
|
@@ -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
|
-
@
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
164
|
+
return delete(instance, force=force)
|
|
131
165
|
|
|
132
166
|
|
|
133
167
|
@main.command()
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
from lamindb_setup._schema import view
|
|
168
|
+
def logout():
|
|
169
|
+
"""Logout."""
|
|
170
|
+
from lamindb_setup._setup_user import logout
|
|
138
171
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
35
|
+
filepath_cache = transform._source_code_artifact.cache()
|
|
36
36
|
target_filename = transform.key
|
|
37
|
-
if not target_filename.endswith(transform.
|
|
38
|
-
target_filename += transform.
|
|
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
|
|
@@ -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
|
|
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.
|
|
85
|
-
assert transform.
|
|
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.
|
|
101
|
-
assert transform.latest_run.report.path == transform.
|
|
102
|
-
assert transform.
|
|
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.
|
|
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.
|
|
132
|
-
assert transform.latest_run.report.path == transform.
|
|
133
|
-
assert transform.
|
|
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.
|
|
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.
|
|
40
|
+
assert transform._source_code_artifact.hash == "T1zmmTJyeEpBxjaHcHcZdg"
|
|
41
41
|
assert transform.latest_run.environment.path.exists()
|
|
42
|
-
assert transform.
|
|
42
|
+
assert transform._source_code_artifact.path.exists()
|
|
43
43
|
|
|
44
44
|
# you can rerun the same script
|
|
45
45
|
result = subprocess.run(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lamin_cli-0.14.0 → lamin_cli-0.16.0}/tests/notebooks/with-title-and-initialized-consecutive.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|