ccburn 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
ccburn/app.py CHANGED
@@ -3,11 +3,13 @@
3
3
  import os
4
4
  import signal
5
5
  import threading
6
+ import time as _time
6
7
  from datetime import datetime, timedelta, timezone
7
8
  from typing import Any
8
9
 
9
10
  from rich.console import Console
10
11
  from rich.live import Live
12
+ from rich.text import Text
11
13
 
12
14
  try:
13
15
  from .data.credentials import CredentialsNotFoundError, TokenExpiredError
@@ -137,6 +139,7 @@ class CCBurnApp:
137
139
  Returns:
138
140
  True if fetch succeeded
139
141
  """
142
+ _ft0 = _time.perf_counter()
140
143
  try:
141
144
  snapshot = None
142
145
 
@@ -155,10 +158,16 @@ class CCBurnApp:
155
158
  self.limit_type,
156
159
  since=self._get_since_datetime(),
157
160
  )
161
+ if self.debug:
162
+ self.console.print(f"[dim] fetch: used cache (age={age:.1f}s) in {_time.perf_counter()-_ft0:.3f}s[/dim]")
158
163
  return True
159
164
 
165
+ _ft1 = _time.perf_counter()
160
166
  # No fresh cached data, fetch from API
161
167
  snapshot = self.client.fetch_usage()
168
+ _ft2 = _time.perf_counter()
169
+ if self.debug:
170
+ self.console.print(f"[dim] fetch: API call took {_ft2-_ft1:.3f}s[/dim]")
162
171
  self.last_snapshot = snapshot
163
172
  self.last_fetch_time = datetime.now(timezone.utc)
164
173
  self.last_error = None
@@ -197,26 +206,123 @@ class CCBurnApp:
197
206
  self.last_error = f"Unexpected error: {e}"
198
207
  return False
199
208
 
209
+ def _fetch_with_loading(self) -> bool:
210
+ """Fetch data while showing a loading message with elapsed time.
211
+
212
+ Returns:
213
+ True if fetch succeeded
214
+ """
215
+ result_holder = {"result": False, "done": False}
216
+
217
+ def fetch_thread():
218
+ result_holder["result"] = self._fetch_and_update()
219
+ result_holder["done"] = True
220
+
221
+ thread = threading.Thread(target=fetch_thread, daemon=True)
222
+ thread.start()
223
+
224
+ start_time = _time.perf_counter()
225
+ with Live(console=self.console, refresh_per_second=10, transient=True) as live:
226
+ while not result_holder["done"]:
227
+ elapsed = _time.perf_counter() - start_time
228
+ text = Text()
229
+ text.append("🔥 ", style="yellow")
230
+ text.append("Fetching data from Anthropic API... ", style="dim")
231
+ text.append(f"{elapsed:.1f}s", style="cyan")
232
+ live.update(text)
233
+ _time.sleep(0.1)
234
+
235
+ thread.join(timeout=1.0)
236
+ return result_holder["result"]
237
+
238
+ def _check_limit_available(self) -> bool:
239
+ """Check if the requested limit type is available in the fetched data.
240
+
241
+ Returns:
242
+ True if limit data is available
243
+ """
244
+ if self.last_snapshot is None:
245
+ return False
246
+ return self.last_snapshot.get_limit(self.limit_type) is not None
247
+
248
+ def _show_unavailable_error(self) -> None:
249
+ """Show helpful error when requested limit type is not available."""
250
+ self.console.print(
251
+ f"[red]{self.limit_type.display_name} data not available.[/red]"
252
+ )
253
+
254
+ # List what IS available
255
+ available = []
256
+ if self.last_snapshot:
257
+ if self.last_snapshot.session:
258
+ session = self.last_snapshot.session
259
+ available.append(
260
+ f" - [cyan]session[/cyan] - 5-hour limit "
261
+ f"({session.utilization_percent:.0f}% used)"
262
+ )
263
+ if self.last_snapshot.monthly:
264
+ monthly = self.last_snapshot.monthly
265
+ available.append(
266
+ f" - [cyan]monthly[/cyan] - Credits usage "
267
+ f"(${monthly.used_credits_dollars:.2f} / ${monthly.monthly_limit_dollars:.2f})"
268
+ )
269
+ if self.last_snapshot.weekly:
270
+ weekly = self.last_snapshot.weekly
271
+ available.append(
272
+ f" - [cyan]weekly[/cyan] - 7-day limit "
273
+ f"({weekly.utilization_percent:.0f}% used)"
274
+ )
275
+ if self.last_snapshot.weekly_sonnet:
276
+ sonnet = self.last_snapshot.weekly_sonnet
277
+ available.append(
278
+ f" - [cyan]weekly-sonnet[/cyan] - 7-day Sonnet limit "
279
+ f"({sonnet.utilization_percent:.0f}% used)"
280
+ )
281
+
282
+ if available:
283
+ self.console.print("\n[dim]Available:[/dim]")
284
+ for item in available:
285
+ self.console.print(item)
286
+ self.console.print("\n[dim]Tip: Run 'ccburn' to auto-detect.[/dim]")
287
+ else:
288
+ self.console.print(
289
+ "\n[dim]No usage data available. Check Claude Code authentication.[/dim]"
290
+ )
291
+
200
292
  def run(self) -> int:
201
293
  """Run the application.
202
294
 
203
295
  Returns:
204
296
  Exit code (0 for success, 1 for error)
