Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
fa5bea5
Update Python version requirements to 3.10+
PabloLION Aug 15, 2025
5f56be7
chore: add autoflake dev dependency
PabloLION Aug 16, 2025
ff683dc
Modernize typing syntax to Python 3.10+ standards
PabloLION Aug 16, 2025
d628b0b
WIP: Remove Any types and improve type safety
PabloLION Aug 16, 2025
c81c8d3
feat: Improve type safety with TypedDict and reduce mypy errors
PabloLION Aug 16, 2025
c07dcc8
feat: Replace JSONSerializable with TypedDict for improved type safety
PabloLION Aug 16, 2025
aaa4909
fix: Improve TypedDict compatibility and JSONSerializable handling
PabloLION Aug 16, 2025
7ac1792
chore: Fix pytest coverage configuration and update .gitignore
PabloLION Aug 16, 2025
6edb4f7
refactor: Improve type safety with Optional/None handling
PabloLION Aug 16, 2025
555215e
refactor: Fix type compatibility issues in data processing modules
PabloLION Aug 16, 2025
92ad5f6
refactor: Improve datetime handling in display controller
PabloLION Aug 16, 2025
1453c8e
fix: Resolve union type arithmetic and protocol compatibility issues
PabloLION Aug 16, 2025
6d5d959
fix: Resolve TypedDict compatibility issues in SessionBlock and analy…
PabloLION Aug 16, 2025
299f116
fix: Resolve return type mismatches with structured TypedDicts
PabloLION Aug 16, 2025
511d5eb
feat: Major type safety improvements across codebase
PabloLION Aug 16, 2025
e29e36e
WIP: Improve safe_get_int function to avoid Any type
PabloLION Aug 17, 2025
eba74e0
chore: Add VS Code settings for pytest test discovery
PabloLION Aug 17, 2025
d6698ab
fix: Restore CLI argument parsing in Settings constructor calls
PabloLION Aug 17, 2025
e092683
feat: Add specific TypedDicts for Claude message types
PabloLION Aug 17, 2025
745c1ca
feat: Migrate data processing to use specific ClaudeJSONEntry types
PabloLION Aug 17, 2025
bcd3157
refactor: Complete type migration by removing RawJSONEntry and UsageData
PabloLION Aug 17, 2025
b3da5d4
fix: Update monitoring layer for TypedDict compatibility
PabloLION Aug 17, 2025
dcd6a2f
feat: Complete display controller type safety improvements
PabloLION Aug 17, 2025
15ad80b
fix: Achieve 100% mypy compliance across entire codebase
PabloLION Aug 17, 2025
8e65e9d
refactor: Reorganize TypedDicts into domain-based types package
PabloLION Aug 17, 2025
42daf02
feat: Add foundation TypedDicts for method return types
PabloLION Aug 17, 2025
25219cb
feat: Add aggregate TypedDicts and fix import issues
PabloLION Aug 17, 2025
acc4f75
feat: Major type safety improvements across codebase
PabloLION Aug 17, 2025
cff5643
refactor: Make detect_timezone_time_preference public with TODO
PabloLION Aug 17, 2025
c35db9a
chore: Remove unused imports with autoflake
PabloLION Aug 17, 2025
8e21860
chore: Add autoflake configuration to pyproject.toml
PabloLION Aug 17, 2025
0655c1c
feat: Eliminate private method access in main code through public APIs
PabloLION Aug 17, 2025
cc640f9
fix: Add type ignore comments for private usage in test files
PabloLION Aug 17, 2025
db2a6bb
chore: Apply ruff formatting and import organization
PabloLION Aug 17, 2025
beaa66b
feat: Implement comprehensive type safety improvements
PabloLION Aug 18, 2025
398011b
fix: Resolve remaining type issues and merge BlockData/BlockDict types
PabloLION Aug 18, 2025
be31485
fix: Complete type safety cleanup and resolve strict mypy issues
PabloLION Aug 18, 2025
570e379
feat: Add new TypedDict definitions for dict[str, JSONSerializable] r…
PabloLION Aug 18, 2025
8a55283
fix: Apply explicit type constructors and eliminate remaining = [] pa…
PabloLION Aug 18, 2025
7834c67
feat: Replace all dict[str, JSONSerializable] with specific TypedDict…
PabloLION Aug 18, 2025
c04a033
feat: Complete (almost) TypedDict migration by adding TokenSource and…
PabloLION Aug 18, 2025
dab2d2d
fix: Complete final TypedDict migration and resolve type errors
PabloLION Aug 18, 2025
4aa13e8
refactor: Remove find_private_usage.py script as it is no longer needed
PabloLION Aug 18, 2025
c8628fc
feat: Enhance backport utilities with HAS_TOMLLIB flag and clean up _…
PabloLION Aug 18, 2025
8397ccb
fix: Remove unsafe locals() usage and improve type safety
PabloLION Aug 18, 2025
055ce80
fix: Simplify BlockData conversion logic in get_token_limit function
PabloLION Aug 18, 2025
78c63f5
fix: Resolve all Pylance type issues in data_processors.py and improv…
PabloLION Aug 18, 2025
70218e4
refactor: Use TypedDict inheritance to eliminate duplication in API t…
PabloLION Aug 18, 2025
465271d
refactor: Replace complex union types with structured TypedDict defin…
PabloLION Aug 18, 2025
9378311
refactor: Add SystemMessageContent and optimize model extraction logic
PabloLION Aug 18, 2025
d7ffc44
refactor: Simplify model name extraction logic in DataConverter class
PabloLION Aug 18, 2025
b57db64
refactor: Update extract_model_name method to indicate potential outd…
PabloLION Aug 18, 2025
d158a85
refactor: Simplify model name extraction logic in DataConverter class
PabloLION Aug 18, 2025
7494eca
refactor: Add TotalAggregatedData type and update aggregation call stack
PabloLION Aug 18, 2025
13ea9d5
refactor: Initialize __all__ as an empty list in terminal and utils p…
PabloLION Aug 18, 2025
1b877f8
fix: Resolve all Pylance type errors in analyzer.py
PabloLION Aug 18, 2025
94cc1f2
fix: Resolve all Pylance type errors in display_controller.py
PabloLION Aug 18, 2025
6477184
fix: Remove unnecessary isinstance checks in reader.py
PabloLION Aug 18, 2025
349a53b
fix: Remove unnecessary isinstance checks in session_monitor.py
PabloLION Aug 18, 2025
58bffc9
fix: Resolve all Pylance type errors in time_utils.py
PabloLION Aug 18, 2025
827aa21
fix: Improve import formatting and add spacing in formatting.py
PabloLION Aug 18, 2025
626cb72
fix: Resolve all Pylance type errors in analyzer.py
PabloLION Aug 18, 2025
1fd8594
refactor: Extract MessageContentBase for common message content fields
PabloLION Aug 18, 2025
f3b9120
refactor: Improve type annotations in analyzer.py
PabloLION Aug 18, 2025
41bbe66
fix: Restore necessary isinstance checks in display_controller.py
PabloLION Aug 18, 2025
5d3b00a
style: Apply ruff formatting and fix import organization
PabloLION Aug 18, 2025
d212a01
style: Reorganize imports and improve code formatting in display_cont…
PabloLION Aug 18, 2025
5fc93fc
fix: Add runtime checks for test compatibility in DisplayController a…
PabloLION Aug 18, 2025
46a063c
style: Reorganize imports and improve code formatting in reader.py
PabloLION Aug 18, 2025
9564d57
fix: Handle optional reset_time field safely in LimitDetectionInfo fo…
PabloLION Aug 18, 2025
6831666
fix: Resolve multiple Pylance diagnostic issues
PabloLION Aug 18, 2025
bef73a6
refactor: Replace untyped empty dict literals with typed constructors
PabloLION Aug 18, 2025
c631c7a
style: Replace untyped empty dict literals with explicit typed constr…
PabloLION Aug 18, 2025
d390c1c
style: Fix remaining empty list literal patterns in return statements
PabloLION Aug 18, 2025
2442d5b
refactor: Replace dictionary literals with TypedDict keyword construc…
PabloLION Aug 18, 2025
8d208a3
refactor: Resolve TypedDict naming conflicts after systematic renaming
PabloLION Aug 18, 2025
51c5478
test: Add type ignore comments for protected method calls in test_dis…
PabloLION Aug 18, 2025
65ce2c3
test: Complete type annotation fixes for test_display_controller.py
PabloLION Aug 18, 2025
7b78ceb
style: Apply ruff auto-formatting across entire src directory
PabloLION Aug 18, 2025
bc689be
test: Apply type assertions and fix unused variables in test_data_rea…
PabloLION Aug 19, 2025
6c6cc29
fix: Correct indentation in test_data_reader.py with statements
PabloLION Aug 19, 2025
e8dd8f9
fix: Correct variable references in test_data_reader.py
PabloLION Aug 19, 2025
0412dc1
style: Apply ruff auto-formatting across entire src directory
PabloLION Aug 19, 2025
9a6a2ff
fix: Resolve all type errors in test_data_reader.py
PabloLION Aug 19, 2025
03bd55e
test: Fix all type errors in api_examples.py
PabloLION Aug 19, 2025
2a579aa
refactor: Replace star imports with explicit imports in types/__init_…
PabloLION Aug 19, 2025
71b0a1c
fix: Complete type error resolution and broken test fixes
PabloLION Aug 19, 2025
94c90fb
style: Apply consistent formatting and spacing in orchestrator and te…
PabloLION Aug 19, 2025
8fce00b
fix: Resolve all remaining type errors in test_monitoring_orchestrato…
PabloLION Aug 19, 2025
d6eb131
fix: Resolve final type errors and linting issues across all test files
PabloLION Aug 19, 2025
b66c60c
fix: Update type hints and imports in test files for consistency and …
PabloLION Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: Complete type error resolution and broken test fixes
- Fixed type errors across all test files using cast() for test data compatibility
- Added proper TypedDict imports and casting patterns with explanatory comments
- Fixed SerializedBlock structure with missing required fields (burnRate, projection, limitMessages)
- Resolved test_monitoring_loop_periodic_updates timing issue by:
  - Restoring original update_interval = 0.1 seconds in test
  - Updating MonitoringOrchestrator to accept float update_interval for fractional timing
