synth-ai 0.4.1__py3-none-any.whl → 0.4.4__py3-none-any.whl

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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (153) hide show
  1. synth_ai/__init__.py +13 -13
  2. synth_ai/cli/__init__.py +6 -15
  3. synth_ai/cli/commands/eval/__init__.py +6 -15
  4. synth_ai/cli/commands/eval/config.py +338 -0
  5. synth_ai/cli/commands/eval/core.py +236 -1091
  6. synth_ai/cli/commands/eval/runner.py +704 -0
  7. synth_ai/cli/commands/eval/validation.py +44 -117
  8. synth_ai/cli/commands/filter/core.py +7 -7
  9. synth_ai/cli/commands/filter/validation.py +2 -2
  10. synth_ai/cli/commands/smoke/core.py +7 -17
  11. synth_ai/cli/commands/status/__init__.py +1 -64
  12. synth_ai/cli/commands/status/client.py +50 -151
  13. synth_ai/cli/commands/status/config.py +3 -83
  14. synth_ai/cli/commands/status/errors.py +4 -13
  15. synth_ai/cli/commands/status/subcommands/__init__.py +2 -8
  16. synth_ai/cli/commands/status/subcommands/config.py +13 -0
  17. synth_ai/cli/commands/status/subcommands/files.py +18 -63
  18. synth_ai/cli/commands/status/subcommands/jobs.py +28 -311
  19. synth_ai/cli/commands/status/subcommands/models.py +18 -62
  20. synth_ai/cli/commands/status/subcommands/runs.py +16 -63
  21. synth_ai/cli/commands/status/subcommands/session.py +67 -172
  22. synth_ai/cli/commands/status/subcommands/summary.py +24 -32
  23. synth_ai/cli/commands/status/subcommands/utils.py +41 -0
  24. synth_ai/cli/commands/status/utils.py +16 -107
  25. synth_ai/cli/commands/train/__init__.py +18 -20
  26. synth_ai/cli/commands/train/errors.py +3 -3
  27. synth_ai/cli/commands/train/prompt_learning_validation.py +15 -16
  28. synth_ai/cli/commands/train/validation.py +7 -7
  29. synth_ai/cli/commands/train/{judge_schemas.py → verifier_schemas.py} +33 -34
  30. synth_ai/cli/commands/train/verifier_validation.py +235 -0
  31. synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +0 -1
  32. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +2 -6
  33. synth_ai/cli/demo_apps/math/config.toml +0 -1
  34. synth_ai/cli/demo_apps/math/modal_task_app.py +2 -6
  35. synth_ai/cli/demo_apps/mipro/task_app.py +25 -47
  36. synth_ai/cli/lib/apps/task_app.py +12 -13
  37. synth_ai/cli/lib/task_app_discovery.py +6 -6
  38. synth_ai/cli/lib/train_cfgs.py +10 -10
  39. synth_ai/cli/task_apps/__init__.py +11 -0
  40. synth_ai/cli/task_apps/commands.py +7 -15
  41. synth_ai/core/env.py +12 -1
  42. synth_ai/core/errors.py +1 -2
  43. synth_ai/core/integrations/cloudflare.py +209 -33
  44. synth_ai/core/tracing_v3/abstractions.py +46 -0
  45. synth_ai/data/__init__.py +3 -30
  46. synth_ai/data/enums.py +1 -20
  47. synth_ai/data/rewards.py +100 -3
  48. synth_ai/products/graph_evolve/__init__.py +1 -2
  49. synth_ai/products/graph_evolve/config.py +16 -16
  50. synth_ai/products/graph_evolve/converters/__init__.py +3 -3
  51. synth_ai/products/graph_evolve/converters/openai_sft.py +7 -7
  52. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +1 -1
  53. synth_ai/products/graph_gepa/__init__.py +23 -0
  54. synth_ai/products/graph_gepa/converters/__init__.py +19 -0
  55. synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
  56. synth_ai/sdk/__init__.py +45 -35
  57. synth_ai/sdk/api/eval/__init__.py +33 -0
  58. synth_ai/sdk/api/eval/job.py +732 -0
  59. synth_ai/sdk/api/research_agent/__init__.py +276 -66
  60. synth_ai/sdk/api/train/builders.py +181 -0
  61. synth_ai/sdk/api/train/cli.py +41 -33
  62. synth_ai/sdk/api/train/configs/__init__.py +6 -4
  63. synth_ai/sdk/api/train/configs/prompt_learning.py +127 -33
  64. synth_ai/sdk/api/train/configs/rl.py +264 -16
  65. synth_ai/sdk/api/train/configs/sft.py +165 -1
  66. synth_ai/sdk/api/train/graph_validators.py +12 -12
  67. synth_ai/sdk/api/train/graphgen.py +169 -51
  68. synth_ai/sdk/api/train/graphgen_models.py +95 -45
  69. synth_ai/sdk/api/train/local_api.py +10 -0
  70. synth_ai/sdk/api/train/pollers.py +36 -0
  71. synth_ai/sdk/api/train/prompt_learning.py +390 -60
  72. synth_ai/sdk/api/train/rl.py +41 -5
  73. synth_ai/sdk/api/train/sft.py +2 -0
  74. synth_ai/sdk/api/train/task_app.py +20 -0
  75. synth_ai/sdk/api/train/validators.py +17 -17
  76. synth_ai/sdk/graphs/completions.py +239 -33
  77. synth_ai/sdk/{judging/schemas.py → graphs/verifier_schemas.py} +23 -23
  78. synth_ai/sdk/learning/__init__.py +35 -5
  79. synth_ai/sdk/learning/context_learning_client.py +531 -0
  80. synth_ai/sdk/learning/context_learning_types.py +294 -0
  81. synth_ai/sdk/learning/prompt_learning_client.py +1 -1
  82. synth_ai/sdk/learning/prompt_learning_types.py +2 -1
  83. synth_ai/sdk/learning/rl/__init__.py +0 -4
  84. synth_ai/sdk/learning/rl/contracts.py +0 -4
  85. synth_ai/sdk/localapi/__init__.py +40 -0
  86. synth_ai/sdk/localapi/apps/__init__.py +28 -0
  87. synth_ai/sdk/localapi/client.py +10 -0
  88. synth_ai/sdk/localapi/contracts.py +10 -0
  89. synth_ai/sdk/localapi/helpers.py +519 -0
  90. synth_ai/sdk/localapi/rollouts.py +93 -0
  91. synth_ai/sdk/localapi/server.py +29 -0
  92. synth_ai/sdk/localapi/template.py +49 -0
  93. synth_ai/sdk/streaming/handlers.py +6 -6
  94. synth_ai/sdk/streaming/streamer.py +10 -6
  95. synth_ai/sdk/task/__init__.py +18 -5
  96. synth_ai/sdk/task/apps/__init__.py +37 -1
  97. synth_ai/sdk/task/client.py +9 -1
  98. synth_ai/sdk/task/config.py +6 -11
  99. synth_ai/sdk/task/contracts.py +137 -95
  100. synth_ai/sdk/task/in_process.py +32 -22
  101. synth_ai/sdk/task/in_process_runner.py +9 -4
  102. synth_ai/sdk/task/rubrics/__init__.py +2 -3
  103. synth_ai/sdk/task/rubrics/loaders.py +4 -4
  104. synth_ai/sdk/task/rubrics/strict.py +3 -4
  105. synth_ai/sdk/task/server.py +76 -16
  106. synth_ai/sdk/task/trace_correlation_helpers.py +190 -139
  107. synth_ai/sdk/task/validators.py +34 -49
  108. synth_ai/sdk/training/__init__.py +7 -16
  109. synth_ai/sdk/tunnels/__init__.py +118 -0
  110. synth_ai/sdk/tunnels/cleanup.py +83 -0
  111. synth_ai/sdk/tunnels/ports.py +120 -0
  112. synth_ai/sdk/tunnels/tunneled_api.py +363 -0
  113. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/METADATA +71 -4
  114. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/RECORD +118 -128
  115. synth_ai/cli/commands/baseline/__init__.py +0 -12
  116. synth_ai/cli/commands/baseline/core.py +0 -636
  117. synth_ai/cli/commands/baseline/list.py +0 -94
  118. synth_ai/cli/commands/eval/errors.py +0 -81
  119. synth_ai/cli/commands/status/formatters.py +0 -164
  120. synth_ai/cli/commands/status/subcommands/pricing.py +0 -23
  121. synth_ai/cli/commands/status/subcommands/usage.py +0 -203
  122. synth_ai/cli/commands/train/judge_validation.py +0 -305
  123. synth_ai/cli/usage.py +0 -159
  124. synth_ai/data/specs.py +0 -36
  125. synth_ai/sdk/api/research_agent/cli.py +0 -428
  126. synth_ai/sdk/api/research_agent/config.py +0 -357
  127. synth_ai/sdk/api/research_agent/job.py +0 -717
  128. synth_ai/sdk/baseline/__init__.py +0 -25
  129. synth_ai/sdk/baseline/config.py +0 -209
  130. synth_ai/sdk/baseline/discovery.py +0 -216
  131. synth_ai/sdk/baseline/execution.py +0 -154
  132. synth_ai/sdk/judging/__init__.py +0 -15
  133. synth_ai/sdk/judging/base.py +0 -24
  134. synth_ai/sdk/judging/client.py +0 -191
  135. synth_ai/sdk/judging/types.py +0 -42
  136. synth_ai/sdk/research_agent/__init__.py +0 -34
  137. synth_ai/sdk/research_agent/container_builder.py +0 -328
  138. synth_ai/sdk/research_agent/container_spec.py +0 -198
  139. synth_ai/sdk/research_agent/defaults.py +0 -34
  140. synth_ai/sdk/research_agent/results_collector.py +0 -69
  141. synth_ai/sdk/specs/__init__.py +0 -46
  142. synth_ai/sdk/specs/dataclasses.py +0 -149
  143. synth_ai/sdk/specs/loader.py +0 -144
  144. synth_ai/sdk/specs/serializer.py +0 -199
  145. synth_ai/sdk/specs/validation.py +0 -250
  146. synth_ai/sdk/tracing/__init__.py +0 -39
  147. synth_ai/sdk/usage/__init__.py +0 -37
  148. synth_ai/sdk/usage/client.py +0 -171
  149. synth_ai/sdk/usage/models.py +0 -261
  150. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/WHEEL +0 -0
  151. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/entry_points.txt +0 -0
  152. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/licenses/LICENSE +0 -0
  153. {synth_ai-0.4.1.dist-info → synth_ai-0.4.4.dist-info}/top_level.txt +0 -0
