hexaqueue-cli 0.4.0__tar.gz → 0.5.0__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 (26) hide show
  1. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/PKG-INFO +10 -5
  2. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/README.md +5 -0
  3. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/pyproject.toml +5 -5
  4. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/pyproject.toml.orig +5 -5
  5. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/adapters/local.py +24 -6
  6. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/collateral.py +11 -0
  7. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/run.py +5 -0
  8. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/workflow.py +13 -2
  9. hexaqueue_cli-0.5.0/src/hexaqueue_cli/domain/models.py +11 -0
  10. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/main.py +43 -17
  11. hexaqueue_cli-0.4.0/src/hexaqueue_cli/domain/models.py +0 -47
  12. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/__init__.py +0 -0
  13. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/adapters/__init__.py +0 -0
  14. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/adapters/presenter.py +0 -0
  15. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/__init__.py +0 -0
  16. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/node.py +0 -0
  17. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/commands/suite.py +0 -0
  18. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/__init__.py +0 -0
  19. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/options.py +0 -0
  20. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/parser.py +0 -0
  21. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/domain/session.py +0 -0
  22. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/__init__.py +0 -0
  23. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/bootstrap.py +0 -0
  24. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/infra/options.py +0 -0
  25. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/ports/__init__.py +0 -0
  26. {hexaqueue_cli-0.4.0 → hexaqueue_cli-0.5.0}/src/hexaqueue_cli/ports/client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexaqueue-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Hexaqueue CLI - Modern developer and operator terminal interface
5
5
  Author: Richard West
6
6
  Author-email: Richard West <dopplereffect.us@gmail.com>
@@ -9,10 +9,10 @@ Requires-Dist: hexaqueue-core
9
9
  Requires-Dist: hexaqueue-server
10
10
  Requires-Dist: hexaqueue-worker
11
11
  Requires-Dist: hexaqueue-workflow
12
- Requires-Dist: hexastack-cli>=0.6.0
13
- Requires-Dist: hexastack-core>=0.6.0
14
- Requires-Dist: hexastack-cqrs>=0.6.0
15
- Requires-Dist: hexastack-grpc>=0.6.0
12
+ Requires-Dist: hexastack-cli>=0.8.0
13
+ Requires-Dist: hexastack-core>=0.8.0
14
+ Requires-Dist: hexastack-cqrs>=0.8.0
15
+ Requires-Dist: hexastack-grpc>=0.8.0
16
16
  Requires-Dist: pyyaml>=6.0.2
17
17
  Requires-Dist: rich>=13.8.0
18
18
  Requires-Dist: typer>=0.15.0
