ap-client 0.5.1.dev1__tar.gz → 0.5.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.5.1.dev1
3
+ Version: 0.5.1.dev3
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pyyaml>=6.0
@@ -9,9 +9,9 @@ Requires-Dist: rich>=13.0.0
9
9
  Requires-Dist: typer>=0.9.0
10
10
  Requires-Dist: websockets>=13.0
11
11
  Provides-Extra: all
12
- Requires-Dist: instance-repo[oss]==1.3.2.dev2; extra == 'all'
12
+ Requires-Dist: instance-repo[oss]==1.3.2.dev3; extra == 'all'
13
13
  Provides-Extra: dataset
14
- Requires-Dist: instance-repo[oss]==1.3.2.dev2; extra == 'dataset'
14
+ Requires-Dist: instance-repo[oss]==1.3.2.dev3; extra == 'dataset'
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  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.
@@ -29,11 +29,22 @@
29
29
 
30
30
  from __future__ import annotations
31
31
 
32
+ import hashlib
32
33
  import inspect
33
34
  import json
35
+ import math
34
36
  import os
37
+ import platform
38
+ import re
39
+ import stat
40
+ import subprocess
41
+ import tempfile
42
+ import threading
35
43
  import time
36
- from typing import TYPE_CHECKING, Any, NoReturn
44
+ from contextlib import contextmanager
45
+ from pathlib import Path
46
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, NoReturn
47
+ from urllib.parse import urlsplit
37
48
 
38
49
  import typer
39
50
 
@@ -82,6 +93,565 @@ class SdkMissing(Exception):
82
93
  super().__init__(message)
83
94
 
84
95
 