@@ -1,92 +1,12 @@
1
- """Configuration utilities for the status command suite.
2
-
3
- Provides helpers to resolve backend URLs, API keys, and request timeouts
4
- from CLI options and environment variables.
5
- """
1
+ """Configuration helpers for status commands."""
6
2
 
7
3
  from __future__ import annotations
8
4
 
9
- import importlib
10
- import os
11
- from collections.abc import Callable
12
5
  from dataclasses import dataclass
13
6
 
14
- DEFAULT_TIMEOUT = 30.0
15
-
16
-
17
- def _load_backend_helpers() -> tuple[str, Callable[[], tuple[str, str]] | None]:
18
- """Attempt to load shared backend helpers from synth_ai.core.env."""
19
- try:
20
- module = importlib.import_module("synth_ai.core.env")
21
- except Exception:
22
- return "https://api.usesynth.ai", None
23
-
24
- default = getattr(module, "PROD_BASE_URL_DEFAULT", "https://api.usesynth.ai")
25
- getter = getattr(module, "get_backend_from_env", None)
26
- return str(default), getter if callable(getter) else None
27
-
28
-
29
- PROD_BASE_URL_DEFAULT, _GET_BACKEND_FROM_ENV = _load_backend_helpers()
30
-
31
-
32
- def _normalize_base_url(raw: str) -> str:
33
- """Ensure the configured base URL includes the /api/v1 prefix."""
34
- base = raw.rstrip("/") if raw else ""
35
- if not base:
36
- return raw
37
- if base.endswith("/api") or base.endswith("/api/v1") or "/api/" in base:
38
- return base
39
- return f"{base}/api/v1"
40
-
41
7
 
