snapshot-cli 1.0.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.
snapshot/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Snapshot — lightweight project snapshot & versioning tool."""
2
+
3
+ __version__ = "1.0.0"
snapshot/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running Snapshot as `python -m snapshot`."""
2
+
3
+ from snapshot.cli import main
4
+
5
+ main()
snapshot/cli.py ADDED
@@ -0,0 +1,75 @@
1
+ import argparse
2
+
3
+ from snapshot.commands.init import init
4
+ from snapshot.commands.list_snap import list_snap
5
+ from snapshot.commands.save_snap import snapshot
6
+ from snapshot.commands.restore_snap import restore
7
+ from snapshot.commands.restore_latest import restore_latest
8
+ from snapshot.commands.info_snap import show_info
9
+ from snapshot.commands.delete_snap import delete
10
+ from snapshot.commands.stats_snap import stats
11
+
12
+ from snapshot.utils.console import print_help
13
+
14
+
15
+ def main():
16
+ parser = argparse.ArgumentParser(
17
+ prog="snap",
18
+ description="Project snapshot manager",
19
+ add_help=False,
20
+ )
21
+
22
+ subparsers = parser.add_subparsers(dest="command")
23
+
24
+ subparsers.add_parser("init")
25
+ subparsers.add_parser("list")
26
+ subparsers.add_parser("stats")
27
+
28
+ save_parser = subparsers.add_parser("save")
29
+ save_parser.add_argument("message")
30
+
31
+ restore_parser = subparsers.add_parser("restore")
32
+ restore_parser.add_argument("snap_id", type=int)
33
+ restore_parser.add_argument("--clean", action="store_true")
34
+
35
+ restore_latest_parser = subparsers.add_parser("restorelatest")
36
+ restore_latest_parser.add_argument("--clean", action="store_true")
37
+
38
+ info_parser = subparsers.add_parser("info")
39
+ info_parser.add_argument("snap_id", type=int)
40
+
41
+ del_parser = subparsers.add_parser("del")
42
+ del_parser.add_argument("snap_id", type=int)
43
+
44
+ args = parser.parse_args()
45
+
46
+ if args.command == "init":
47
+ init()
48
+
49
+ elif args.command == "save":
50
+ snapshot(args.message)
51
+
52
+ elif args.command == "restore":
53
+ restore(args.snap_id, clean=args.clean)
54
+
55
+ elif args.command == "restorelatest":
56
+ restore_latest(is_clean=args.clean)
57
+
58
+ elif args.command == "del":
59
+ delete(args.snap_id)
60
+
61
+ elif args.command == "info":
62
+ show_info(args.snap_id)
63
+
64
+ elif args.command == "stats":
65
+ stats()
66
+
67
+ elif args.command == "list":
68
+ list_snap()
69
+
70
+ else:
71
+ print_help()
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1 @@
1
+ # Commands sub-package
@@ -0,0 +1,64 @@
1
+ from snapshot.config import snap_dir, os
2
+ from snapshot.utils.metadata_loader import load_metadata, save_metadata
3
+ from snapshot.utils.console import (
4
+ console, success, error, warning, info,
5
+ PRIMARY, WARNING, ERROR, MUTED,
6
+ )
7
+ from rich.panel import Panel
8
+ from rich.prompt import Confirm
9
+
10
+ def delete(snap_id):
11
+ if not os.path.exists(snap_dir):
12
+ error("Project not initialized.")
13
+ return
14
+
15
+ metadata = load_metadata()
16
+
17
+ snapshot = next(
18
+ (
19
+ item
20
+ for item in metadata["snapshots"]
21
+ if item["save_id"] == snap_id
22
+ ),
23
+ None,
24
+ )
25
+
26
+ if snapshot is None:
27
+ error(f"Snapshot #{snap_id} not found.")
28
+ return
29
+
30
+ console.print(
31
+ Panel(
32
+ "[bold]This will permanently delete the selected snapshot.[/bold]\n"
33
+ "The snapshot archive and its metadata entry will be removed.",
34
+ title="[bold]! Snapshot Deletion[/bold]",
35
+ title_align="left",
36
+ border_style=WARNING,
37
+ padding=(1, 2),
38
+ )
39
+ )
40
+ console.print()
41
+
42
+ confirmed = Confirm.ask(
43
+ f"Delete snapshot #{snap_id}?",
44
+ default=False,
45
+ )
46
+
47
+ if not confirmed:
48
+ info("Deletion cancelled.")
49
+ return
50
+
51
+ zip_path = snapshot["file"]
52
+
53
+ if os.path.exists(zip_path):
54
+ os.remove(zip_path)
55
+
56
+ metadata["snapshots"] = [
57
+ item
58
+ for item in metadata["snapshots"]
59
+ if item["save_id"] != snap_id
60
+ ]
61
+
62
+ save_metadata(metadata)
63
+
64
+ success(f"Deleted snapshot #{snap_id}")
@@ -0,0 +1,44 @@
1
+ from snapshot.config import snap_dir, os
2
+ from snapshot.utils.metadata_loader import load_metadata
3
+ from snapshot.utils.console import (
4
+ console,
5
+ error,
6
+ info,
7
+ PRIMARY, INFO, MUTED,
8
+ )
9
+ from rich.panel import Panel
10
+
11
+
12
+ def show_info(snap_id):
13
+ if not os.path.exists(snap_dir):
14
+ error("Project not initialized. Run [bold]snap init[/bold] first.")
15
+ return
16
+
17
+ metadata = load_metadata()
18
+
19
+ snapshot = next(
20
+ (
21
+ item
22
+ for item in metadata["snapshots"]
23
+ if item["save_id"] == snap_id
24
+ ),
25
+ None,
26
+ )
27
+
28
+ if snapshot is None:
29
+ error(f"Snapshot [bold]#{snap_id}[/bold] not found.")
30
+ return
31
+
32
+ console.print()
33
+ console.print(
34
+ Panel(
35
+ f"[bold]Message:[/bold] [{INFO}]{snapshot['snap_message']}[/{INFO}]\n"
36
+ f"[bold]Created:[/bold] [{INFO}]{snapshot['created_on'][:19]}[/{INFO}]\n"
37
+ f"[bold]Archive:[/bold] [{MUTED}]{snapshot['file']}[/{MUTED}]",
38
+ title=f"[bold {PRIMARY}]Snapshot #{snapshot['save_id']}[/bold {PRIMARY}]",
39
+ border_style=MUTED,
40
+ padding=(1, 2),
41
+ expand=False,
42
+ )
43
+ )
44
+ console.print()
@@ -0,0 +1,17 @@
1
+ from snapshot.config import os, snap_dir, snapshots_dir, metadata_default
2
+ from snapshot.utils.console import success, warning
3
+ import json
4
+
5
+ def init():
6
+
7
+ # Snap directory creation
8
+ if os.path.exists(snap_dir):
9
+ warning("Project already initialized.")
10
+ return
11
+
12
+ os.makedirs(snapshots_dir)
13
+
14
+ with open(f"{snap_dir}/metadata.json", "w") as file:
15
+ json.dump(metadata_default, file, indent=4)
16
+
17
+ success("Project initialized successfully!")
@@ -0,0 +1,58 @@
1
+ from snapshot.config import snap_dir, os
2
+ from snapshot.utils.metadata_loader import load_metadata
3
+ from snapshot.utils.console import (
4
+ console, error, info, print_header,
5
+ PRIMARY, SUCCESS, MUTED, INFO,
6
+ )
7
+ from rich.table import Table
8
+ from rich.panel import Panel
9
+
10
+ def list_snap():
11
+ if not os.path.exists(snap_dir):
12
+ error("Project not initialized. Run [bold]snap init[/bold] first.")
13
+ return
14
+
15
+ metadata = load_metadata()
16
+ snapshots = metadata["snapshots"]
17
+
18
+ print_header()
19
+
20
+ if not snapshots:
21
+ info("No snapshots found. Run [bold]snap save <message>[/bold] to create one.")
22
+ return
23
+
24
+ table = Table(
25
+ show_header=True,
26
+ header_style=f"bold {PRIMARY}",
27
+ border_style=MUTED,
28
+ padding=(0, 2),
29
+ show_edge=False,
30
+ show_lines=True,
31
+ row_styles=["", "dim"],
32
+ )
33
+
34
+ table.add_column("#", style=f"bold {PRIMARY}", justify="right", min_width=4)
35
+ table.add_column("Message", style="bold white", min_width=20)
36
+ table.add_column("Created", style=INFO, min_width=20)
37
+ table.add_column("Archive Path", style=MUTED, min_width=16)
38
+
39
+ for snap in snapshots:
40
+ # Truncate the datetime for cleaner display
41
+ created = snap["created_on"][:19]
42
+ table.add_row(
43
+ str(snap["save_id"]),
44
+ snap["snap_message"],
45
+ created,
46
+ snap.get("file", "—"),
47
+ )
48
+
49
+ console.print(
50
+ Panel(
51
+ table,
52
+ title=f"[bold {PRIMARY}]Snapshots[/bold {PRIMARY}]",
53
+ subtitle=f"[dim]{len(snapshots)} snapshot{'s' if len(snapshots) != 1 else ''}[/dim]",
54
+ border_style=MUTED,
55
+ padding=(1, 1),
56
+ )
57
+ )
58
+ console.print()
@@ -0,0 +1,21 @@
1
+ from snapshot.config import snap_dir, os
2
+ from snapshot.utils.metadata_loader import load_metadata
3
+ from snapshot.commands.restore_snap import restore
4
+
5
+ from snapshot.utils.console import error
6
+
7
+ def restore_latest(is_clean=False):
8
+ if not os.path.exists(snap_dir):
9
+ error("Project not initialized. Run [bold]snap init[/bold] first.")
10
+ return
11
+
12
+ metadata = load_metadata()
13
+ snapshots = metadata["snapshots"]
14
+
15
+ if not snapshots:
16
+ error("No snapshots found.")
17
+ return
18
+
19
+ latest_snap = snapshots[-1]
20
+
21
+ restore(snap_id=latest_snap["save_id"], clean=is_clean)
@@ -0,0 +1,73 @@
1
+ from snapshot.config import snap_dir, current_dir, os
2
+ from snapshot.utils.file_manager import unzip_dir, clear_contents
3
+ from snapshot.utils.metadata_loader import load_metadata
4
+ from snapshot.utils.console import (
5
+ console, success, error, warning, info,
6
+ PRIMARY, WARNING, ERROR, MUTED,
7
+ )
8
+ from rich.panel import Panel
9
+ from rich.prompt import Confirm
10
+
11
+ def restore(snap_id, clean=False):
12
+ if not os.path.exists(snap_dir):
13
+ error("Project not initialized. Run [bold]snap init[/bold] first.")
14
+ return
15
+
16
+ metadata = load_metadata()
17
+
18
+ snapshot = next(
19
+ (item for item in metadata["snapshots"] if item["save_id"] == snap_id),
20
+ None,
21
+ )
22
+
23
+ if snapshot is None:
24
+ error(f"Snapshot [bold]#{snap_id}[/bold] not found.")
25
+ return
26
+
27
+
28
+ console.print()
29
+ info(f"Snapshot [bold]#{snapshot['save_id']}[/bold] — \"{snapshot['snap_message']}\"")
30
+ info(f"Created: {snapshot['created_on'][:19]}")
31
+ console.print()
32
+
33
+
34
+ if clean:
35
+ console.print(
36
+ Panel(
37
+ "[bold]This is a clean restore.[/bold]\n"
38
+ "All current project files will be [bold]permanently deleted[/bold]\n"
39
+ "before extracting the snapshot.",
40
+ title="[bold]! Destructive Operation[/bold]",
41
+ title_align="left",
42
+ border_style=WARNING,
43
+ padding=(1, 2),
44
+ )
45
+ )
46
+ else:
47
+ warning(
48
+ "Restoring will overwrite any conflicting files in the project directory."
49
+ )
50
+
51
+ console.print()
52
+
53
+ confirmed = Confirm.ask(
54
+ f" [{PRIMARY}]Restore snapshot #{snap_id}?[/{PRIMARY}]",
55
+ default=False,
56
+ )
57
+
58
+ if not confirmed:
59
+ info("Restore cancelled.")
60
+ return
61
+
62
+ with console.status(
63
+ f"[{PRIMARY}] Restoring snapshot #{snap_id}…[/{PRIMARY}]",
64
+ spinner="dots",
65
+ spinner_style=PRIMARY,
66
+ ):
67
+ if clean:
68
+ clear_contents(current_dir)
69
+
70
+ zip_path = snapshot["file"]
71
+ unzip_dir(zip_path)
72
+
73
+ success(f"Snapshot [bold]#{snap_id}[/bold] restored successfully!")
@@ -0,0 +1,36 @@
1
+ from snapshot.config import snap_dir, current_dir, os, snapshots_dir
2
+ from snapshot.utils.metadata_loader import save_metadata, load_metadata
3
+ from snapshot.utils.file_manager import zip_dir
4
+ from snapshot.utils.console import console, success, error, info, PRIMARY, MUTED
5
+ from datetime import datetime
6
+
7
+ def snapshot(snap_message):
8
+ if not os.path.exists(snap_dir):
9
+ error("Project not initialized. Run [bold]snap init[/bold] first.")
10
+ return
11
+
12
+ metadata = load_metadata()
13
+
14
+ save_no = len(metadata["snapshots"]) + 1
15
+ zip_path = f"{snapshots_dir}/{save_no}.zip"
16
+
17
+ with console.status(
18
+ f"[{PRIMARY}] Creating snapshot #{save_no}…[/{PRIMARY}]",
19
+ spinner="dots",
20
+ spinner_style=PRIMARY,
21
+ ):
22
+ zip_dir(current_dir, zip_path)
23
+
24
+ created_on = str(datetime.now())
25
+
26
+ metadata["snapshots"].append({
27
+ "snap_message": snap_message,
28
+ "save_id": save_no,
29
+ "file": zip_path,
30
+ "created_on": created_on
31
+ })
32
+
33
+ save_metadata(metadata)
34
+
35
+ success(f"Snapshot [bold]#{save_no}[/bold] saved — \"{snap_message}\"")
36
+ info(f"Archive: [{MUTED}]{zip_path}[/{MUTED}]")
@@ -0,0 +1,83 @@
1
+ from snapshot.config import snap_dir, os
2
+ from snapshot.utils.metadata_loader import load_metadata
3
+ from snapshot.utils.file_manager import get_directory_size, human_size
4
+
5
+ from snapshot.utils.console import (
6
+ console,
7
+ error,
8
+ PRIMARY,
9
+ MUTED,
10
+ )
11
+ from rich.table import Table
12
+ from rich.panel import Panel
13
+
14
+ def stats():
15
+
16
+ if not os.path.exists(snap_dir):
17
+ error("Project not initialized. Run [bold]snap init[/bold] first.")
18
+ return
19
+
20
+ metadata = load_metadata()
21
+ snapshots = metadata["snapshots"]
22
+
23
+ snap_count = len(snapshots)
24
+ snap_size = get_directory_size(snap_dir)
25
+
26
+ latest_snap = snapshots[-1] if snapshots else None
27
+ oldest_snap = snapshots[0] if snapshots else None
28
+
29
+ table = Table(
30
+ show_header=True,
31
+ header_style=f"bold {PRIMARY}",
32
+ border_style=MUTED,
33
+ padding=(0, 2),
34
+ show_edge=False,
35
+ show_lines=True,
36
+ )
37
+
38
+ table.add_column("Metric", style=f"bold {PRIMARY}")
39
+ table.add_column("Value", style="bold white")
40
+
41
+ table.add_row(
42
+ "Total Snapshots",
43
+ str(snap_count)
44
+ )
45
+
46
+ table.add_row(
47
+ "Storage Used",
48
+ human_size(snap_size)
49
+ )
50
+
51
+ if oldest_snap:
52
+ table.add_row(
53
+ "Oldest Snapshot",
54
+ f"#{oldest_snap['save_id']}"
55
+ )
56
+
57
+ if latest_snap:
58
+ table.add_row(
59
+ "Latest Snapshot",
60
+ f"#{latest_snap['save_id']}"
61
+ )
62
+
63
+ table.add_row(
64
+ "Latest Message",
65
+ latest_snap["snap_message"]
66
+ )
67
+
68
+ table.add_row(
69
+ "Created",
70
+ latest_snap["created_on"][:19]
71
+ )
72
+
73
+ console.print(
74
+ Panel(
75
+ table,
76
+ title=f"[bold {PRIMARY}]Statistics[/bold {PRIMARY}]",
77
+ subtitle=f"[dim]{snap_count} snapshot{'s' if snap_count != 1 else ''}[/dim]",
78
+ border_style=MUTED,
79
+ padding=(1, 1),
80
+ )
81
+ )
82
+
83
+ console.print()
snapshot/config.py ADDED
@@ -0,0 +1,8 @@
1
+ import os
2
+
3
+ current_dir = os.getcwd()
4
+ snap_dir = ".snap"
5
+ snapshots_dir = f"{snap_dir}/snapshots"
6
+ metadata_default = {
7
+ "snapshots": []
8
+ }
@@ -0,0 +1 @@
1
+ # Utils sub-package
@@ -0,0 +1,102 @@
1
+
2
+
3
+ from rich.console import Console
4
+ from rich.theme import Theme
5
+ from rich.panel import Panel
6
+ from rich.text import Text
7
+ from rich.table import Table
8
+ import sys, io
9
+
10
+
11
+ if sys.platform == "win32":
12
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
13
+
14
+
15
+ PRIMARY = "#3B82F6"
16
+ SUCCESS = "#10B981"
17
+ WARNING = "#F59E0B"
18
+ ERROR = "#EF4444"
19
+ INFO = "#06B6D4"
20
+ MUTED = "#6B7280"
21
+
22
+
23
+ snap_theme = Theme({
24
+ "primary": PRIMARY,
25
+ "success": f"bold {SUCCESS}",
26
+ "warning": f"bold {WARNING}",
27
+ "error": f"bold {ERROR}",
28
+ "info": INFO,
29
+ "muted": MUTED,
30
+ "header": f"bold {PRIMARY}",
31
+ })
32
+
33
+ console = Console(theme=snap_theme)
34
+
35
+
36
+
37
+ def success(message: str) -> None:
38
+
39
+ console.print(f" [success]+[/success] {message}")
40
+
41
+ def error(message: str) -> None:
42
+
43
+ console.print(f" [error]x[/error] [error]{message}[/error]")
44
+
45
+ def warning(message: str) -> None:
46
+
47
+ console.print(f" [warning]![/warning] [warning]{message}[/warning]")
48
+
49
+ def info(message: str) -> None:
50
+
51
+ console.print(f" [info]>[/info] [info]{message}[/info]")
52
+
53
+
54
+
55
+ SNAP_LOGO = Text.from_markup(
56
+ f"[bold {PRIMARY}]Snap[/bold {PRIMARY}] [dim]- Local Project Snapshots[/dim]"
57
+ )
58
+
59
+ def print_header() -> None:
60
+
61
+ console.print()
62
+ console.print(
63
+ Panel(
64
+ SNAP_LOGO,
65
+ border_style=PRIMARY,
66
+ padding=(0, 2),
67
+ )
68
+ )
69
+ console.print()
70
+
71
+
72
+
73
+ def print_help() -> None:
74
+
75
+ print_header()
76
+
77
+ table = Table(
78
+ show_header=True,
79
+ header_style=f"bold {PRIMARY}",
80
+ border_style=MUTED,
81
+ padding=(0, 2),
82
+ show_edge=False,
83
+ show_lines=False,
84
+ )
85
+ table.add_column("Command", style="bold white", min_width=28)
86
+ table.add_column("Description", style="dim")
87
+
88
+ table.add_row("snap init", "Initialize a new Snap project")
89
+ table.add_row("snap save <message>", "Save a snapshot with a message")
90
+ table.add_row("snap list", "List all saved snapshots")
91
+ table.add_row("snap restore <id>", "Restore a snapshot by ID")
92
+ table.add_row("snap restore <id> --clean", "Restore & remove current files first")
93
+
94
+ console.print(
95
+ Panel(
96
+ table,
97
+ title=f"[bold {PRIMARY}]Available Commands[/bold {PRIMARY}]",
98
+ border_style=MUTED,
99
+ padding=(1, 1),
100
+ )
101
+ )
102
+ console.print()
@@ -0,0 +1,62 @@
1
+ from snapshot.config import current_dir, snap_dir, os
2
+ from snapshot.utils.snapignore import load_ignore_patterns, should_ignore
3
+ import zipfile, shutil
4
+ from pathlib import Path
5
+
6
+ SYSTEM_PRESERVE = {
7
+ ".snap",
8
+ ".git"
9
+ }
10
+
11
+ def zip_dir(dir_path, zip_name):
12
+
13
+ ignore_patterns = load_ignore_patterns()
14
+
15
+ with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
16
+ for root, dirs, files in os.walk(dir_path):
17
+ dirs[:] = [d for d in dirs if d != ".snap"]
18
+ dirs[:] = [d for d in dirs if d != ".git"]
19
+ dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d), ignore_patterns)]
20
+ for file in files:
21
+ file_path = os.path.join(root, file)
22
+ if should_ignore(file_path, ignore_patterns):
23
+ continue
24
+ # arcname preserves directory structure relative to dir_path
25
+ arcname = os.path.relpath(file_path, start=dir_path)
26
+ zipf.write(file_path, arcname)
27
+
28
+ def unzip_dir(zip_path):
29
+ # Extract all files to a directory
30
+ with zipfile.ZipFile(f'{zip_path}', 'r') as zip_ref:
31
+ zip_ref.extractall(current_dir)
32
+
33
+ def clear_contents(dir_path):
34
+ folder = Path(dir_path)
35
+
36
+ for item in folder.iterdir():
37
+
38
+ if item.name in SYSTEM_PRESERVE:
39
+ continue
40
+
41
+ if item.is_file() or item.is_symlink():
42
+ item.unlink()
43
+
44
+ elif item.is_dir():
45
+ shutil.rmtree(item)
46
+
47
+ def get_directory_size(path):
48
+ total_size = 0
49
+ for dirpath, dirnames, filenames in os.walk(path):
50
+ for f in filenames:
51
+ fp = os.path.join(dirpath, f)
52
+ # Skip symbolic links to avoid infinite loops or errors
53
+ if not os.path.islink(fp):
54
+ total_size += os.path.getsize(fp)
55
+ return total_size
56
+
57
+ def human_size(size):
58
+ for unit in ["B", "KB", "MB", "GB", "TB"]:
59
+ if size < 1024:
60
+ return f"{size:.1f} {unit}"
61
+ size /= 1024
62
+ return f"{size:.1f} PB"
@@ -0,0 +1,11 @@
1
+ from snapshot.config import snap_dir
2
+ import json
3
+
4
+
5
+ def load_metadata():
6
+ with open(f"{snap_dir}/metadata.json", "r") as file:
7
+ return json.load(file)
8
+
9
+ def save_metadata(data):
10
+ with open(f"{snap_dir}/metadata.json", "w") as file:
11
+ json.dump(data, file, indent=4)
@@ -0,0 +1,36 @@
1
+ from snapshot.config import current_dir, os
2
+ from fnmatch import fnmatch
3
+
4
+ def load_ignore_patterns():
5
+ snap_ignore_file = f"{current_dir}/.snapignore"
6
+
7
+ if not os.path.exists(snap_ignore_file):
8
+ return []
9
+
10
+ with open(snap_ignore_file, "r") as file:
11
+ patterns = file.readlines()
12
+
13
+ return [
14
+ line.strip()
15
+ for line in patterns
16
+ if line.strip() and not line.strip().startswith("#")
17
+ ]
18
+
19
+ def should_ignore(file_path, ignore_patterns):
20
+ relative_path = os.path.relpath(file_path, current_dir)
21
+ relative_path = relative_path.replace("\\", "/")
22
+
23
+ for pattern in ignore_patterns:
24
+ pattern = pattern.strip()
25
+
26
+ # Folder ignore
27
+ if pattern.endswith("/"):
28
+ folder = pattern.rstrip("/")
29
+ if relative_path.startswith(folder + "/"):
30
+ return True
31
+
32
+ # Wildcard/file ignore
33
+ if fnmatch(relative_path, pattern):
34
+ return True
35
+
36
+ return False
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: snapshot-cli
3
+ Version: 1.0.0
4
+ Summary: A lightweight project snapshot & versioning tool for developers.
5
+ Project-URL: Homepage, https://github.com/Sparkleeop/snapshot
6
+ Project-URL: Repository, https://github.com/Sparkleeop/snapshot
7
+ Project-URL: Issues, https://github.com/Sparkleeop/snapshot/issues
8
+ Author: Sparkleeop
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backup,cli,developer-tools,snapshot,versioning
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Version Control
23
+ Classifier: Topic :: System :: Archiving :: Backup
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: rich>=13.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Snapshot
29
+
30
+ Snapshot is a lightweight code snapshotting tool that allows you to save and restore your entire project at any point in time.
31
+
32
+ Think of it as a simpler alternative to Git when you just want quick save points without branches, commits, remotes, or repositories.
33
+
34
+ ## Features
35
+
36
+ * Save snapshots of your entire project
37
+ * Restore previous snapshots at any time
38
+ * Ignore files and directories using `.snapignore`
39
+ * View snapshot history
40
+ * View detailed snapshot information
41
+ * Clean restore mode for exact project restoration
42
+ * Rich terminal interface
43
+
44
+ ## Installation
45
+
46
+ ### From PyPI
47
+
48
+ ```bash
49
+ pip install snapshot-cli
50
+ ```
51
+
52
+ ### From source (development)
53
+
54
+ ```bash
55
+ git clone https://github.com/Sparkleeop/snapshot.git
56
+ cd snapshot
57
+ pip install -e .
58
+ ```
59
+
60
+ This installs the `snap` command globally in your environment.
61
+
62
+ ## Usage
63
+
64
+ Initialize Snapshot in your project root:
65
+
66
+ ```bash
67
+ snap init
68
+ ```
69
+
70
+ Create a snapshot:
71
+
72
+ ```bash
73
+ snap save "before authentication rewrite"
74
+ ```
75
+
76
+ List all snapshots, you can also find <snap id> here:
77
+
78
+ ```bash
79
+ snap list
80
+ ```
81
+
82
+ View information about a snapshot:
83
+
84
+ ```bash
85
+ snap info <snap id>
86
+ ```
87
+
88
+ Restore a snapshot:
89
+
90
+ ```bash
91
+ snap restore <snap id>
92
+ ```
93
+
94
+ Perform a clean restore (removes existing project files before restoring):
95
+
96
+ ```bash
97
+ snap restore <snap id> --clean
98
+ ```
99
+
100
+ Perform a restore to the latest snap:
101
+
102
+ ```bash
103
+ snap restorelatest <--clean>
104
+ ```
105
+
106
+ Delete a snapshot:
107
+
108
+ ```bash
109
+ snap del <snap id>
110
+ ```
111
+
112
+ Get the overall project statistics:
113
+
114
+ ```bash
115
+ snap stats
116
+ ```
117
+
118
+ ## .snapignore
119
+
120
+ Use a `.snapignore` file in your project root to exclude files and directories from snapshots.
121
+
122
+ Example:
123
+
124
+ ```text
125
+ .env
126
+ venv
127
+ __pycache__
128
+ node_modules
129
+ ```
130
+
131
+ ## Publishing to PyPI
132
+
133
+ Build and publish:
134
+
135
+ ```bash
136
+ pip install build twine
137
+ python -m build
138
+ twine upload dist/*
139
+ ```
140
+
141
+ ## Roadmap
142
+
143
+ * [ ] Compare snapshots (`snap diff <id1> <id2>`)
144
+ * [ ] Snapshot export/import
145
+ * [ ] Automatic snapshots
146
+ * [ ] Head snapshot
@@ -0,0 +1,23 @@
1
+ snapshot/__init__.py,sha256=r1pMGvqaRsjDDJGMKna5yy-k1lJ5rh7QXVR6icqdnng,90
2
+ snapshot/__main__.py,sha256=eMS7zUGgdFZNP7H2tLMvu5HwrTAl4_VuYhPkhh0NR7U,93
3
+ snapshot/cli.py,sha256=IE6pXsiN_oeU7W89qvRVhliXOfUvdoZ7Z7A0spPhDEg,1987
4
+ snapshot/config.py,sha256=6_Q8nniIIcTuMe2td_81F85J0nOtPdkJe216AyyNtjE,139
5
+ snapshot/commands/__init__.py,sha256=4GrMf1xtemyxvRgGiO_s8UY-rU1-OWiTLFEEqsxrfCw,23
6
+ snapshot/commands/delete_snap.py,sha256=6U-jRDb3R0qybBUK25ZSRXxvv-WE210abYSjkAKzyXg,1551
7
+ snapshot/commands/info_snap.py,sha256=jG8VCc3LBaSFHeK1vAENRMH5s_AIsu6EJmKtdc_SS8k,1195
8
+ snapshot/commands/init.py,sha256=okAJhwk3mMEojLuH7Bok9O-JsX-hShS-4iYw0hGE2_c,470
9
+ snapshot/commands/list_snap.py,sha256=yWWMUc3rtyGMtG0r7B65gCK73FH0eYCY-xJd3NZwCos,1773
10
+ snapshot/commands/restore_latest.py,sha256=oPEa7kogEFmW5Bz8q8zjqIUKXol6XrgLkrCKgBNYMlM,599
11
+ snapshot/commands/restore_snap.py,sha256=hDzK_PO3qJ3cf2h1NhK3T9WAgkr1PWBBvk3j2FE_BzM,2109
12
+ snapshot/commands/save_snap.py,sha256=LBz9kAbx2nYvRKciiKdHprzFswKUI8WBY3glJ01hnCU,1145
13
+ snapshot/commands/stats_snap.py,sha256=tAzymqrOdR3Ulxp1vm8IDTwVMsv5SUpQHRZ73d8Ip7k,1937
14
+ snapshot/utils/__init__.py,sha256=XK7YigtLd7LrOc-A-lCxnddKXJwKO3AGhQm25NicqDA,20
15
+ snapshot/utils/console.py,sha256=YpA2vhmxIJ5pkuRvcszrXAVSGZWVi-GwTsWC0BE8H00,2414
16
+ snapshot/utils/file_manager.py,sha256=0sZXI2-Kd-FNYzQy4fKFNy9PQGuwzzuZ9LUvmT6WNXk,1999
17
+ snapshot/utils/metadata_loader.py,sha256=5vee72a1QSZxxeddnBJvU2QfEscjg6d2bSjqoju3jEQ,287
18
+ snapshot/utils/snapignore.py,sha256=fEGsSFaQtgNxbJrPku0h4yfvd1TW1fc1DoN8ugzA8aw,964
19
+ snapshot_cli-1.0.0.dist-info/METADATA,sha256=YTaD1a1kJCqHOdOqm2gsUTJHhpfgP7mYq4deO2OjZps,3030
20
+ snapshot_cli-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
21
+ snapshot_cli-1.0.0.dist-info/entry_points.txt,sha256=GwaBhg8iWc-7_sWVJH06bIRAPz7UQx_JImklisVA_os,43
22
+ snapshot_cli-1.0.0.dist-info/licenses/LICENSE,sha256=Af-wt_ji-mnOaX1XYmul1ha_KhtUzmV4Lu6IG5e8LO0,1067
23
+ snapshot_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ snap = snapshot.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sparkleeop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.