codex-manager 6.0.0__tar.gz → 8.0.0__tar.gz

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.
Files changed (87) hide show
  1. {codex_manager-6.0.0 → codex_manager-8.0.0}/PKG-INFO +1 -1
  2. {codex_manager-6.0.0 → codex_manager-8.0.0}/pyproject.toml +1 -1
  3. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/__init__.py +1 -1
  4. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/cli.py +11 -2
  5. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/cooldown.py +16 -3
  6. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/PKG-INFO +1 -1
  7. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/SOURCES.txt +1 -0
  8. codex_manager-8.0.0/tests/test_backup_ux.py +36 -0
  9. {codex_manager-6.0.0 → codex_manager-8.0.0}/README.md +0 -0
  10. {codex_manager-6.0.0 → codex_manager-8.0.0}/setup.cfg +0 -0
  11. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/account_status.py +0 -0
  12. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/args.py +0 -0
  13. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/backup.py +0 -0
  14. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/cloud.py +0 -0
  15. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/config.py +0 -0
  16. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/credentials.py +0 -0
  17. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/doctor.py +0 -0
  18. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/list_backups.py +0 -0
  19. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/profile.py +0 -0
  20. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/prune.py +0 -0
  21. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/prune_backups.py +0 -0
  22. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/purge.py +0 -0
  23. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/recommend.py +0 -0
  24. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/registry.py +0 -0
  25. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/remove.py +0 -0
  26. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/restore.py +0 -0
  27. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/status.py +0 -0
  28. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/sync.py +0 -0
  29. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/ui.py +0 -0
  30. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/use_account.py +0 -0
  31. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/utils.py +0 -0
  32. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/dependency_links.txt +0 -0
  33. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/entry_points.txt +0 -0
  34. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/requires.txt +0 -0
  35. {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/top_level.txt +0 -0
  36. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_account_status_coverage.py +0 -0
  37. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_account_status_more2.py +0 -0
  38. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_args_cli.py +0 -0
  39. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_args_cli_3.py +0 -0
  40. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_args_cli_extra.py +0 -0
  41. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_backup.py +0 -0
  42. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_backup2.py +0 -0
  43. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli2.py +0 -0
  44. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage.py +0 -0
  45. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_2.py +0 -0
  46. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_3.py +0 -0
  47. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_4.py +0 -0
  48. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_5.py +0 -0
  49. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_6.py +0 -0
  50. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_7.py +0 -0
  51. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_8.py +0 -0
  52. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_extra.py +0 -0
  53. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_more.py +0 -0
  54. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cloud.py +0 -0
  55. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_config.py +0 -0
  56. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cooldown.py +0 -0
  57. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cooldown2.py +0 -0
  58. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_credentials2.py +0 -0
  59. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_doctor.py +0 -0
  60. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_doctor2.py +0 -0
  61. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_dry_run_coverage.py +0 -0
  62. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups.py +0 -0
  63. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups2.py +0 -0
  64. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups3.py +0 -0
  65. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups_coverage.py +0 -0
  66. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_next_gen_upgrade.py +0 -0
  67. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_profile.py +0 -0
  68. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_prune.py +0 -0
  69. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_prune_backups.py +0 -0
  70. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_prune_backups2.py +0 -0
  71. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_purge.py +0 -0
  72. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_recommend.py +0 -0
  73. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_recommend2.py +0 -0
  74. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_registry_coverage_more.py +0 -0
  75. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_registry_dry_run_coverage.py +0 -0
  76. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_remove.py +0 -0
  77. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_restore.py +0 -0
  78. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_restore2.py +0 -0
  79. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_status.py +0 -0
  80. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_status2.py +0 -0
  81. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_status3.py +0 -0
  82. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_sync.py +0 -0
  83. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_sync2.py +0 -0
  84. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_ui.py +0 -0
  85. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_ui_2.py +0 -0
  86. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_use.py +0 -0
  87. {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_use2.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-manager
3
- Version: 6.0.0
3
+ Version: 8.0.0
4
4
  Summary: Codex account snapshot manager
5
5
  Author-email: Dhruv <dhruv13x@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codex-manager"
3
- version = "6.0.0"
3
+ version = "8.0.0"
4
4
  description = "Codex account snapshot manager"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "6.0.0"
3
+ __version__ = "8.0.0"
@@ -72,7 +72,8 @@ def list_entries_from_args(args: Any) -> list[BackupEntry]:
72
72
  sys.exit(1)
73
73
 
74
74
  if latest_per_email:
75
- all_entries.sort(key=lambda entry: entry.created_at, reverse=True)
75
+ # Sort by reset_at descending so the newest state is picked
76
+ all_entries.sort(key=lambda entry: entry.reset_at, reverse=True)
76
77
  seen_emails: dict[str, BackupEntry] = {}
77
78
  for entry in all_entries:
78
79
  if entry.email not in seen_emails:
@@ -166,6 +167,8 @@ def build_live_status(args: Any) -> CooldownStatus | None:
166
167
  validation_status="live",
167
168
  proposed_archive_name=live_status.proposed_archive_name,
168
169
  remaining_seconds=max(0, remaining_seconds),
170
+ quota_text=live_status.quota_text,
171
+ quota_percent_left=live_status.quota_percent_left,
169
172
  is_expired=live_status.is_expired,
170
173
  )
171
174
 
@@ -284,7 +287,13 @@ def handle_status(args: Any) -> None:
284
287
 
285
288
 
286
289
  def handle_backup(args: Any) -> None:
287
- archive_path, metadata_path, metadata = perform_backup(args)
290
+ try:
291
+ archive_path, metadata_path, metadata = perform_backup(args)
292
+ except FileExistsError as exc:
293
+ console.print(f"[bold red]Stop:[/] {exc}")
294
+ console.print(f"[dim]Note: Backups are named after the account's weekly reset time.[/]")
295
+ console.print(f"[dim]If you want to update your current snapshot, run with: [bold white]--force[/][/]")
296
+ sys.exit(1)
288
297
 
289
298
  if getattr(args, "cloud", False):
290
299
  cp = get_cloud_provider(args)
@@ -16,6 +16,8 @@ class CooldownStatus:
16
16
  validation_status: str
17
17
  proposed_archive_name: str
18
18
  remaining_seconds: int
19
+ quota_text: str | None = None
20
+ quota_percent_left: int | None = None
19
21
  is_expired: bool = False
20
22
 
21
23
 
@@ -34,9 +36,6 @@ def evaluate_entry(entry: BackupEntry, now: datetime | None = None) -> CooldownS
34
36
  remaining_seconds = int((next_available_at - current).total_seconds())
35
37
  status = "ready" if remaining_seconds <= 0 else "cooldown"
36
38
 
37
- # metadata for entry might have is_expired
38
- # we need to load it if not already there, but BackupEntry doesn't have it.
39
- # However, list_backups.py build_backup_entry could be updated to include it.
40
39
  is_expired = getattr(entry, "is_expired", False)
41
40
 
42
41
  return CooldownStatus(
@@ -48,6 +47,8 @@ def evaluate_entry(entry: BackupEntry, now: datetime | None = None) -> CooldownS
48
47
  validation_status="backup",
49
48
  proposed_archive_name=entry.archive_path.name,
50
49
  remaining_seconds=max(0, remaining_seconds),
50
+ quota_text=getattr(entry, "quota_text", None),
51
+ quota_percent_left=getattr(entry, "quota_percent_left", None),
51
52
  is_expired=is_expired,
52
53
  )
53
54
 
@@ -90,6 +91,8 @@ def evaluate_records(
90
91
  validation_status="registry",
91
92
  proposed_archive_name=existing_status.proposed_archive_name,
92
93
  remaining_seconds=max(0, remaining_seconds),
94
+ quota_text=reg_entry.get("quota_text"),
95
+ quota_percent_left=reg_entry.get("quota_percent_left"),
93
96
  is_expired=reg_entry.get("is_expired", False)
94
97
  )
95
98
  else:
@@ -107,6 +110,8 @@ def evaluate_records(
107
110
  validation_status="registry",
108
111
  proposed_archive_name="none",
109
112
  remaining_seconds=max(0, remaining_seconds),
113
+ quota_text=reg_entry.get("quota_text"),
114
+ quota_percent_left=reg_entry.get("quota_percent_left"),
110
115
  is_expired=reg_entry.get("is_expired", False)
111
116
  )
112
117
  )
