Say when a run finished and when a failure happened
`fluksio status` listed recent runs and recent failures with no time on them, so a red line said nothing about whether it was from a minute ago or last week. Both carry an age now, spelled the way a duration is, and the runs listing gained one too. The fields were already on the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
This commit is contained in:
@@ -19,6 +19,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Callable, Iterable, Iterator
|
from collections.abc import Callable, Iterable, Iterator
|
||||||
from contextlib import contextmanager, nullcontext
|
from contextlib import contextmanager, nullcontext
|
||||||
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -804,7 +805,8 @@ def _status_screen(client: Client) -> Any:
|
|||||||
)
|
)
|
||||||
+ Text(
|
+ Text(
|
||||||
f"{str(row.get('flow', '')):<16}"
|
f"{str(row.get('flow', '')):<16}"
|
||||||
f"{(row.get('duration_ms') or 0) / 1000:7.1f}s",
|
f"{(row.get('duration_ms') or 0) / 1000:7.1f}s"
|
||||||
|
f" {_ago(row.get('finished_at') or row.get('created_at')):>9}",
|
||||||
style="dim",
|
style="dim",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -816,6 +818,7 @@ def _status_screen(client: Client) -> Any:
|
|||||||
)
|
)
|
||||||
parts.append(
|
parts.append(
|
||||||
Text(f" {where} ", style="red")
|
Text(f" {where} ", style="red")
|
||||||
|
+ Text(f"{_ago(event.get('ts')):>9} ", style="dim")
|
||||||
+ Text(str(event.get("detail", ""))[:100], style="dim")
|
+ Text(str(event.get("detail", ""))[:100], style="dim")
|
||||||
)
|
)
|
||||||
return Group(*parts)
|
return Group(*parts)
|
||||||
@@ -857,9 +860,9 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|||||||
#: value is read.
|
#: value is read.
|
||||||
PARAMS_WIDTH = 80
|
PARAMS_WIDTH = 80
|
||||||
|
|
||||||
#: What the columns before the inputs take: the id, status, flow, duration and
|
#: What the columns before the inputs take: the id, status, flow, duration,
|
||||||
#: stamp, with their spacing.
|
#: age and stamp, with their spacing.
|
||||||
LISTING_WIDTH = 84
|
LISTING_WIDTH = 95
|
||||||
|
|
||||||
|
|
||||||
def _declared(client: Client, flows: Iterable[str]) -> dict[str, dict[str, Any]]:
|
def _declared(client: Client, flows: Iterable[str]) -> dict[str, dict[str, Any]]:
|
||||||
@@ -883,6 +886,28 @@ def _declared(client: Client, flows: Iterable[str]) -> dict[str, dict[str, Any]]
|
|||||||
return known
|
return known
|
||||||
|
|
||||||
|
|
||||||
|
def _ago(stamp: Any) -> str:
|
||||||
|
"""How long ago something happened, in the notation the screens use.
|
||||||
|
|
||||||
|
A duration and an age read together, so they are spelled the same way:
|
||||||
|
seconds, then minutes, hours, days.
|
||||||
|
"""
|
||||||
|
if not stamp:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
then = datetime.fromisoformat(str(stamp))
|
||||||
|
except ValueError:
|
||||||
|
return ""
|
||||||
|
if then.tzinfo is None:
|
||||||
|
# Everything the engine records is UTC; only some spellings say so.
|
||||||
|
then = then.replace(tzinfo=UTC)
|
||||||
|
seconds = max((datetime.now(UTC) - then).total_seconds(), 0)
|
||||||
|
for span, unit in ((86400, "d"), (3600, "h"), (60, "min")):
|
||||||
|
if seconds >= span:
|
||||||
|
return f"{seconds / span:.0f}{unit} ago"
|
||||||
|
return f"{seconds:.0f}s ago"
|
||||||
|
|
||||||
|
|
||||||
def _stamp(row: dict[str, Any]) -> str:
|
def _stamp(row: dict[str, Any]) -> str:
|
||||||
"""What code a run ran: the commit, whether it was dirty, and the digest.
|
"""What code a run ran: the commit, whether it was dirty, and the digest.
|
||||||
|
|
||||||
@@ -925,7 +950,8 @@ def cmd_runs(args: argparse.Namespace) -> int:
|
|||||||
params = params[: room - 3] + "..."
|
params = params[: room - 3] + "..."
|
||||||
_say(
|
_say(
|
||||||
f"{row['id']} {_status(row['status'], 9)} {row['flow']:<16} "
|
f"{row['id']} {_status(row['status'], 9)} {row['flow']:<16} "
|
||||||
f"{row['duration_ms'] / 1000:7.1f}s {_stamp(row):<22} {params}"
|
f"{row['duration_ms'] / 1000:7.1f}s {_ago(row.get('created_at')):>9} "
|
||||||
|
f"{_stamp(row):<22} {params}"
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -520,6 +520,24 @@ def test_two_files_of_one_name_are_refused(tmp_path) -> None:
|
|||||||
_import(*_module_of(second), second)
|
_import(*_module_of(second), second)
|
||||||
|
|
||||||
|
|
||||||
|
def test_how_long_ago_reads_like_a_duration() -> None:
|
||||||
|
"""A failure with no time on it says nothing about whether it is current."""
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
|
from fluksio.sdk.cli import _ago
|
||||||
|
|
||||||
|
def then(**delta):
|
||||||
|
return (datetime.now(UTC) - timedelta(**delta)).isoformat()
|
||||||
|
|
||||||
|
assert _ago(then(seconds=5)) == "5s ago"
|
||||||
|
assert _ago(then(minutes=3)) == "3min ago"
|
||||||
|
assert _ago(then(hours=2)) == "2h ago"
|
||||||
|
assert _ago(then(days=3)) == "3d ago"
|
||||||
|
# What the engine stores is UTC whether or not the spelling says so.
|
||||||
|
assert _ago(datetime.now(UTC).replace(tzinfo=None).isoformat()) == "0s ago"
|
||||||
|
assert _ago(None) == ""
|
||||||
|
|
||||||
|
|
||||||
def test_a_serve_limit_is_refused_as_a_flag_not_as_a_traceback(capsys) -> None:
|
def test_a_serve_limit_is_refused_as_a_flag_not_as_a_traceback(capsys) -> None:
|
||||||
"""These are written into the environment before the settings are built."""
|
"""These are written into the environment before the settings are built."""
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
Reference in New Issue
Block a user