205
297
  """
206
- # Show loading message for TUI mode
207
- if not self.json_output and not self.compact and not self.once:
208
- self.console.print("[dim]🔥 Loading ccburn...[/dim]", end="\r")
298
+ _t0 = _time.perf_counter()
299
+ show_loading = not self.json_output and not self.compact
209
300
 
301
+ _t1 = _time.perf_counter()
210
302
  if not self._initialize():
211
303
  return 1
304
+ _t2 = _time.perf_counter()
212
305
 
213
- # Initial fetch
214
- if not self._fetch_and_update():
306
+ # Initial fetch - show loading timer for interactive modes
307
+ if show_loading:
308
+ fetch_success = self._fetch_with_loading()
309
+ else:
310
+ fetch_success = self._fetch_and_update()
311
+
312
+ if not fetch_success:
215
313
  if self.last_snapshot is None:
216
- # No cached data available - error should have been printed in _fetch_and_update
217
314
  self.console.print("[red]Failed to fetch usage data. Check your credentials.[/red]")
218
315
  return 1
219
316
 
317
+ # Check if requested limit type is available
318
+ if not self._check_limit_available():
319
+ self._show_unavailable_error()
320
+ return 1
321
+
322
+ _t3 = _time.perf_counter()
323
+ if self.debug:
324
+ self.console.print(f"[dim]Timing: init={_t2-_t1:.3f}s fetch={_t3-_t2:.3f}s total={_t3-_t0:.3f}s[/dim]")
325
+
220
326
  # Handle different output modes
221
327
  if self.json_output:
222
328
  return self._run_json()
@@ -258,6 +364,7 @@ class CCBurnApp:
258
364
  self.last_snapshot.session,
259
365
  self.last_snapshot.weekly,
260
366
  self.last_snapshot.weekly_sonnet,
367
+ self.last_snapshot.monthly,
261
368
  budget_pace,
262
369
  )
263
370
  self.console.print(output)
@@ -418,6 +525,25 @@ class CCBurnApp:
418
525
  "status": metrics.status,
419
526
  }
420
527
 
528
+ # Add monthly credits if available
529
+ monthly_data = self.last_snapshot.monthly
530
+ if monthly_data:
531
+ metrics = calculate_burn_metrics(monthly_data, self.snapshots)
532
+ days_left = int(
533
+ (monthly_data.resets_at - datetime.now(timezone.utc)).total_seconds() / 86400
534
+ )
535
+ output["limits"]["monthly"] = {
536
+ "utilization": monthly_data.utilization,
537
+ "budget_pace": metrics.budget_pace,
538
+ "resets_at": monthly_data.resets_at.isoformat(),
539
+ "resets_in_days": days_left,
540
+ "window_hours": monthly_data.window_hours,
541
+ "status": metrics.status,
542
+ "used_credits_dollars": monthly_data.used_credits_dollars,
543
+ "monthly_limit_dollars": monthly_data.monthly_limit_dollars,
544
+ "remaining_dollars": monthly_data.remaining_dollars,
545
+ }
546
+
421
547
  # Add burn rate and projection for selected limit
422
548
  limit_data = self.last_snapshot.get_limit(self.limit_type)
423
549
  if limit_data:
ccburn/cli.py CHANGED
@@ -99,6 +99,15 @@ WeeklyIntervalOption = typer.Option(
99
99
  max=300,
100
100
  )
101
101
 
102
+ MonthlyIntervalOption = typer.Option(
103
+ 60,
104
+ "--interval",
105
+ "-i",
106
+ help="Refresh interval in seconds (default: 60 for monthly).",
107
+ min=1,
108
+ max=300,
109
+ )
110
+
102
111
  DebugOption = typer.Option(
103
112
  False,
104
113
  "--debug",
@@ -236,6 +245,34 @@ def create_weekly_sonnet_command(app: typer.Typer) -> None:
236
245
  )
237
246
 
238
247
 
248
+ def create_monthly_command(app: typer.Typer) -> None:
249
+ """Create the monthly subcommand."""
250
+
251
+ @app.command()
252
+ def monthly(
253
+ json_output: bool = JsonOption,
254
+ once: bool = OnceOption,
255
+ compact: bool = CompactOption,
256
+ since: str | None = SinceOption,
257
+ interval: int = MonthlyIntervalOption,
258
+ debug: bool = DebugOption,
259
+ ) -> None:
260
+ """Display monthly credit usage (enterprise accounts).
261
+
262
+ Shows your monthly credit budget and current usage.
263
+ Resets on the 1st of each month.
264
+ """
265
+ run_app(
266
+ LimitType.MONTHLY,
267
+ json_output=json_output,
268
+ once=once,
269
+ compact=compact,
270
+ since=since,
271
+ interval=interval,
272
+ debug=debug,
273
+ )
274
+
275
+
239
276
  def create_clear_history_command(app: typer.Typer) -> None:
240
277
  """Create the clear-history command."""
241
278
 
@@ -281,4 +318,5 @@ def register_commands(app: typer.Typer) -> None:
281
318
  create_session_command(app)
282
319
  create_weekly_command(app)
283
320
  create_weekly_sonnet_command(app)
321
+ create_monthly_command(app)
284
322
  create_clear_history_command(app)
ccburn/data/history.py CHANGED
@@ -6,9 +6,9 @@ from datetime import datetime, timedelta, timezone
6
6
  from pathlib import Path
7
7
 
8
8
  try:
9
- from .models import LimitData, LimitType, UsageSnapshot
9
+ from .models import LimitData, LimitType, MonthlyLimitData, UsageSnapshot
10
10
  except ImportError:
11
- from ccburn.data.models import LimitData, LimitType, UsageSnapshot
11
+ from ccburn.data.models import LimitData, LimitType, MonthlyLimitData, UsageSnapshot
12
12
 
13
13
 
14
14
  logger = logging.getLogger(__name__)
@@ -39,6 +39,12 @@ class HistoryDB:
39
39
  seven_day_opus_utilization REAL,
40
40
  seven_day_opus_resets_at TEXT,
41
41
 
42
+ -- Monthly credits (enterprise)
43
+ monthly_limit_cents INTEGER,
44
+ monthly_used_credits_cents REAL,
45
+ monthly_utilization REAL,
46
+ monthly_resets_at TEXT,
47
+
42
48
  -- Raw API response for debugging
43
49
  raw_response TEXT
44
50
  );
@@ -84,6 +90,7 @@ class HistoryDB:
84
90
  str(self.db_path) if isinstance(self.db_path, Path) else self.db_path,
85
91
  detect_types=sqlite3.PARSE_DECLTYPES,
86
92
  timeout=10.0, # Wait up to 10 seconds for locks
93
+ check_same_thread=False, # Allow cross-thread access for loading spinner
87
94
  )
88
95
  self._conn.row_factory = sqlite3.Row
89
96
  # Enable WAL mode for better concurrent read/write performance
@@ -104,6 +111,19 @@ class HistoryDB:
104
111
  conn.executescript(self.SCHEMA)
105
112
  conn.commit()
106
113
 
114
+ # Schema migration: Add monthly columns if they don't exist
115
+ cursor = conn.execute("PRAGMA table_info(usage_snapshots)")
116
+ columns = {row[1] for row in cursor.fetchall()}
117
+
118
+ if "monthly_utilization" not in columns:
119
+ conn.executescript("""
120
+ ALTER TABLE usage_snapshots ADD COLUMN monthly_limit_cents INTEGER;
121
+ ALTER TABLE usage_snapshots ADD COLUMN monthly_used_credits_cents REAL;
122
+ ALTER TABLE usage_snapshots ADD COLUMN monthly_utilization REAL;
123
+ ALTER TABLE usage_snapshots ADD COLUMN monthly_resets_at TEXT;
124
+ """)
125
+ conn.commit()
126
+
107
127
  def save_snapshot(self, snapshot: UsageSnapshot) -> None:
108
128
  """Save a usage snapshot to the database.
