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.
- {codex_manager-6.0.0 → codex_manager-8.0.0}/PKG-INFO +1 -1
- {codex_manager-6.0.0 → codex_manager-8.0.0}/pyproject.toml +1 -1
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/__init__.py +1 -1
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/cli.py +11 -2
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/cooldown.py +16 -3
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/PKG-INFO +1 -1
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/SOURCES.txt +1 -0
- codex_manager-8.0.0/tests/test_backup_ux.py +36 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/README.md +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/setup.cfg +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/account_status.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/args.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/backup.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/cloud.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/config.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/credentials.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/doctor.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/list_backups.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/profile.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/prune.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/prune_backups.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/purge.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/recommend.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/registry.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/remove.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/restore.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/status.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/sync.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/ui.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/use_account.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager/utils.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/dependency_links.txt +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/entry_points.txt +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/requires.txt +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/src/codex_manager.egg-info/top_level.txt +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_account_status_coverage.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_account_status_more2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_args_cli.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_args_cli_3.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_args_cli_extra.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_backup.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_backup2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_3.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_4.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_5.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_6.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_7.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_8.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_extra.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cli_coverage_more.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cloud.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_config.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cooldown.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_cooldown2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_credentials2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_doctor.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_doctor2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_dry_run_coverage.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups3.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_list_backups_coverage.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_next_gen_upgrade.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_profile.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_prune.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_prune_backups.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_prune_backups2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_purge.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_recommend.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_recommend2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_registry_coverage_more.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_registry_dry_run_coverage.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_remove.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_restore.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_restore2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_status.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_status2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_status3.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_sync.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_sync2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_ui.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_ui_2.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_use.py +0 -0
- {codex_manager-6.0.0 → codex_manager-8.0.0}/tests/test_use2.py +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
|
-
|
|
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
|
-
|
|
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"),
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|