42
- def _default_base_url() -> str:
43
- """Compute the default backend base URL using env vars or helper module."""
44
- for var in ("SYNTH_BACKEND_BASE_URL", "BACKEND_BASE_URL", "SYNTH_BASE_URL"):
45
- val = os.getenv(var)
46
- if val:
47
- return _normalize_base_url(val)
48
- if _GET_BACKEND_FROM_ENV:
49
- try:
50
- base, _ = _GET_BACKEND_FROM_ENV()
51
- return _normalize_base_url(base)
52
- except Exception:
53
- pass
54
- return _normalize_base_url(PROD_BASE_URL_DEFAULT)
55
-
56
-
57
- def _resolve_api_key(cli_key: str | None) -> tuple[str | None, str | None]:
58
- """Resolve the API key from CLI input or known environment variables."""
59
- if cli_key:
60
- return cli_key, "--api-key"
61
- for var in ("SYNTH_BACKEND_API_KEY", "SYNTH_API_KEY", "DEFAULT_DEV_API_KEY"):
62
- val = os.getenv(var)
63
- if val:
64
- return val, var
65
- return None, None
66
-
67
-
68
- @dataclass()
8
+ @dataclass
69
9
  class BackendConfig:
70
- """Configuration bundle shared across status commands."""
71
-
72
10
  base_url: str
