ap-client 0.2.2.dev0__tar.gz → 0.2.2.dev2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.2.2.dev0
3
+ Version: 0.2.2.dev2
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pyyaml>=6.0
@@ -935,6 +935,11 @@ class APIClient:
935
935
  keyword: Optional[str] = None,
936
936
  visibility: Optional[str] = None,
937
937
  benchmark_id: Optional[str] = None,
938
+ owned_by_me: bool = False,
939
+ updated_from: Optional[str] = None,
940
+ updated_to: Optional[str] = None,
941
+ sort_by: Optional[str] = None,
942
+ order: Optional[str] = None,
938
943
  include_deprecated: bool = False,
939
944
  environment: Optional[str] = None,
940
945
  page: int = 1,
@@ -942,7 +947,20 @@ class APIClient:
942
947
  ) -> dict:
943
948
  """一页 dataset series(原始 PagedSuccess envelope)。
944
949
 
945
- 服务端没有 owner/mine/l1 过滤参数,调用方需要时只能在客户端做页内过滤。
950
+ 服务端在**分页之前**完成筛选与排序,所以响应里的 ``total`` 就是过滤后的计数
951
+ (``docs/openapi.json`` 的 ``GET /datasets/series``)。可用过滤参数:
952
+
953
+ * ``q``/``keyword``:对 ``dataset_name`` + ``display_name`` 模糊匹配
954
+ (``keyword`` 是 ``q`` 的废弃别名,仅当 ``q`` 为空时生效);
955
+ * ``visibility``:逗号分隔多值,同维 OR(逐项 ``public``/``private``);
956
+ * ``benchmark_id``:精确匹配绑定,特殊值 ``none`` 表示未绑定;
957
+ * ``owned_by_me``:只保留 owner == 调用方身份的 dataset(需登录态);
958
+ * ``updated_from``/``updated_to``:``updated_at`` 闭区间(RFC3339,可只给一侧);
959
+ * ``environment``:默认 ``online``。
960
+
961
+ 排序用 ``sort_by``(``updated_at``/``dataset_name``)+``order``(``asc``/``desc``)。
962
+ **没有按任意 uid 过滤 owner 的参数**:``owned_by_me`` 只能是"我自己",查别人的
963
+ dataset 仍要在客户端过滤。非法取值由服务端 400 拒绝。
946
964
  """
947
965
  params: dict = {"page": page, "page_size": page_size}
948
966
  if q:
@@ -953,6 +971,16 @@ class APIClient:
953
971
  params["visibility"] = visibility
954
972
  if benchmark_id:
955
973
  params["benchmark_id"] = benchmark_id
974
+ if owned_by_me:
975
+ params["owned_by_me"] = "true"
976
+ if updated_from:
977
+ params["updated_from"] = updated_from
978
+ if updated_to:
979
+ params["updated_to"] = updated_to
980
+ if sort_by:
981
+ params["sort_by"] = sort_by
982
+ if order:
983
+ params["order"] = order
956
984
  if include_deprecated:
957
985
  params["include_deprecated"] = "true"
958
986
  if environment:
@@ -8,10 +8,14 @@ is implemented in split_publish.py and never publishes a whole version.
8
8
  from __future__ import annotations
9
9
 
10
10
  import json
11
- from typing import Any, Optional
11
+ import re
12
+ from typing import TYPE_CHECKING, Any, List, Optional
12
13
 
13
14
  import typer
14
15
 
16
+ if TYPE_CHECKING: # pragma: no cover - 仅用于类型标注,运行时不 import
17
+ from datetime import datetime
18
+
15
19
  __all__ = [
16
20
  "dataset_version_app",
17
21
  "dataset_split_app",
@@ -104,6 +108,11 @@ _VISIBILITIES = ("private", "public")
104
108
  #: 服务端 version 枚举(设计 §9:上游的 publishing/failed 是 ingest 任务状态,不在此列)。
105
109
  _VERSION_STATUSES = ("draft", "published", "deprecated", "archived")
106
110
  _RUN_TYPES = ("train", "eval")
111
+ #: ``dataset list`` 的服务端排序键与方向(``GET /apis/v1/datasets/series``)。
112
+ _SORT_KEYS = ("updated_at", "dataset_name")
113
+ _SORT_ORDERS = ("asc", "desc")
114
+ #: 服务端 ``benchmark_id`` 的特殊取值:未绑定 benchmark 的治理查询。
115
+ _NO_BENCHMARK = "none"
107
116
 
108
117
  _FORMAT_HELP = "Output format: plain/table/json/yaml (default: AP_FORMAT or command default)"
109
118
 
@@ -274,6 +283,71 @@ def _resolve_benchmark_id(client, value: str) -> str:
274
283
  )
