ap-client 0.6.1.dev9__tar.gz → 0.6.1.dev11__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.
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/PKG-INFO +1 -1
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/fs_commands.py +25 -7
- ap_client-0.6.1.dev11/ap_client/fs_transfer.py +245 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/irepo_sdk.py +39 -1
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/pyproject.toml +1 -1
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/__init__.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/akpool_commands.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/api.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/cli.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/config.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/dataset_commands.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/dataset_name.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/dataset_register.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/dfs_commands.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/exporter.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/instance_commands.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/instance_options.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/managed_ak.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/manifest_view.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/ossutil_hash.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/ossutil_io.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/profile_commands.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/repo_read_guard.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/share_api.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/split_publish.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/tbb.py +0 -0
- {ap_client-0.6.1.dev9 → ap_client-0.6.1.dev11}/ap_client/waiter.py +0 -0
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
命令与后端映射:
|
|
4
4
|
|
|
5
5
|
* ``fs ls`` → ``repo.list_user_data(uid, subpath=...)``
|
|
6
|
-
* ``fs get`` →
|
|
7
|
-
* ``fs put`` → ``repo.upload_user_data`` / ``repo.upload_user_data_dir``
|
|
6
|
+
* ``fs get/put`` → CLI 并发传输层,沿用 SDK 精确对象 STS 与网络路由
|
|
8
7
|
* ``fs access ls`` → ``repo.list_user_data_acl(uid, subpath=...)``
|
|
9
8
|
* ``fs access grant`` → ``repo.grant_user_data(..., subpath=...)``
|
|
10
9
|
* ``fs access revoke`` → ``repo.revoke_user_data(..., subpath=...)``
|
|
@@ -310,9 +309,18 @@ def fs_get(
|
|
|
310
309
|
target: Optional[str] = typer.Option(
|
|
311
310
|
None, "--target", help="Local target path (default: the remote file name in CWD)"
|
|
312
311
|
),
|
|
312
|
+
concurrency: int = typer.Option(
|
|
313
|
+
8, "--concurrency", "-j", min=1, max=64, help="Maximum concurrent transfer requests"
|
|
314
|
+
),
|
|
315
|
+
progress: Optional[bool] = typer.Option(
|
|
316
|
+
None,
|
|
317
|
+
"--progress/--no-progress",
|
|
318
|
+
help="Show transfer progress on stderr (default: terminal only)",
|
|
319
|
+
),
|
|
313
320
|
output_format: str = typer.Option(None, "--format", help=_FORMAT_HELP),
|
|
314
321
|
):
|
|
315
322
|
"""Download one file from a user storage space."""
|
|
323
|
+
from .fs_transfer import UserDataTransfer
|
|
316
324
|
from .irepo_sdk import sdk_error_exit
|
|
317
325
|
|
|
318
326
|
output_format = _fmt(output_format)
|
|
@@ -325,7 +333,9 @@ def fs_get(
|
|
|
325
333
|
try:
|
|
326
334
|
if local.parent and str(local.parent) not in ("", "."):
|
|
327
335
|
local.parent.mkdir(parents=True, exist_ok=True)
|
|
328
|
-
written = repo
|
|
336
|
+
written = UserDataTransfer(repo, concurrency=concurrency, progress=progress).get(
|
|
337
|
+
uid, remote, local
|
|
338
|
+
)
|
|
329
339
|
except Exception as exc: # noqa: BLE001
|
|
330
340
|
sdk_error_exit(exc, what=f"user storage {uid} {remote}", stage="download")
|
|
331
341
|
|
|
@@ -344,9 +354,18 @@ def fs_put(
|
|
|
344
354
|
None, "--source", help="Local source path (default: <filepath> relative to CWD)"
|
|
345
355
|
),
|
|
346
356
|
overwrite: bool = typer.Option(False, "--overwrite", help="Allow overwriting remote objects"),
|
|
357
|
+
concurrency: int = typer.Option(
|
|
358
|
+
8, "--concurrency", "-j", min=1, max=64, help="Maximum concurrent transfer requests"
|
|
359
|
+
),
|
|
360
|
+
progress: Optional[bool] = typer.Option(
|
|
361
|
+
None,
|
|
362
|
+
"--progress/--no-progress",
|
|
363
|
+
help="Show transfer progress on stderr (default: terminal only)",
|
|
364
|
+
),
|
|
347
365
|
output_format: str = typer.Option(None, "--format", help=_FORMAT_HELP),
|
|
348
366
|
):
|
|
349
367
|
"""Upload a local file (or directory) into a user storage space."""
|
|
368
|
+
from .fs_transfer import UserDataTransfer
|
|
350
369
|
from .irepo_sdk import sdk_error_exit
|
|
351
370
|
|
|
352
371
|
output_format = _fmt(output_format)
|
|
@@ -363,10 +382,9 @@ def fs_put(
|
|
|
363
382
|
remote = _remote_subpath(filepath, directory)
|
|
364
383
|
is_dir = local.is_dir()
|
|
365
384
|
try:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
outcome = repo.upload_user_data(uid, str(local), remote, overwrite=overwrite)
|
|
385
|
+
outcome: Any = UserDataTransfer(repo, concurrency=concurrency, progress=progress).put(
|
|
386
|
+
uid, local, remote, overwrite=overwrite
|
|
387
|
+
)
|
|
370
388
|
except Exception as exc: # noqa: BLE001
|
|
371
389
|
sdk_error_exit(exc, what=f"user storage {uid} {remote}", stage="upload")
|
|
372
390
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Bounded user-storage transfers through the SDK's authorization and routing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import threading
|
|
8
|
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.progress import (
|
|
13
|
+
BarColumn,
|
|
14
|
+
DownloadColumn,
|
|
15
|
+
Progress,
|
|
16
|
+
TaskProgressColumn,
|
|
17
|
+
TextColumn,
|
|
18
|
+
TimeRemainingColumn,
|
|
19
|
+
TransferSpeedColumn,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .ossutil_io import OssOperationError, _WriteBucket
|
|
23
|
+
|
|
24
|
+
PART_SIZE = 8 * 1024 * 1024
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TransferProgress:
|
|
28
|
+
def __init__(self, description: str, enabled: bool | None):
|
|
29
|
+
console = Console(stderr=True, markup=False, highlight=False)
|
|
30
|
+
self.display = Progress(
|
|
31
|
+
TextColumn("{task.description}"),
|
|
32
|
+
BarColumn(),
|
|
33
|
+
TaskProgressColumn(),
|
|
34
|
+
DownloadColumn(),
|
|
35
|
+
TransferSpeedColumn(),
|
|
36
|
+
TimeRemainingColumn(),
|
|
37
|
+
TextColumn("files {task.fields[files_done]}/{task.fields[files_total]}"),
|
|
38
|
+
console=console,
|
|
39
|
+
disable=not (console.is_terminal if enabled is None else enabled),
|
|
40
|
+
)
|
|
41
|
+
self.description = description
|
|
42
|
+
self.lock = threading.Lock()
|
|
43
|
+
self.sizes: dict[str, int] = {}
|
|
44
|
+
self.done: dict[str, int] = {}
|
|
45
|
+
self.files_done = 0
|
|
46
|
+
self.total_bytes = 0
|
|
47
|
+
self.completed_bytes = 0
|
|
48
|
+
|
|
49
|
+
def start(self, sizes: dict[str, int], *, unknown: bool = False):
|
|
50
|
+
self.sizes = dict(sizes)
|
|
51
|
+
self.total_bytes = sum(sizes.values())
|
|
52
|
+
self.known_total = not unknown
|
|
53
|
+
self.task = self.display.add_task(
|
|
54
|
+
self.description,
|
|
55
|
+
total=None if unknown else self.total_bytes,
|
|
56
|
+
files_done=0,
|
|
57
|
+
files_total=len(sizes),
|
|
58
|
+
)
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __enter__(self):
|
|
62
|
+
self.display.start()
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, kind, value, traceback):
|
|
66
|
+
if kind is not None:
|
|
67
|
+
self.display.update(self.task, description=self.description + " failed")
|
|
68
|
+
self.display.stop()
|
|
69
|
+
|
|
70
|
+
def callback(self, key: str):
|
|
71
|
+
def update(consumed, total):
|
|
72
|
+
with self.lock:
|
|
73
|
+
size = max(0, int(total or 0))
|
|
74
|
+
self.known_total = True
|
|
75
|
+
self.total_bytes += size - self.sizes[key]
|
|
76
|
+
self.sizes[key] = size
|
|
77
|
+
# Multipart callbacks and retries can arrive out of order.
|
|
78
|
+
old = self.done.get(key, 0)
|
|
79
|
+
self.done[key] = min(size, max(old, consumed))
|
|
80
|
+
self.completed_bytes += self.done[key] - old
|
|
81
|
+
self._render()
|
|
82
|
+
|
|
83
|
+
return update
|
|
84
|
+
|
|
85
|
+
def finish(self, key: str, *, skipped: bool = False):
|
|
86
|
+
with self.lock:
|
|
87
|
+
self.known_total = True
|
|
88
|
+
if skipped:
|
|
89
|
+
# Skipped bytes were not transferred; do not inflate the speed.
|
|
90
|
+
self.total_bytes -= self.sizes[key]
|
|
91
|
+
self.sizes[key] = 0
|
|
92
|
+
old = self.done.get(key, 0)
|
|
93
|
+
self.done[key] = self.sizes[key]
|
|
94
|
+
self.completed_bytes += self.done[key] - old
|
|
95
|
+
self.files_done += 1
|
|
96
|
+
self._render()
|
|
97
|
+
|
|
98
|
+
def _render(self):
|
|
99
|
+
self.display.update(
|
|
100
|
+
self.task,
|
|
101
|
+
completed=self.completed_bytes,
|
|
102
|
+
total=self.total_bytes if self.known_total else None,
|
|
103
|
+
files_done=self.files_done,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class UserDataTransfer:
|
|
108
|
+
def __init__(self, repo, *, concurrency: int = 8, progress: bool | None = None):
|
|
109
|
+
if not 1 <= concurrency <= 64:
|
|
110
|
+
raise ValueError("concurrency must be between 1 and 64")
|
|
111
|
+
self.repo, self.concurrency, self.progress = repo, concurrency, progress
|
|
112
|
+
self.content = getattr(repo, "_content", None)
|
|
113
|
+
if not callable(getattr(self.content, "_execute_oss", None)):
|
|
114
|
+
raise OssOperationError("User storage transfer requires a compatible instance-repo SDK")
|
|
115
|
+
from instance_repo.store.oss import _import_oss2
|
|
116
|
+
|
|
117
|
+
self.oss = _import_oss2()
|
|
118
|
+
|
|
119
|
+
def _key(self, uid: str, path: str) -> str:
|
|
120
|
+
if not str(uid).strip():
|
|
121
|
+
raise ValueError("uid must not be empty")
|
|
122
|
+
return self.repo._user_data_prefix(uid) + self.repo._clean_user_data_subpath(path)
|
|
123
|
+
|
|
124
|
+
def put(self, uid: str, source: Path, remote: str, *, overwrite: bool = False):
|
|
125
|
+
from instance_repo._routing import _OperationIntent
|
|
126
|
+
|
|
127
|
+
source = Path(source)
|
|
128
|
+
base = self._key(uid, remote)
|
|
129
|
+
directory = source.is_dir()
|
|
130
|
+
files = []
|
|
131
|
+
if directory:
|
|
132
|
+
for root, dirs, names in os.walk(source):
|
|
133
|
+
dirs.sort()
|
|
134
|
+
for name in sorted(names):
|
|
135
|
+
local = Path(root) / name
|
|
136
|
+
key = self._key(
|
|
137
|
+
uid, remote.rstrip("/") + "/" + local.relative_to(source).as_posix()
|
|
138
|
+
)
|
|
139
|
+
files.append((local, key))
|
|
140
|
+
else:
|
|
141
|
+
files.append((source, base))
|
|
142
|
+
if any(not local.is_file() for local, _ in files):
|
|
143
|
+
raise ValueError("Upload sources must be regular files")
|
|
144
|
+
sizes = {key: local.stat().st_size for local, key in files}
|
|
145
|
+
workers = min(self.concurrency, max(1, len(files)))
|
|
146
|
+
part_threads = max(1, self.concurrency // workers)
|
|
147
|
+
report = TransferProgress("Uploading", self.progress).start(sizes)
|
|
148
|
+
|
|
149
|
+
def transfer(item):
|
|
150
|
+
local, key = item
|
|
151
|
+
write_started = False
|
|
152
|
+
# Keep the SDK's resume records across failures and later invocations.
|
|
153
|
+
store = self.oss.ResumableStore()
|
|
154
|
+
|
|
155
|
+
def upload(bucket):
|
|
156
|
+
nonlocal write_started
|
|
157
|
+
if not overwrite and not write_started and bucket.object_exists(key):
|
|
158
|
+
return {"key": key, "skipped": True}
|
|
159
|
+
write_started = True
|
|
160
|
+
self.oss.resumable_upload(
|
|
161
|
+
_WriteBucket(bucket, overwrite),
|
|
162
|
+
key,
|
|
163
|
+
str(local),
|
|
164
|
+
store=store,
|
|
165
|
+
num_threads=part_threads,
|
|
166
|
+
part_size=PART_SIZE,
|
|
167
|
+
multipart_threshold=PART_SIZE,
|
|
168
|
+
headers={} if overwrite else {"x-oss-forbid-overwrite": "true"},
|
|
169
|
+
progress_callback=report.callback(key),
|
|
170
|
+
)
|
|
171
|
+
return {"key": key, "skipped": False}
|
|
172
|
+
|
|
173
|
+
# Exact object scope retains subdirectory/file ACL behavior.
|
|
174
|
+
result = self.content._execute_oss(_OperationIntent.NON_DOWNLOAD, key, None, upload)
|
|
175
|
+
report.finish(key, skipped=result["skipped"])
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
with report:
|
|
179
|
+
if not files:
|
|
180
|
+
return []
|
|
181
|
+
if workers == 1:
|
|
182
|
+
outcomes = [transfer(item) for item in files]
|
|
183
|
+
else:
|
|
184
|
+
outcomes = self._parallel(files, transfer, workers)
|
|
185
|
+
return outcomes if directory else outcomes[0]
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _parallel(files, transfer, workers):
|
|
189
|
+
outcomes = [None] * len(files)
|
|
190
|
+
items = iter(enumerate(files))
|
|
191
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
192
|
+
pending = {
|
|
193
|
+
pool.submit(transfer, item): index
|
|
194
|
+
for index, item in [next(items) for _ in range(workers)]
|
|
195
|
+
}
|
|
196
|
+
try:
|
|
197
|
+
while pending:
|
|
198
|
+
done, _ = wait(pending, return_when=FIRST_COMPLETED)
|
|
199
|
+
for future in done:
|
|
200
|
+
outcomes[pending.pop(future)] = future.result()
|
|
201
|
+
for _ in done:
|
|
202
|
+
entry = next(items, None)
|
|
203
|
+
if entry is not None:
|
|
204
|
+
index, item = entry
|
|
205
|
+
pending[pool.submit(transfer, item)] = index
|
|
206
|
+
except BaseException:
|
|
207
|
+
for future in pending:
|
|
208
|
+
future.cancel()
|
|
209
|
+
raise
|
|
210
|
+
return outcomes
|
|
211
|
+
|
|
212
|
+
def get(self, uid: str, remote: str, target: Path) -> str:
|
|
213
|
+
from instance_repo._routing import _OperationIntent
|
|
214
|
+
|
|
215
|
+
key = self._key(uid, remote)
|
|
216
|
+
target = Path(target)
|
|
217
|
+
if target.exists() and not target.is_file():
|
|
218
|
+
raise ValueError("Download target must be a regular file")
|
|
219
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
report = TransferProgress("Downloading", self.progress).start({key: 0}, unknown=True)
|
|
221
|
+
# Keep an existing destination intact until every range has completed.
|
|
222
|
+
with (
|
|
223
|
+
report,
|
|
224
|
+
tempfile.TemporaryDirectory(prefix=".ap-fs-get-", dir=target.parent) as scratch,
|
|
225
|
+
):
|
|
226
|
+
partial = Path(scratch) / "data"
|
|
227
|
+
store = self.oss.ResumableDownloadStore(root=scratch)
|
|
228
|
+
|
|
229
|
+
def download(bucket):
|
|
230
|
+
self.oss.resumable_download(
|
|
231
|
+
bucket,
|
|
232
|
+
key,
|
|
233
|
+
str(partial),
|
|
234
|
+
store=store,
|
|
235
|
+
num_threads=self.concurrency,
|
|
236
|
+
part_size=PART_SIZE,
|
|
237
|
+
multiget_threshold=PART_SIZE,
|
|
238
|
+
progress_callback=report.callback(key),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self.content._execute_oss(_OperationIntent.DOWNLOAD, key, None, download)
|
|
242
|
+
partial.chmod(0o600)
|
|
243
|
+
os.replace(partial, target)
|
|
244
|
+
report.finish(key)
|
|
245
|
+
return str(target)
|
|
@@ -955,7 +955,9 @@ def load_repo(
|
|
|
955
955
|
_Transport = None
|
|
956
956
|
if _Transport is not None:
|
|
957
957
|
kwargs["transport"] = _Transport(
|
|
958
|
-
cfg.base_url,
|
|
958
|
+
cfg.base_url,
|
|
959
|
+
cfg.token_key or "",
|
|
960
|
+
env=api_env,
|
|
959
961
|
timeout=request_timeout,
|
|
960
962
|
)
|
|
961
963
|
|
|
@@ -968,9 +970,45 @@ def load_repo(
|
|
|
968
970
|
repo = _Repo(**kwargs)
|
|
969
971
|
_bind_dataset_profile(repo)
|
|
970
972
|
_install_oss_probe_cache(repo, verbose=cfg.verbose)
|
|
973
|
+
_install_user_data_directory_listing(repo)
|
|
971
974
|
return repo
|
|
972
975
|
|
|
973
976
|
|
|
977
|
+
def _install_user_data_directory_listing(repo: Any) -> bool:
|
|
978
|
+
"""Keep the directory boundary lost by instance-repo 1.4.0 list_user_data.
|
|
979
|
+
|
|
980
|
+
Use the SDK's path validation and content store so STS authorization,
|
|
981
|
+
endpoint selection and pagination retain their normal behavior. Normalize
|
|
982
|
+
here until the SDK dependency includes its directory-prefix fix.
|
|
983
|
+
"""
|
|
984
|
+
content = getattr(repo, "_content", None)
|
|
985
|
+
prefix_for = getattr(repo, "_user_data_prefix", None)
|
|
986
|
+
clean = getattr(repo, "_clean_user_data_subpath", None)
|
|
987
|
+
if not all(
|
|
988
|
+
callable(fn)
|
|
989
|
+
for fn in (
|
|
990
|
+
getattr(repo, "list_user_data", None),
|
|
991
|
+
prefix_for,
|
|
992
|
+
clean,
|
|
993
|
+
getattr(content, "list_objects", None),
|
|
994
|
+
)
|
|
995
|
+
):
|
|
996
|
+
return False
|
|
997
|
+
|
|
998
|
+
def list_user_data(uid: str, subpath: str = "") -> list[str]:
|
|
999
|
+
from instance_repo.errors import SchemaError
|
|
1000
|
+
|
|
1001
|
+
if not str(uid).strip():
|
|
1002
|
+
raise SchemaError("uid must not be empty")
|
|
1003
|
+
prefix = prefix_for(uid)
|
|
1004
|
+
if str(subpath).strip():
|
|
1005
|
+
prefix += clean(subpath) + "/"
|
|
1006
|
+
return content.list_objects(prefix)
|
|
1007
|
+
|
|
1008
|
+
repo.list_user_data = list_user_data
|
|
1009
|
+
return True
|
|
1010
|
+
|
|
1011
|
+
|
|
974
1012
|
def _bind_dataset_profile(repo: Any) -> None:
|
|
975
1013
|
"""Supply the configured context missing from SDK 1.3.5 dataset discovery.
|
|
976
1014
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ap-client"
|
|
7
|
-
version = "0.6.1.
|
|
7
|
+
version = "0.6.1.dev11"
|
|
8
8
|
description = "Agent Platform API Client & CLI"
|
|
9
9
|
readme = { text = "A lightweight Python SDK and command line interface for Agent Platform. It provides helpers for configuring API access and managing templates, datasets, jobs, and groups.", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|