73
11
  api_key: str | None
74
- timeout: float = DEFAULT_TIMEOUT
75
-
76
- @property
77
- def headers(self) -> dict[str, str]:
78
- if not self.api_key:
79
- return {}
80
- return {"Authorization": f"Bearer {self.api_key}"}
81
-
82
-
83
- def resolve_backend_config(
84
- *,
85
- base_url: str | None,
86
- api_key: str | None,
87
- timeout: float | None = None,
88
- ) -> BackendConfig:
89
- """Resolve the backend configuration from CLI options/environment."""
90
- resolved_url = _normalize_base_url(base_url) if base_url else _default_base_url()
91
- key, _ = _resolve_api_key(api_key)
92
- return BackendConfig(base_url=resolved_url, api_key=key, timeout=timeout or DEFAULT_TIMEOUT)
12
+ timeout: float = 30.0
@@ -1,20 +1,11 @@
1
- from __future__ import annotations
2
-
3
- """
4
- Custom error hierarchy for status CLI commands.
5
- """
1
+ """Errors for status CLI helpers."""
6
2
 
3
+ from __future__ import annotations
7
4
 
8
5
 
9
6
  class StatusAPIError(RuntimeError):
10
- """Raised when the backend returns a non-success response."""
7
+ """Raised when status API calls fail."""
11
8
 
12
- def __init__(self, message: str, status_code: int | None = None):
9
+ def __init__(self, message: str, *, status_code: int | None = None) -> None:
13
10
  super().__init__(message)
14
11
  self.status_code = status_code
15
-
16
-
17
- class StatusCLIError(RuntimeError):
18
- """Raised for client-side validation errors."""
19
-
20
- pass
@@ -1,9 +1,3 @@
1
- """
2
- Subcommands for the status CLI namespace.
3
- """
1
+ """Status subcommands."""
4
2
 
