diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index a595a482f..069f2d2b1 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from types import ModuleType from typing import Any -from . import backups, config, flakes, machines, secrets, vms, webui +from . import backups, config, flakes, history, machines, secrets, vms, webui from .custom_logger import setup_logging from .dirs import get_clan_flake_toplevel, is_clan_flake from .ssh import cli as ssh_cli @@ -111,6 +111,9 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser: parser_vms = subparsers.add_parser("vms", help="manage virtual machines") vms.register_parser(parser_vms) + parser_history = subparsers.add_parser("history", help="manage history") + history.register_parser(parser_history) + if argcomplete: argcomplete.autocomplete(parser) diff --git a/pkgs/clan-cli/clan_cli/flakes/__init__.py b/pkgs/clan-cli/clan_cli/flakes/__init__.py index d1a5893a1..7cf2ba6df 100644 --- a/pkgs/clan-cli/clan_cli/flakes/__init__.py +++ b/pkgs/clan-cli/clan_cli/flakes/__init__.py @@ -1,8 +1,6 @@ # !/usr/bin/env python3 import argparse -from clan_cli.flakes.add import register_add_parser -from clan_cli.flakes.history import register_list_parser from clan_cli.flakes.inspect import register_inspect_parser from .create import register_create_parser @@ -18,9 +16,5 @@ def register_parser(parser: argparse.ArgumentParser) -> None: ) create_parser = subparser.add_parser("create", help="Create a clan flake") register_create_parser(create_parser) - add_parser = subparser.add_parser("add", help="Add a clan flake") - register_add_parser(add_parser) - list_parser = subparser.add_parser("list", help="List recently used flakes") - register_list_parser(list_parser) inspect_parser = subparser.add_parser("inspect", help="Inspect a clan flake") register_inspect_parser(inspect_parser) diff --git a/pkgs/clan-cli/clan_cli/flakes/add.py b/pkgs/clan-cli/clan_cli/flakes/add.py deleted file mode 100644 index c8abe7a0d..000000000 --- a/pkgs/clan-cli/clan_cli/flakes/add.py +++ /dev/null @@ -1,22 +0,0 @@ -# !/usr/bin/env python3 -import argparse -from pathlib import Path - -from clan_cli.flakes.history import push_history - -from ..async_cmd import CmdOut, runforcli - - -async def add_flake(path: Path) -> dict[str, CmdOut]: - push_history(path) - return {} - - -def add_flake_command(args: argparse.Namespace) -> None: - runforcli(add_flake, args.path) - - -# takes a (sub)parser and configures it -def register_add_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument("path", type=Path, help="Path to the flake", default=Path(".")) - parser.set_defaults(func=add_flake_command) diff --git a/pkgs/clan-cli/clan_cli/history/__init__.py b/pkgs/clan-cli/clan_cli/history/__init__.py new file mode 100644 index 000000000..5edb8d0e2 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/history/__init__.py @@ -0,0 +1,22 @@ +# !/usr/bin/env python3 +import argparse + +from .add import register_add_parser +from .list import register_list_parser +from .update import register_update_parser + + +# takes a (sub)parser and configures it +def register_parser(parser: argparse.ArgumentParser) -> None: + subparser = parser.add_subparsers( + title="command", + description="the command to run", + help="the command to run", + required=True, + ) + add_parser = subparser.add_parser("add", help="Add a clan flake") + register_add_parser(add_parser) + list_parser = subparser.add_parser("list", help="List recently used flakes") + register_list_parser(list_parser) + update_parser = subparser.add_parser("update", help="Update a clan flake") + register_update_parser(update_parser) diff --git a/pkgs/clan-cli/clan_cli/flakes/history.py b/pkgs/clan-cli/clan_cli/history/add.py similarity index 50% rename from pkgs/clan-cli/clan_cli/flakes/history.py rename to pkgs/clan-cli/clan_cli/history/add.py index b3e84a455..7e4102627 100644 --- a/pkgs/clan-cli/clan_cli/flakes/history.py +++ b/pkgs/clan-cli/clan_cli/history/add.py @@ -1,14 +1,15 @@ # !/usr/bin/env python3 import argparse import dataclasses +import datetime import json -from dataclasses import dataclass -from datetime import datetime +import os from pathlib import Path from typing import Any -from clan_cli.dirs import user_history_file +from clan_cli.flakes.inspect import FlakeConfig, inspect_flake +from ..dirs import user_history_file from ..locked_open import locked_open @@ -19,10 +20,12 @@ class EnhancedJSONEncoder(json.JSONEncoder): return super().default(o) -@dataclass +@dataclasses.dataclass class HistoryEntry: path: str last_used: str + dir_datetime: str + flake: FlakeConfig def list_history() -> list[HistoryEntry]: @@ -42,7 +45,14 @@ def list_history() -> list[HistoryEntry]: return logs -def push_history(path: Path) -> list[HistoryEntry]: +def get_dir_time(path: Path) -> str: + # Get the last modified dir time in seconds + dir_mtime = os.path.getmtime(path) + dir_datetime = datetime.datetime.fromtimestamp(dir_mtime).isoformat() + return dir_datetime + + +def add_history(path: Path) -> list[HistoryEntry]: user_history_file().parent.mkdir(parents=True, exist_ok=True) logs = list_history() @@ -51,24 +61,34 @@ def push_history(path: Path) -> list[HistoryEntry]: for entry in logs: if entry.path == str(path): found = True - entry.last_used = datetime.now().isoformat() + entry.last_used = datetime.datetime.now().isoformat() + + flake = inspect_flake(path, "defaultVM") + + flake.flake_url = str(flake.flake_url) + dir_datetime = get_dir_time(path) + + history = HistoryEntry( + flake=flake, + dir_datetime=dir_datetime, + path=str(path), + last_used=datetime.datetime.now().isoformat(), + ) if not found: - logs.append( - HistoryEntry(path=str(path), last_used=datetime.now().isoformat()) - ) + logs.append(history) - f.write(json.dumps(logs, cls=EnhancedJSONEncoder)) + f.write(json.dumps(logs, cls=EnhancedJSONEncoder, indent=4)) f.truncate() return logs -def list_history_command(args: argparse.Namespace) -> None: - for history_entry in list_history(): - print(history_entry.path) +def add_history_command(args: argparse.Namespace) -> None: + add_history(args.path) # takes a (sub)parser and configures it -def register_list_parser(parser: argparse.ArgumentParser) -> None: - parser.set_defaults(func=list_history_command) +def register_add_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument("path", type=Path, help="Path to the flake", default=Path(".")) + parser.set_defaults(func=add_history_command) diff --git a/pkgs/clan-cli/clan_cli/history/list.py b/pkgs/clan-cli/clan_cli/history/list.py new file mode 100644 index 000000000..aae04f6c1 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/history/list.py @@ -0,0 +1,14 @@ +# !/usr/bin/env python3 +import argparse + +from .add import list_history + + +def list_history_command(args: argparse.Namespace) -> None: + for history_entry in list_history(): + print(history_entry.path) + + +# takes a (sub)parser and configures it +def register_list_parser(parser: argparse.ArgumentParser) -> None: + parser.set_defaults(func=list_history_command) diff --git a/pkgs/clan-cli/clan_cli/history/update.py b/pkgs/clan-cli/clan_cli/history/update.py new file mode 100644 index 000000000..d39182b73 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/history/update.py @@ -0,0 +1,35 @@ +# !/usr/bin/env python3 +import argparse +import copy +import datetime +import json +from pathlib import Path + +from ..dirs import user_history_file +from ..locked_open import locked_open +from .add import EnhancedJSONEncoder, HistoryEntry, get_dir_time, list_history + + +def update_history() -> list[HistoryEntry]: + with locked_open(user_history_file(), "w+") as f: + logs = list_history() + new_logs = [] + for entry in logs: + new_entry = copy.deepcopy(entry) + new_time = get_dir_time(Path(entry.path)) + if new_time != entry.dir_datetime: + new_entry.dir_datetime = new_time + new_entry.last_used = datetime.datetime.now().isoformat() + new_logs.append(new_entry) + f.write(json.dumps(new_logs, cls=EnhancedJSONEncoder, indent=4)) + f.truncate() + return new_logs + + +def add_update_command(args: argparse.Namespace) -> None: + update_history() + + +# takes a (sub)parser and configures it +def register_update_parser(parser: argparse.ArgumentParser) -> None: + parser.set_defaults(func=add_update_command) diff --git a/pkgs/clan-cli/clan_cli/webui/routers/flake.py b/pkgs/clan-cli/clan_cli/webui/routers/flake.py index 000c9d8a3..b8bbec767 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/flake.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/flake.py @@ -17,7 +17,7 @@ from clan_cli.webui.api_outputs import ( ) from ...async_cmd import run -from ...flakes import add, create +from ...flakes import create from ...nix import nix_command, nix_flake_show from ..tags import Tags @@ -45,16 +45,6 @@ async def get_attrs(url: AnyUrl | Path) -> list[str]: return flake_attrs -@router.post("/api/flake/history", tags=[Tags.flake]) -async def flake_history_append(flake_dir: Path) -> None: - await add.add_flake(flake_dir) - - -@router.get("/api/flake/history", tags=[Tags.flake]) -async def flake_history_list() -> list[Path]: - return [] - - # TODO: Check for directory traversal @router.get("/api/flake/attrs", tags=[Tags.flake]) async def inspect_flake_attrs(url: AnyUrl | Path) -> FlakeAttrResponse: diff --git a/pkgs/clan-cli/tests/test_flakes_cli.py b/pkgs/clan-cli/tests/test_flakes_cli.py index da9d0abee..af483920a 100644 --- a/pkgs/clan-cli/tests/test_flakes_cli.py +++ b/pkgs/clan-cli/tests/test_flakes_cli.py @@ -7,7 +7,7 @@ from fixtures_flakes import FlakeForTest from pytest import CaptureFixture from clan_cli.dirs import user_history_file -from clan_cli.flakes.history import HistoryEntry +from clan_cli.history.add import HistoryEntry if TYPE_CHECKING: pass diff --git a/pkgs/clan-vm-manager/clan_vm_manager/models.py b/pkgs/clan-vm-manager/clan_vm_manager/models.py index 1062f8365..15c116549 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/models.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/models.py @@ -73,45 +73,6 @@ class VM: # start/end indexes can be used optionally for pagination def get_initial_vms(start: int = 0, end: int | None = None) -> list[VM]: - # vms = [ - # VM( - # base=VMBase( - # icon=assets.loc / "cybernet.jpeg", - # name="Cybernet Clan", - # url="clan://cybernet.lol", - # _path=Path(__file__).parent.parent / "test_democlan", - # status=False, - # ), - # ), - # VM( - # base=VMBase( - # icon=assets.loc / "zenith.jpeg", - # name="Zenith Clan", - # url="clan://zenith.lol", - # _path=Path(__file__).parent.parent / "test_democlan", - # status=False, - # ) - # ), - # VM( - # base=VMBase( - # icon=assets.loc / "firestorm.jpeg", - # name="Firestorm Clan", - # url="clan://firestorm.lol", - # _path=Path(__file__).parent.parent / "test_democlan", - # status=False, - # ), - # ), - # VM( - # base=VMBase( - # icon=assets.loc / "placeholder.jpeg", - # name="Placeholder Clan", - # url="clan://demo.lol", - # _path=Path(__file__).parent.parent / "test_democlan", - # status=True, - # ), - # ), - # ] - vm_list = [] # TODO: list_history() should return a list of dicts, not a list of paths