cctally 1.10.3 → 1.11.1
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.
- package/CHANGELOG.md +22 -0
- package/bin/_cctally_alerts.py +14 -18
- package/bin/_cctally_cache.py +28 -29
- package/bin/_cctally_cache_report.py +938 -0
- package/bin/_cctally_config.py +31 -22
- package/bin/_cctally_core.py +94 -8
- package/bin/_cctally_dashboard.py +621 -7
- package/bin/_cctally_db.py +42 -30
- package/bin/_cctally_record.py +26 -26
- package/bin/_cctally_setup.py +28 -26
- package/bin/_cctally_tui.py +47 -1
- package/bin/_cctally_update.py +41 -33
- package/bin/_lib_changelog.py +3 -1
- package/bin/_lib_share_templates.py +31 -13
- package/bin/cctally +214 -495
- package/dashboard/static/assets/index-BJ16SzRL.js +18 -0
- package/dashboard/static/assets/index-C1xH9GBW.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-Cy59E7Ru.js +0 -18
- package/dashboard/static/assets/index-Dp14ELVt.css +0 -1
package/bin/_cctally_update.py
CHANGED
|
@@ -102,12 +102,19 @@ worker / polling thread:
|
|
|
102
102
|
accessor; eager re-export from this sibling means the cctally
|
|
103
103
|
namespace exposes them unchanged.
|
|
104
104
|
|
|
105
|
+
What lives in bin/_cctally_core (promoted 2026-05-22, #84):
|
|
106
|
+
- All ``UPDATE_*`` path constants: ``UPDATE_STATE_PATH``,
|
|
107
|
+
``UPDATE_SUPPRESS_PATH``, ``UPDATE_LOCK_PATH``, ``UPDATE_LOG_PATH``,
|
|
108
|
+
``UPDATE_LOG_ROTATED_PATH``, ``UPDATE_CHECK_LAST_FETCH_PATH``.
|
|
109
|
+
Moved bodies in this sibling read them via call-time
|
|
110
|
+
``_cctally_core.UPDATE_STATE_PATH`` etc. Tests patch via
|
|
111
|
+
``monkeypatch.setattr(_cctally_core, "UPDATE_STATE_PATH", tmp)`` —
|
|
112
|
+
the conftest helper ``redirect_paths()`` covers this for the full
|
|
113
|
+
set, ``tests/test_update.py:update_paths`` covers it for the
|
|
114
|
+
UPDATE_* subset. The legacy ``setitem(ns, …)`` pattern is forbidden
|
|
115
|
+
by ``test_no_old_style_test_patches_for_promoted_globals``.
|
|
116
|
+
|
|
105
117
|
What stays in bin/cctally:
|
|
106
|
-
- All ``UPDATE_*`` path constants (source-of-truth at L2001-2023);
|
|
107
|
-
consumed via ``c = _cctally(); c.UPDATE_STATE_PATH`` etc. in moved
|
|
108
|
-
code so ``monkeypatch.setitem(ns, "UPDATE_STATE_PATH", tmp)`` in
|
|
109
|
-
``tests/test_update.py`` propagates transparently — no sibling-side
|
|
110
|
-
patches needed. Mirrors Phase D #17/#18 precedent.
|
|
111
118
|
- ``ORIGINAL_SYS_ARGV`` / ``ORIGINAL_ENTRYPOINT`` /
|
|
112
119
|
``_UPDATE_WORKER`` — module-level globals written by
|
|
113
120
|
``cmd_dashboard`` at boot (``global`` statement at L23205);
|
|
@@ -200,6 +207,7 @@ def _cctally():
|
|
|
200
207
|
|
|
201
208
|
# === Honest imports from extracted homes ===================================
|
|
202
209
|
# Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3.
|
|
210
|
+
import _cctally_core
|
|
203
211
|
from _cctally_core import eprint, _now_utc
|
|
204
212
|
from _cctally_config import save_config
|
|
205
213
|
|
|
@@ -405,7 +413,7 @@ def _load_update_state() -> dict[str, Any] | None:
|
|
|
405
413
|
"""
|
|
406
414
|
c = _cctally()
|
|
407
415
|
try:
|
|
408
|
-
text =
|
|
416
|
+
text = _cctally_core.UPDATE_STATE_PATH.read_text(encoding="utf-8")
|
|
409
417
|
except FileNotFoundError:
|
|
410
418
|
return None
|
|
411
419
|
try:
|
|
@@ -437,12 +445,12 @@ def _save_update_state(state: dict[str, Any]) -> None:
|
|
|
437
445
|
writers via ``UPDATE_LOCK_PATH`` (spec §5.3).
|
|
438
446
|
"""
|
|
439
447
|
c = _cctally()
|
|
440
|
-
|
|
448
|
+
_cctally_core.UPDATE_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
441
449
|
payload = (
|
|
442
450
|
json.dumps(state, indent=2, sort_keys=True) + "\n"
|
|
443
451
|
).encode("utf-8")
|
|
444
|
-
tmp =
|
|
445
|
-
f"{
|
|
452
|
+
tmp = _cctally_core.UPDATE_STATE_PATH.with_name(
|
|
453
|
+
f"{_cctally_core.UPDATE_STATE_PATH.name}.tmp.{os.getpid()}"
|
|
446
454
|
)
|
|
447
455
|
fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
|
|
448
456
|
try:
|
|
@@ -450,7 +458,7 @@ def _save_update_state(state: dict[str, Any]) -> None:
|
|
|
450
458
|
os.fsync(fd)
|
|
451
459
|
finally:
|
|
452
460
|
os.close(fd)
|
|
453
|
-
os.replace(str(tmp), str(
|
|
461
|
+
os.replace(str(tmp), str(_cctally_core.UPDATE_STATE_PATH))
|
|
454
462
|
|
|
455
463
|
|
|
456
464
|
def _load_update_suppress() -> dict[str, Any]:
|
|
@@ -462,7 +470,7 @@ def _load_update_suppress() -> dict[str, Any]:
|
|
|
462
470
|
c = _cctally()
|
|
463
471
|
default = {"_schema": 1, "skipped_versions": [], "remind_after": None}
|
|
464
472
|
try:
|
|
465
|
-
text =
|
|
473
|
+
text = _cctally_core.UPDATE_SUPPRESS_PATH.read_text(encoding="utf-8")
|
|
466
474
|
except FileNotFoundError:
|
|
467
475
|
return default
|
|
468
476
|
try:
|
|
@@ -489,12 +497,12 @@ def _save_update_suppress(suppress: dict[str, Any]) -> None:
|
|
|
489
497
|
"""Persist ``update-suppress.json`` atomically. Same idiom as
|
|
490
498
|
:func:`_save_update_state`."""
|
|
491
499
|
c = _cctally()
|
|
492
|
-
|
|
500
|
+
_cctally_core.UPDATE_SUPPRESS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
493
501
|
payload = (
|
|
494
502
|
json.dumps(suppress, indent=2, sort_keys=True) + "\n"
|
|
495
503
|
).encode("utf-8")
|
|
496
|
-
tmp =
|
|
497
|
-
f"{
|
|
504
|
+
tmp = _cctally_core.UPDATE_SUPPRESS_PATH.with_name(
|
|
505
|
+
f"{_cctally_core.UPDATE_SUPPRESS_PATH.name}.tmp.{os.getpid()}"
|
|
498
506
|
)
|
|
499
507
|
fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
|
|
500
508
|
try:
|
|
@@ -502,7 +510,7 @@ def _save_update_suppress(suppress: dict[str, Any]) -> None:
|
|
|
502
510
|
os.fsync(fd)
|
|
503
511
|
finally:
|
|
504
512
|
os.close(fd)
|
|
505
|
-
os.replace(str(tmp), str(
|
|
513
|
+
os.replace(str(tmp), str(_cctally_core.UPDATE_SUPPRESS_PATH))
|
|
506
514
|
|
|
507
515
|
|
|
508
516
|
def _read_lock_pid(fd: int) -> int | None:
|
|
@@ -543,9 +551,9 @@ def _acquire_update_lock() -> int:
|
|
|
543
551
|
COMMAND=cctally update
|
|
544
552
|
"""
|
|
545
553
|
c = _cctally()
|
|
546
|
-
|
|
554
|
+
_cctally_core.UPDATE_LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
547
555
|
fd = os.open(
|
|
548
|
-
str(
|
|
556
|
+
str(_cctally_core.UPDATE_LOCK_PATH), os.O_CREAT | os.O_RDWR, 0o644
|
|
549
557
|
)
|
|
550
558
|
try:
|
|
551
559
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -622,16 +630,16 @@ def _rotate_update_log_if_needed() -> None:
|
|
|
622
630
|
"""
|
|
623
631
|
c = _cctally()
|
|
624
632
|
try:
|
|
625
|
-
size =
|
|
633
|
+
size = _cctally_core.UPDATE_LOG_PATH.stat().st_size
|
|
626
634
|
except FileNotFoundError:
|
|
627
635
|
return
|
|
628
636
|
if size < c.UPDATE_LOG_ROTATE_BYTES:
|
|
629
637
|
return
|
|
630
638
|
try:
|
|
631
|
-
|
|
639
|
+
_cctally_core.UPDATE_LOG_ROTATED_PATH.unlink()
|
|
632
640
|
except FileNotFoundError:
|
|
633
641
|
pass
|
|
634
|
-
|
|
642
|
+
_cctally_core.UPDATE_LOG_PATH.rename(_cctally_core.UPDATE_LOG_ROTATED_PATH)
|
|
635
643
|
|
|
636
644
|
|
|
637
645
|
def _log_update_event(log_fd, event: str, **kv: Any) -> None:
|
|
@@ -892,7 +900,7 @@ def _self_heal_current_version() -> None:
|
|
|
892
900
|
"""
|
|
893
901
|
c = _cctally()
|
|
894
902
|
try:
|
|
895
|
-
if (
|
|
903
|
+
if (_cctally_core.CHANGELOG_PATH.parent / ".git").exists():
|
|
896
904
|
return
|
|
897
905
|
fresh = _release_read_latest_release_version()
|
|
898
906
|
if fresh is None:
|
|
@@ -1029,7 +1037,7 @@ def _is_update_check_due(config: dict) -> bool:
|
|
|
1029
1037
|
return False
|
|
1030
1038
|
ttl_hours = check_cfg.get("ttl_hours", c.UPDATE_DEFAULT_TTL_HOURS)
|
|
1031
1039
|
try:
|
|
1032
|
-
mtime =
|
|
1040
|
+
mtime = _cctally_core.UPDATE_CHECK_LAST_FETCH_PATH.stat().st_mtime
|
|
1033
1041
|
except FileNotFoundError:
|
|
1034
1042
|
return True
|
|
1035
1043
|
return (time.time() - mtime) >= ttl_hours * 3600
|
|
@@ -1052,8 +1060,8 @@ def _do_update_check() -> None:
|
|
|
1052
1060
|
c = _cctally()
|
|
1053
1061
|
# Touch marker FIRST — crash safety: a dead process mid-fetch must
|
|
1054
1062
|
# not trigger another fetch within the TTL window.
|
|
1055
|
-
|
|
1056
|
-
|
|
1063
|
+
_cctally_core.UPDATE_CHECK_LAST_FETCH_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
1064
|
+
_cctally_core.UPDATE_CHECK_LAST_FETCH_PATH.touch()
|
|
1057
1065
|
|
|
1058
1066
|
method = c._detect_install_method(mutate=True)
|
|
1059
1067
|
|
|
@@ -1143,15 +1151,15 @@ def cmd_update_check_internal(args) -> int:
|
|
|
1143
1151
|
"""
|
|
1144
1152
|
c = _cctally()
|
|
1145
1153
|
# Ensure APP_DIR exists so log + state writes succeed on first run.
|
|
1146
|
-
|
|
1154
|
+
_cctally_core.UPDATE_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
1147
1155
|
try:
|
|
1148
|
-
with open(
|
|
1156
|
+
with open(_cctally_core.UPDATE_LOG_PATH, "a", encoding="utf-8") as log_fd:
|
|
1149
1157
|
_log_update_event(log_fd, "CHECK_START")
|
|
1150
1158
|
c._do_update_check()
|
|
1151
1159
|
_log_update_event(log_fd, "CHECK_EXIT", rc=0)
|
|
1152
1160
|
except Exception as e:
|
|
1153
1161
|
try:
|
|
1154
|
-
with open(
|
|
1162
|
+
with open(_cctally_core.UPDATE_LOG_PATH, "a", encoding="utf-8") as log_fd:
|
|
1155
1163
|
_log_update_event(log_fd, "CHECK_EXIT", rc=1, error=str(e)[:200])
|
|
1156
1164
|
except Exception:
|
|
1157
1165
|
pass
|
|
@@ -1615,10 +1623,10 @@ def _do_update_install(
|
|
|
1615
1623
|
quoted = " ".join(shlex.quote(c2) for c2 in cmd)
|
|
1616
1624
|
print(f"Would run: {quoted}")
|
|
1617
1625
|
return 0
|
|
1618
|
-
|
|
1626
|
+
_cctally_core.UPDATE_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
1619
1627
|
lock_fd = c._acquire_update_lock()
|
|
1620
1628
|
try:
|
|
1621
|
-
with open(
|
|
1629
|
+
with open(_cctally_core.UPDATE_LOG_PATH, "a", encoding="utf-8") as log_fd:
|
|
1622
1630
|
_log_update_event(log_fd, "INSTALL_START", method=method.method)
|
|
1623
1631
|
for step_name, cmd in steps:
|
|
1624
1632
|
_log_update_event(log_fd, "STEP_START", name=step_name)
|
|
@@ -1790,9 +1798,9 @@ class UpdateWorker:
|
|
|
1790
1798
|
try:
|
|
1791
1799
|
method = c._detect_install_method(mutate=True)
|
|
1792
1800
|
c._preflight_install(method, version)
|
|
1793
|
-
|
|
1801
|
+
_cctally_core.UPDATE_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
1794
1802
|
lock_fd = c._acquire_update_lock()
|
|
1795
|
-
log_fd = open(
|
|
1803
|
+
log_fd = open(_cctally_core.UPDATE_LOG_PATH, "a", encoding="utf-8")
|
|
1796
1804
|
_log_update_event(log_fd, "INSTALL_START", method=method.method)
|
|
1797
1805
|
for step_name, cmd in c._build_update_steps(method, version):
|
|
1798
1806
|
self._emit(run_id, {"type": "step", "name": step_name})
|
|
@@ -1923,8 +1931,8 @@ class _DashboardUpdateCheckThread(threading.Thread):
|
|
|
1923
1931
|
# silently disable the polling cadence for the rest
|
|
1924
1932
|
# of the dashboard's lifetime.
|
|
1925
1933
|
try:
|
|
1926
|
-
|
|
1927
|
-
with open(
|
|
1934
|
+
_cctally_core.UPDATE_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
1935
|
+
with open(_cctally_core.UPDATE_LOG_PATH, "a", encoding="utf-8") as log_fd:
|
|
1928
1936
|
_log_update_event(
|
|
1929
1937
|
log_fd, "CHECK_FAILED", error=str(e)[:200]
|
|
1930
1938
|
)
|
package/bin/_lib_changelog.py
CHANGED
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import sys
|
|
15
15
|
|
|
16
|
+
import _cctally_core
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
def _cctally():
|
|
18
20
|
"""Call-time accessor for the ``cctally`` module (project memory
|
|
@@ -35,7 +37,7 @@ def _read_latest_changelog_version() -> tuple[str, str] | None:
|
|
|
35
37
|
"""
|
|
36
38
|
c = _cctally()
|
|
37
39
|
try:
|
|
38
|
-
text =
|
|
40
|
+
text = _cctally_core.CHANGELOG_PATH.read_text(encoding="utf-8")
|
|
39
41
|
except FileNotFoundError:
|
|
40
42
|
return None
|
|
41
43
|
m = c.RELEASE_HEADER_RE.search(text)
|
|
@@ -21,6 +21,18 @@ from collections.abc import Callable, Mapping
|
|
|
21
21
|
from dataclasses import dataclass
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
+
# NOTE: ``_cctally_core`` is imported lazily inside ``_release_version()``
|
|
25
|
+
# (the sole consumer in this module) so that loading this file directly
|
|
26
|
+
# via ``importlib.util.spec_from_file_location`` — the established
|
|
27
|
+
# pattern used by ``_render_share_v2_fixture.py``,
|
|
28
|
+
# ``_compose_share_v2_fixture.py``, and ``tests/test_lib_share_templates.py``
|
|
29
|
+
# — does NOT depend on ``bin/`` being on ``sys.path`` at module-import
|
|
30
|
+
# time. A module-top ``import _cctally_core`` would raise
|
|
31
|
+
# ``ModuleNotFoundError`` for any standalone loader that hasn't already
|
|
32
|
+
# arranged for ``_cctally_core`` to be importable, regressing the
|
|
33
|
+
# parallel-loader pattern this module uses for ``_lib_share`` itself
|
|
34
|
+
# (see ``_import_share_lib`` below).
|
|
35
|
+
|
|
24
36
|
|
|
25
37
|
# --- Panel set ---
|
|
26
38
|
#
|
|
@@ -288,22 +300,28 @@ def _utc_now() -> _dt.datetime:
|
|
|
288
300
|
def _release_version() -> str:
|
|
289
301
|
"""Read CHANGELOG-stamped latest version.
|
|
290
302
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
303
|
+
Resolves through ``_cctally_core.CHANGELOG_PATH`` so the kernel's
|
|
304
|
+
env override (``CCTALLY_TEST_CHANGELOG_PATH``) and any test-side
|
|
305
|
+
``monkeypatch.setattr(_cctally_core, "CHANGELOG_PATH", ...)`` both
|
|
306
|
+
propagate transparently. Falls back to ``"dev"`` when CHANGELOG is
|
|
307
|
+
unreadable or has no stamped release entry yet (pre-release dev
|
|
308
|
+
builds).
|
|
294
309
|
|
|
295
|
-
Parallel to
|
|
296
|
-
(re-exported on
|
|
297
|
-
|
|
310
|
+
Parallel to ``_lib_changelog._read_latest_changelog_version``
|
|
311
|
+
(re-exported on ``bin/cctally`` under the historical
|
|
312
|
+
``_release_read_latest_release_version`` name) — intentionally
|
|
298
313
|
duplicated so the template module stays free of any
|
|
299
|
-
|
|
314
|
+
``bin/cctally`` import. If CHANGELOG header format changes, update both.
|
|
315
|
+
|
|
316
|
+
``_cctally_core`` is imported lazily here (rather than at module top)
|
|
317
|
+
so that standalone loaders — see the note at the top of this file
|
|
318
|
+
and the ``_import_share_lib`` pattern below — can exec this module
|
|
319
|
+
without ``bin/`` already being on ``sys.path``. By the time
|
|
320
|
+
``_release_version()`` is actually called, the caller (CLI boot,
|
|
321
|
+
dashboard, or fixture driver) has resolved the sibling layout.
|
|
300
322
|
"""
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if p:
|
|
304
|
-
path = Path(p)
|
|
305
|
-
else:
|
|
306
|
-
path = Path(__file__).resolve().parent.parent / "CHANGELOG.md"
|
|
323
|
+
import _cctally_core # noqa: PLC0415 — lazy by design (see module-top NOTE)
|
|
324
|
+
path = _cctally_core.CHANGELOG_PATH
|
|
307
325
|
try:
|
|
308
326
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
309
327
|
if line.startswith("## [") and "Unreleased" not in line:
|