cctally 1.98.0 → 1.99.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.
@@ -57,6 +57,22 @@ class CLIHelpFormatter(
57
57
  kwargs.setdefault("max_help_position", 30)
58
58
  super().__init__(prog, **kwargs) # type: ignore[arg-type]
59
59
 
60
+ def _get_help_string(self, action: argparse.Action) -> "str | None":
61
+ """Never state a default for a required argument.
62
+
63
+ CPython 3.13 stopped appending ``(default: ...)`` to a required
64
+ argument's help; 3.11 and 3.12 still append it. Without this the same
65
+ parser prints two different help texts depending on the interpreter,
66
+ so `cctally account attribute --help` renders a meaningless
67
+ ``(default: None)`` under `--since` on the two older versions and the
68
+ goldened help diverges on every lane that is not 3.13. Pinning the
69
+ 3.13 rule for every supported version is what makes `--help` a
70
+ property of this program rather than of the interpreter running it.
71
+ """
72
+ if action.required:
73
+ return action.help
74
+ return super()._get_help_string(action)
75
+
60
76
 
61
77
  _PHYSICAL_PROVIDER_SOURCES = ("claude", "codex")
62
78
 
@@ -2611,12 +2627,18 @@ def _build_account_parser(subparsers, name, *, help_text, xref=None):
2611
2627
  show <ref> Show one account's identity + attribution summary.
2612
2628
  label <ref> <name>
2613
2629
  Set a durable user label (survives db rebuild).
2630
+ attribute <ref> --since <iso> [--until <iso>]
2631
+ Attribute already-recorded Codex quota windows
2632
+ and spend to a known Codex account. Previews by
2633
+ default; --yes applies; --retract withdraws.
2614
2634
 
2615
2635
  Examples:
2616
2636
  cctally account list
2617
2637
  cctally account list --json
2618
2638
  cctally account show work@example.com
2619
2639
  cctally account label 3f2a1b work
2640
+ cctally account attribute work --since 2025-12-01T00:00:00Z
2641
+ cctally account attribute work --since 2025-12-01T00:00:00Z --yes
2620
2642
  """),
2621
2643
  )
2622
2644
  acct_sub = acct_p.add_subparsers(dest="account_action", required=True)
@@ -2633,6 +2655,65 @@ def _build_account_parser(subparsers, name, *, help_text, xref=None):
2633
2655
  acct_label.add_argument("ref", help="Account ref (label / email / key prefix)")
2634
2656
  acct_label.add_argument("label", help="New label")
2635
2657
  acct_label.set_defaults(func=c.cmd_account)
2658
+ acct_attr = acct_sub.add_parser(
2659
+ "attribute",
2660
+ help="Attribute recorded Codex windows and spend to an account",
2661
+ formatter_class=CLIHelpFormatter,
2662
+ description=textwrap.dedent("""\
2663
+ Attribute already-recorded Codex quota windows and spend to a known
2664
+ Codex account (#500).
2665
+
2666
+ Codex only. A Claude ref is rejected, and so is the literal
2667
+ 'unattributed' — attributing data TO the sentinel is not a fact an
2668
+ operator can assert.
2669
+
2670
+ --since is required and --until optional; both are timezone-aware ISO
2671
+ instants normalized to UTC, and the range is half-open [since, until).
2672
+ A naive value is a usage error. An omitted --until means "up to now".
2673
+
2674
+ Attribution is asserted per WHOLE physical window group. A group the
2675
+ range covers only partially is refused rather than split; widen the
2676
+ range and rerun.
2677
+
2678
+ Preview is the default and writes nothing. --yes applies, all or
2679
+ nothing over the groups the range could have meant: a partial_group,
2680
+ native_account_conflict, assertion_conflict or spend_account_conflict
2681
+ refusal stops the whole run and nothing is written. A group that is
2682
+ not account weekly quota (not_weekly, model_scoped) is reported as
2683
+ refused and left alone WITHOUT blocking the rest, because a time
2684
+ range selected it rather than you naming it.
2685
+
2686
+ --retract withdraws assertions instead, selecting over the durable
2687
+ assertion records rather than over observations, so a dormant or split
2688
+ assertion can still be tombstoned.
2689
+
2690
+ Exit codes: 0 preview / empty selection / no-op / successful apply;
2691
+ 2 every validation failure and every refusal; 3 operational failure.
2692
+
2693
+ Examples:
2694
+ cctally account attribute work --since 2025-12-01T00:00:00Z
2695
+ cctally account attribute work --since 2025-12-01T00:00:00Z \\
2696
+ --until 2026-07-01T00:00:00Z --yes
2697
+ cctally account attribute work --since 2025-12-01T00:00:00Z \\
2698
+ --retract --yes
2699
+ """),
2700
+ )
2701
+ acct_attr.add_argument("ref", help="Codex account ref (label / email / key prefix)")
2702
+ acct_attr.add_argument(
2703
+ "--since", dest="since", required=True,
2704
+ help="Inclusive start, timezone-aware ISO instant (required).")
2705
+ acct_attr.add_argument(
2706
+ "--until", dest="until",
2707
+ help="Exclusive end, timezone-aware ISO instant (default: now).")
2708
+ acct_attr.add_argument(
2709
+ "--retract", dest="retract", action="store_true",
2710
+ help="Withdraw matching assertions instead of recording one.")
2711
+ acct_attr.add_argument(
2712
+ "--yes", dest="yes", action="store_true",
2713
+ help="Apply the plan (default is a preview that writes nothing).")
2714
+ acct_attr.add_argument("--json", dest="emit_json", action="store_true",
2715
+ help="Emit the stamped-first camelCase JSON envelope.")
2716
+ acct_attr.set_defaults(func=c.cmd_account)
2636
2717
 
2637
2718
 
2638
2719
  def _build_alerts_parser(subparsers, name, *, help_text, xref=None):