ap-client 0.5.3.dev5__tar.gz → 0.5.3.dev7__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 (23) hide show
  1. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/PKG-INFO +3 -1
  2. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/irepo_sdk.py +2 -2
  3. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/ossutil_commands.py +176 -44
  4. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/ossutil_io.py +19 -1
  5. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/pyproject.toml +3 -1
  6. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/__init__.py +0 -0
  7. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/akpool_commands.py +0 -0
  8. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/api.py +0 -0
  9. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/cli.py +0 -0
  10. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/config.py +0 -0
  11. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/dataset_commands.py +0 -0
  12. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/dataset_register.py +0 -0
  13. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/exporter.py +0 -0
  14. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/fs_commands.py +0 -0
  15. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/instance_commands.py +0 -0
  16. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/managed_ak.py +0 -0
  17. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/manifest_view.py +0 -0
  18. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/profile_commands.py +0 -0
  19. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/repo_read_guard.py +0 -0
  20. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/share_api.py +0 -0
  21. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/split_publish.py +0 -0
  22. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/tbb.py +0 -0
  23. {ap_client-0.5.3.dev5 → ap_client-0.5.3.dev7}/ap_client/waiter.py +0 -0
@@ -1,13 +1,15 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.5.3.dev5
3
+ Version: 0.5.3.dev7
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
+ Requires-Dist: click>=8.0.0
6
7
  Requires-Dist: irepo-artifact==0.2.1
7
8
  Requires-Dist: pyyaml>=6.0
8
9
  Requires-Dist: requests>=2.28.0
9
10
  Requires-Dist: rich>=13.0.0
10
11
  Requires-Dist: typer>=0.9.0
12
+ Requires-Dist: urllib3>=1.26
11
13
  Requires-Dist: websockets>=13.0
12
14
  Provides-Extra: all
13
15
  Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.5.dev3; extra == 'all'
@@ -1001,9 +1001,9 @@ def load_ossutil_repo() -> tuple[Any, dict]:
1001
1001
  cfg = get_config(verbose=_api._verbose_override, cluster=_api._cluster_override)
1002
1002
  cluster = _cluster_from_headers(cfg.headers)
1003
1003
  if not cluster:
1004
- raise ConfigurationError("Set --cluster / AP_CLUSTER before using ap ossutil")
1004
+ raise ConfigurationError("Set --cluster / AP_CLUSTER before using ap oss")
1005
1005
  if not cfg.token_key:
1006
- raise ConfigurationError("Set AP_API_KEY before using ap ossutil")
1006
+ raise ConfigurationError("Set AP_API_KEY before using ap oss")
1007
1007
  transport = Transport(cfg.base_url, cfg.token_key, env=_env_header(cfg.headers))
1008
1008
  if cfg.verbose:
1009
1009
  # STS material must stay redacted even when general HTTP redaction is disabled.
@@ -4,11 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import threading
6
6
  from pathlib import Path
7
- from typing import Optional
7
+ from typing import Annotated, Optional
8
8
 
9
9
  import typer
10
10
 
11
- from .ossutil_io import OssBinding, OssFiles, OssUsageError, direction_for, safe_error
11
+ from .ossutil_io import OssBinding, OssFiles, OssUsageError, safe_error
12
12
 