- Applied consistent patterns for Mock object access with pyright ignore comments
- All 516 tests now pass with proper type safety

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
PabloLION and claude committed Aug 19, 2025
commit 71b0a1c6b719f6e69eb4a058ffc346741525bf20
6 changes: 3 additions & 3 deletions src/claude_monitor/monitoring/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
class MonitoringOrchestrator:
"""Orchestrates monitoring components following SRP."""

def __init__(self, update_interval: int = 10, data_path: str | None = None) -> None:
def __init__(self, update_interval: float = 10, data_path: str | None = None) -> None:
"""Initialize orchestrator with components.

Args:
update_interval: Seconds between updates
update_interval: Seconds between updates (can be fractional)
data_path: Optional path to Claude data directory
"""
self.update_interval: int = update_interval
self.update_interval: float = update_interval

self.data_manager: DataManager = DataManager(cache_ttl=5, data_path=data_path)
self.session_monitor: SessionMonitor = SessionMonitor()
Expand Down
69 changes: 52 additions & 17 deletions src/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Shared pytest fixtures for Claude Monitor tests."""

from datetime import datetime, timezone
from typing import cast
from unittest.mock import Mock

import pytest
Expand Down Expand Up @@ -87,7 +88,7 @@ def sample_assistant_data() -> RawJSONEntry:
@pytest.fixture
def sample_user_data() -> RawJSONEntry:
"""Sample user-type data for testing."""
return {
return cast(RawJSONEntry, {
"timestamp": "2024-01-01T12:00:00Z",
"type": "user",
"usage": {
Expand All @@ -99,33 +100,33 @@ def sample_user_data() -> RawJSONEntry:
"model": "claude-3-haiku",
"message_id": "msg_123",
"request_id": "req_456",
}
}) # Test data with simplified structure


@pytest.fixture
def sample_malformed_data() -> RawJSONEntry:
"""Sample malformed data for testing error handling."""
return {
return cast(RawJSONEntry, {
"timestamp": "invalid_timestamp",
"message": "not_a_dict",
"usage": {"input_tokens": "not_a_number", "output_tokens": None},
}
}) # Test data with invalid types for error testing


@pytest.fixture
def sample_minimal_data() -> RawJSONEntry:
"""Sample minimal valid data for testing."""
return {
return cast(RawJSONEntry, {
"timestamp": "2024-01-01T12:00:00Z",
"usage": {"input_tokens": 100, "output_tokens": 50},
"request_id": "req_456",
}
}) # Minimal test data structure


@pytest.fixture
def sample_empty_tokens_data() -> RawJSONEntry:
"""Sample data with empty/zero tokens for testing."""
return {
return cast(RawJSONEntry, {
"timestamp": "2024-01-01T12:00:00Z",
"usage": {
"input_tokens": 0,
Expand All @@ -134,13 +135,13 @@ def sample_empty_tokens_data() -> RawJSONEntry:
"cache_read_input_tokens": 0,
},
"request_id": "req_456",
}
}) # Test data with zero token values


@pytest.fixture
def sample_duplicate_data() -> list[RawJSONEntry]:
"""Sample data for testing duplicate detection."""
return [
return cast(list[RawJSONEntry], [
{
"timestamp": "2024-01-01T12:00:00Z",
"message_id": "msg_1",
Expand All @@ -159,7 +160,7 @@ def sample_duplicate_data() -> list[RawJSONEntry]:
"request_id": "req_2",
"usage": {"input_tokens": 200, "output_tokens": 75},
},
]
]) # Test data with duplicate message IDs


@pytest.fixture
Expand Down Expand Up @@ -302,36 +303,70 @@ def mock_session_monitor() -> Mock:
@pytest.fixture
def sample_monitoring_data() -> AnalysisResult:
"""Sample monitoring data structure for testing."""
return {
return cast(AnalysisResult, {
"blocks": [
{
"id": "session_1",
"isActive": True,
"isGap": False,
"startTime": "2024-01-01T12:00:00Z",
"endTime": "2024-01-01T17:00:00Z",
"actualEndTime": "2024-01-01T17:00:00Z",
"tokenCounts": {"inputTokens": 800, "outputTokens": 200, "cacheCreationInputTokens": 0, "cacheReadInputTokens": 0},
"totalTokens": 1000,
"costUSD": 0.05,
"startTime": "2024-01-01T12:00:00Z",
"models": ["claude-3-haiku"],
"perModelStats": {},
"sentMessagesCount": 5,
"durationMinutes": 300.0,
"entries": [],
"entries_count": 5,
},
{
"id": "session_2",
"isActive": False,
"isGap": False,
"startTime": "2024-01-01T11:00:00Z",
"endTime": "2024-01-01T12:00:00Z",
"actualEndTime": "2024-01-01T12:00:00Z",
"tokenCounts": {"inputTokens": 400, "outputTokens": 100, "cacheCreationInputTokens": 0, "cacheReadInputTokens": 0},
"totalTokens": 500,
"costUSD": 0.025,
"startTime": "2024-01-01T11:00:00Z",
"models": ["claude-3-haiku"],
"perModelStats": {},
"sentMessagesCount": 3,
"durationMinutes": 60.0,
"entries": [],
"entries_count": 3,
},
]
}
],
"metadata": {
"generated_at": "2024-01-01T12:00:00Z",
"hours_analyzed": 24,
"entries_processed": 8,
"blocks_created": 2,
"limits_detected": 0,
"load_time_seconds": 0.1,
"transform_time_seconds": 0.05,
"cache_used": False,
"quick_start": False,
},
"entries_count": 8,
"total_tokens": 1500,
"total_cost": 0.075,
}) # Complete test monitoring data


@pytest.fixture
def sample_session_data() -> RawJSONEntry:
"""Sample session data for testing."""
return {
return cast(RawJSONEntry, {
"id": "session_1",
"isActive": True,
"totalTokens": 1000,
"costUSD": 0.05,
"startTime": "2024-01-01T12:00:00Z",
}
}) # Session test data with simplified structure


@pytest.fixture
Expand Down
96 changes: 58 additions & 38 deletions src/tests/test_aggregator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for data aggregator module."""