@@ -26,8 +26,13 @@ Description-Content-Type: text/markdown
26
26
  [![PyPI: hexaqueue-cli](https://img.shields.io/pypi/v/hexaqueue-cli.svg)](https://pypi.org/project/hexaqueue-cli/)
27
27
  [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
28
28
  [![Coverage](https://codecov.io/github/TheTrueSCU/hexaqueue/graph/badge.svg?component=hexaqueue_cli)](https://codecov.io/github/TheTrueSCU/hexaqueue)
29
+ [![Part of Hexaqueue](https://img.shields.io/badge/part%20of-hexaqueue-blue.svg)](https://dopplereffect.us/hexaqueue/)
30
+ [![Built with Hexastack](https://img.shields.io/badge/built%20with-hexastack-blueviolet.svg)](https://dopplereffect.us/hexastack/)
31
+ [![Governed by Hexaqual](https://img.shields.io/badge/governed%20by-hexaqual-10b981.svg)](https://dopplereffect.us/hexaqual/)
29
32
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../../LICENSE)
30
33
 
34
+ > Part of the [**Hexaqueue Scheduler**](https://dopplereffect.us/hexaqueue/) · Built on [**Hexastack**](https://dopplereffect.us/hexastack/) · Governed by [**Hexaqual**](https://dopplereffect.us/hexaqual/).
35
+
31
36
  ---
32
37
 
33
38
  ## 🏗️ Architecture & Dependencies
@@ -5,8 +5,13 @@
5
5
  [![PyPI: hexaqueue-cli](https://img.shields.io/pypi/v/hexaqueue-cli.svg)](https://pypi.org/project/hexaqueue-cli/)
6
6
  [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
7
7
  [![Coverage](https://codecov.io/github/TheTrueSCU/hexaqueue/graph/badge.svg?component=hexaqueue_cli)](https://codecov.io/github/TheTrueSCU/hexaqueue)
8
+ [![Part of Hexaqueue](https://img.shields.io/badge/part%20of-hexaqueue-blue.svg)](https://dopplereffect.us/hexaqueue/)
9
+ [![Built with Hexastack](https://img.shields.io/badge/built%20with-hexastack-blueviolet.svg)](https://dopplereffect.us/hexastack/)
10
+ [![Governed by Hexaqual](https://img.shields.io/badge/governed%20by-hexaqual-10b981.svg)](https://dopplereffect.us/hexaqual/)
8
11
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../../LICENSE)
9
12
 
13
+ > Part of the [**Hexaqueue Scheduler**](https://dopplereffect.us/hexaqueue/) · Built on [**Hexastack**](https://dopplereffect.us/hexastack/) · Governed by [**Hexaqual**](https://dopplereffect.us/hexaqual/).
14
+
10
15
  ---
11
16
 
12
17
  ## 🏗️ Architecture & Dependencies
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hexaqueue-cli"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Hexaqueue CLI - Modern developer and operator terminal interface"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -10,10 +10,10 @@ dependencies = [
10
10
  "hexaqueue-server",
11
11
  "hexaqueue-worker",
12
12
  "hexaqueue-workflow",
13
- "hexastack-cli>=0.6.0",
14
- "hexastack-core>=0.6.0",
15
- "hexastack-cqrs>=0.6.0",
16
- "hexastack-grpc>=0.6.0",
13
+ "hexastack-cli>=0.8.0",
14
+ "hexastack-core>=0.8.0",
15
+ "hexastack-cqrs>=0.8.0",
16
+ "hexastack-grpc>=0.8.0",
17
17
  "pyyaml>=6.0.2",
18
18
  "rich>=13.8.0",
19
19
  "typer>=0.15.0",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hexaqueue-cli"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Hexaqueue CLI - Modern developer and operator terminal interface"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -13,10 +13,10 @@ dependencies = [
13
13
  "hexaqueue-server",
14
14
  "hexaqueue-worker",
15
15
  "hexaqueue-workflow",
16
- "hexastack-cli>=0.6.0",
17
- "hexastack-core>=0.6.0",
18
- "hexastack-cqrs>=0.6.0",
19
- "hexastack-grpc>=0.6.0",
16
+ "hexastack-cli>=0.8.0",
17
+ "hexastack-core>=0.8.0",
18
+ "hexastack-cqrs>=0.8.0",
19
+ "hexastack-grpc>=0.8.0",
20
20
  "pyyaml>=6.0.2",
21
21
  "rich>=13.8.0",
22
22
  "typer>=0.15.0",
@@ -8,6 +8,7 @@ Notes/Architectural Intent:
8
8
  from __future__ import annotations
9
9
 
10
10
  from collections.abc import AsyncIterator
11
+ from pathlib import Path
11
12
  from uuid import uuid4
12
13
 
13
14
  from hexaqueue_cli.domain.models import ClusterStatsReport
@@ -46,7 +47,7 @@ def _check_job_mutation_permission(
46
47
  ) -> None:
47
48
  """Verify that caller has permission to mutate the job."""
48
49
  owner = _extract_job_owner(job)
49
- if owner != "default" and owner != user_id and not elevate:
50
+ if owner != user_id and not elevate:
50
51
  msg = (
51
52
  f"Permission denied: You are not the owner of job '{job.id}' (owned by '{owner}'). "
52
53
  f"To {action_name} this job, explicit administrative elevation (--admin / elevate=true) is required."
@@ -105,7 +106,7 @@ class LocalClientAdapter(ClientPort):
105
106
  self, run_id: str, user_id: str = "default", elevate: bool = False
106
107
  ) -> RunStatusReport:
107
108
  """Cancel run in local in-process controller."""
108
- if not elevate and user_id != "default":
109
+ if not elevate:
109
110
  jobs = await self._session.controller.list_jobs()
110
111
  run_jobs = [j for j in jobs if j.run_id == run_id]
111
112
  for j in run_jobs:
@@ -262,6 +263,22 @@ class LocalClientAdapter(ClientPort):
262
263
  checksum = (
263
264
  checksum_sha256 if len(checksum_sha256) == 64 else checksum_sha256.zfill(64)
264
265
  )
266
+ staging_uri = f"memory://staging/collateral/{name}"
267
+ if target_path and Path(target_path).is_file():
268
+ if ".." in name or "/" in name or "\\" in name or Path(name).is_absolute():
269
+ raise ValueError(
270
+ f"Collateral name '{name}' contains illegal path components."
271
+ )
272
+ content = Path(target_path).read_bytes()
273
+ alloc = await self._session.storage.allocate_scratch(
274
+ job_id="collateral", size_mb=max(1, len(content) // (1024 * 1024))
275
+ )
276
+ mount_path = Path(alloc.mount_path).resolve()
277
+ dest = (mount_path / name).resolve()
278
+ if not dest.is_relative_to(mount_path):
279
+ raise ValueError("Path traversal detected in collateral destination.")
280
+ dest.write_bytes(content)
281
+ staging_uri = f"file://{dest}"
265
282
  return CollateralBundle(
266
283
  id=f"col-{uuid4().hex[:8]}",
267
284
  job_id="global",
@@ -271,7 +288,7 @@ class LocalClientAdapter(ClientPort):
271
288
  tier=tier,
272
289
  kind=kind,
273
290
  state=CollateralState.REGISTERED,
274
- staging_uri=f"s3://staging/collateral/{name}",
291
+ staging_uri=staging_uri,
275
292
  )
276
293
 
277
294
  async def create_bastion_session(
@@ -288,13 +305,14 @@ class LocalClientAdapter(ClientPort):
288
305
  "requires explicit administrative elevation (--admin)."
289
306
  )
290
307
  raise PermissionDeniedError(msg)
291
- return PtySessionInfo(
308
+ req = PtySessionRequest(
292
309
  session_id=session_id or f"bastion-{uuid4().hex[:8]}",
293
310
  job_id=f"bastion-{node_id}",
294
311
  user_id=user_id,
295
- pid=99999,
296
- is_active=True,
312
+ roles=["admin", "operator"],
313
+ command=["/bin/sh"],
297
314
  )
315
+ return await self._session.pty.create_session(req, job_owner=user_id)
298
316
 
299
317
 
300
318
  __all__ = [
@@ -78,6 +78,16 @@ def push_collateral_cmd(
78
78
  sha256_hash = hashlib.sha256(content).hexdigest()
79
79
  size_bytes = len(content)
80
80
  asset_name = name or file_path.name
81
+ if (
82
+ ".." in asset_name
83
+ or "/" in asset_name
84
+ or "\\" in asset_name
85
+ or Path(asset_name).is_absolute()
86
+ ):
87
+ console.print(
88
+ f"[bold red]Error:[/] Collateral name '{asset_name}' contains illegal path components."
89
+ )
90
+ raise typer.Exit(code=1)
81
91
 
82
92
  resolved_tier = CollateralTier(tier.upper())
83
93
  resolved_kind = CollateralKind(kind.upper())
@@ -91,6 +101,7 @@ def push_collateral_cmd(
91
101
  checksum_sha256=sha256_hash,
92
102
  tier=resolved_tier,
93
103
  kind=resolved_kind,
104
+ target_path=str(file_path),
94
105
  user_id=user,
95
106
  elevate=admin,
96
107
  )
@@ -66,6 +66,11 @@ async def _execute_submit_and_watch(
66
66
  presenter.render_run_status(report, resolved_fmt)
67
67
 
68
68
  if not watch:
69
+ if resolved_fmt in ("table", "rich"):
70
+ console.print(
71
+ "[bold yellow]Notice:[/] Local session submitted in detached mode. "
72
+ "In-process execution will terminate when the process exits. Use --watch to await completion."
73
+ )
69
74
  return
70
75
 
71
76
  with console.status(f"[bold blue]Executing run {report.run_id}...[/]"):
@@ -16,6 +16,7 @@ import typer
16
16
  from hexaflow.adapters.storage.sqlite import SqliteStateStore
17
17
  from hexaflow.domain.models import WorkflowDefinition
18
18
  from hexaflow.dsl.builder import Workflow
19
+ from hexastack_core.adapters.storage import LocalStorageAdapter
19
20
  from rich.console import Console
20
21
 
21
22
  from hexaqueue_cli.adapters.presenter import CliPresenter
@@ -192,8 +193,12 @@ def submit_cmd(
192
193
  port = None
193
194
  dispatcher = NotificationDispatcher(notification_port=port)
194
195
  store = SqliteStateStore(db_path=db_path)
196
+ artifacts_dir = Path(db_path).parent / "artifacts"
197
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
198
+ storage = LocalStorageAdapter(root_dir=artifacts_dir)
195
199
  engine = HexaqueueDistributedEngine(
196
200
  state_store=store,
201
+ storage=storage,
197
202
  config=config,
198
203
  notification_dispatcher=dispatcher,
199
204
  )
@@ -274,7 +279,10 @@ def resume_cmd(
274
279
  resolved_fmt = resolve_format(format_type)
275
280
 
276
281
  store = SqliteStateStore(db_path=db_path)
277
- engine = HexaqueueDistributedEngine(state_store=store)
282
+ artifacts_dir = Path(db_path).parent / "artifacts"
283
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
284
+ storage = LocalStorageAdapter(root_dir=artifacts_dir)
285
+ engine = HexaqueueDistributedEngine(state_store=store, storage=storage)
278
286
 
279
287
  if resolved_fmt in ("table", "rich"):
280
288
  console.print(f"[bold yellow]Resuming workflow run:[/] [cyan]{run_id}[/]")
@@ -311,7 +319,10 @@ def abort_cmd(
311
319
  workflow = load_workflow_from_target(target)
312
320
  resolved_fmt = resolve_format(format_type)
313
321
  store = SqliteStateStore(db_path=db_path)
314
- engine = HexaqueueDistributedEngine(state_store=store)
322
+ artifacts_dir = Path(db_path).parent / "artifacts"
323
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
324
+ storage = LocalStorageAdapter(root_dir=artifacts_dir)
325
+ engine = HexaqueueDistributedEngine(state_store=store, storage=storage)
315
326
 
316
327
  if resolved_fmt in ("table", "rich"):
317
328
  console.print(f"[bold red]Aborting workflow run:[/] [cyan]{run_id}[/]")
@@ -0,0 +1,11 @@
1
+ """Domain models for CLI cluster monitoring and statistics.
2
+
3
+ Notes/Architectural Intent:
4
+ Defines presentation and reporting payloads for `hq stat`, `hq top`, and `hq nodes`.
5
+ """
6
+
7
+ from hexaqueue_core.domain.cqrs import ClusterStatsReport
8
+
9
+ __all__ = [
10
+ "ClusterStatsReport",
11
+ ]
@@ -7,6 +7,7 @@ Notes/Architectural Intent:
7
7
 
8
8
  import asyncio
9
9
  from pathlib import Path
10
+ from typing import Any
10
11
  from uuid import uuid4
11
12
 
12
13
  import typer
@@ -148,7 +149,8 @@ async def _stream_live_logs(
148
149
  async for chunk in client.stream_logs(job_id, follow=True, tail=tail):
149
150
  _print_log_chunk(chunk)
150
151
  except (asyncio.CancelledError, KeyboardInterrupt):
151
- pass
152
+ # User interruption (Ctrl+C) or coroutine cancellation; exit live log streaming cleanly.
153
+ return
152
154
 
153
155
 
154
156
  @app.command("logs")
@@ -451,6 +453,16 @@ def top_cmd(
451
453
  asyncio.run(_top())
452
454
 
453
455
 
456
+ def _extract_job_owner(job: Any) -> str:
457
+ """Extract owner identity from job tags or user attribute."""
458
+ for tag in getattr(job, "tags", []):
459
+ if tag.startswith("owner:"):
460
+ return tag.split(":", 1)[1]
461
+ if getattr(job, "user", "default") != "default":
462
+ return job.user
463
+ return getattr(job, "env", {}).get("HEXAQUEUE_OWNER", "default")
464
+
465
+
454
466
  @app.command("exec")
455
467
  def exec_cmd(
456
468
  job_id: str = typer.Argument(..., help="Job ID to execute command in"),
@@ -458,6 +470,9 @@ def exec_cmd(
458
470
  ..., help="Command vector to run inside job environment"
459
471
  ),
460
472
  user: str = typer.Option("default", "--user", "-u", help="Requesting username"),
473
+ admin: bool = typer.Option(
474
+ False, "--admin", help="Assert explicit administrative elevation"
475
+ ),
461
476
  ) -> None:
462
477
  """Spawn an interactive PTY session inside a running job environment.
463
478
 
@@ -465,6 +480,7 @@ def exec_cmd(
465
480
  job_id: Target running job identifier.
466
481
  command: Command vector to execute in terminal PTY.
467
482
  user: Requesting user identity for RBAC validation.
483
+ admin: Whether explicit administrative elevation is requested.
468
484
 
469
485
  Notes/Architectural Intent:
470
486
  Connects local terminal to remote worker PTY master bridge with RBAC authorization.
@@ -472,15 +488,18 @@ def exec_cmd(
472
488
 
473
489
  async def _exec() -> None:
474
490
  client = LocalClientAdapter()
475
- request = PtySessionRequest(
476
- session_id=f"pty-{uuid4().hex[:8]}",
477
- job_id=job_id,
478
- user_id=user,
479
- roles=["operator"],
480
- command=command,
481
- )
482
491
  try:
483
- session_info = await client.create_pty_session(request, job_owner=user)
492
+ job = await client.get_job(job_id)
493
+ job_owner = _extract_job_owner(job)
494
+ roles = ["operator"] if admin else []
495
+ request = PtySessionRequest(
496
+ session_id=f"pty-{uuid4().hex[:8]}",
497
+ job_id=job_id,
498
+ user_id=user,
499
+ roles=roles,
500
+ command=command,
501
+ )
502
+ session_info = await client.create_pty_session(request, job_owner=job_owner)
484
503
  console.print(
485
504
  f"[bold green]✓[/] Attached PTY session '{session_info.session_id}' "
486
505
  f"to job '{job_id}' (pid: {session_info.pid})"
@@ -498,12 +517,16 @@ def exec_cmd(
498
517
  def attach_cmd(
499
518
  job_id: str = typer.Argument(..., help="Job ID to attach interactive shell to"),
500
519
  user: str = typer.Option("default", "--user", "-u", help="Requesting username"),
520
+ admin: bool = typer.Option(
521
+ False, "--admin", help="Assert explicit administrative elevation"
522
+ ),
501
523
  ) -> None:
502
524
  """Attach an interactive bash shell to a running job.
503
525
 
504
526
  Args:
505
527
  job_id: Target running job identifier.
506
528
  user: Requesting user identity for RBAC validation.
529
+ admin: Whether explicit administrative elevation is requested.
507
530
 
508
531
  Notes/Architectural Intent:
509
532
  Convenience alias launching an interactive bash terminal inside the target job.
@@ -511,15 +534,18 @@ def attach_cmd(
511
534
 
512
535
  async def _attach() -> None:
513
536
  client = LocalClientAdapter()
514
- request = PtySessionRequest(
515
- session_id=f"pty-{uuid4().hex[:8]}",
516
- job_id=job_id,
517
- user_id=user,
518
- roles=["operator"],
519
- command=["/bin/bash"],
520
- )
521
537
  try:
522
- session_info = await client.create_pty_session(request, job_owner=user)
538
+ job = await client.get_job(job_id)
539
+ job_owner = _extract_job_owner(job)
540
+ roles = ["operator"] if admin else []
541
+ request = PtySessionRequest(
542
+ session_id=f"pty-{uuid4().hex[:8]}",
543
+ job_id=job_id,
544
+ user_id=user,
545
+ roles=roles,
546
+ command=["/bin/bash"],
547
+ )
548
+ session_info = await client.create_pty_session(request, job_owner=job_owner)
523
549
  console.print(
524
550
  f"[bold green]✓[/] Attached shell session '{session_info.session_id}' "
525
551
  f"to job '{job_id}' (pid: {session_info.pid})"
@@ -1,47 +0,0 @@
1
- """Domain models for CLI cluster monitoring and statistics.
2
-
3
- Notes/Architectural Intent:
4
- Defines presentation and reporting payloads for `hq stat`, `hq top`, and `hq nodes`.
5
- """
6
-
7
- from datetime import UTC, datetime
8
-
9
- from pydantic import BaseModel, ConfigDict, Field
10
-
11
-
12
- class ClusterStatsReport(BaseModel):
13
- """Aggregate cluster state, backlog, and worker capacity metrics.
14
-
15
- Args:
16
- total_runs: Count of active/historical runs.
17
- total_jobs: Total jobs registered in cluster.
18
- running_jobs: Jobs actively executing in compute worker slots.
19
- pending_jobs: Jobs queued and awaiting eligible compute slots.
20
- blocked_jobs: Jobs blocked on dependencies or administrative holds.
21
- completed_jobs: Jobs terminated successfully.
22
- failed_jobs: Jobs terminated with failure or error.
23
- active_workers: Count of active worker daemons.
24
- timestamp: Report generation timestamp.
25
- """
26
-
27
- model_config = ConfigDict(frozen=True, extra="forbid")
28
-
29
- active_workers: int = Field(
30
- default=1, ge=0, description="Active worker daemons count"
31
- )
32
- blocked_jobs: int = Field(default=0, ge=0, description="Blocked jobs count")
33
- completed_jobs: int = Field(default=0, ge=0, description="Completed jobs count")
34
- failed_jobs: int = Field(default=0, ge=0, description="Failed jobs count")
35
- pending_jobs: int = Field(default=0, ge=0, description="Pending jobs count")
36
- running_jobs: int = Field(default=0, ge=0, description="Running jobs count")
37
- timestamp: datetime = Field(
38
- default_factory=lambda: datetime.now(UTC),
39
- description="Stats snapshot timestamp",
40
- )
41
- total_jobs: int = Field(default=0, ge=0, description="Total jobs count")
42
- total_runs: int = Field(default=0, ge=0, description="Total runs count")
43
-
44
-
45
- __all__ = [
46
- "ClusterStatsReport",
47
- ]