109
129
 
@@ -152,6 +172,18 @@ class HistoryDB:
152
172
  snapshot.weekly_opus.resets_at.isoformat() if snapshot.weekly_opus else None
153
173
  )
154
174
 
175
+ # Monthly credits data
176
+ monthly_limit = (
177
+ snapshot.monthly.monthly_limit_cents if snapshot.monthly else None
178
+ )
179
+ monthly_used = (
180
+ snapshot.monthly.used_credits_cents if snapshot.monthly else None
181
+ )
182
+ monthly_util = snapshot.monthly.utilization if snapshot.monthly else None
183
+ monthly_resets = (
184
+ snapshot.monthly.resets_at.isoformat() if snapshot.monthly else None
185
+ )
186
+
155
187
  conn.execute(
156
188
  """
157
189
  INSERT INTO usage_snapshots (
@@ -160,8 +192,10 @@ class HistoryDB:
160
192
  seven_day_all_utilization, seven_day_all_resets_at,
161
193
  seven_day_sonnet_utilization, seven_day_sonnet_resets_at,
162
194
  seven_day_opus_utilization, seven_day_opus_resets_at,
195
+ monthly_limit_cents, monthly_used_credits_cents,
196
+ monthly_utilization, monthly_resets_at,
163
197
  raw_response
164
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
198
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
165
199
  """,
166
200
  (
167
201
  snapshot.timestamp.isoformat(),
@@ -173,6 +207,10 @@ class HistoryDB:
173
207
  sonnet_resets,
174
208
  opus_util,
175
209
  opus_resets,
210
+ monthly_limit,
211
+ monthly_used,
212
+ monthly_util,
213
+ monthly_resets,
176
214
  snapshot.raw_response,
177
215
  ),
178
216
  )
@@ -324,12 +362,27 @@ class HistoryDB:
324
362
  limit_type=LimitType.WEEKLY, # Same window as weekly
325
363
  )
326
364
 
365
+ # Parse monthly credits
366
+ monthly = None
367
+ monthly_util = row["monthly_utilization"] if "monthly_utilization" in row.keys() else None
368
+ if monthly_util is not None:
369
+ resets_at = datetime.fromisoformat(row["monthly_resets_at"])
370
+ if resets_at.tzinfo is None:
371
+ resets_at = resets_at.replace(tzinfo=timezone.utc)
372
+ monthly = MonthlyLimitData(
373
+ monthly_limit_cents=row["monthly_limit_cents"],
374
+ used_credits_cents=row["monthly_used_credits_cents"],
375
+ utilization=monthly_util,
376
+ resets_at=resets_at,
377
+ )
378
+
327
379
  return UsageSnapshot(
328
380
  timestamp=timestamp,
329
381
  session=session,
330
382
  weekly=weekly,
331
383
  weekly_sonnet=weekly_sonnet,
332
384
  weekly_opus=weekly_opus,
385
+ monthly=monthly,
333
386
  raw_response=row["raw_response"],
334
387
  )
335
388
  except (ValueError, KeyError, TypeError) as e:
ccburn/data/models.py CHANGED
@@ -6,16 +6,22 @@ from enum import Enum
6
6
 
7
7
 
8
8
  class LimitType(str, Enum):
9
- """The three usage limits we track."""
9
+ """The usage limits we track."""
10
10
 
11
11
  SESSION = "session" # 5-hour rolling
12
12
  WEEKLY = "weekly" # 7-day all models
13
13
  WEEKLY_SONNET = "weekly-sonnet" # 7-day sonnet only
14
+ MONTHLY = "monthly" # Monthly credits (enterprise)
14
15
 
15
16
  @property
16
- def window_hours(self) -> int:
17
- """Get the window duration in hours."""
18
- return 5 if self == LimitType.SESSION else 168 # 7 * 24
17
+ def window_hours(self) -> int | None:
18
+ """Get the window duration in hours. None for variable-length windows."""
19
+ if self == LimitType.SESSION:
20
+ return 5
21
+ elif self == LimitType.MONTHLY:
22
+ return None # Variable - calculated at runtime based on month
23
+ else:
24
+ return 168 # 7 * 24
19
25
 
20
26
  @property
21
27
  def display_name(self) -> str:
@@ -24,16 +30,18 @@ class LimitType(str, Enum):
24
30
  LimitType.SESSION: "Session (5h)",
25
31
  LimitType.WEEKLY: "Weekly",
26
32
  LimitType.WEEKLY_SONNET: "Weekly Sonnet",
33
+ LimitType.MONTHLY: "Monthly Credits",
27
34
  }[self]
28
35
 
29
36
  @property
30
- def api_field(self) -> str:
31
- """Get the corresponding API field name."""
37
+ def api_field(self) -> str | None:
38
+ """Get the corresponding API field name. None for extra_usage-based types."""
32
39
  return {
33
40
  LimitType.SESSION: "five_hour",
34
41
  LimitType.WEEKLY: "seven_day",
35
42
  LimitType.WEEKLY_SONNET: "seven_day_sonnet",
36
- }[self]
43
+ LimitType.MONTHLY: None, # Uses extra_usage block
44
+ }.get(self)
37
45
 
38
46
 
39
47
  @dataclass
@@ -47,7 +55,8 @@ class LimitData:
47
55
  @property
48
56
  def window_hours(self) -> int:
49
57
  """Get the window duration in hours."""
50
- return self.limit_type.window_hours
58
+ hours = self.limit_type.window_hours
59
+ return hours if hours is not None else 168 # Fallback for type safety
51
60
 
52
61
  @property
53
62
  def window_start(self) -> datetime:
@@ -60,6 +69,51 @@ class LimitData:
60
69
  return self.utilization * 100
61
70
 
62
71
 
72
+ @dataclass
73
+ class MonthlyLimitData:
74
+ """Data for monthly credit usage (enterprise accounts)."""
75
+
76
+ monthly_limit_cents: int # e.g., 30000 = $300.00
77
+ used_credits_cents: float # e.g., 7475.0 = $74.75
78
+ utilization: float # 0.0 to 1.0 normalized
79
+ resets_at: datetime # 1st of next month UTC
80
+ limit_type: LimitType = LimitType.MONTHLY
81
+
82
+ @property
83
+ def monthly_limit_dollars(self) -> float:
84
+ """Get monthly limit in dollars."""
85
+ return self.monthly_limit_cents / 100.0
86
+
87
+ @property
88
+ def used_credits_dollars(self) -> float:
89
+ """Get used credits in dollars."""
90
+ return self.used_credits_cents / 100.0
91
+
92
+ @property
93
+ def remaining_dollars(self) -> float:
94
+ """Get remaining credits in dollars."""
95
+ return self.monthly_limit_dollars - self.used_credits_dollars
96
+
97
+ @property
98
+ def window_hours(self) -> int:
99
+ """Get days in current month * 24."""
100
+ now = datetime.now(timezone.utc)
101
+ first_of_month = datetime(now.year, now.month, 1, tzinfo=timezone.utc)
102
+ days_in_month = (self.resets_at - first_of_month).days
103
+ return days_in_month * 24
104
+
105
+ @property
106
+ def window_start(self) -> datetime:
107
+ """1st of current month at 00:00 UTC."""
108
+ now = datetime.now(timezone.utc)
109
+ return datetime(now.year, now.month, 1, tzinfo=timezone.utc)
110
+
111
+ @property
112
+ def utilization_percent(self) -> float:
113
+ """Get utilization as a percentage (0-100)."""
114
+ return self.utilization * 100
115
+
116
+
63
117
  @dataclass
64
118
  class UsageSnapshot:
65
119
  """A point-in-time snapshot of all usage limits."""
@@ -69,14 +123,16 @@ class UsageSnapshot:
69
123
  weekly: LimitData | None # seven_day from API
70
124
  weekly_sonnet: LimitData | None # seven_day_sonnet from API
71
125
  weekly_opus: LimitData | None # seven_day_opus from API (tracked but not displayed)
126
+ monthly: MonthlyLimitData | None = None # extra_usage from API (enterprise)
72
127
  raw_response: str | None = None # Original JSON for debugging
73
128
 
74
- def get_limit(self, limit_type: LimitType) -> LimitData | None:
129
+ def get_limit(self, limit_type: LimitType) -> LimitData | MonthlyLimitData | None:
75
130
  """Get limit data by type."""
76
131
  return {
77
132
  LimitType.SESSION: self.session,
78
133
  LimitType.WEEKLY: self.weekly,
79
134
  LimitType.WEEKLY_SONNET: self.weekly_sonnet,
135
+ LimitType.MONTHLY: self.monthly,
80
136
  }.get(limit_type)
81
137
 
82
138
  @classmethod
@@ -125,12 +181,39 @@ class UsageSnapshot:
125
181
  except (ValueError, TypeError):
126
182
  pass
127
183
 
184
+ # Parse monthly credits from extra_usage (enterprise accounts)
185
+ monthly = None
186
+ extra_usage = data.get("extra_usage")
187
+ if extra_usage and isinstance(extra_usage, dict):
188
+ if extra_usage.get("is_enabled") and extra_usage.get("monthly_limit"):
189
+ try:
190
+ monthly_limit = int(extra_usage["monthly_limit"])
191
+ used_credits = float(extra_usage.get("used_credits", 0))
192
+ utilization_pct = float(extra_usage.get("utilization", 0))
193
+
194
+ # Calculate resets_at as 1st of next month UTC
195
+ now = datetime.now(timezone.utc)
196
+ if now.month == 12:
197
+ resets_at = datetime(now.year + 1, 1, 1, tzinfo=timezone.utc)
198
+ else:
199
+ resets_at = datetime(now.year, now.month + 1, 1, tzinfo=timezone.utc)
200
+
201
+ monthly = MonthlyLimitData(
202
+ monthly_limit_cents=monthly_limit,
203
+ used_credits_cents=used_credits,
204
+ utilization=utilization_pct / 100.0, # Normalize to 0-1
205
+ resets_at=resets_at,
206
+ )
207
+ except (ValueError, TypeError, KeyError):
208
+ pass
209
+
128
210
  return cls(
129
211
  timestamp=timestamp,
130
212
  session=parse_limit(data.get("five_hour"), LimitType.SESSION),
131
213
  weekly=parse_limit(data.get("seven_day"), LimitType.WEEKLY),
132
214
  weekly_sonnet=parse_limit(data.get("seven_day_sonnet"), LimitType.WEEKLY_SONNET),
133
215
  weekly_opus=weekly_opus,
216
+ monthly=monthly,
134
217
  raw_response=json.dumps(data),
135
218
  )
136
219
 
ccburn/display/chart.py CHANGED
@@ -9,10 +9,10 @@ from rich.console import Console, ConsoleOptions, Group, RenderableType
9
9
  from rich.jupyter import JupyterMixin
10
10
 
11
11
  try:
12
- from ..data.models import BurnMetrics, LimitData, UsageSnapshot
12
+ from ..data.models import BurnMetrics, LimitData, MonthlyLimitData, UsageSnapshot
13
13
  from ..utils.formatting import get_utilization_color
14
14
  except ImportError:
15
- from ccburn.data.models import BurnMetrics, LimitData, UsageSnapshot
15
+ from ccburn.data.models import BurnMetrics, LimitData, MonthlyLimitData, UsageSnapshot
16
16
  from ccburn.utils.formatting import get_utilization_color
17
17
 
18
18
 
@@ -21,7 +21,7 @@ class BurnupChart(JupyterMixin):
21
21
 