5
- from .files import files_group # noqa: F401
6
- from .jobs import jobs_group # noqa: F401
7
- from .models import models_group # noqa: F401
8
- from .runs import runs_group # noqa: F401
9
- from .summary import summary_command # noqa: F401
3
+ from __future__ import annotations
@@ -0,0 +1,13 @@
1
+ """Status configuration helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+
9
+
10
+
11
+ @dataclass
12
+ class StatusConfig:
13
+ base_url: str = "https://api.usesynth.ai"
@@ -1,79 +1,34 @@
1
- """`synth files` command group."""
1
+ """Status files subcommand."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
 
7
7
  import click
8
- from rich.json import JSON
9
8
 
10
- from ..client import StatusAPIClient
11
- from ..errors import StatusAPIError
12
- from ..formatters import console, files_table, print_json
13
- from ..utils import bail, common_options, resolve_context_config
9
+ from .config import StatusConfig
10
+ from .utils import StatusAPIClient, print_json
14
11
 
15
12
 
16
- @click.group("files", help="Manage training files.")
17
- @click.pass_context
18
- def files_group(ctx: click.Context) -> None: # pragma: no cover - Click wiring
19
- ctx.ensure_object(dict)
20
-
21
-
22
- @files_group.command("list")
23
- @common_options()
24
- @click.option("--purpose", type=click.Choice(["fine-tune", "validation"]))
25
- @click.option("--limit", type=int, default=20, show_default=True)
26
- @click.option("--json", "output_json", is_flag=True)
27
- @click.pass_context
28
- def list_files(
29
- ctx: click.Context,
30
- base_url: str | None,
31
- api_key: str | None,
32
- timeout: float,
33
- purpose: str | None,
34
- limit: int,
35
- output_json: bool,
36
- ) -> None:
37
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
38
-
39
- async def _run() -> None:
40
- try:
41
- async with StatusAPIClient(cfg) as client:
42
- files = await client.list_files(purpose=purpose, limit=limit)
43
- if output_json:
44
- print_json(files)
45
- else:
46
- console.print(files_table(files))
47
- except StatusAPIError as exc:
48
- bail(f"Backend error: {exc}")
49
-
50
- asyncio.run(_run())
13
+ @click.group("files")
14
+ def files_group() -> None:
15
+ return None
51
16
 
52
17
 
53
18
  @files_group.command("get")
54
- @common_options()
55
19
  @click.argument("file_id")
56
- @click.option("--json", "output_json", is_flag=True)
57
- @click.pass_context
58
- def get_file(
59
- ctx: click.Context,
60
- base_url: str | None,
61
- api_key: str | None,
62
- timeout: float,
63
- file_id: str,
64
- output_json: bool,
65
- ) -> None:
66
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
67
-
20
+ @click.option("--json", "as_json", is_flag=True, default=False)
21
+ def get_file(file_id: str, as_json: bool) -> None:
68
22
  async def _run() -> None:
69
- try:
70
- async with StatusAPIClient(cfg) as client:
71
- file_info = await client.get_file(file_id)
72
- if output_json:
73
- print_json(file_info)
74
- else:
75
- console.print(JSON.from_data(file_info))
76
- except StatusAPIError as exc:
77
- bail(f"Backend error: {exc}")
23
+ config = StatusConfig()
24
+ async with StatusAPIClient(config) as client:
25
+ data = await client.get_file(file_id) # type: ignore[attr-defined]
26
+ if as_json:
27
+ print_json(data)
28
+ else:
29
+ click.echo(data)
78
30
 
79
31
  asyncio.run(_run())