275
284
 
276
285
 
286
+ def _visibility_values(values: Optional[List[str]]) -> list[str]:
287
+ """``--visibility`` 归一成服务端的逗号分隔多值形式(同维 OR)。
288
+
289
+ 既支持重复给(``--visibility public --visibility private``),也支持逗号分隔
290
+ (``--visibility public,private``);逐项校验枚举,非法值 exit 2,不等到服务端 400。
291
+ """
292
+ resolved: list[str] = []
293
+ for raw in values or []:
294
+ for part in str(raw).split(","):
295
+ part = part.strip()
296
+ if not part:
297
+ continue
298
+ checked = _check_choice(part, _VISIBILITIES, "--visibility")
299
+ if checked not in resolved:
300
+ resolved.append(checked)
301
+ return resolved
302
+
303
+
304
+ #: ``--updated-from/--updated-to`` 的 RFC3339 形状:日期 + 分隔符 + 时间 + 可选小数秒 + 带
305
+ #: 偏移的时区(与 ``time.Parse(time.RFC3339, ...)`` 的接受面基本一致)。
306
+ _UPDATED_AT_PATTERN = re.compile(
307
+ r"^(\d{4}-\d{2}-\d{2})[Tt ](\d{2}:\d{2}:\d{2})(\.\d+)?([Zz]|[+-]\d{2}:?\d{2})$"
308
+ )
309
+
310
+
311
+ def _parse_updated_at(option: str, raw: str) -> "datetime":
312
+ """把 ``--updated-from/--updated-to`` 解析成带时区的 ``datetime``;形状不对则 exit 2。
313
+
314
+ 只做前置校验,下发给服务端的仍是用户原样输入的字符串——服务端的
315
+ ``datasetUpdatedRangeFromQuery`` 才是最终解释方(它会把入参归一到 UTC 并做同样的
316
+ 区间倒挂检查)。小数秒超过 6 位时截断,避免 Python <3.11 的 ``fromisoformat`` 把
317
+ 服务端能接受的纳秒时间戳误判成非法。
318
+ """
319
+ from datetime import datetime
320
+
321
+ match = _UPDATED_AT_PATTERN.match(raw)
322
+ if match is None:
323
+ raise _fail(
324
+ f"{option} 取值非法:{raw!r}",
325
+ "用带时区的 RFC3339 时间戳,例如 2026-03-01T00:00:00Z 或 2026-03-02T12:30:00+08:00",
326
+ exit_code=2,
327
+ )
328
+ date, clock, fraction, zone = match.groups()
329
+ fraction = (fraction or "")[:7] # ".123456789" → ".123456"
330
+ zone = "+00:00" if zone in ("Z", "z") else zone
331
+ if len(zone) == 5: # "+0800" → "+08:00"
332
+ zone = f"{zone[:3]}:{zone[3:]}"
333
+ return datetime.fromisoformat(f"{date}T{clock}{fraction}{zone}")
334
+
335
+
336
+ def _updated_range(from_value: Optional[str], to_value: Optional[str]) -> tuple[str, str]:
337
+ """校验 ``--updated-from/--updated-to``(闭区间,可只给一侧),原样返回待下发字符串。"""
338
+ lower = str(from_value or "").strip()
339
+ upper = str(to_value or "").strip()
340
+ parsed_lower = _parse_updated_at("--updated-from", lower) if lower else None
341
+ parsed_upper = _parse_updated_at("--updated-to", upper) if upper else None
342
+ if parsed_lower is not None and parsed_upper is not None and parsed_lower > parsed_upper:
343
+ raise _fail(
344
+ "--updated-from 不能晚于 --updated-to",
345
+ "区间是闭区间;调换两个时间戳,或只给一侧",
346
+ exit_code=2,
347
+ )
348
+ return lower, upper
349
+
350
+
277
351
  def _dataset_id_or_fail(client, dataset: str, *, stage: str) -> tuple[str, dict]:
