Skip to content

fix: accept colon-less +0100 last_updated in is_running() on Python 3.9/3.10 - #5477

Open
headbouyJB wants to merge 2 commits into
springfall2008:mainfrom
headbouyJB:fix-5181-str2time
Open

headbouyJB wants to merge 2 commits into
springfall2008:mainfrom
headbouyJB:fix-5181-str2time

Conversation

@headbouyJB

@headbouyJB headbouyJB commented Oct 10, 2026 •

Copy link
Copy Markdown

Problem

record_status() writes the status entity's last_updated attribute with TIME_FORMAT (%Y-%m-%dT%H:%M:%S%z, const.py), and strftime('%z') never emits a colon in the UTC offset. datetime.fromisoformat() only accepts the colon-less form from Python 3.11, so on 3.9/3.10 is_running() fails to parse Predbat's own timestamp and returns False — the web dashboard shows a false "(unhealthy)" status and the API ping endpoint 500s. Installs on 3.11+ never see it, which is why this was not caught internally.

Change

Following the first of the two options in the automated triage on #5181 (keep the naive tolerance at the reader, rather than changing the writer): changing record_status() to emit .isoformat() would alter the format of an attribute other consumers read — web.py already parses this same attribute with str2time() — while the reader-side change leaves what is stored untouched and simply accepts what has always been written.

In is_running(): parse with utils.str2time() first (its strptime fallback handles +0100 on all Python versions); on ValueError, fall back to datetime.fromisoformat() so a legacy naive last_updated — still persisted on installs upgraded from before record_status() wrote a timezone-aware value — reaches the existing naive-now comparison branch instead of being rejected as unparseable (preserving the tolerance added in b8cb0350). The outer except (ValueError, TypeError) still returns False.

load_plan() is left unchanged: its plan_last_updated is written via .isoformat() (always the colon form), so that call site is not affected by this bug — and swapping it to str2time() would have regressed its naive-timestamp fallback to UTC.

Tests

Extended the existing is_running() coverage in apps/predbat/tests/test_web_functions.py: a fresh last_updated must report running in all three formats the attribute can hold — +0100 (the writer's format), +01:00, and the legacy naive form. Each case also spies on predbat.str2time and requires the value to be handed to it, so reverting the fix fails the test on every Python version, not only 3.9/3.10.

$ python3 unit_test.py -t web_functions
...
Test: is_running() handles both naive and timezone-aware last_updated
**** Web functions tests completed ****
**** Test web_functions PASSED in 0.00s (end 19:05:37) ****
**** All tests passed (total time: 0.00s) ****

CI runs Python 3.11+, where fromisoformat() accepts +0100, so the format cases alone would pass there without the fix; the str2time() spy is what makes the test fail on CI if the fix is reverted. The underlying bug was reproduced on Python 3.9.6 against the repo's real helpers:

>>> s = datetime.now().astimezone().strftime("%Y-%m-%dT%H:%M:%S%z")   # '2026-10-10T19:06:00+0100'
>>> datetime.fromisoformat(s)
ValueError: Invalid isoformat string: '2026-10-10T19:06:00+0100'      # the bug
>>> str2time(s)                                                        # parses
>>> str2time(str(datetime.now()))  -> ValueError
>>> datetime.fromisoformat(str(datetime.now()))                        # fallback parses, naive-now branch reached

pre-commit (ruff, black, cspell) passes on the changed files.

Closes #5181

AI assistance

Drafted with AI assistance (Warden, an OpenClaw agent running local Qwen models); reviewed and submitted by me.

….9/3.10

record_status() writes last_updated with TIME_FORMAT (%Y-%m-%dT%H:%M:%S%z);
strftime('%z') never emits a colon, and datetime.fromisoformat() only
accepts the colon-less form from Python 3.11, so is_running() parsed the
attribute as invalid on 3.9/3.10 and reported a false unhealthy status.

Parse with utils.str2time() first (its strptime fallback handles +0100 on
all versions), falling back to datetime.fromisoformat() on ValueError so
legacy naive last_updated values still reach the existing naive-now
branch (springfall2008#5181 review: keep the naive tolerance added in b8cb035).

load_plan() is left untouched: its timestamp is written via .isoformat()
(colon form), so it is not affected.

Adds fresh-timestamp coverage for +0100, +01:00 and naive last_updated to
the existing is_running() tests.
@headbouyJB headbouyJB changed the title fix: use str2time() instead of datetime.fromisoformat() in is_running() and load_plan() fix: accept colon-less +0100 last_updated in is_running() on Python 3.9/3.10 Oct 10, 2026
@springfall2008
springfall2008 requested a balanced review from Copilot October 10, 2026 18:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The regression test does not distinguish the fixed implementation on the Python version used by CI.

1 open finding
What changed in this PR

Fixes Python 3.9/3.10 health checks by parsing status timestamps with the cross-version helper while preserving legacy naive timestamps.

Changes:

  • Uses str2time() with a naive-timestamp fallback.
  • Adds coverage for supported timestamp formats.
File Description
apps/​predbat/​predbat.py Makes status timestamp parsing cross-version compatible.
apps/​predbat/​tests/​test_web_functions.py Adds timestamp-format regression cases.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +230 to +246
now_local = datetime.now(timezone.utc).astimezone()
fresh_formats = {
"colon-less offset (record_status writer format)": now_local.strftime("%Y-%m-%dT%H:%M:%S%z"),
"colon offset": now_local.isoformat(),
"naive legacy": str(datetime.now()),
}
for label, stamp in fresh_formats.items():
set_entity(my_predbat, status_entity, state="Idle", error=False, last_updated=stamp)
try:
result = my_predbat.is_running()
except (TypeError, ValueError) as e:
print(f" ERROR: is_running() raised on a fresh {label} ({stamp}): {e}")
failed += 1
else:
if result is not True:
print(f" ERROR: expected is_running() True for a fresh {label} ({stamp}), got: {result}")
failed += 1
The Python that CI runs accepts +0100, +01:00 and naive strings through
datetime.fromisoformat() alone, so the fresh-timestamp cases passed with
or without the fix. Spy on predbat.str2time and require each value to be
handed to it, so reverting the fix fails the test on every Python version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: use str2time() instead of datetime.fromisoformat() in is_running() and load_plan() for Python 3.9/3.10 compatibility

2 participants