From d48791b463bfb0517bb2891979674eb6004b6607 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic Date: Sat, 14 Mar 2026 10:36:45 -0700 Subject: [PATCH 1/3] fix: README structure and code quality nits - Update project structure diagram to show docs/ under src/copilot_usage/ instead of top-level (fixes #21) - Replace verbose lambda default_factory with default_factory=list in models.py toolRequests field - Remove model_token_map indirection in parser.py build_session_summary; build active_metrics directly - Delegate cost command date filtering to render_cost_view via _filter_sessions instead of inline reimplementation Closes #21 Closes #23 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 26 ++++++++++++++------------ src/copilot_usage/cli.py | 19 +++++-------------- src/copilot_usage/models.py | 4 +--- src/copilot_usage/report.py | 4 ++++ 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 899ef2b..e7bb631 100644 --- a/README.md +++ b/README.md @@ -204,20 +204,22 @@ Add `V=1` for verbose output: `make check V=1` cli-tools/ ├── src/ │ └── copilot_usage/ -│ ├── cli.py # Click commands + interactive loop + watchdog auto-refresh -│ ├── models.py # Pydantic data models -│ ├── parser.py # events.jsonl parsing -│ ├── pricing.py # Model cost multipliers -│ ├── logging_config.py # Loguru configuration -│ └── report.py # Rich terminal output +│ ├── cli.py # Click commands + interactive loop + watchdog auto-refresh +│ ├── models.py # Pydantic data models +│ ├── parser.py # events.jsonl parsing +│ ├── pricing.py # Model cost multipliers +│ ├── logging_config.py # Loguru configuration +│ ├── report.py # Rich terminal output +│ └── docs/ # Developer docs +│ ├── architecture.md +│ ├── changelog.md +│ ├── implementation.md +│ └── plan.md ├── tests/ -│ ├── copilot_usage/ # Unit tests -│ └── e2e/ # End-to-end tests with fixtures +│ ├── copilot_usage/ # Unit tests +│ └── e2e/ # End-to-end tests with fixtures ├── docs/ -│ ├── plan.md -│ ├── architecture.md -│ ├── changelog.md -│ └── implementation.md +│ └── changelog.md # Top-level changelog ├── Makefile └── pyproject.toml ``` diff --git a/src/copilot_usage/cli.py b/src/copilot_usage/cli.py index 19bdf97..329af2f 100644 --- a/src/copilot_usage/cli.py +++ b/src/copilot_usage/cli.py @@ -388,20 +388,11 @@ def cost( try: sessions = get_all_sessions(path) - # Filter by date range - since_aware = _ensure_aware(since) - until_aware = _ensure_aware(until) - filtered = sessions - if since_aware is not None or until_aware is not None: - filtered = [ - s - for s in sessions - if s.start_time is not None - and (since_aware is None or s.start_time >= since_aware) - and (until_aware is None or s.start_time <= until_aware) - ] - - render_cost_view(filtered) + render_cost_view( + sessions, + since=_ensure_aware(since), + until=_ensure_aware(until), + ) except Exception as exc: # noqa: BLE001 click.echo(f"Error: {exc}", err=True) sys.exit(1) diff --git a/src/copilot_usage/models.py b/src/copilot_usage/models.py index 700bbc6..419af6f 100644 --- a/src/copilot_usage/models.py +++ b/src/copilot_usage/models.py @@ -103,9 +103,7 @@ class AssistantMessageData(BaseModel): interactionId: str = "" reasoningText: str | None = None reasoningOpaque: str | None = None - toolRequests: list[dict[str, object]] = Field( - default_factory=lambda: list[dict[str, object]]() - ) + toolRequests: list[dict[str, object]] = Field(default_factory=list) class SessionShutdownData(BaseModel): diff --git a/src/copilot_usage/report.py b/src/copilot_usage/report.py index 7f01f18..f7bd94e 100644 --- a/src/copilot_usage/report.py +++ b/src/copilot_usage/report.py @@ -848,14 +848,18 @@ def render_full_summary( def render_cost_view( sessions: list[SessionSummary], *, + since: datetime | None = None, + until: datetime | None = None, target_console: Console | None = None, ) -> None: """Render per-session, per-model cost breakdown. + Filters sessions by date range when *since* and/or *until* are given. For active sessions, appends a "↳ Since last shutdown" row with N/A for premium and the active model calls / output tokens. """ console = target_console or Console() + sessions = _filter_sessions(sessions, since, until) if not sessions: console.print("[yellow]No sessions found.[/yellow]") From ded1d3e6221d6e6f1152eea26a3a966f27b49e45 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic Date: Sat, 14 Mar 2026 11:42:41 -0700 Subject: [PATCH 2/3] test: add since/until filter test for render_cost_view Copilot review: new since/until params on render_cost_view had no test coverage. Added test verifying sessions outside range are excluded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/copilot_usage/test_report.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index aeb3f7e..a26dad5 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -1134,11 +1134,15 @@ def test_no_historical_data(self) -> None: # --------------------------------------------------------------------------- -def _capture_cost_view(sessions: list[SessionSummary]) -> str: +def _capture_cost_view( + sessions: list[SessionSummary], + since: datetime | None = None, + until: datetime | None = None, +) -> str: """Capture Rich output from render_cost_view to a plain string.""" buf = StringIO() console = Console(file=buf, force_terminal=True, width=120) - render_cost_view(sessions, target_console=console) + render_cost_view(sessions, since=since, until=until, target_console=console) return buf.getvalue() @@ -1321,3 +1325,15 @@ def test_historical_model_table_present(self) -> None: output = _capture_full_summary([session]) assert "Per-Model Breakdown" in output assert "claude-sonnet-4" in output + + def test_since_until_filters_sessions(self) -> None: + """render_cost_view since/until params exclude sessions outside range.""" + early = _make_session( + start_time=datetime(2025, 1, 10, tzinfo=UTC), name="Early" + ) + late = _make_session(start_time=datetime(2025, 1, 20, tzinfo=UTC), name="Late") + since = datetime(2025, 1, 15, tzinfo=UTC) + until = datetime(2025, 1, 25, tzinfo=UTC) + output = _capture_cost_view([early, late], since=since, until=until) + assert "Late" in output + assert "Early" not in output From 8be983c96bd97379ad8f80b5e52e358eda4927a6 Mon Sep 17 00:00:00 2001 From: Sasa Junuzovic Date: Sat, 14 Mar 2026 11:54:45 -0700 Subject: [PATCH 3/3] fix: restore typed default_factory for toolRequests (pyright) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/copilot_usage/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/copilot_usage/models.py b/src/copilot_usage/models.py index 419af6f..700bbc6 100644 --- a/src/copilot_usage/models.py +++ b/src/copilot_usage/models.py @@ -103,7 +103,9 @@ class AssistantMessageData(BaseModel): interactionId: str = "" reasoningText: str | None = None reasoningOpaque: str | None = None - toolRequests: list[dict[str, object]] = Field(default_factory=list) + toolRequests: list[dict[str, object]] = Field( + default_factory=lambda: list[dict[str, object]]() + ) class SessionShutdownData(BaseModel):