@@ -145,6 +150,7 @@ def print_statuses_table(statuses: list[CooldownStatus], live_email: str | None
145
150
  table = Table(show_header=True, header_style="bold bright_magenta")
146
151
  table.add_column("Account", style="bright_cyan")
147
152
  table.add_column("Status", justify="center", no_wrap=True)
153
+ table.add_column("Quota", justify="right", style="bright_yellow")
148
154
  table.add_column("Available", justify="right", style="bright_yellow")
149
155
  table.add_column("Session Start", justify="right", style="dim")
150
156
  table.add_column("Reset At", justify="right", style="dim")
@@ -161,9 +167,16 @@ def print_statuses_table(statuses: list[CooldownStatus], live_email: str | None
161
167
  else:
162
168
  status_display = f"[bold bright_green]{status.status.upper()}[/]" if status.status == "ready" else f"[bold bright_yellow]{status.status.upper()}[/]"
163
169
 
170
+ quota_display = (
171
+ f"{status.quota_percent_left}%"
172
+ if status.quota_percent_left is not None
173
+ else "unknown"
174
+ )
175
+
164
176
  table.add_row(
165
177
  account_display,
166
178
  status_display,
179
+ quota_display,
167
180
  format_remaining(status.remaining_seconds),
168
181
  status.session_start_at.strftime("%Y-%m-%d %H:%M:%S"),
169
182
  status.next_available_at.strftime("%Y-%m-%d %H:%M:%S"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-manager
3
- Version: 6.0.0
3
+ Version: 8.0.0
4
4
  Summary: Codex account snapshot manager
5
5
  Author-email: Dhruv <dhruv13x@gmail.com>
6
6
  License: MIT
@@ -37,6 +37,7 @@ tests/test_args_cli_3.py
37
37
  tests/test_args_cli_extra.py
38
38
  tests/test_backup.py
39
39
  tests/test_backup2.py
40
+ tests/test_backup_ux.py
40
41
  tests/test_cli2.py
41
42
  tests/test_cli_coverage.py
42
43
  tests/test_cli_coverage_2.py
@@ -0,0 +1,36 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from types import SimpleNamespace
4
+ from codex_manager.cli import handle_backup
5
+ import pytest
6
+
7
+ def test_handle_backup_file_exists_clean_error(tmp_path, capsys, mocker):
8
+ # Setup
9
+ backup_dir = tmp_path / "backups"
10
+ backup_dir.mkdir()
11
+ archive_name = "2026-01-01-test@example.com-codex.tar.gz"
12
+ (backup_dir / archive_name).write_text("existing")
13
+
14
+ args = SimpleNamespace(
15
+ command="backup",
16
+ backup_dir=str(backup_dir),
17
+ force=False,
18
+ cloud=False,
19
+ dry_run=False
20
+ )
21
+
22
+ # Mock perform_backup to raise the error like it does in real life
23
+ mocker.patch("codex_manager.cli.perform_backup", side_effect=FileExistsError(f"Archive already exists: {archive_name}. Use --force to overwrite."))
24
+
25
+ with pytest.raises(SystemExit) as exc:
26
+ handle_backup(args)
27
+
28
+ assert exc.value.code == 1
29
+ captured = capsys.readouterr()
30
+ assert "Stop:" in captured.out
31
+ assert "--force" in captured.out
32
+ assert "weekly reset time" in captured.out
33
+
34
+ if __name__ == "__main__":
35
+ # Just a simple run to see it visually if needed
36
+ pass
File without changes
File without changes