from datetime import datetime, timezone
from pathlib import Path

import pytest

Expand All @@ -10,6 +11,19 @@
AggregatedStatsData,
UsageAggregator,
)
from claude_monitor.types import CompleteAggregatedUsage


def get_daily_result_date(result: CompleteAggregatedUsage) -> str:
"""Get date from daily aggregation result, which should always have date set."""
assert "date" in result, "Daily aggregation result should have date field"
return result["date"] # type: ignore[return-value,no-any-return] # Daily aggregation always sets date


def get_monthly_result_month(result: CompleteAggregatedUsage) -> str:
"""Get month from monthly aggregation result, which should always have month set."""
assert "month" in result, "Monthly aggregation result should have month field"
return result["month"] # type: ignore[return-value,no-any-return] # Monthly aggregation always sets month


class TestAggregatedStats:
Expand Down Expand Up @@ -206,7 +220,7 @@ def test_add_entry_with_unknown_model(self) -> None:
cache_creation_tokens=0,
cache_read_tokens=0,
cost_usd=0.001,
model=None,
model="unknown",
message_id="msg_1",
request_id="req_1",
)
Expand Down Expand Up @@ -247,7 +261,7 @@ def test_to_dict_daily(self) -> None:

result = period.to_dict("date")

assert result["date"] == "2024-01-01"
assert get_daily_result_date(result) == "2024-01-01"
assert result["input_tokens"] == 1000
assert result["output_tokens"] == 500
assert result["cache_creation_tokens"] == 100
Expand All @@ -273,7 +287,7 @@ def test_to_dict_monthly(self) -> None:

result = period.to_dict("month")

assert result["month"] == "2024-01"
assert get_monthly_result_month(result) == "2024-01"
assert result["input_tokens"] == 10000
assert result["total_cost"] == 0.5

Expand All @@ -282,7 +296,7 @@ class TestUsageAggregator:
"""Test cases for UsageAggregator class."""

@pytest.fixture
def aggregator(self, tmp_path) -> UsageAggregator:
def aggregator(self, tmp_path: Path) -> UsageAggregator:
"""Create a UsageAggregator instance."""
return UsageAggregator(data_path=str(tmp_path))

Expand Down Expand Up @@ -335,7 +349,7 @@ def test_aggregate_daily_basic(

# Check first day (Jan 1 - 4 entries: 2 at 10AM, 2 at 2PM)
jan1 = result[0]
assert jan1["date"] == "2024-01-01"
assert get_daily_result_date(jan1) == "2024-01-01"
assert jan1["input_tokens"] == 400 # 4 entries * 100
assert jan1["output_tokens"] == 200 # 4 entries * 50
assert jan1["total_cost"] == 0.004 # 4 entries * 0.001
Expand All @@ -355,8 +369,8 @@ def test_aggregate_daily_with_date_filter(

# Should have Jan 15 and Jan 31 (entries on those days are within the filter)
assert len(result) == 2
assert result[0]["date"] == "2024-01-15"
assert result[1]["date"] == "2024-01-31"
assert get_daily_result_date(result[0]) == "2024-01-15"
assert get_daily_result_date(result[1]) == "2024-01-31"

def test_aggregate_monthly_basic(
self, aggregator: UsageAggregator, sample_entries: list[UsageEntry]
Expand All @@ -369,7 +383,7 @@ def test_aggregate_monthly_basic(

# Check January
jan = result[0]
assert jan["month"] == "2024-01"
assert get_monthly_result_month(jan) == "2024-01"
assert jan["input_tokens"] == 1400 # 14 entries * 100
assert jan["output_tokens"] == 700 # 14 entries * 50
assert (
Expand All @@ -380,7 +394,7 @@ def test_aggregate_monthly_basic(

# Check February
feb = result[1]
assert feb["month"] == "2024-02"
assert get_monthly_result_month(feb) == "2024-02"
assert feb["input_tokens"] == 600 # 3 entries * 200
assert feb["output_tokens"] == 300 # 3 entries * 100
assert feb["total_cost"] == 0.006 # 3 entries * 0.002
Expand All @@ -397,7 +411,7 @@ def test_aggregate_monthly_with_date_filter(

# Should only have February
assert len(result) == 1
assert result[0]["month"] == "2024-02"
assert get_monthly_result_month(result[0]) == "2024-02"

def test_aggregate_from_blocks_daily(
self, aggregator: UsageAggregator, sample_entries: list[UsageEntry]
Expand Down Expand Up @@ -435,7 +449,7 @@ def test_aggregate_from_blocks_daily(
result = aggregator.aggregate_from_blocks(blocks, "daily")

assert len(result) >= 2 # At least 2 days of data
assert result[0]["date"] == "2024-01-01"
assert get_daily_result_date(result[0]) == "2024-01-01"

def test_aggregate_from_blocks_monthly(
self, aggregator: UsageAggregator, sample_entries: list[UsageEntry]
Expand All @@ -454,8 +468,8 @@ def test_aggregate_from_blocks_monthly(
result = aggregator.aggregate_from_blocks([block], "monthly")

assert len(result) == 2 # Jan and Feb
assert result[0]["month"] == "2024-01"
assert result[1]["month"] == "2024-02"
assert get_monthly_result_month(result[0]) == "2024-01"
assert get_monthly_result_month(result[1]) == "2024-02"

def test_aggregate_from_blocks_invalid_view_type(
self, aggregator: UsageAggregator
Expand Down Expand Up @@ -488,25 +502,31 @@ def test_calculate_totals_empty(self, aggregator: UsageAggregator) -> None:

def test_calculate_totals_with_data(self, aggregator: UsageAggregator) -> None:
"""Test calculating totals with aggregated data."""
aggregated_data = [
{
"date": "2024-01-01",
"input_tokens": 1000,
"output_tokens": 500,
"cache_creation_tokens": 100,
"cache_read_tokens": 50,
"total_cost": 0.05,
"entries_count": 10,
},
{
"date": "2024-01-02",
"input_tokens": 2000,
"output_tokens": 1000,
"cache_creation_tokens": 200,
"cache_read_tokens": 100,
"total_cost": 0.10,
"entries_count": 20,
},
from claude_monitor.types import CompleteAggregatedUsage

aggregated_data: list[CompleteAggregatedUsage] = [
CompleteAggregatedUsage(
date="2024-01-01",
input_tokens=1000,
output_tokens=500,
cache_creation_tokens=100,
cache_read_tokens=50,
total_cost=0.05,
entries_count=10,
models_used=[],
model_breakdowns={},
),
CompleteAggregatedUsage(
date="2024-01-02",
input_tokens=2000,
output_tokens=1000,
cache_creation_tokens=200,
cache_read_tokens=100,
total_cost=0.10,
entries_count=20,
models_used=[],
model_breakdowns={},
),
]

result = aggregator.calculate_totals(aggregated_data)
Expand Down Expand Up @@ -573,9 +593,9 @@ def test_period_sorting(self, aggregator: UsageAggregator) -> None:
# Test daily sorting
daily_result = aggregator.aggregate_daily(entries)
assert len(daily_result) == 3
assert daily_result[0]["date"] == "2024-01-01"
assert daily_result[1]["date"] == "2024-01-10"
assert daily_result[2]["date"] == "2024-01-15"
assert get_daily_result_date(daily_result[0]) == "2024-01-01"
assert get_daily_result_date(daily_result[1]) == "2024-01-10"
assert get_daily_result_date(daily_result[2]) == "2024-01-15"

# Test monthly sorting
monthly_entries = [
Expand Down Expand Up @@ -616,6 +636,6 @@ def test_period_sorting(self, aggregator: UsageAggregator) -> None:

monthly_result = aggregator.aggregate_monthly(monthly_entries)
assert len(monthly_result) == 3
assert monthly_result[0]["month"] == "2024-01"
assert monthly_result[1]["month"] == "2024-02"
assert monthly_result[2]["month"] == "2024-03"
assert get_monthly_result_month(monthly_result[0]) == "2024-01"
assert get_monthly_result_month(monthly_result[1]) == "2024-02"
assert get_monthly_result_month(monthly_result[2]) == "2024-03"
Loading