32
+
33
+
34
+ __all__ = ["files_group"]
@@ -1,334 +1,51 @@
1
- """`synth jobs` command group implementation."""
1
+ """Status jobs subcommand."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from typing import Any
7
6
 
8
7
  import click
9
8
 
10
- from ..client import StatusAPIClient
11
- from ..errors import StatusAPIError
12
- from ..formatters import (
13
- console,
14
- events_panel,
15
- job_panel,
16
- jobs_table,
17
- metrics_table,
18
- print_json,
19
- runs_table,
20
- )
21
- from ..utils import bail, common_options, parse_relative_time, resolve_context_config
9
+ from .config import StatusConfig
10
+ from .utils import StatusAPIClient, print_json
22
11
 
23
12
 
24
- @click.group("jobs", help="Manage training jobs.")
25
- @click.pass_context
26
- def jobs_group(ctx: click.Context) -> None: # pragma: no cover - Click wiring
27
- ctx.ensure_object(dict)
28
-
29
-
30
- def _print_or_json(items: Any, output_json: bool) -> None:
31
- if output_json:
32
- print_json(items)
33
- elif isinstance(items, list):
34
- console.print(jobs_table(items))
35
- else:
36
- console.print(job_panel(items))
13
+ @click.group("jobs")
14
+ def jobs_group() -> None:
15
+ return None
37
16
 
38
17
 
39
18
  @jobs_group.command("list")
40
- @common_options()
41
- @click.option(
42
- "--status",
43
- type=click.Choice(["queued", "running", "succeeded", "failed", "cancelled"]),
44
- help="Filter by job status.",
45
- )
46
- @click.option(
47
- "--type",
48
- "job_type",
49
- type=click.Choice(["sft_offline", "sft_online", "rl_online", "dpo", "sft"]),
50
- help="Filter by training job type.",
51
- )
52
- @click.option("--created-after", help="Filter by creation date (ISO8601 or relative like '24h').")
53
- @click.option("--limit", default=20, show_default=True, type=int)
54
- @click.option("--json", "output_json", is_flag=True, help="Emit raw JSON.")
55
- @click.pass_context
56
- def list_jobs(
57
- ctx: click.Context,
58
- base_url: str | None,
59
- api_key: str | None,
60
- timeout: float,
61
- status: str | None,
62
- job_type: str | None,
63
- created_after: str | None,
64
- limit: int,
65
- output_json: bool,
66
- ) -> None:
67
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
68
- created_filter = parse_relative_time(created_after)
69
-
70
- async def _run() -> None:
71
- try:
72
- async with StatusAPIClient(cfg) as client:
73
- jobs = await client.list_jobs(
74
- status=status,
75
- job_type=job_type,
76
- created_after=created_filter,
77
- limit=limit,
78
- )
79
- _print_or_json(jobs, output_json)
80
- except StatusAPIError as exc:
81
- bail(f"Backend error: {exc}")
82
-
83
- asyncio.run(_run())
84
-
85
-
86
- @jobs_group.command("get")
87
- @common_options()
88
- @click.argument("job_id")
89
- @click.option("--json", "output_json", is_flag=True)
90
- @click.pass_context
91
- def get_job(
92
- ctx: click.Context,
93
- base_url: str | None,
94
- api_key: str | None,
95
- timeout: float,
96
- job_id: str,
97
- output_json: bool,
98
- ) -> None:
99
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
100
-
101
- async def _run() -> None:
102
- try:
103
- async with StatusAPIClient(cfg) as client:
104
- job = await client.get_job(job_id)
105
- _print_or_json(job, output_json)
106
- except StatusAPIError as exc:
107
- bail(f"Backend error: {exc}")
108
-
109
- asyncio.run(_run())
110
-
111
-
112
- @jobs_group.command("history")
113
- @common_options()
114
- @click.argument("job_id")
115
- @click.option("--json", "output_json", is_flag=True)
116
- @click.pass_context
117
- def job_history(
118
- ctx: click.Context,
119
- base_url: str | None,
120
- api_key: str | None,
121
- timeout: float,
122
- job_id: str,
123
- output_json: bool,
124
- ) -> None:
125
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
126
-
19
+ @click.option("--json", "as_json", is_flag=True, default=False)
20
+ @click.option("--status", default=None)
21
+ def list_jobs(as_json: bool, status: str | None) -> None:
127
22
  async def _run() -> None:
128
- try:
129
- async with StatusAPIClient(cfg) as client:
130
- runs = await client.list_job_runs(job_id)
131
- if output_json:
132
- print_json(runs)
133
- else:
134
- console.print(runs_table(runs))
135
- except StatusAPIError as exc:
136
- bail(f"Backend error: {exc}")
23
+ config = StatusConfig()
24
+ async with StatusAPIClient(config) as client:
25
+ data = await client.list_jobs(status=status) # type: ignore[attr-defined]
26
+ if as_json:
27
+ print_json(data)
28
+ else:
29
+ click.echo(data)
137
30
 
138
31
  asyncio.run(_run())
139
32
 
140
33
 
141
- @jobs_group.command("timeline")
142
- @common_options()
143
- @click.argument("job_id")
144
- @click.option("--json", "output_json", is_flag=True)
145
- @click.pass_context
146
- def job_timeline(
147
- ctx: click.Context,
148
- base_url: str | None,
149
- api_key: str | None,
150
- timeout: float,
151
- job_id: str,
152
- output_json: bool,
153
- ) -> None:
154
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
155
-
156
- async def _run() -> None:
157
- try:
158
- async with StatusAPIClient(cfg) as client:
159
- timeline = await client.get_job_timeline(job_id)
160
- if output_json:
161
- print_json(timeline)
162
- else:
163
- console.print(events_panel(timeline))
164
- except StatusAPIError as exc:
165
- bail(f"Backend error: {exc}")
166
-
167
- asyncio.run(_run())
168
-
169
-
170
- @jobs_group.command("metrics")
171
- @common_options()
172
- @click.argument("job_id")
173
- @click.option("--json", "output_json", is_flag=True)
174
- @click.pass_context
175
- def job_metrics(
176
- ctx: click.Context,
177
- base_url: str | None,
178
- api_key: str | None,
179
- timeout: float,
180
- job_id: str,
181
- output_json: bool,
182
- ) -> None:
183
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
184
-
185
- async def _run() -> None:
186
- try:
187
- async with StatusAPIClient(cfg) as client:
188
- metrics = await client.get_job_metrics(job_id)
189
- if output_json:
190
- print_json(metrics)
191
- else:
192
- console.print(metrics_table(metrics))
193
- except StatusAPIError as exc:
194
- bail(f"Backend error: {exc}")
195
-
196
- asyncio.run(_run())
197
-
198
-
199
- @jobs_group.command("config")
200
- @common_options()
201
- @click.argument("job_id")
202
- @click.option("--json", "output_json", is_flag=True)
203
- @click.pass_context
204
- def job_config(
205
- ctx: click.Context,
206
- base_url: str | None,
207
- api_key: str | None,
208
- timeout: float,
209
- job_id: str,
210
- output_json: bool,
211
- ) -> None:
212
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
213
-
214
- async def _run() -> None:
215
- try:
216
- async with StatusAPIClient(cfg) as client:
217
- config = await client.get_job_config(job_id)
218
- if output_json:
219
- print_json(config)
220
- else:
221
- console.print(job_panel({"job_id": job_id, "config": config}))
222
- except StatusAPIError as exc:
223
- bail(f"Backend error: {exc}")
224
-
225
- asyncio.run(_run())
226
-
227
-
228
- @jobs_group.command("status")
229
- @common_options()
230
- @click.argument("job_id")
231
- @click.option("--json", "output_json", is_flag=True)
232
- @click.pass_context
233
- def job_status(
234
- ctx: click.Context,
235
- base_url: str | None,
236
- api_key: str | None,
237
- timeout: float,
238
- job_id: str,
239
- output_json: bool,
240
- ) -> None:
241
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
242
-
243
- async def _run() -> None:
244
- try:
245
- async with StatusAPIClient(cfg) as client:
246
- status = await client.get_job_status(job_id)
247
- if output_json:
248
- print_json(status)
249
- else:
250
- console.print(f"[bold]{job_id}[/bold]: {status.get('status', 'unknown')}")
251
- except StatusAPIError as exc:
252
- bail(f"Backend error: {exc}")
253
-
254
- asyncio.run(_run())
255
-
256
-
257
- @jobs_group.command("cancel")
258
- @common_options()
34
+ @jobs_group.command("logs")
259
35
  @click.argument("job_id")
260
- @click.pass_context
261
- def cancel_job(
262
- ctx: click.Context,
263
- base_url: str | None,
264
- api_key: str | None,
265
- timeout: float,
266
- job_id: str,
267
- ) -> None:
268
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
269
-
36
+ @click.option("--json", "as_json", is_flag=True, default=False)
37
+ @click.option("--tail", default=None, type=int)
38
+ def logs(job_id: str, as_json: bool, tail: int | None) -> None:
270
39
  async def _run() -> None:
271
- try:
272
- async with StatusAPIClient(cfg) as client:
273
- resp = await client.cancel_job(job_id)
274
- console.print(resp.get("message") or f"[yellow]Cancellation requested for {job_id}[/yellow]")
275
- except StatusAPIError as exc:
276
- bail(f"Backend error: {exc}")
40
+ config = StatusConfig()
41
+ async with StatusAPIClient(config) as client:
42
+ data = await client.get_job_events(job_id, limit=tail) # type: ignore[attr-defined]
43
+ if as_json:
44
+ print_json(data)
45
+ else:
46
+ click.echo(data)
277
47
 
278
48
  asyncio.run(_run())
279
49
 
280
50
 
281
- @jobs_group.command("logs")
282
- @common_options()
283
- @click.argument("job_id")
284
- @click.option("--since", help="Only show events emitted after the provided timestamp/relative offset.")
285
- @click.option("--tail", type=int, help="Show only the last N events.")
286
- @click.option("--follow/--no-follow", default=False, help="Poll for new events.")
287
- @click.option("--json", "output_json", is_flag=True)
288
- @click.pass_context
289
- def job_logs(
290
- ctx: click.Context,
291
- base_url: str | None,
292
- api_key: str | None,
293
- timeout: float,
294
- job_id: str,
295
- since: str | None,
296
- tail: int | None,
297
- follow: bool,
298
- output_json: bool,
299
- ) -> None:
300
- cfg = resolve_context_config(ctx, base_url=base_url, api_key=api_key, timeout=timeout)
301
- since_filter = parse_relative_time(since)
302
-
303
- async def _loop() -> None:
304
- seen_ids: set[str] = set()
305
- cursor: str | None = None
306
- try:
307
- async with StatusAPIClient(cfg) as client:
308
- while True:
309
- events = await client.get_job_events(
310
- job_id,
311
- since=cursor or since_filter,
312
- limit=tail,
313
- after=cursor,
314
- )
315
- new_events: list[dict[str, Any]] = []
316
- for event in events:
317
- event_id = str(event.get("event_id") or event.get("id") or event.get("timestamp"))
318
- if event_id in seen_ids:
319
- continue
320
- seen_ids.add(event_id)
321
- new_events.append(event)
322
- if new_events:
323
- cursor = str(new_events[-1].get("event_id") or new_events[-1].get("id") or "")
324
- if output_json:
325
- print_json(new_events)
326
- else:
327
- console.print(events_panel(new_events))
328
- if not follow:
329
- break
330
- await asyncio.sleep(2.0)
331
- except StatusAPIError as exc:
332
- bail(f"Backend error: {exc}")
333
-
334
- asyncio.run(_loop())
51
+ __all__ = ["jobs_group"]