ap-client 0.5.3.dev1__tar.gz → 0.5.3.dev3__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.dev1 → ap_client-0.5.3.dev3}/PKG-INFO +1 -1
  2. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/ossutil_commands.py +61 -8
  3. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/ossutil_io.py +8 -29
  4. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/pyproject.toml +1 -1
  5. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/__init__.py +0 -0
  6. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/akpool_commands.py +0 -0
  7. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/api.py +0 -0
  8. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/cli.py +0 -0
  9. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/config.py +0 -0
  10. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/dataset_commands.py +0 -0
  11. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/dataset_register.py +0 -0
  12. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/exporter.py +0 -0
  13. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/fs_commands.py +0 -0
  14. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/instance_commands.py +0 -0
  15. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/irepo_sdk.py +0 -0
  16. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/managed_ak.py +0 -0
  17. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/manifest_view.py +0 -0
  18. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/profile_commands.py +0 -0
  19. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/repo_read_guard.py +0 -0
  20. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/share_api.py +0 -0
  21. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/split_publish.py +0 -0
  22. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/tbb.py +0 -0
  23. {ap_client-0.5.3.dev1 → ap_client-0.5.3.dev3}/ap_client/waiter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.5.3.dev1
3
+ Version: 0.5.3.dev3
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: irepo-artifact==0.2.1
@@ -42,16 +42,62 @@ def _format(value: Optional[str]) -> str:
42
42
  return _normalize_output_format(value, keep_table=True)
43
43
 
44
44
 
45
- def _emit(value: dict, output_format: str, *, short: bool = False) -> None:
45
+ def _tree(entries: list[dict], root: str) -> None:
46
+ from rich.console import Console
47
+ from rich.text import Text
48
+ from rich.tree import Tree
49
+
50
+ base = root.rstrip("/") + "/" if root else ""
51
+ if entries and not all(row["path"].startswith(base) for row in entries):
52
+ base = root.rsplit("/", 1)[0] + "/" if "/" in root else ""
53
+ tree = Tree(Text(base or "./", style="bold cyan"), guide_style="dim")
54
+ nodes: dict = {}
55
+ for row in entries:
56
+ relative = row["path"][len(base) :] if row["path"].startswith(base) else row["path"]
57
+ if not relative.rstrip("/"):
58
+ continue
59
+ parts = relative.rstrip("/").split("/")
60
+ children = nodes
61
+ for index, part in enumerate(parts):
62
+ directory = (
63
+ index < len(parts) - 1 or row["type"] == "directory" or row["path"].endswith("/")
64
+ )
65
+ key = (part, directory)
66
+ node = children.setdefault(key, {"children": {}})
67
+ if index == len(parts) - 1:
68
+ node["size"] = row.get("size")
69
+ children = node["children"]
70
+
71
+ def add(parent, children):
72
+ for (name, directory), node in sorted(
73
+ children.items(), key=lambda item: (not item[0][1], item[0][0].casefold(), item[0][0])
74
+ ):
75
+ label = Text(name + ("/" if directory else ""), style="bold blue" if directory else "")
76
+ if not directory and node.get("size") is not None:
77
+ size = float(node["size"])
78
+ unit = "B"
79
+ for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
80
+ if size < 1024 or unit == "TiB":
81
+ break
82
+ size /= 1024
83
+ label.append(f" {int(size) if unit == 'B' else f'{size:.1f}'} {unit}", style="dim")
84
+ add(parent.add(label), node["children"])
85
+
86
+ add(tree, nodes)
87
+ Console().print(tree)
88
+
89
+
90
+ def _emit(value: dict, output_format: str, *, short: bool = False, root: str = "") -> None:
46
91
  if output_format in ("json", "yaml"):
47
92
  from .cli import _print_formatted
48
93
 
49
94
  _print_formatted(value, output_format)
50
95
  elif "entries" in value:
51
- for row in value["entries"]:
52
- typer.echo(
53
- row["path"] if short else f"{row['path']}\t{row.get('size', '-')}\t{row['type']}"
54
- )
96
+ if short:
97
+ for row in value["entries"]:
98
+ typer.echo(row["path"])
99
+ else:
100
+ _tree(value["entries"], root)
55
101
  if value["is_truncated"]:
56
102
  typer.echo(
57
103
  "Listing truncated; continue with --marker " + value["next_marker"], err=True
@@ -88,6 +134,7 @@ def _fail(exc: Exception) -> None:
88
134
  def oss_ls(
89
135
  path: str = typer.Argument(..., help="Full oss:// URI or prefix relative to repo-config"),
90
136
  directory: bool = typer.Option(False, "--directory", "-d", help="List only the current level"),
137
+ recursive: bool = typer.Option(False, "--recursive", "-r", help="Expand all directory levels"),
91
138
  short: bool = typer.Option(False, "--short-format", "-s", help="Print only paths"),
92
139
  limited_num: Optional[int] = typer.Option(None, "--limited-num", min=1),
93
140
  marker: str = typer.Option("", "--marker", help="Resume after this OSS URI or relative path"),
@@ -97,15 +144,20 @@ def oss_ls(
97
144
  payer: Optional[str] = typer.Option(None, "--payer"),
98
145
  output_format: Optional[str] = typer.Option(None, "--output-format", "--format"),
99
146
  ):
100
- """List matching objects recursively; use -d for one directory level."""
147
+ """Show one directory level as a tree; use -r to expand all levels."""
101
148
  try:
102
149
  fmt = _format(output_format)
150
+ if recursive and directory:
151
+ raise OssUsageError("Use either --recursive or --directory")
103
152
  files = _files()
104
153
  _configure(files, encoding_type, payer)
154
+ remote = (
155
+ files.resolve(path, prefix=True) if recursive else files.resolve(path, directory=True)
156
+ )
105
157
  _emit(
106
158
  files.list(
107
- files.resolve(path, prefix=True),
108
- recursive=not directory,
159
+ remote,
160
+ recursive=recursive,
109
161
  limit=limited_num,
110
162
  marker=marker,
111
163
  include=tuple(include or ()),
@@ -113,6 +165,7 @@ def oss_ls(
113
165
  ),
114
166
  fmt,
115
167
  short=short,
168
+ root=remote.path,
116
169
  )
117
170
  except Exception as exc:
118
171
  _fail(exc)
@@ -265,7 +265,6 @@ class OssFiles:
265
265
  state_dir: Path | None = None,
266
266
  ):
267
267
  self.repo, self.binding = repo, binding
268
- self._datasets: dict[str, str] = {}
269
268
  self.encoding_type = None
270
269
  self.payer = None
271
270
  self.disable_crc64 = False
@@ -285,41 +284,18 @@ class OssFiles:
285
284
  def resolve(self, value: str, **kwargs) -> RemotePath:
286
285
  return self.binding.resolve(value, encoding_type=self.encoding_type, **kwargs)
287
286
 
288
- def _dataset_for(self, scope: str, *, list_only: bool = False) -> str:
287
+ def _dataset_for(self, scope: str) -> str | None:
289
288
  if not scope.startswith(self.binding.prefix):
290
289
  raise OssUsageError("OSS path is outside repo-config prefix")
291
- relative = scope[len(self.binding.prefix) :]
292
- parts = relative.rstrip("/").split("/") if relative else []
293
- if not parts:
294
- raise OssUsageError("Specify a path within a Dataset, for example a/b/")
295
- candidates = ["/".join(parts[:2]), parts[0]] if len(parts) >= 2 else [parts[0]]
296
- for name in dict.fromkeys(candidates):
297
- namespace = self.binding.prefix + name + "/"
298
- if not scope.startswith(namespace):
299
- continue
300
- if name in self._datasets:
301
- return self._datasets[name]
302
- try:
303
- series = self.repo.datasets.get(name, fallback_to_oss=False)
304
- except Exception as exc:
305
- if getattr(exc, "biz_code", None) not in (92001, 94021):
306
- raise
307
- continue
308
- if getattr(series, "dataset_name", None) != name:
309
- raise OssOperationError("Dataset metadata does not match the requested path")
310
- self._datasets[name] = name
311
- return name
312
- if list_only and scope.startswith(self.binding.prefix + candidates[0] + "/"):
313
- # The server owns discovery authorization for an unregistered namespace.
314
- return candidates[0]
315
- raise OssUsageError("OSS path does not identify a registered Dataset")
290
+ relative = scope[len(self.binding.prefix) :].rstrip("/")
291
+ return "/".join(relative.split("/")[:2]) if relative else None
316
292
 
317
293
  def _credentials(self, scope: str, permission: str, list_only: bool) -> dict:
318
294
  cred = dict(
319
295
  self.repo.transport.get_oss_credentials(
320
296
  self.binding.bucket,
321
297
  scope,
322
- dataset_name=self._dataset_for(scope, list_only=list_only),
298
+ dataset_name=self._dataset_for(scope),
323
299
  permission=permission,
324
300
  list_only=list_only,
325
301
  )
@@ -439,12 +415,15 @@ class OssFiles:
439
415
  self,
440
416
  remote: RemotePath,
441
417
  *,
442
- recursive: bool = True,
418
+ recursive: bool = False,
443
419
  limit: int | None = None,
444
420
  marker: str = "",
445
421
  include: tuple[str, ...] = (),
446
422
  exclude: tuple[str, ...] = (),
447
423
  ) -> dict:
424
+ # A Dataset root is a directory even when the user omits its trailing slash.
425
+ if remote.path and not remote.path.endswith("/") and len(remote.path.split("/")) == 2:
426
+ remote = self.binding.resolve(remote.path, directory=True)
448
427
  if marker:
449
428
  marker = self.resolve(marker, prefix=True).key
450
429
  if not marker.startswith(remote.key):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.5.3.dev1"
7
+ version = "0.5.3.dev3"
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"