13
13
  ossutil_app = typer.Typer(
14
14
  help="OSS file operations within the server-configured storage prefix", no_args_is_help=True
@@ -16,7 +16,7 @@ ossutil_app = typer.Typer(
16
16
 
17
17
 
18
18
  def register(app: typer.Typer) -> None:
19
- app.add_typer(ossutil_app, name="ossutil")
19
+ app.add_typer(ossutil_app, name="oss")
20
20
 
21
21
 
22
22
  def _files() -> OssFiles:
@@ -218,38 +218,67 @@ def oss_cat(
218
218
  _fail(exc)
219
219
 
220
220
 
221
- @ossutil_app.command("cp")
222
- def oss_cp(
223
- src: str = typer.Argument(..., help="Local path or full oss:// URI"),
224
- dst: str = typer.Argument(..., help="Local path or full oss:// URI"),
225
- recursive: bool = typer.Option(False, "--recursive", "-r"),
226
- force: bool = typer.Option(False, "--force", "-f", help="Overwrite without confirmation"),
227
- update: bool = typer.Option(False, "--update", "-u", help="Copy only missing or older targets"),
228
- jobs: int = typer.Option(3, "--jobs", "--job", "-j", min=1, max=10000),
229
- parallel: int = typer.Option(4, "--parallel", min=1, max=10000),
230
- bigfile_threshold: int = typer.Option(100 * 1024 * 1024, "--bigfile-threshold", min=0),
231
- part_size: Optional[int] = typer.Option(None, "--part-size", min=1),
232
- checkpoint_dir: Optional[Path] = typer.Option(None, "--checkpoint-dir"),
233
- include: Optional[list[str]] = typer.Option(None, "--include"),
234
- exclude: Optional[list[str]] = typer.Option(None, "--exclude"),
235
- only_current_dir: bool = typer.Option(False, "--only-current-dir"),
236
- disable_ignore_error: bool = typer.Option(False, "--disable-ignore-error"),
237
- disable_crc64: bool = typer.Option(False, "--disable-crc64"),
238
- disable_all_symlink: bool = typer.Option(False, "--disable-all-symlink"),
239
- byte_range: Optional[str] = typer.Option(
240
- None, "--range", help="Download bytes: START-END, START-, or -END"
241
- ),
242
- meta: Optional[str] = typer.Option(
243
- None, "--meta", help="Object metadata: header:value#header:value"
244
- ),
245
- start_time: Optional[int] = typer.Option(None, "--start-time", min=0),
246
- end_time: Optional[int] = typer.Option(None, "--end-time", min=0),
247
- encoding_type: Optional[str] = typer.Option(None, "--encoding-type"),
248
- payer: Optional[str] = typer.Option(None, "--payer"),
249
- output_format: Optional[str] = typer.Option(None, "--output-format", "--format"),
250
- ):
251
- """Upload, download or copy objects. One operand must be a full oss:// URI.
252
- Downloads require private-network access."""
221
+ Recursive = Annotated[bool, typer.Option("--recursive", "-r")]
222
+ Force = Annotated[bool, typer.Option("--force", "-f", help="Overwrite without confirmation")]
223
+ Update = Annotated[bool, typer.Option("--update", "-u", help="Copy only missing or older targets")]
224
+ Jobs = Annotated[int, typer.Option("--jobs", "--job", "-j", min=1, max=10000)]
225
+ Parallel = Annotated[int, typer.Option("--parallel", min=1, max=10000)]
226
+ BigFileThreshold = Annotated[int, typer.Option("--bigfile-threshold", min=0)]
227
+ PartSize = Annotated[Optional[int], typer.Option("--part-size", min=1)]
228
+ CheckpointDir = Annotated[Optional[Path], typer.Option("--checkpoint-dir")]
229
+ Include = Annotated[Optional[list[str]], typer.Option("--include")]
230
+ Exclude = Annotated[Optional[list[str]], typer.Option("--exclude")]
231
+ OnlyCurrentDir = Annotated[bool, typer.Option("--only-current-dir")]
232
+ DisableIgnoreError = Annotated[bool, typer.Option("--disable-ignore-error")]
233
+ DisableCrc64 = Annotated[bool, typer.Option("--disable-crc64")]
234
+ DisableAllSymlink = Annotated[bool, typer.Option("--disable-all-symlink")]
235
+ ByteRange = Annotated[
236
+ Optional[str], typer.Option("--range", help="Download bytes: START-END, START-, or -END")
237
+ ]
238
+ Meta = Annotated[
239
+ Optional[str], typer.Option("--meta", help="Object metadata: header:value#header:value")
240
+ ]
241
+ StartTime = Annotated[Optional[int], typer.Option("--start-time", min=0)]
242
+ EndTime = Annotated[Optional[int], typer.Option("--end-time", min=0)]
243
+ EncodingType = Annotated[Optional[str], typer.Option("--encoding-type")]
244
+ Payer = Annotated[Optional[str], typer.Option("--payer")]
245
+ OutputFormat = Annotated[Optional[str], typer.Option("--output-format", "--format")]
246
+
247
+ #: Operand forms that are always local paths. Everything else (including a bare
248
+ #: relative path) addresses the repo-config prefix.
249
+ _LOCAL_PREFIXES = ("./", "../", "/", "~")
250
+
251
+
252
+ def _looks_local(value: str) -> bool:
253
+ return value in (".", "..", "-") or value.startswith(_LOCAL_PREFIXES)
254
+
255
+
256
+ def _require_remote(value: str, argument: str) -> None:
257
+ if not value.startswith("oss://") and _looks_local(value):
258
+ raise OssUsageError(
259
+ f"{argument} must be an oss:// URI or a repo-relative path; "
260
+ "use get or put for local files"
261
+ )
262
+
263
+
264
+ def _require_local(value: str, argument: str) -> None:
265
+ if value.startswith("oss://"):
266
+ raise OssUsageError(f"{argument} must be a local path; use cp for OSS-to-OSS copies")
267
+
268
+
269
+ def _transfer(direction: str, src: str, dst: str, ctx: typer.Context) -> None:
270
+ options = ctx.params
271
+ recursive = bool(options.get("recursive"))
272
+ force = bool(options.get("force"))
273
+ update = bool(options.get("update"))
274
+ only_current_dir = bool(options.get("only_current_dir"))
275
+ disable_ignore_error = bool(options.get("disable_ignore_error"))
276
+ disable_crc64 = bool(options.get("disable_crc64"))
277
+ disable_all_symlink = bool(options.get("disable_all_symlink"))
278
+ byte_range = options.get("byte_range")
279
+ meta = options.get("meta")
280
+ start_time = options.get("start_time")
281
+ end_time = options.get("end_time")
253
282
  lock = threading.Lock()
254
283
 
255
284
  def progress(path, done, total):
@@ -275,12 +304,23 @@ def oss_cp(
275
304
  typer.echo("Enter y or n", err=True)
276
305
 
277
306
  try:
278
- fmt = _format(output_format)
307
+ fmt = _format(options.get("output_format"))
279
308
  if start_time is not None and end_time is not None and start_time > end_time:
280
309
  raise OssUsageError("--start-time cannot be later than --end-time")
281
- direction_for(src, dst)
310
+ if direction == "download":
311
+ _require_remote(src, "REMOTE")
312
+ _require_local(dst, "LOCAL")
313
+ elif direction == "upload":
314
+ _require_local(src, "LOCAL")
315
+ _require_remote(dst, "REMOTE")
316
+ else:
317
+ _require_remote(src, "SRC")
318
+ _require_remote(dst, "DST")
282
319
  files = _files()
283
- _configure(files, encoding_type, payer)
320
+ from .irepo_sdk import _is_verbose
321
+
322
+ files.verbose = _is_verbose()
323
+ _configure(files, options.get("encoding_type"), options.get("payer"))
284
324
  files.disable_crc64 = disable_crc64
285
325
  files.disable_all_symlink = disable_all_symlink
286
326
  files.byte_range = None
@@ -309,20 +349,24 @@ def oss_cp(
309
349
  "--meta requires standard content headers or x-oss-meta-* headers"
310
350
  )
311
351
  files.write_headers[key] = value
312
- files.multipart_threshold, files.part_size = bigfile_threshold, part_size
352
+ files.multipart_threshold = options.get("bigfile_threshold", 100 * 1024 * 1024)
353
+ files.part_size = options.get("part_size")
354
+ checkpoint_dir = options.get("checkpoint_dir")
313
355
  if checkpoint_dir is not None:
314
- files.state_dir = checkpoint_dir
356
+ # ctx.params carries the raw value; Typer only converts to Path in the callback.
357
+ files.state_dir = Path(checkpoint_dir)
315
358
  result = files.cp(
316
359
  src,
317
360
  dst,
361
+ direction=direction,
318
362
  recursive=recursive,
319
363
  overwrite=force,
320
- jobs=jobs,
321
- parallel=parallel,
364
+ jobs=options.get("jobs", 3),
365
+ parallel=options.get("parallel", 4),
322
366
  progress=progress,
323
367
  update=update,
324
- include=tuple(include or ()),
325
- exclude=tuple(exclude or ()),
368
+ include=tuple(options.get("include") or ()),
369
+ exclude=tuple(options.get("exclude") or ()),
326
370
  only_current_dir=only_current_dir,
327
371
  stop_on_error=disable_ignore_error,
328
372
  confirm=None if force or update else confirm,
@@ -335,3 +379,91 @@ def oss_cp(
335
379
  _emit(result, fmt)
336
380
  if result["failed_count"]:
337
381
  raise typer.Exit(1)
382
+
383
+
384
+ @ossutil_app.command("get")
385
+ def oss_get(
386
+ ctx: typer.Context,
387
+ remote: str = typer.Argument(..., help="Remote oss:// URI or repo-relative path"),
388
+ local: str = typer.Argument(".", help="Local file or directory to write"),
389
+ recursive: Recursive = False,
390
+ force: Force = False,
391
+ update: Update = False,
392
+ jobs: Jobs = 3,
393
+ parallel: Parallel = 4,
394
+ bigfile_threshold: BigFileThreshold = 100 * 1024 * 1024,
395
+ part_size: PartSize = None,
396
+ checkpoint_dir: CheckpointDir = None,
397
+ include: Include = None,
398
+ exclude: Exclude = None,
399
+ only_current_dir: OnlyCurrentDir = False,
400
+ disable_ignore_error: DisableIgnoreError = False,
401
+ disable_crc64: DisableCrc64 = False,
402
+ byte_range: ByteRange = None,
403
+ start_time: StartTime = None,
404
+ end_time: EndTime = None,
405
+ encoding_type: EncodingType = None,
406
+ payer: Payer = None,
407
+ output_format: OutputFormat = None,
408
+ ):
409
+ """Download objects to the local filesystem. Requires private-network access."""
410
+ _transfer("download", remote, local, ctx)
411
+
412
+
413
+ @ossutil_app.command("put")
414
+ def oss_put(
415
+ ctx: typer.Context,
416
+ local: str = typer.Argument(..., help="Local file or directory to upload"),
417
+ remote: str = typer.Argument(..., help="Remote oss:// URI or repo-relative path"),
418
+ recursive: Recursive = False,
419
+ force: Force = False,
420
+ update: Update = False,
421
+ jobs: Jobs = 3,
422
+ parallel: Parallel = 4,
423
+ bigfile_threshold: BigFileThreshold = 100 * 1024 * 1024,
424
+ part_size: PartSize = None,
425
+ checkpoint_dir: CheckpointDir = None,
426
+ include: Include = None,
427
+ exclude: Exclude = None,
428
+ only_current_dir: OnlyCurrentDir = False,
429
+ disable_ignore_error: DisableIgnoreError = False,
430
+ disable_crc64: DisableCrc64 = False,
431
+ disable_all_symlink: DisableAllSymlink = False,
432
+ meta: Meta = None,
433
+ start_time: StartTime = None,
434
+ end_time: EndTime = None,
435
+ encoding_type: EncodingType = None,
436
+ payer: Payer = None,
437
+ output_format: OutputFormat = None,
438
+ ):
439
+ """Upload a local file or directory to OSS."""
440
+ _transfer("upload", local, remote, ctx)
441
+
442
+
443
+ @ossutil_app.command("cp")
444
+ def oss_cp(
445
+ ctx: typer.Context,
446
+ src: str = typer.Argument(..., help="Remote oss:// URI or repo-relative path"),
447
+ dst: str = typer.Argument(..., help="Remote oss:// URI or repo-relative path"),
448
+ recursive: Recursive = False,
449
+ force: Force = False,
450
+ update: Update = False,
451
+ jobs: Jobs = 3,
452
+ parallel: Parallel = 4,
453
+ bigfile_threshold: BigFileThreshold = 100 * 1024 * 1024,
454
+ part_size: PartSize = None,
455
+ checkpoint_dir: CheckpointDir = None,
456
+ include: Include = None,
457
+ exclude: Exclude = None,
458
+ only_current_dir: OnlyCurrentDir = False,
459
+ disable_ignore_error: DisableIgnoreError = False,
460
+ disable_crc64: DisableCrc64 = False,
461
+ meta: Meta = None,
462
+ start_time: StartTime = None,
463
+ end_time: EndTime = None,
464
+ encoding_type: EncodingType = None,
465
+ payer: Payer = None,
466
+ output_format: OutputFormat = None,
467
+ ):
468
+ """Copy objects within OSS. Use get/put for local transfers."""
469
+ _transfer("copy", src, dst, ctx)
@@ -245,6 +245,9 @@ class _RoutingBucket:
245
245
 
246
246
 
247
247
  def safe_error(exc: Exception) -> str:
248
+ if isinstance(exc, ImportError):
249
+ # ImportError messages only name the missing module; nothing sensitive is included.
250
+ return f"{type(exc).__name__}: {exc}"
248
251
  if isinstance(exc, (OssUsageError, OssOperationError, OSError)):
249
252
  return str(exc)
250
253
  # oss2 exceptions may stringify signed requests and arbitrary response payloads.
@@ -274,6 +277,7 @@ class OssFiles:
274
277
  self.write_headers: dict[str, str] = {}
275
278
  self.byte_range = None
276
279
  self.disable_all_symlink = False
280
+ self.verbose = False
277
281
  if oss_module is None:
278
282
  from instance_repo.store.oss import _import_oss2
279
283
 
@@ -791,6 +795,7 @@ class OssFiles:
791
795
  src: str,
792
796
  dst: str,
793
797
  *,
798
+ direction: str | None = None,
794
799
  recursive: bool,
795
800
  overwrite: bool,
796
801
  jobs: int,
@@ -805,7 +810,12 @@ class OssFiles:
805
810
  start_time: int | None = None,
806
811
  end_time: int | None = None,
807
812
  ) -> dict:
808
- direction = direction_for(src, dst)
813
+ if direction is None:
814
+ # Library convenience for direct callers; the CLI commands pass the
815
+ # direction explicitly so relative remote paths stay unambiguous.
816
+ direction = direction_for(src, dst)
817
+ elif direction not in ("upload", "download", "copy"):
818
+ raise OssUsageError("direction must be upload, download or copy")
809
819
  remote_src = self.resolve(src, directory=recursive) if direction != "upload" else None
810
820
  remote_dst = None
811
821
  if direction != "download":
@@ -991,6 +1001,14 @@ class OssFiles:
991
1001
  iter(items()), transfer, jobs if recursive else 1, stop_on_error
992
1002
  ):
993
1003
  if error:
1004
+ if self.verbose:
1005
+ # Frames only: exception text may embed signed URLs or STS material.
1006
+ import sys
1007
+ import traceback
1008
+
1009
+ sys.stderr.write("Traceback (most recent call last):\n")
1010
+ sys.stderr.writelines(traceback.format_tb(error.__traceback__))
1011
+ sys.stderr.write(f"error: {safe_error(error)}\n")
994
1012
  failed.append({"path": name, "error": safe_error(error)})
995
1013
  elif copied:
996
1014
  completed.append(name)
@@ -4,12 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.5.3.dev5"
7
+ version = "0.5.3.dev7"
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"
11
11
  dependencies = [
12
12
  "typer>=0.9.0",
13
+ "click>=8.0.0",
14
+ "urllib3>=1.26",
13
15
  "requests>=2.28.0",
14
16
  "rich>=13.0.0",
15
17
  "PyYAML>=6.0",