22
22
  def __init__(
23
23
  self,
24
- limit_data: LimitData | None,
24
+ limit_data: LimitData | MonthlyLimitData | None,
25
25
  snapshots: list[UsageSnapshot],
26
26
  since_duration: timedelta | None = None,
27
27
  explicit_height: int | None = None,
@@ -253,8 +253,6 @@ class BurnupChart(JupyterMixin):
253
253
  num_ticks = 5
254
254
  tick_positions = []
255
255
  tick_labels = []
256
- # Use date format for windows > 24 hours
257
- use_date_format = display_hours > 24
258
256
  for i in range(num_ticks):
259
257
  hours = i * display_hours / (num_ticks - 1)
260
258
  tick_positions.append(hours)
@@ -267,7 +265,10 @@ class BurnupChart(JupyterMixin):
267
265
  else:
268
266
  local_time = local_time.replace(second=0, microsecond=0)
269
267
  # Format based on window size
270
- if use_date_format:
268
+ if display_hours > 168: # > 7 days (monthly)
269
+ # Show month/day for multi-week windows (e.g., "2/8" for Feb 8)
270
+ tick_labels.append(f"{local_time.month}/{local_time.day}")
271
+ elif display_hours > 24:
271
272
  # Show day and time for multi-day windows (e.g., "Mon 15h")
272
273
  tick_labels.append(local_time.strftime("%a %Hh"))
273
274
  else:
@@ -291,7 +292,9 @@ class BurnupChart(JupyterMixin):
291
292
  local_now = local_now.replace(second=0, microsecond=0) + timedelta(minutes=1)
292
293
  else:
293
294
  local_now = local_now.replace(second=0, microsecond=0)
294
- if use_date_format:
295
+ if display_hours > 168:
296
+ tick_labels.append(f"{local_now.month}/{local_now.day} {local_now.hour}h")
297
+ elif display_hours > 24:
295
298
  tick_labels.append(local_now.strftime("%a %Hh"))
296
299
  else:
297
300
  tick_labels.append(local_now.strftime("%H:%M"))
@@ -313,7 +316,9 @@ class BurnupChart(JupyterMixin):
313
316
  else:
314
317
  local_hits_100 = local_hits_100.replace(second=0, microsecond=0)
315
318
  tick_positions.append(hits_100_hours_for_tick)
316
- if use_date_format:
319
+ if display_hours > 168:
320
+ tick_labels.append(f"{local_hits_100.month}/{local_hits_100.day} {local_hits_100.hour}h")
321
+ elif display_hours > 24:
317
322
  tick_labels.append(local_hits_100.strftime("%a %Hh"))
318
323
  else:
319
324
  tick_labels.append(local_hits_100.strftime("%H:%M"))
@@ -358,7 +363,7 @@ class BurnupChart(JupyterMixin):
358
363
 
359
364
 
360
365
  def create_simple_chart(
361
- limit_data: LimitData | None,
366
+ limit_data: LimitData | MonthlyLimitData | None,
362
367
  snapshots: list[UsageSnapshot],
363
368
  width: int = 80,
364
369
  height: int = 15,
ccburn/display/gauges.py CHANGED
@@ -9,11 +9,11 @@ from rich.table import Table
9
9
  from rich.text import Text
10
10
 
11
11
  try:
12
- from ..data.models import LimitData, LimitType
12
+ from ..data.models import LimitData, LimitType, MonthlyLimitData
13
13
  from ..utils.calculator import calculate_budget_pace
14
14
  from ..utils.formatting import format_reset_time, get_utilization_color
15
15
  except ImportError:
16
- from ccburn.data.models import LimitData, LimitType
16
+ from ccburn.data.models import LimitData, LimitType, MonthlyLimitData
17
17
  from ccburn.utils.calculator import calculate_budget_pace
18
18
  from ccburn.utils.formatting import format_reset_time, get_utilization_color
19
19
 
@@ -64,6 +64,23 @@ def _supports_emoji() -> bool:
64
64
  return _emoji_support_cache
65
65
 
66
66
 
67
+ def format_credits(dollars: float) -> str:
68
+ """Format dollar amount for display.
69
+
70
+ Args:
71
+ dollars: Amount in dollars
72
+
73
+ Returns:
74
+ Formatted string like "$74.75" or "$1,234"
75
+ """
76
+ if dollars >= 1000:
77
+ return f"${dollars:,.0f}"
78
+ elif dollars >= 100:
79
+ return f"${dollars:.0f}"
80
+ else:
81
+ return f"${dollars:.2f}"
82
+
83
+
67
84
  def get_pace_emoji(utilization: float, budget_pace: float, ascii_fallback: bool = False) -> str:
68
85
  """Get emoji indicator based on utilization vs budget pace.
69
86
 
@@ -90,7 +107,9 @@ def get_pace_emoji(utilization: float, budget_pace: float, ascii_fallback: bool
90
107
  return "[=]" if use_ascii else "🔥" # On pace - normal burn
91
108
 
92
109
 
93
- def create_header(limit_type: LimitType, limit_data: LimitData | None) -> Table:
110
+ def create_header(
111
+ limit_type: LimitType, limit_data: LimitData | MonthlyLimitData | None
112
+ ) -> Table:
94
113
  """Create the header line with limit name and reset countdown.
95
114
 
96
115
  Args:
@@ -129,13 +148,13 @@ def create_header(limit_type: LimitType, limit_data: LimitData | None) -> Table:
129
148
 
130
149
 
131
150
  def create_gauge_section(
132
- limit_data: LimitData | None,
151
+ limit_data: LimitData | MonthlyLimitData | None,
133
152
  budget_pace: float,
134
153
  ) -> Table:
135
154
  """Create the 2-bar gauge section for a limit.
136
155
 
137
156
  Args:
138
- limit_data: Current limit data
157
+ limit_data: Current limit data (LimitData or MonthlyLimitData)
139
158
  budget_pace: Percentage of window elapsed (0-1)
140
159
 
141
160
  Returns:
@@ -144,18 +163,18 @@ def create_gauge_section(
144
163
  table = Table.grid(padding=(0, 1), expand=True)
145
164
  table.add_column(width=14) # Label
146
165
  table.add_column(ratio=1) # Bar
147
- table.add_column(width=8, justify="right") # Value
166
+ table.add_column(width=18, justify="right") # Value (wider for dollar amounts)
148
167
 
149
168
  if limit_data is None:
150
169
  # Show empty/loading state
151
170
  table.add_row(
152
171
  Text("📊 Usage", style="dim"),
153
- ProgressBar(total=100, completed=0, style=Style(color="dim")),
172
+ ProgressBar(total=100, completed=0, style=Style(color="grey37")),
154
173
  Text("--%", style="dim"),
155
174
  )
156
175
  table.add_row(
157
176
  Text("⏳ Elapsed", style="dim"),
158
- ProgressBar(total=100, completed=0, style=Style(color="dim")),
177
+ ProgressBar(total=100, completed=0, style=Style(color="grey37")),
159
178
  Text("--%", style="dim"),
160
179
  )
161
180
  return table
@@ -178,10 +197,18 @@ def create_gauge_section(
178
197
  usage_label.append("📊 ", style="")
179
198
  usage_label.append("Usage", style=f"bold {usage_color}")
180
199
 
200
+ # Format value text - dollars for monthly, percentage for others
201
+ if isinstance(limit_data, MonthlyLimitData):
202
+ used = format_credits(limit_data.used_credits_dollars)
203
+ total = format_credits(limit_data.monthly_limit_dollars)
204
+ value_text = Text(f"{used} / {total}", style=usage_color)
205
+ else:
206
+ value_text = Text(f"{utilization_percent:.0f}%", style=usage_color)
207
+
181
208
  table.add_row(
182
209
  usage_label,
183
210
  usage_bar,
184
- Text(f"{utilization_percent:.0f}%", style=usage_color),
211
+ value_text,
185
212
  )
186
213
 
187
214
  # Time Elapsed bar - always blue
@@ -210,17 +237,19 @@ def create_compact_output(
210
237
  session: LimitData | None,
211
238
  weekly: LimitData | None,
212
239
  weekly_sonnet: LimitData | None,
240
+ monthly: MonthlyLimitData | None,
213
241
  budget_pace_session: float,
214
242
  ) -> str:
215
243
  """Create compact single-line output for status bars.
216
244
 
217
- Format: Session: 🧊 62% (2h14m) | Weekly: 🔥 29% | Sonnet: 🧊 1%
245
+ Format: Session: 🧊 62% (2h14m) | Weekly: 🔥 29% | Sonnet: 🧊 1% | Monthly: 🧊 $74.75
218
246
  Emojis indicate pace: 🧊 (behind), 🔥 (on pace), 🚨 (ahead)
219
247
 
220
248
  Args:
221
249
  session: Session limit data
222
250
  weekly: Weekly limit data
223
251
  weekly_sonnet: Weekly sonnet limit data
252
+ monthly: Monthly credits data
224
253
  budget_pace_session: Budget pace for session (for status indicator)
225
254
 
226
255
  Returns:
@@ -240,24 +269,29 @@ def create_compact_output(
240
269
  pace = calculate_budget_pace(session.resets_at, session.window_hours)
241
270
  emoji = get_pace_emoji(session.utilization, pace)
242
271
  parts.append(f"Session: {emoji} {session.utilization*100:.0f}% {time_str}".strip())
243
- else:
244
- parts.append("Session: --")
245
272
 
246
273
  # Weekly
247
274
  if weekly:
248
275
  pace = calculate_budget_pace(weekly.resets_at, weekly.window_hours)
249
276
  emoji = get_pace_emoji(weekly.utilization, pace)
250
277
  parts.append(f"Weekly: {emoji} {weekly.utilization*100:.0f}%")
251
- else:
252
- parts.append("Weekly: --")
253
278
 
254
279
  # Sonnet
255
280
  if weekly_sonnet:
256
281
  pace = calculate_budget_pace(weekly_sonnet.resets_at, weekly_sonnet.window_hours)
257
282
  emoji = get_pace_emoji(weekly_sonnet.utilization, pace)
258
283
  parts.append(f"Sonnet: {emoji} {weekly_sonnet.utilization*100:.0f}%")
259
- else:
260
- parts.append("Sonnet: --")
284
+
285
+ # Monthly credits
286
+ if monthly:
287
+ pace = calculate_budget_pace(monthly.resets_at, monthly.window_hours)
288
+ emoji = get_pace_emoji(monthly.utilization, pace)
289
+ dollars = format_credits(monthly.used_credits_dollars)
290
+ parts.append(f"Monthly: {emoji} {dollars}")
291
+
292
+ # If nothing available, show placeholder
293
+ if not parts:
294
+ parts.append("No data available")
261
295
 
262
296
  return " | ".join(parts)
263
297
 
ccburn/display/layout.py CHANGED
@@ -8,12 +8,18 @@ from rich.layout import Layout
8
8
  from rich.text import Text
9
9
 
10
10
  try:
11
- from ..data.models import BurnMetrics, LimitData, LimitType, UsageSnapshot
11
+ from ..data.models import BurnMetrics, LimitData, LimitType, MonthlyLimitData, UsageSnapshot
12
12
  from ..utils.calculator import calculate_budget_pace, calculate_burn_metrics
13
13
  from .chart import BurnupChart
14
14
  from .gauges import create_gauge_section, create_header
15
15
  except ImportError:
16
- from ccburn.data.models import BurnMetrics, LimitData, LimitType, UsageSnapshot
16
+ from ccburn.data.models import (
17
+ BurnMetrics,
18
+ LimitData,
19
+ LimitType,
20
+ MonthlyLimitData,
21
+ UsageSnapshot,
22
+ )
17
23
  from ccburn.display.chart import BurnupChart
18
24
  from ccburn.display.gauges import create_gauge_section, create_header
19
25
  from ccburn.utils.calculator import calculate_budget_pace, calculate_burn_metrics
@@ -24,7 +30,7 @@ class BurnupLayout:
24
30
 
25
31
  MIN_WIDTH = 40
26
32
  MIN_HEIGHT = 10
27
- COMPACT_WIDTH = 60
33
+ COMPACT_WIDTH = 40
28
34
  COMPACT_HEIGHT = 15
29
35
 
30
36
  def __init__(self, console: Console | None = None):
@@ -34,7 +40,7 @@ class BurnupLayout:
34
40
  console: Rich Console instance (creates one if not provided)
35
41
  """
36
42
  self.console = console or Console()
37
- self._last_limit_data: LimitData | None = None
43
+ self._last_limit_data: LimitData | MonthlyLimitData | None = None
38
44
  self._last_snapshots: list[UsageSnapshot] = []
39
45
  self._last_metrics: BurnMetrics | None = None
40
46
  self._error_message: str | None = None
@@ -64,7 +70,7 @@ class BurnupLayout:
64
70
  def update(
65
71
  self,
66
72
  limit_type: LimitType,
67
- limit_data: LimitData | None,
73
+ limit_data: LimitData | MonthlyLimitData | None,
68
74
  snapshots: list[UsageSnapshot],
69
75
  error: str | None = None,
70
76
  stale_since: datetime | None = None,
ccburn/main.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
 
4
4
  import typer
5
+ from rich.console import Console
5
6
 
6
7
  try:
7
8
  from .cli import (
@@ -15,6 +16,7 @@ try:
15
16
  run_app,
16
17
  )
17
18
  from .data.models import LimitType
19
+ from .data.usage_client import UsageClient
18
20
  except ImportError:
19
21
  from ccburn.cli import (
20
22
  CompactOption,
@@ -27,6 +29,31 @@ except ImportError:
27
29
  run_app,
28
30
  )
29
31
  from ccburn.data.models import LimitType
32
+ from ccburn.data.usage_client import UsageClient
33
+
34
+
35
+ def auto_detect_limit_type() -> LimitType | None:
36
+ """Auto-detect which limit type to use based on available data.
37
+
38
+ Priority: session -> monthly -> weekly
39
+
40
+ Returns:
41
+ Best available LimitType, or None if no data available.
42
+ """
43
+ try:
44
+ client = UsageClient()
45
+ snapshot = client.fetch_usage()
46
+
47
+ if snapshot.session is not None:
48
+ return LimitType.SESSION
49
+ if snapshot.monthly is not None:
50
+ return LimitType.MONTHLY
51
+ if snapshot.weekly is not None:
52
+ return LimitType.WEEKLY
53
+
54
+ return None
55
+ except Exception:
56
+ return None
30
57
 
31
58
 
32
59
  app = typer.Typer(
@@ -60,14 +87,14 @@ def main(
60
87
  """ccburn - Claude Code usage limit visualizer.
61
88
 
62
89
  Visualize your Claude Code usage limits with real-time burn-up charts.
63
- Shows 5-hour rolling session limit by default.
90
+ Auto-detects available data (session or monthly credits).
64
91
 
65
92
  \b
66
93
  Examples:
67
- ccburn # Live TUI showing session limit
68
- ccburn session # Same as above (explicit)
69
- ccburn weekly # Show 7-day weekly limit
70
- ccburn weekly-sonnet # Show 7-day Sonnet limit
94
+ ccburn # Auto-detect best available data
95
+ ccburn session # Explicit: 5-hour rolling session limit
96
+ ccburn weekly # Explicit: 7-day weekly limit
97
+ ccburn monthly # Explicit: Monthly credits (enterprise)
71
98
  ccburn --json # JSON output
72
99
  ccburn --compact # Single-line for status bars
73
100
  ccburn --once # Print once and exit
@@ -81,10 +108,20 @@ def main(
81
108
  typer.echo("ccburn 1.0.0")
82
109
  raise typer.Exit()
83
110
 
84
- # If no subcommand, default to session
111
+ # If no subcommand, auto-detect best available limit type
85
112
  if ctx.invoked_subcommand is None:
113
+ console = Console()
114
+
115
+ # Auto-detect which data is available
116
+ limit_type = auto_detect_limit_type()
117
+
118
+ if limit_type is None:
119
+ console.print("[red]No usage data available.[/red]")
120
+ console.print("\n[dim]Check that Claude Code is running and authenticated.[/dim]")
121
+ raise typer.Exit(1)
122
+
86
123
  run_app(
87
- LimitType.SESSION,
124
+ limit_type,
88
125
  json_output=json_output,
89
126
  once=once,
90
127
  compact=compact,
@@ -3,9 +3,15 @@
3
3
  from datetime import datetime, timedelta, timezone
4
4
 
5
5
  try:
6
- from ..data.models import BurnMetrics, LimitData, LimitType, UsageSnapshot
6
+ from ..data.models import BurnMetrics, LimitData, LimitType, MonthlyLimitData, UsageSnapshot
7
7
  except ImportError:
8
- from ccburn.data.models import BurnMetrics, LimitData, LimitType, UsageSnapshot
8
+ from ccburn.data.models import (
9
+ BurnMetrics,
10
+ LimitData,
11
+ LimitType,
12
+ MonthlyLimitData,
13
+ UsageSnapshot,
14
+ )
9
15
 
10
16
 
11
17
  def calculate_budget_pace(resets_at: datetime, window_hours: float) -> float:
@@ -93,9 +99,10 @@ def calculate_burn_rate(
93
99
  # Check that data spans at least min_span_pct of the window
94
100
  # e.g., for 5h session at 10%, need 30 min of data
95
101
  # e.g., for 168h weekly at 10%, need ~17 hours of data
102
+ # Cap at 6 hours max so monthly (744h) doesn't require 3+ days of data
96
103
  if first_timestamp and last_timestamp:
97
104
  span_hours = (last_timestamp - first_timestamp).total_seconds() / 3600
98
- min_span_hours = window_hours * min_span_pct
105
+ min_span_hours = min(window_hours * min_span_pct, 6.0)
99
106
  if span_hours < min_span_hours:
100
107
  return 0.0
101
108
 
@@ -208,7 +215,7 @@ def get_status(utilization: float, budget_pace: float) -> str:
208
215
 
209
216
 
210
217
  def calculate_burn_metrics(
211
- limit_data: LimitData,
218
+ limit_data: LimitData | MonthlyLimitData,
212
219
  snapshots: list[UsageSnapshot],
213
220
  ) -> BurnMetrics:
214
221
  """Calculate all burn metrics for a limit.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccburn
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Terminal-based Claude Code usage limit visualizer with real-time burn-up charts
5
5
  Author: JuanjoFuchs
6
6
  License-Expression: MIT
@@ -0,0 +1,22 @@
1
+ ccburn/__init__.py,sha256=u8tlHJ2bTam19CROn2nufmcfCONN9BwlRLY-8AOu8Os,191
2
+ ccburn/app.py,sha256=VaXkI2CpMqH71KvZelTx4hpmllwQ6tIHlC5Ytya3bsk,22877
3
+ ccburn/cli.py,sha256=FRDA2zDJjMvjLPG66g-PqZNRJQN0dVYkdO4M4pdZmEM,8109
4
+ ccburn/main.py,sha256=giLHAxugFP8Go0i2TMryJBAbhNLw2LYQe2IavzfnMrE,3636
5
+ ccburn/data/__init__.py,sha256=ZczEZwodQ-MMO5F7fVNsyIpUCRY8Ya9W4pwdOOJWxm4,803
6
+ ccburn/data/credentials.py,sha256=wDiiTkZZDBjnYspvtWJ_52xXdTdIgBndLlfFMi-peZ8,5228
7
+ ccburn/data/history.py,sha256=a318JaiXucHY1dUA-xCQlhSY-K9q4Z9w4JR568lYB9U,15284
8
+ ccburn/data/models.py,sha256=d1PrxBXfxPC_eN44VaPrmndsMvq5eZtkjDYEVdRXd-0,8864
9
+ ccburn/data/usage_client.py,sha256=_dGwmI5vYPk4S-HUe2_fnTwSuAfTPaOFff7mKPFnhps,4570
10
+ ccburn/display/__init__.py,sha256=aL7TV53kU5oxlIwJ8M17stG2aC6UeGB-pj2u5BOpegs,495
11
+ ccburn/display/chart.py,sha256=vyk-INnzIgkzJmEf1YKKntzlS_XhDdfKuybV2NDmRAA,16394
12
+ ccburn/display/gauges.py,sha256=yWrQh3WBb7hm7GLb1kvIyxrtBQL5_t8zqxoD1OA9SqI,11852
13
+ ccburn/display/layout.py,sha256=6y-jG3epA0SRzfYdPbv2WACD1GGP-Ign6UbK6fCkUTg,8367
14
+ ccburn/utils/__init__.py,sha256=N6EzUX9hUJkuga_l9Ci3of1CWNtQgpNmMmNyY2DgYrg,1119
15
+ ccburn/utils/calculator.py,sha256=66DycRT2o92ahA5P75u-EfTwWddNqPzn-msggkCfRx0,7895
16
+ ccburn/utils/formatting.py,sha256=MEVIohBmvSur0hcc67oyYRDooiUMf0rPa4LO1fc2Ud4,4174
17
+ ccburn-0.4.0.dist-info/licenses/LICENSE,sha256=Qf2mqNi2qJ35JytfoTdR1SgYhZ2Mt4Ohcf-tu_MuYC0,1068
18
+ ccburn-0.4.0.dist-info/METADATA,sha256=g5l8YNh2o4lQLGDPgEVpGpTNmcpE6GBxN7ZIU6LVM0g,7343
19
+ ccburn-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ ccburn-0.4.0.dist-info/entry_points.txt,sha256=GfFQ5VusMR8RJ9meygqWjaErdmYsf_arbILzf64WjLU,43
21
+ ccburn-0.4.0.dist-info/top_level.txt,sha256=SM8TwGQZqQKKIQObVWQkfpA0OI4gRut7bPl-iM3g5RI,7
22
+ ccburn-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,22 +0,0 @@
1
- ccburn/__init__.py,sha256=u8tlHJ2bTam19CROn2nufmcfCONN9BwlRLY-8AOu8Os,191
2
- ccburn/app.py,sha256=IvIFOX10AFD103P5XiIenkEB2clAq2o3XJR6lceMVhE,17775
3
- ccburn/cli.py,sha256=dvh6V8I1TQlqbCv4-5j3ANRdKpoQKPHgA2QrBZUSkJY,7120
4
- ccburn/main.py,sha256=TqWLl9xxOtbpTQr-ObomzSLG3jNec2GZ6RKEQYYdGLg,2474
5
- ccburn/data/__init__.py,sha256=ZczEZwodQ-MMO5F7fVNsyIpUCRY8Ya9W4pwdOOJWxm4,803
6
- ccburn/data/credentials.py,sha256=wDiiTkZZDBjnYspvtWJ_52xXdTdIgBndLlfFMi-peZ8,5228
7
- ccburn/data/history.py,sha256=ouBxrXpMp_eTs0kba1Bg55TI6bsBSMToJ32tH1wNHQI,12879
8
- ccburn/data/models.py,sha256=Sd2T36gH6OaNHl9zRlnnQXI-ziBA8Gl6rPYQIzmr7G4,5403
9
- ccburn/data/usage_client.py,sha256=_dGwmI5vYPk4S-HUe2_fnTwSuAfTPaOFff7mKPFnhps,4570
10
- ccburn/display/__init__.py,sha256=aL7TV53kU5oxlIwJ8M17stG2aC6UeGB-pj2u5BOpegs,495
11
- ccburn/display/chart.py,sha256=7Rg9KeeeDjr8jwvEhHbmBYmZ0Wj3N1ijfzsQy6u6kqA,15917
12
- ccburn/display/gauges.py,sha256=qPjFmTJd3RcwMfpmONFHByno5ByDPaVpvDttHnfct1E,10628
13
- ccburn/display/layout.py,sha256=DlgmH3G2RbpyYfWYD6YP1IlohYmhWBSiqjM7Y7fto-U,8244
14
- ccburn/utils/__init__.py,sha256=N6EzUX9hUJkuga_l9Ci3of1CWNtQgpNmMmNyY2DgYrg,1119
15
- ccburn/utils/calculator.py,sha256=m-XATeOqgD5Z4ZranuI7QSlSb4pnW43_rtaJSF2Te6Q,7706
16
- ccburn/utils/formatting.py,sha256=MEVIohBmvSur0hcc67oyYRDooiUMf0rPa4LO1fc2Ud4,4174
17
- ccburn-0.3.1.dist-info/licenses/LICENSE,sha256=Qf2mqNi2qJ35JytfoTdR1SgYhZ2Mt4Ohcf-tu_MuYC0,1068
18
- ccburn-0.3.1.dist-info/METADATA,sha256=UnlFr_QEFUjXnwh0ho0iw_1KHYesD7g_uzOL5DJSWjk,7343
19
- ccburn-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- ccburn-0.3.1.dist-info/entry_points.txt,sha256=GfFQ5VusMR8RJ9meygqWjaErdmYsf_arbILzf64WjLU,43
21
- ccburn-0.3.1.dist-info/top_level.txt,sha256=SM8TwGQZqQKKIQObVWQkfpA0OI4gRut7bPl-iM3g5RI,7
22
- ccburn-0.3.1.dist-info/RECORD,,