278
352
  """取 ``dataset_id``(access/claim 都要),失败按设计 §5 的固定文案退出。"""
279
353
  from .api import APIError
@@ -493,13 +567,38 @@ def dataset_list(
493
567
  None, "--query", help="Fuzzy match on dataset name / description"
494
568
  ),
495
569
  benchmark: Optional[str] = typer.Option(
496
- None, "--benchmark", help="Benchmark name or source_id"
570
+ None,
571
+ "--benchmark",
572
+ help=f"Benchmark name or source_id; '{_NO_BENCHMARK}' selects datasets with no binding",
497
573
  ),
498
- visibility: Optional[str] = typer.Option(
499
- None, "--visibility", help=f"Filter by visibility: {'/'.join(_VISIBILITIES)}"
574
+ visibility: Optional[List[str]] = typer.Option(
575
+ None,
576
+ "--visibility",
577
+ help=f"Filter by visibility: {'/'.join(_VISIBILITIES)}; repeat or comma-separate for OR",
578
+ ),
579
+ mine: bool = typer.Option(
580
+ False,
581
+ "--mine",
582
+ help="Only datasets owned by the caller (server-side owned_by_me=true)",
500
583
  ),
501
584
  owner: Optional[str] = typer.Option(
502
- None, "--owner", help="Filter by owner uid (client-side, current page only)"
585
+ None,
586
+ "--owner",
587
+ help="Filter by owner uid (client-side; the server only filters by the caller: --mine)",
588
+ ),
589
+ updated_from: Optional[str] = typer.Option(
590
+ None,
591
+ "--updated-from",
592
+ help="Inclusive RFC3339 lower bound on updated_at (metadata edits only)",
593
+ ),
594
+ updated_to: Optional[str] = typer.Option(
595
+ None, "--updated-to", help="Inclusive RFC3339 upper bound on updated_at"
596
+ ),
597
+ sort_by: Optional[str] = typer.Option(
598
+ None, "--sort-by", help=f"Sort key: {'/'.join(_SORT_KEYS)} (server default: updated_at)"
599
+ ),
600
+ order: Optional[str] = typer.Option(
601
+ None, "--order", help=f"Sort direction: {'/'.join(_SORT_ORDERS)} (server default: desc)"
503
602
  ),
504
603
  page: Optional[int] = typer.Option(
505
604
  None,
@@ -548,7 +647,12 @@ def dataset_list(
548
647
  for name, value in (
549
648
  ("--benchmark", benchmark),
550
649
  ("--visibility", visibility),
650
+ ("--mine", mine),
551
651
  ("--owner", owner),
652
+ ("--updated-from", updated_from),
653
+ ("--updated-to", updated_to),
654
+ ("--sort-by", sort_by),
655
+ ("--order", order),
552
656
  ("--page", page),
553
657
  ("--page-size", page_size),
554
658
  )
@@ -570,21 +674,43 @@ def dataset_list(
570
674
  _print_formatted(result, output_format)
571
675
  return
572
676
 
573
- resolved_visibility = (
574
- _check_choice(visibility, _VISIBILITIES, "--visibility") if visibility else ""
575
- )
677
+ resolved_visibility = _visibility_values(visibility)
678
+ if mine and str(owner or "").strip():
679
+ # --mine 是服务端的 owned_by_me(只认调用方自己),与"查别人的 uid"互斥;
680
+ # 两个一起下发只会得到空结果,不如直接报用法错。
681
+ raise _fail(
682
+ "--mine 与 --owner 不能同时给出",
683
+ "--mine 查自己(服务端过滤);要查别人的 dataset 用 --owner <uid>(客户端过滤)",
684
+ exit_code=2,
685
+ )
686
+ resolved_sort_by = _check_choice(sort_by, _SORT_KEYS, "--sort-by") if sort_by else ""
687
+ resolved_order = _check_choice(order, _SORT_ORDERS, "--order") if order else ""
688
+ resolved_from, resolved_to = _updated_range(updated_from, updated_to)
689
+
576
690
  client = get_client()
577
- benchmark_id = _resolve_benchmark_id(client, benchmark) if benchmark else ""
691
+ if str(benchmark or "").strip() == _NO_BENCHMARK:
692
+ # 服务端的治理查询:benchmark_id=none 表示"未绑定 benchmark",不经过名称解析。
693
+ benchmark_id = _NO_BENCHMARK
694
+ else:
695
+ benchmark_id = _resolve_benchmark_id(client, benchmark) if benchmark else ""
696
+
697
+ # 服务端在分页前完成筛选/排序,这些条件对每一页都成立,自动翻页可以直接复用。
698
+ filters: dict[str, Any] = {
699
+ "q": resolved_query or None,
700
+ "visibility": ",".join(resolved_visibility) or None,
701
+ "benchmark_id": benchmark_id or None,
702
+ "owned_by_me": mine,
703
+ "updated_from": resolved_from or None,
704
+ "updated_to": resolved_to or None,
705
+ "sort_by": resolved_sort_by or None,
706
+ "order": resolved_order or None,
707
+ }
578
708
 
579
709
  resolved_page = page or 1
580
- # 自动翻页沿用主干旧口径的 500 条/页;显式分页仍是默认 20。
581
- resolved_page_size = page_size or (500 if page is None else 20)
710
+ # 自动翻页用 100 条/页(500 对一次全量拉取偏大);显式分页仍是默认 20。
711
+ resolved_page_size = page_size or (100 if page is None else 20)
582
712
  records, pagination = client.list_dataset_series(
583
- q=resolved_query or None,
584
- visibility=resolved_visibility or None,
585
- benchmark_id=benchmark_id or None,
586
- page=resolved_page,
587
- page_size=resolved_page_size,
713
+ page=resolved_page, page_size=resolved_page_size, **filters
588
714
  )
589
715
  records = [record for record in (records or []) if isinstance(record, dict)]
590
716
 
@@ -597,11 +723,7 @@ def dataset_list(
597
723
  while records and (total is None or seen < total) and guard < 100:
598
724
  guard += 1
599
725
  more, pagination = client.list_dataset_series(
600
- q=resolved_query or None,
601
- visibility=resolved_visibility or None,
602
- benchmark_id=benchmark_id or None,
603
- page=resolved_page + guard,
604
- page_size=resolved_page_size,
726
+ page=resolved_page + guard, page_size=resolved_page_size, **filters
605
727
  )
606
728
  more = [record for record in (more or []) if isinstance(record, dict)]
607
729
  if not more:
@@ -617,13 +739,16 @@ def dataset_list(
617
739
  wanted_owner = str(owner or "").strip()
618
740
  payload: dict[str, Any] = {}
619
741
  if wanted_owner:
620
- # 服务端没有 owner 查询参数(设计 §8.5),只能页内过滤,必须在 stderr 说清楚。
742
+ # 服务端只有 owned_by_me(按调用方身份过滤),没有任意 uid 的 owner 参数,
743
+ # 所以只能在客户端过滤,必须在 stderr 说清楚。
621
744
  _note(
622
- f"--owner 只在客户端过滤当前页:服务端不支持 owner 查询,"
745
+ f"--owner 只在客户端过滤:服务端只支持按调用方身份过滤(--mine),"
623
746
  f"本次在 {len(records)} 条结果里筛 owner == {wanted_owner}"
624
747
  )
625
748
  records = [record for record in records if str(record.get("owner") or "") == wanted_owner]
626
749
  payload["owner_filter"] = wanted_owner
750
+ if mine:
751
+ payload["owned_by_me"] = True
627
752
 
628
753
  rows = [_dataset_row(record) for record in records]
629
754
  payload["datasets"] = [_legacy_dataset_record(record) for record in records]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.2.2.dev0"
7
+ version = "0.2.2.dev2"
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"