96
+ _OSS_PROBE_CACHE_VERSION = 1
97
+ _OSS_PROBE_CACHE_TTL = 300.0
98
+ _OSS_PROBE_CACHE_MAX_BYTES = 64 * 1024
99
+ _OSS_PROBE_CACHE_MAX_ENTRIES = 128
100
+ _NETWORK_FINGERPRINT_UNSET = object()
101
+
102
+
103
+ def _oss_probe_cache_path() -> Path:
104
+ return Path.home() / ".ap" / ".cache" / "oss-probe.json"
105
+
106
+
107
+ def _run_network_command(command: tuple[str, ...]) -> bytes | None:
108
+ if not command or not os.path.isabs(command[0]):
109
+ return None
110
+ environment = {
111
+ key: value
112
+ for key, value in os.environ.items()
113
+ if key.upper() in {"LANG", "LC_ALL", "SYSTEMROOT", "WINDIR"}
114
+ }
115
+ environment.update({"LANG": "C", "LC_ALL": "C"})
116
+ try:
117
+ result = subprocess.run(
118
+ command,
119
+ check=False,
120
+ capture_output=True,
121
+ env=environment,
122
+ timeout=1.0,
123
+ )
124
+ except (OSError, subprocess.SubprocessError):
125
+ return None
126
+ if result.returncode != 0 or not result.stdout.strip():
127
+ return None
128
+ return result.stdout[:32768]
129
+
130
+
131
+ def _read_network_file(path: str) -> bytes | None:
132
+ try:
133
+ with open(path, "rb") as handle:
134
+ value = handle.read(32769)
135
+ except OSError:
136
+ return None
137
+ if not value or len(value) > 32768:
138
+ return None
139
+ return value
140
+
141
+
142
+ def _network_context_parts(system: str) -> list[bytes]:
143
+ if system == "darwin":
144
+ primary = _run_network_command(("/usr/sbin/scutil", "--nwi"))
145
+ if primary is None:
146
+ return []
147
+ route = _run_network_command(("/sbin/route", "-n", "get", "default"))
148
+ return [primary, *([route] if route is not None else [])]
149
+ if system == "linux":
150
+ route = _read_network_file("/proc/net/route")
151
+ if route is None:
152
+ return []
153
+ parts = [route]
154
+ for path in ("/proc/net/ipv6_route", "/etc/resolv.conf"):
155
+ value = _read_network_file(path)
156
+ if value is not None:
157
+ parts.append(value)
158
+ try:
159
+ parts.append(f"netns:{os.stat('/proc/self/ns/net').st_ino}".encode("ascii"))
160
+ except OSError:
161
+ pass
162
+ try:
163
+ interfaces = sorted(Path("/sys/class/net").glob("*/address"))
164
+ except OSError:
165
+ interfaces = []
166
+ for address_path in interfaces:
167
+ value = _read_network_file(str(address_path))
168
+ if value is not None:
169
+ parts.append(address_path.parent.name.encode("utf-8") + b"=" + value)
170
+ return parts
171
+ return []
172
+
173
+
174
+ def _network_fingerprint() -> str | None:
175
+ system = platform.system().strip().lower()
176
+ parts = _network_context_parts(system)
177
+ if not parts:
178
+ return None
179
+ digest = hashlib.sha256()
180
+ digest.update(system.encode("ascii", errors="replace"))
181
+ for part in parts:
182
+ digest.update(b"\0")
183
+ digest.update(part)
184
+ return f"{system}:{digest.hexdigest()}"
185
+
186
+
187
+ def _cache_endpoint(endpoint: str) -> str | None:
188
+ value = str(endpoint or "").strip()
189
+ if not value or len(value) > 2048:
190
+ return None
191
+ try:
192
+ parsed = urlsplit(value if "://" in value else "//" + value)
193
+ hostname = parsed.hostname
194
+ port = parsed.port
195
+ except ValueError:
196
+ return None
197
+ if not hostname:
198
+ return None
199
+ host = hostname.lower()
200
+ if ":" in host:
201
+ host = f"[{host}]"
202
+ scheme = parsed.scheme.lower()
203
+ authority = f"{host}:{port}" if port is not None else host
204
+ return f"{scheme}://{authority}" if scheme else authority
205
+
206
+
207
+ def _validate_oss_probe_cache(value: Any) -> list[dict[str, Any]]:
208
+ if (
209
+ not isinstance(value, dict)
210
+ or set(value) != {"version", "entries"}
211
+ or type(value.get("version")) is not int
212
+ or value["version"] != _OSS_PROBE_CACHE_VERSION
213
+ ):
214
+ raise ValueError("invalid OSS probe cache version")
215
+ entries = value.get("entries")
216
+ if not isinstance(entries, list) or len(entries) > _OSS_PROBE_CACHE_MAX_ENTRIES:
217
+ raise ValueError("invalid OSS probe cache entries")
218
+ validated = []
219
+ required = {"endpoint", "network", "reachable", "timestamp"}
220
+ for entry in entries:
221
+ if not isinstance(entry, dict) or set(entry) != required:
222
+ raise ValueError("invalid OSS probe cache entry")
223
+ endpoint = entry["endpoint"]
224
+ network = entry["network"]
225
+ reachable = entry["reachable"]
226
+ timestamp = entry["timestamp"]
227
+ if (
228
+ not isinstance(endpoint, str)
229
+ or not endpoint
230
+ or len(endpoint) > 2048
231
+ or _cache_endpoint(endpoint) != endpoint
232
+ ):
233
+ raise ValueError("invalid OSS probe cache endpoint")
234
+ if (
235
+ not isinstance(network, str)
236
+ or len(network) > 128
237
+ or re.fullmatch(r"[a-z0-9_-]+:[0-9a-f]{64}", network) is None
238
+ ):
239
+ raise ValueError("invalid OSS probe cache network")
240
+ if not isinstance(reachable, bool):
241
+ raise ValueError("invalid OSS probe cache result")
242
+ if (
243
+ isinstance(timestamp, bool)
244
+ or not isinstance(timestamp, (int, float))
245
+ or not math.isfinite(timestamp)
246
+ or timestamp < 0
247
+ ):
248
+ raise ValueError("invalid OSS probe cache timestamp")
249
+ validated.append(entry)
250
+ return validated
251
+
252
+
253
+ def _private_path_stat(path: Path, *, directory: bool) -> os.stat_result:
254
+ path_stat = path.lstat()
255
+ expected = stat.S_ISDIR if directory else stat.S_ISREG
256
+ if not expected(path_stat.st_mode):
257
+ raise PermissionError(f"unsafe OSS probe cache path: {path}")
258
+ getuid = getattr(os, "getuid", None)
259
+ if callable(getuid) and path_stat.st_uid != getuid():
260
+ raise PermissionError(f"OSS probe cache is not owned by current user: {path}")
261
+ if stat.S_IMODE(path_stat.st_mode) & 0o077:
262
+ raise PermissionError(f"OSS probe cache permissions are too broad: {path}")
263
+ return path_stat
264
+
265
+
266
+ def _prepare_oss_probe_cache_dir(path: Path) -> None:
267
+ path.mkdir(mode=0o700, parents=True, exist_ok=True)
268
+ path_stat = path.lstat()
269
+ if not stat.S_ISDIR(path_stat.st_mode):
270
+ raise PermissionError(f"unsafe OSS probe cache directory: {path}")
271
+ getuid = getattr(os, "getuid", None)
272
+ if callable(getuid) and path_stat.st_uid != getuid():
273
+ raise PermissionError(f"OSS probe cache directory is not owned by current user: {path}")
274
+ os.chmod(path, 0o700)
275
+
276
+
277
+ @contextmanager
278
+ def _oss_probe_cache_lock(
279
+ path: Path,
280
+ *,
281
+ name: str = ".oss-probe.lock",
282
+ timeout: float = 1.0,
283
+ ) -> Iterator[bool]:
284
+ try:
285
+ import fcntl
286
+
287
+ _prepare_oss_probe_cache_dir(path.parent)
288
+ flags = os.O_CREAT | os.O_RDWR
289
+ if hasattr(os, "O_NOFOLLOW"):
290
+ flags |= os.O_NOFOLLOW
291
+ fd = os.open(path.parent / name, flags, 0o600)
292
+ lock_stat = os.fstat(fd)
293
+ getuid = getattr(os, "getuid", None)
294
+ if not stat.S_ISREG(lock_stat.st_mode) or (
295
+ callable(getuid) and lock_stat.st_uid != getuid()
296
+ ):
297
+ raise PermissionError("unsafe OSS probe cache lock")
298
+ os.fchmod(fd, 0o600)
299
+ deadline = time.monotonic() + timeout
300
+ while True:
301
+ try:
302
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
303
+ break
304
+ except BlockingIOError:
305
+ if time.monotonic() >= deadline:
306
+ raise TimeoutError("timed out waiting for OSS probe cache lock")
307
+ time.sleep(0.01)
308
+ except Exception:
309
+ if "fd" in locals():
310
+ os.close(fd)
311
+ yield False
312
+ return
313
+ try:
314
+ yield True
315
+ finally:
316
+ fcntl.flock(fd, fcntl.LOCK_UN)
317
+ os.close(fd)
318
+
319
+
320
+ def _oss_probe_endpoint_lock_name(endpoint: str) -> str:
321
+ digest = hashlib.sha256(endpoint.encode("utf-8")).hexdigest()[:24]
322
+ return f".oss-probe-{digest}.lock"
323
+
324
+
325
+ def _read_oss_probe_cache(path: Path) -> list[dict[str, Any]]:
326
+ file_stat = _private_path_stat(path, directory=False)
327
+ if file_stat.st_size > _OSS_PROBE_CACHE_MAX_BYTES:
328
+ raise ValueError("invalid OSS probe cache file")
329
+ flags = os.O_RDONLY
330
+ if hasattr(os, "O_NOFOLLOW"):
331
+ flags |= os.O_NOFOLLOW
332
+ fd = os.open(path, flags)
333
+ try:
334
+ rendered = os.read(fd, _OSS_PROBE_CACHE_MAX_BYTES + 1)
335
+ finally:
336
+ os.close(fd)
337
+ if len(rendered) > _OSS_PROBE_CACHE_MAX_BYTES:
338
+ raise ValueError("invalid OSS probe cache file")
339
+ return _validate_oss_probe_cache(json.loads(rendered.decode("utf-8")))
340
+
341
+
342
+ def _write_oss_probe_cache(path: Path, entries: list[dict[str, Any]]) -> None:
343
+ cache_dir = path.parent
344
+ _prepare_oss_probe_cache_dir(cache_dir)
345
+ fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=cache_dir)
346
+ temporary_path = Path(temporary)
347
+ try:
348
+ os.fchmod(fd, 0o600)
349
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
350
+ fd = -1
351
+ json.dump(
352
+ {"version": _OSS_PROBE_CACHE_VERSION, "entries": entries},
353
+ handle,
354
+ separators=(",", ":"),
355
+ sort_keys=True,
356
+ )
357
+ handle.flush()
358
+ os.fsync(handle.fileno())
359
+ os.replace(temporary_path, path)
360
+ os.chmod(path, 0o600)
361
+ finally:
362
+ if fd >= 0:
363
+ os.close(fd)
364
+ try:
365
+ temporary_path.unlink()
366
+ except FileNotFoundError:
367
+ pass
368
+
369
+
370
+ def _probe_cache_log(verbose: bool, status: str, endpoint: str) -> None:
371
+ if verbose:
372
+ typer.echo(f"OSS probe cache {status}: {endpoint}", err=True)
373
+
374
+
375
+ def _cached_oss_probe(
376
+ endpoint: str,
377
+ probe: Any,
378
+ *,
379
+ verbose: bool,
380
+ fingerprints: dict[str, str],
381
+ state_lock: Any,
382
+ fingerprint: Any = _NETWORK_FINGERPRINT_UNSET,
383
+ checked_at: Callable[[float], None] | None = None,
384
+ ) -> bool:
385
+ def run_probe() -> bool:
386
+ reachable = bool(probe(endpoint))
387
+ if checked_at is not None:
388
+ checked_at(time.time())
389
+ return reachable
390
+
391
+ cache_endpoint = _cache_endpoint(endpoint)
392
+ if fingerprint is _NETWORK_FINGERPRINT_UNSET:
393
+ fingerprint = _network_fingerprint()
394
+ if cache_endpoint is None or fingerprint is None:
395
+ status = "bypass (network context unavailable)" if fingerprint is None else "bypass"
396
+ _probe_cache_log(verbose, status, cache_endpoint or "invalid endpoint")
397
+ return run_probe()
398
+
399
+ with state_lock:
400
+ fingerprints[cache_endpoint] = fingerprint
401
+ path = _oss_probe_cache_path()
402
+ endpoint_lock_name = _oss_probe_endpoint_lock_name(cache_endpoint)
403
+ with _oss_probe_cache_lock(path, name=endpoint_lock_name, timeout=4.0) as endpoint_locked:
404
+ if not endpoint_locked:
405
+ _probe_cache_log(verbose, "bypass (cache busy)", cache_endpoint)
406
+ return run_probe()
407
+
408
+ now = time.time()
409
+ status = "miss"
410
+ with _oss_probe_cache_lock(path) as read_locked:
411
+ if not read_locked:
412
+ _probe_cache_log(verbose, "bypass (cache unavailable)", cache_endpoint)
413
+ return run_probe()
414
+ try:
415
+ entries = _read_oss_probe_cache(path)
416
+ except FileNotFoundError:
417
+ entries = []
418
+ except Exception:
419
+ entries = []
420
+ status = "invalid"
421
+
422
+ for entry in entries:
423
+ if entry["endpoint"] != cache_endpoint or entry["network"] != fingerprint:
424
+ continue
425
+ age = now - entry["timestamp"]
426
+ if 0 <= age < _OSS_PROBE_CACHE_TTL:
427
+ _probe_cache_log(verbose, "hit", cache_endpoint)
428
+ if checked_at is not None:
429
+ checked_at(entry["timestamp"])
430
+ return entry["reachable"]
431
+ status = "stale"
432
+ break
433
+
434
+ _probe_cache_log(verbose, status, cache_endpoint)
435
+ reachable = run_probe()
436
+ now = time.time()
437
+ with _oss_probe_cache_lock(path) as write_locked:
438
+ if not write_locked:
439
+ _probe_cache_log(verbose, "write skipped (cache busy)", cache_endpoint)
440
+ return reachable
441
+ try:
442
+ latest_entries = _read_oss_probe_cache(path)
443
+ except Exception:
444
+ latest_entries = []
445
+ fresh_entries = [
446
+ entry
447
+ for entry in latest_entries
448
+ if 0 <= now - entry["timestamp"] < _OSS_PROBE_CACHE_TTL
449
+ and not (entry["endpoint"] == cache_endpoint and entry["network"] == fingerprint)
450
+ ]
451
+ fresh_entries.append(
452
+ {
453
+ "endpoint": cache_endpoint,
454
+ "network": fingerprint,
455
+ "reachable": reachable,
456
+ "timestamp": now,
457
+ }
458
+ )
459
+ try:
460
+ _write_oss_probe_cache(path, fresh_entries[-_OSS_PROBE_CACHE_MAX_ENTRIES:])
461
+ except Exception:
462
+ _probe_cache_log(verbose, "write failed", cache_endpoint)
463
+ return reachable
464
+
465
+
466
+ def _invalidate_oss_probe_cache(
467
+ endpoint: str,
468
+ fingerprint: str | None,
469
+ *,
470
+ verbose: bool,
471
+ ) -> None:
472
+ cache_endpoint = _cache_endpoint(endpoint)
473
+ if cache_endpoint is None:
474
+ return
475
+ path = _oss_probe_cache_path()
476
+ endpoint_lock_name = _oss_probe_endpoint_lock_name(cache_endpoint)
477
+ with _oss_probe_cache_lock(path, name=endpoint_lock_name, timeout=4.0) as endpoint_locked:
478
+ if not endpoint_locked:
479
+ return
480
+ with _oss_probe_cache_lock(path) as locked:
481
+ if not locked:
482
+ return
483
+ try:
484
+ entries = _read_oss_probe_cache(path)
485
+ except Exception:
486
+ return
487
+ remaining = [
488
+ entry
489
+ for entry in entries
490
+ if not (
491
+ entry["endpoint"] == cache_endpoint
492
+ and (fingerprint is None or entry["network"] == fingerprint)
493
+ )
494
+ ]
495
+ if len(remaining) == len(entries):
496
+ return
497
+ try:
498
+ _write_oss_probe_cache(path, remaining)
499
+ except Exception:
500
+ return
501
+ _probe_cache_log(verbose, "invalidated after connection error", cache_endpoint)
502
+
503
+
504
+ def _sdk_reachability_classifier() -> Callable[[BaseException], bool] | None:
505
+ try:
506
+ from instance_repo._routing import _is_reachability_error
507
+ except (ImportError, AttributeError):
508
+ return None
509
+ return _is_reachability_error
510
+
511
+
512
+ def _install_connection_error_invalidation(
513
+ content: Any,
514
+ fingerprints: dict[str, str],
515
+ *,
516
+ verbose: bool,
517
+ classifier: Callable[[BaseException], bool],
518
+ state_lock: Any,
519
+ ) -> bool:
520
+ open_bucket = getattr(content, "_open_bucket", None)
521
+ execute = getattr(content, "_do", None)
522
+ if not callable(open_bucket) or not callable(execute):
523
+ return False
524
+
525
+ active = threading.local()
526
+
527
+ def tracked_open_bucket(*args: Any, **kwargs: Any) -> Any:
528
+ endpoint = kwargs.get("endpoint")
529
+ if endpoint is None and len(args) >= 3:
530
+ endpoint = args[2]
531
+ active.endpoint = endpoint if isinstance(endpoint, str) else None
532
+ try:
533
+ return open_bucket(*args, **kwargs)
534
+ except Exception:
535
+ active.endpoint = None
536
+ raise
537
+
538
+ def tracked_execute(*args: Any, **kwargs: Any) -> Any:
539
+ try:
540
+ return execute(*args, **kwargs)
541
+ except Exception as exc:
542
+ endpoint = getattr(active, "endpoint", None)
543
+ if endpoint and classifier(exc):
544
+ cache_endpoint = _cache_endpoint(endpoint)
545
+ with state_lock:
546
+ fingerprint = fingerprints.get(cache_endpoint or "")
547
+ probe_cache = getattr(content, "_probe_cache", None)
548
+ if isinstance(probe_cache, dict):
549
+ probe_cache.pop(endpoint, None)
550
+ _invalidate_oss_probe_cache(endpoint, fingerprint, verbose=verbose)
551
+ raise
552
+ finally:
553
+ active.endpoint = None
554
+
555
+ try:
556
+ content._open_bucket = tracked_open_bucket
557
+ content._do = tracked_execute
558
+ except (AttributeError, TypeError):
559
+ try:
560
+ content._open_bucket = open_bucket
561
+ content._do = execute
562
+ except (AttributeError, TypeError):
563
+ pass
564
+ return False
565
+ return True
566
+
567
+
568
+ def _install_oss_probe_cache(repo: Any, *, verbose: bool) -> bool:
569
+ content = getattr(repo, "_content", None)
570
+ probe = getattr(content, "_probe_private_endpoint", None)
571
+ sdk_cached_probe = getattr(content, "_probe_cached", None)
572
+ probe_cache = getattr(content, "_probe_cache", None)
573
+ open_bucket = getattr(content, "_open_bucket", None)
574
+ execute = getattr(content, "_do", None)
575
+ classifier = _sdk_reachability_classifier()
576
+ if (
577
+ getattr(content, "_ap_probe_cache_installed", False)
578
+ or not callable(probe)
579
+ or not callable(sdk_cached_probe)
580
+ or not isinstance(probe_cache, dict)
581
+ or not callable(open_bucket)
582
+ or not callable(execute)
583
+ or classifier is None
584
+ ):
585
+ return False
586
+
587
+ fingerprints: dict[str, str] = {}
588
+ fingerprint_context = threading.local()
589
+ state_lock = threading.RLock()
590
+
591
+ def cached_probe(endpoint: str) -> bool:
592
+ fingerprint = getattr(fingerprint_context, "value", _NETWORK_FINGERPRINT_UNSET)
593
+ return _cached_oss_probe(
594
+ endpoint,
595
+ probe,
596
+ verbose=verbose,
597
+ fingerprints=fingerprints,
598
+ state_lock=state_lock,
599
+ fingerprint=fingerprint,
600
+ checked_at=lambda timestamp: setattr(fingerprint_context, "checked_at", timestamp),
601
+ )
602
+
603
+ memory_networks: dict[str, str] = {}
604
+
605
+ def network_aware_cached_probe(endpoint: str) -> bool:
606
+ with state_lock:
607
+ fingerprint = _network_fingerprint()
608
+ if fingerprint is not None and memory_networks.get(endpoint) != fingerprint:
609
+ probe_cache.pop(endpoint, None)
610
+ if fingerprint is not None:
611
+ memory_networks[endpoint] = fingerprint
612
+ fingerprint_context.value = fingerprint
613
+ fingerprint_context.checked_at = None
614
+ try:
615
+ reachable = bool(sdk_cached_probe(endpoint))
616
+ # SDK timestamps newly acquired results. A disk-cache hit must
617
+ # retain its original age instead of receiving another 300s.
618
+ if fingerprint_context.checked_at is not None and endpoint in probe_cache:
619
+ probe_cache[endpoint] = (reachable, fingerprint_context.checked_at)
620
+ return reachable
621
+ finally:
622
+ del fingerprint_context.value
623
+ del fingerprint_context.checked_at
624
+
625
+ try:
626
+ content._probe_private_endpoint = cached_probe
627
+ content._probe_cached = network_aware_cached_probe
628
+ content._ap_probe_cache_installed = True
629
+ except (AttributeError, TypeError):
630
+ try:
631
+ content._probe_private_endpoint = probe
632
+ content._probe_cached = sdk_cached_probe
633
+ if hasattr(content, "_ap_probe_cache_installed"):
634
+ del content._ap_probe_cache_installed
635
+ except (AttributeError, TypeError):
636
+ pass
637
+ return False
638
+ if not _install_connection_error_invalidation(
639
+ content,
640
+ fingerprints,
641
+ verbose=verbose,
642
+ classifier=classifier,
643
+ state_lock=state_lock,
644
+ ):
645
+ try:
646
+ content._probe_private_endpoint = probe
647
+ content._probe_cached = sdk_cached_probe
648
+ del content._ap_probe_cache_installed
649
+ except (AttributeError, TypeError):
650
+ pass
651
+ return False
652
+ return True
653
+
654
+
85
655
  # ---------------------------------------------------------------------------
86
656
  # Repo 构造
87
657
  # ---------------------------------------------------------------------------
@@ -284,7 +854,9 @@ def load_repo(
284
854
  _setdefault_env(_LEGACY_TOKEN_VAR, cfg.token_key or "")
285
855
  _setdefault_env(_LEGACY_STORAGE_ENV_VAR, effective_storage_env)
286
856
 
287
- return _Repo(**kwargs)
857
+ repo = _Repo(**kwargs)
858
+ _install_oss_probe_cache(repo, verbose=cfg.verbose)
859
+ return repo
288
860
 
289
861
 
290
862
  def _init_parameters(repo_cls: type) -> frozenset[str]:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.5.1.dev1"
7
+ version = "0.5.1.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"
@@ -18,10 +18,10 @@ dependencies = [
18
18
 
19
19
  [project.optional-dependencies]
20
20
  dataset = [
21
- "instance-repo[oss]==1.3.2.dev2",
21
+ "instance-repo[oss]==1.3.2.dev3",
22
22
  ]
23
23
  all = [
24
- "instance-repo[oss]==1.3.2.dev2",
24
+ "instance-repo[oss]==1.3.2.dev3",
25
25
  ]
26
26
 
27
27
  [project.scripts]