From 87d4aa3b10aa23a620d5a51a968ae47110c43dd1 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 15 Jan 2024 11:34:13 +0530 Subject: [PATCH 01/11] feat: cache get-app artifacts by commit_hash --- bench/app.py | 122 +++++++++++++++++++++++++++++++++++----- bench/commands/make.py | 8 +++ bench/utils/__init__.py | 12 +++- 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/bench/app.py b/bench/app.py index f07251e4..7a66ec08 100755 --- a/bench/app.py +++ b/bench/app.py @@ -6,10 +6,12 @@ import re import shutil import subprocess import sys +import tarfile import typing from collections import OrderedDict from datetime import date from functools import lru_cache +from pathlib import Path from urllib.parse import urlparse # imports - third party imports @@ -19,16 +21,11 @@ import git # imports - module imports import bench from bench.exceptions import NotInBenchDirectoryError -from bench.utils import ( - UNSET_ARG, - fetch_details_from_tag, - get_available_folder_name, - is_bench_directory, - is_git_url, - is_valid_frappe_branch, - log, - run_frappe_cmd, -) +from bench.utils import (UNSET_ARG, fetch_details_from_tag, + get_available_folder_name, get_bench_cache_path, + is_bench_directory, is_git_url, + is_valid_frappe_branch, log, run_frappe_cmd) +from bench.utils.app import check_existing_dir from bench.utils.bench import build_assets, install_python_dev_dependencies from bench.utils.render import step @@ -166,6 +163,7 @@ class App(AppMeta): branch: str = None, bench: "Bench" = None, soft_link: bool = False, + commit_hash = None, *args, **kwargs, ): @@ -173,6 +171,7 @@ class App(AppMeta): self.soft_link = soft_link self.required_by = None self.local_resolution = [] + self.commit_hash = commit_hash super().__init__(name, branch, *args, **kwargs) @step(title="Fetching App {repo}", success="App {repo} Fetched") @@ -283,6 +282,95 @@ class App(AppMeta): branch=self.tag, required=self.local_resolution, ) + + + """ + Get App Cache + + Since get-app affects only the `apps`, `env`, and `sites` + bench sub directories. If we assume deterministic builds + when get-app is called, the `apps/app_name` sub dir can be + cached. + + In subsequent builds this would save time by not having to: + - clone repository + - install frontend dependencies + - building frontend assets + as all of this is contained in the `apps/app_name` sub dir. + + Code that updates the `env` and `sites` subdirs still need + to be run. + """ + + def get_app_path(self) -> Path: + return Path(self.bench.name) / "apps" / self.app_name + + def get_app_cache_path(self, is_compressed=False) -> Path: + assert self.commit_hash is not None + + cache_path = get_bench_cache_path("apps") + ext = "tgz" if is_compressed else "tar" + tarfile_name = f"{self.app_name}-{self.commit_hash[:10]}.{ext}" + return cache_path / tarfile_name + + def get_cached(self) -> bool: + if not self.commit_hash: + return False + + cache_path = self.get_app_cache_path() + mode = "r" + + # Check if cache exists without gzip + if not cache_path.is_file(): + cache_path = self.get_app_cache_path(True) + mode = "r:gz" + + # Check if cache exists with gzip + if not cache_path.is_file(): + return + + app_path = self.get_app_path() + if app_path.is_dir(): + shutil.rmtree(app_path) + + click.secho(f"{self.app_name} being installed from cache", fg="yellow") + with tarfile.open(cache_path, mode) as tar: + tar.extractall(app_path.parent) + + return True + + def install_cached(self) -> None: + """ + TODO: + - check if cache is being set + - check if app is being set from cache + - complete install_cached + - check if app is being installed correctly from cache + """ + raise NotImplementedError("TODO: complete this function") + + def set_cache(self, compress_artifacts=False) -> bool: + if not self.commit_hash: + return False + + app_path = self.get_app_path() + if not app_path.is_dir() or not is_valid_app_dir(app_path): + return False + + cwd = os.getcwd() + cache_path = self.get_app_cache_path(compress_artifacts) + mode = "w:gz" if compress_artifacts else "w" + + os.chdir(app_path.parent) + with tarfile.open(cache_path, mode) as tar: + tar.add(app_path.name) + os.chdir(cwd) + return True + + +def is_valid_app_dir(app_path: Path) -> bool: + # TODO: Check from content if valid frappe app root + return True def make_resolution_plan(app: App, bench: "Bench"): @@ -346,6 +434,8 @@ def get_app( soft_link=False, init_bench=False, resolve_deps=False, + commit_hash=None, + compress_artifacts=False, ): """bench get-app clones a Frappe App from remote (GitHub or any other git server), and installs it on the current bench. This also resolves dependencies based on the @@ -357,10 +447,9 @@ def get_app( import bench as _bench import bench.cli as bench_cli from bench.bench import Bench - from bench.utils.app import check_existing_dir bench = Bench(bench_path) - app = App(git_url, branch=branch, bench=bench, soft_link=soft_link) + app = App(git_url, branch=branch, bench=bench, soft_link=soft_link, commit_hash=commit_hash) git_url = app.url repo_name = app.repo branch = app.tag @@ -417,6 +506,10 @@ def get_app( verbose=verbose, ) return + + if app.get_cached(): + app.install_cached() + return dir_already_exists, cloned_path = check_existing_dir(bench_path, repo_name) to_clone = not dir_already_exists @@ -442,6 +535,9 @@ def get_app( or click.confirm("Do you want to reinstall the existing application?") ): app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench) + + app.set_cache(compress_artifacts) + def install_resolved_deps( @@ -451,8 +547,6 @@ def install_resolved_deps( skip_assets=False, verbose=False, ): - from bench.utils.app import check_existing_dir - if "frappe" in resolution: # Terminal dependency del resolution["frappe"] diff --git a/bench/commands/make.py b/bench/commands/make.py index 7369e9c8..2c52ad04 100755 --- a/bench/commands/make.py +++ b/bench/commands/make.py @@ -151,6 +151,9 @@ def drop(path): default=False, help="Resolve dependencies before installing app", ) +@click.option("--commit-hash", default=None, help="Required for caching get-app artifacts.") +@click.option("--cache-artifacts", is_flag=True, default=False, help="Whether to cache get-app artifacts. Needs commit-hash.") +@click.option("--compress-artifacts", is_flag=True, default=False, help="Whether to gzip get-app artifacts that are to be cached.") def get_app( git_url, branch, @@ -160,6 +163,9 @@ def get_app( soft_link=False, init_bench=False, resolve_deps=False, + commit_hash=None, + cache_artifacts=False, + compress_artifacts=False, ): "clone an app from the internet and set it up in your bench" from bench.app import get_app @@ -172,6 +178,8 @@ def get_app( soft_link=soft_link, init_bench=init_bench, resolve_deps=resolve_deps, + commit_hash=commit_hash if cache_artifacts else None, + compress_artifacts=compress_artifacts, ) diff --git a/bench/utils/__init__.py b/bench/utils/__init__.py index 3fe17ad2..bd07ec4b 100644 --- a/bench/utils/__init__.py +++ b/bench/utils/__init__.py @@ -7,8 +7,9 @@ import subprocess import sys from functools import lru_cache from glob import glob +from pathlib import Path from shlex import split -from typing import List, Tuple +from typing import List, Optional, Tuple # imports - third party imports import click @@ -50,6 +51,15 @@ def is_frappe_app(directory: str) -> bool: return bool(is_frappe_app) +def get_bench_cache_path(sub_dir: Optional[str]) -> Path: + relative_path = "~/.cache/bench" + if sub_dir and not sub_dir.startswith("/"): + relative_path += f"/{sub_dir}" + + cache_path = os.path.expanduser(relative_path) + cache_path = Path(cache_path) + cache_path.mkdir(parents=True, exist_ok=True) + return cache_path @lru_cache(maxsize=None) def is_valid_frappe_branch(frappe_path: str, frappe_branch: str): From 7bcea6099da1a41ad530bdd85ff337f627039b80 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 15 Jan 2024 13:40:26 +0530 Subject: [PATCH 02/11] fix: prevent circular dependency - put check_existing_dir back in its place --- bench/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bench/app.py b/bench/app.py index 7a66ec08..45a9c3d8 100755 --- a/bench/app.py +++ b/bench/app.py @@ -25,7 +25,6 @@ from bench.utils import (UNSET_ARG, fetch_details_from_tag, get_available_folder_name, get_bench_cache_path, is_bench_directory, is_git_url, is_valid_frappe_branch, log, run_frappe_cmd) -from bench.utils.app import check_existing_dir from bench.utils.bench import build_assets, install_python_dev_dependencies from bench.utils.render import step @@ -447,6 +446,7 @@ def get_app( import bench as _bench import bench.cli as bench_cli from bench.bench import Bench + from bench.utils.app import check_existing_dir bench = Bench(bench_path) app = App(git_url, branch=branch, bench=bench, soft_link=soft_link, commit_hash=commit_hash) @@ -547,6 +547,7 @@ def install_resolved_deps( skip_assets=False, verbose=False, ): + from bench.utils.app import check_existing_dir if "frappe" in resolution: # Terminal dependency del resolution["frappe"] From 10bb5a47949512c59ac04919912e2249711c926a Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 15 Jan 2024 15:56:32 +0530 Subject: [PATCH 03/11] fix: update install_app with using_cached flag --- bench/app.py | 43 ++++++++++++++++++++++++------------------- bench/utils/bench.py | 6 +++++- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/bench/app.py b/bench/app.py index 45a9c3d8..8584ffef 100755 --- a/bench/app.py +++ b/bench/app.py @@ -21,10 +21,17 @@ import git # imports - module imports import bench from bench.exceptions import NotInBenchDirectoryError -from bench.utils import (UNSET_ARG, fetch_details_from_tag, - get_available_folder_name, get_bench_cache_path, - is_bench_directory, is_git_url, - is_valid_frappe_branch, log, run_frappe_cmd) +from bench.utils import ( + UNSET_ARG, + fetch_details_from_tag, + get_available_folder_name, + get_bench_cache_path, + is_bench_directory, + is_git_url, + is_valid_frappe_branch, + log, + run_frappe_cmd, +) from bench.utils.bench import build_assets, install_python_dev_dependencies from bench.utils.render import step @@ -225,6 +232,7 @@ class App(AppMeta): resolved=False, restart_bench=True, ignore_resolution=False, + using_cached=False ): import bench.cli from bench.utils.app import get_app_name @@ -245,6 +253,7 @@ class App(AppMeta): skip_assets=skip_assets, restart_bench=restart_bench, resolution=self.local_resolution, + using_cached=using_cached, ) @step(title="Cloning and installing {repo}", success="App {repo} Installed") @@ -332,22 +341,12 @@ class App(AppMeta): if app_path.is_dir(): shutil.rmtree(app_path) - click.secho(f"{self.app_name} being installed from cache", fg="yellow") + click.secho(f"Getting {self.app_name} from cache", fg="yellow") with tarfile.open(cache_path, mode) as tar: tar.extractall(app_path.parent) return True - def install_cached(self) -> None: - """ - TODO: - - check if cache is being set - - check if app is being set from cache - - complete install_cached - - check if app is being installed correctly from cache - """ - raise NotImplementedError("TODO: complete this function") - def set_cache(self, compress_artifacts=False) -> bool: if not self.commit_hash: return False @@ -359,6 +358,11 @@ class App(AppMeta): cwd = os.getcwd() cache_path = self.get_app_cache_path(compress_artifacts) mode = "w:gz" if compress_artifacts else "w" + + message = "Caching get-app artifacts" + if compress_artifacts: + message += " (compressed)" + click.secho(message) os.chdir(app_path.parent) with tarfile.open(cache_path, mode) as tar: @@ -456,7 +460,7 @@ def get_app( bench_setup = False restart_bench = not init_bench frappe_path, frappe_branch = None, None - + if resolve_deps: resolution = make_resolution_plan(app, bench) click.secho("Following apps will be installed", fg="bright_blue") @@ -508,7 +512,7 @@ def get_app( return if app.get_cached(): - app.install_cached() + app.install(verbose=verbose, skip_assets=skip_assets, restart_bench=restart_bench, using_cached=True) return dir_already_exists, cloned_path = check_existing_dir(bench_path, repo_name) @@ -645,6 +649,7 @@ def install_app( restart_bench=True, skip_assets=False, resolution=UNSET_ARG, + using_cached=False, ): import bench.cli as bench_cli from bench.bench import Bench @@ -672,14 +677,14 @@ def install_app( if conf.get("developer_mode"): install_python_dev_dependencies(apps=app, bench_path=bench_path, verbose=verbose) - if os.path.exists(os.path.join(app_path, "package.json")): + if not using_cached and os.path.exists(os.path.join(app_path, "package.json")): yarn_install = "yarn install --verbose" if verbose else "yarn install" bench.run(yarn_install, cwd=app_path) bench.apps.sync(app_name=app, required=resolution, branch=tag, app_dir=app_path) if not skip_assets: - build_assets(bench_path=bench_path, app=app) + build_assets(bench_path=bench_path, app=app, using_cached=using_cached) if restart_bench: # Avoiding exceptions here as production might not be set-up diff --git a/bench/utils/bench.py b/bench/utils/bench.py index 44a1c457..743318f0 100644 --- a/bench/utils/bench.py +++ b/bench/utils/bench.py @@ -349,10 +349,14 @@ def restart_process_manager(bench_path=".", web_workers=False): exec_cmd(f"overmind restart {worker}", cwd=bench_path) -def build_assets(bench_path=".", app=None): +def build_assets(bench_path=".", app=None, using_cached=False): command = "bench build" if app: command += f" --app {app}" + + if using_cached: + command += " --using-cached" + exec_cmd(command, cwd=bench_path, env={"BENCH_DEVELOPER": "1"}) From d177d8ff633fe04a685754d7b8f52a19a8e8c2bf Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 18 Jan 2024 13:04:50 +0530 Subject: [PATCH 04/11] fix: check if installed FF supports use-cached --- bench/utils/bench.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bench/utils/bench.py b/bench/utils/bench.py index 743318f0..7e0b111f 100644 --- a/bench/utils/bench.py +++ b/bench/utils/bench.py @@ -354,11 +354,17 @@ def build_assets(bench_path=".", app=None, using_cached=False): if app: command += f" --app {app}" - if using_cached: + if using_cached and can_use_cached(bench_path): command += " --using-cached" exec_cmd(command, cwd=bench_path, env={"BENCH_DEVELOPER": "1"}) +def can_use_cached(bench_path=".") -> bool: + cmd = ["bench", "can-use-cached"] + return_code = subprocess.call( + cmd, cwd=bench_path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + return not return_code def handle_version_upgrade(version_upgrade, bench_path, force, reset, conf): from bench.utils import log, pause_exec From 683a421e43d04a4ef6ea11f95f2ce562bbafcfca Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 18 Jan 2024 16:23:04 +0530 Subject: [PATCH 05/11] refactor: use env to trigger using-cached flag Click supports pulling args from an envvar if it is present, this would be quicker and cleaner than calling a dummy command to check if the feature is supported --- bench/utils/bench.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bench/utils/bench.py b/bench/utils/bench.py index 7e0b111f..306fa623 100644 --- a/bench/utils/bench.py +++ b/bench/utils/bench.py @@ -354,17 +354,11 @@ def build_assets(bench_path=".", app=None, using_cached=False): if app: command += f" --app {app}" - if using_cached and can_use_cached(bench_path): - command += " --using-cached" + env = {"BENCH_DEVELOPER": "1"} + if using_cached: + env["USING_CACHED"] = "1" - exec_cmd(command, cwd=bench_path, env={"BENCH_DEVELOPER": "1"}) - -def can_use_cached(bench_path=".") -> bool: - cmd = ["bench", "can-use-cached"] - return_code = subprocess.call( - cmd, cwd=bench_path, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - ) - return not return_code + exec_cmd(command, cwd=bench_path, env=env) def handle_version_upgrade(version_upgrade, bench_path, force, reset, conf): from bench.utils import log, pause_exec From 0e2e8b4da3abb74c55643568f6c30ab86a4ec353 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 18 Jan 2024 17:44:25 +0530 Subject: [PATCH 06/11] fix: remove unused nodemodules before caching --- bench/app.py | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/bench/app.py b/bench/app.py index 8584ffef..9321ea00 100755 --- a/bench/app.py +++ b/bench/app.py @@ -335,7 +335,7 @@ class App(AppMeta): # Check if cache exists with gzip if not cache_path.is_file(): - return + return False app_path = self.get_app_path() if app_path.is_dir(): @@ -352,28 +352,64 @@ class App(AppMeta): return False app_path = self.get_app_path() - if not app_path.is_dir() or not is_valid_app_dir(app_path): + if not app_path.is_dir(): return False cwd = os.getcwd() cache_path = self.get_app_cache_path(compress_artifacts) mode = "w:gz" if compress_artifacts else "w" - message = "Caching get-app artifacts" + message = f"Caching ${self.app_name} app directory" if compress_artifacts: message += " (compressed)" click.secho(message) + self.prune_app_directory() os.chdir(app_path.parent) with tarfile.open(cache_path, mode) as tar: tar.add(app_path.name) os.chdir(cwd) return True + def prune_app_directory(self): + app_path = self.get_app_path() + remove_unused_node_modules(app_path) + -def is_valid_app_dir(app_path: Path) -> bool: - # TODO: Check from content if valid frappe app root - return True +def remove_unused_node_modules(app_path: Path) -> None: + """ + Erring a bit the side of caution; since there is no explicit way + to check if node_modules are utilized, this function checks if Vite + is being used to build the frontend code. + + Since most popular Frappe apps use Vite to build their frontends, + this method should suffice. + + Note: root package.json is ignored cause those usually belong to + apps that do not have a build step and so their node_modules are + utilized during runtime. + """ + + for p in app_path.iterdir(): + if not p.is_dir(): + continue + + package_json = p / "package.json" + if not package_json.is_file(): + continue + + node_modules = p / "node_modules" + if not node_modules.is_dir(): + continue + + can_delete = False + with package_json.open("r", encoding="utf-8") as f: + package_json = json.loads(f.read()) + build_script = package_json.get("scripts", {}).get("build", "") + can_delete = "vite build" in build_script + + if can_delete: + shutil.rmtree(node_modules) def make_resolution_plan(app: App, bench: "Bench"): From 30f301e3ffa8a2561160ec740d2d046355905c97 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 18 Jan 2024 18:06:08 +0530 Subject: [PATCH 07/11] fix: wrap tarfile with error handling - ensure return to cwd after tarring --- bench/app.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/bench/app.py b/bench/app.py index 9321ea00..0eebdc2e 100755 --- a/bench/app.py +++ b/bench/app.py @@ -365,11 +365,19 @@ class App(AppMeta): click.secho(message) self.prune_app_directory() + + success = False os.chdir(app_path.parent) - with tarfile.open(cache_path, mode) as tar: - tar.add(app_path.name) - os.chdir(cwd) - return True + try: + with tarfile.open(cache_path, mode) as tar: + tar.add(app_path.name) + success = True + except Exception: + log(f"Failed to cache {app_path}", level=3) + success = False + finally: + os.chdir(cwd) + return success def prune_app_directory(self): app_path = self.get_app_path() From 42ac015bff96b2318d359f24a5ded7a48c0a1a18 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 19 Jan 2024 14:59:28 +0530 Subject: [PATCH 08/11] feat: add bench app-cache helper command --- .gitignore | 3 + bench/commands/__init__.py | 2 + bench/commands/utils.py | 18 ++++++ bench/utils/bench.py | 124 ++++++++++++++++++++++++++++++++++++- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 35826d3c..8d81a51f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # MAC OS .DS_Store +# VS Code +.vscode/ + # Vim Gitignore ## Swap [._]*.s[a-v][a-z] diff --git a/bench/commands/__init__.py b/bench/commands/__init__.py index 5ef14212..1daf6146 100755 --- a/bench/commands/__init__.py +++ b/bench/commands/__init__.py @@ -72,6 +72,7 @@ bench_command.add_command(switch_to_develop) from bench.commands.utils import ( + app_cache_helper, backup_all_sites, bench_src, disable_production, @@ -108,6 +109,7 @@ bench_command.add_command(disable_production) bench_command.add_command(bench_src) bench_command.add_command(find_benches) bench_command.add_command(migrate_env) +bench_command.add_command(app_cache_helper) from bench.commands.setup import setup diff --git a/bench/commands/utils.py b/bench/commands/utils.py index 9882e8f0..5b1e5c1f 100644 --- a/bench/commands/utils.py +++ b/bench/commands/utils.py @@ -176,3 +176,21 @@ def migrate_env(python, backup=True): from bench.utils.bench import migrate_env migrate_env(python=python, backup=backup) + + +@click.command("app-cache", help="View or remove items belonging to bench get-app cache") +@click.option("--clear", is_flag=True, default=False, help="Remove all items") +@click.option( + "--remove-app", + default="", + help="Removes all items that match provided app name", +) +@click.option( + "--remove-hash", + default="", + help="Removes all items that matches provided commit-hash", +) +def app_cache_helper(clear=False, remove_app="", remove_hash=""): + from bench.utils.bench import cache_helper + + cache_helper(clear, remove_app, remove_hash) diff --git a/bench/utils/bench.py b/bench/utils/bench.py index 306fa623..188283b2 100644 --- a/bench/utils/bench.py +++ b/bench/utils/bench.py @@ -4,11 +4,13 @@ import json import logging import os import re +import shutil import subprocess import sys from functools import lru_cache from glob import glob from json.decoder import JSONDecodeError +from pathlib import Path # imports - third party imports import click @@ -16,7 +18,8 @@ import click # imports - module imports import bench from bench.exceptions import PatchError, ValidationError -from bench.utils import exec_cmd, get_bench_name, get_cmd_output, log, which +from bench.utils import (exec_cmd, get_bench_cache_path, get_bench_name, + get_cmd_output, log, which) logger = logging.getLogger(bench.PROJECT_NAME) @@ -353,13 +356,14 @@ def build_assets(bench_path=".", app=None, using_cached=False): command = "bench build" if app: command += f" --app {app}" - + env = {"BENCH_DEVELOPER": "1"} if using_cached: env["USING_CACHED"] = "1" - + exec_cmd(command, cwd=bench_path, env=env) + def handle_version_upgrade(version_upgrade, bench_path, force, reset, conf): from bench.utils import log, pause_exec @@ -638,3 +642,117 @@ To switch to your required branch, run the following commands: bench switch-to-b ) sys.exit(1) + + +def cache_helper(clear=False, remove_app="", remove_hash="") -> None: + can_remove = bool(remove_hash or remove_app) + if not clear and not can_remove: + cache_list() + elif can_remove: + cache_remove(remove_app, remove_hash) + elif clear: + cache_clear() + else: + pass # unreachable + + +def cache_list() -> None: + from datetime import datetime + + tot_size = 0 + tot_items = 0 + + printed_header = False + for item in get_bench_cache_path("apps").iterdir(): + if item.suffix not in [".tar", ".tgz"]: + continue + + stat = item.stat() + size_mb = stat.st_size / 1_000_000 + created = datetime.fromtimestamp(stat.st_ctime) + accessed = datetime.fromtimestamp(stat.st_atime) + + app = item.name.split("-")[0] + tot_items += 1 + tot_size += stat.st_size + compressed = item.suffix == ".tgz" + + if not printed_header: + click.echo( + f"{'APP':15} " + f"{'FILE':25} " + f"{'SIZE':>13} " + f"{'COMPRESSED'} " + f"{'CREATED':19} " + f"{'ACCESSED':19} " + ) + printed_header = True + + click.echo( + f"{app:15} " + f"{item.name:25} " + f"{size_mb:10.3f} MB " + f"{str(compressed):10} " + f"{created:%Y-%m-%d %H:%M:%S} " + f"{accessed:%Y-%m-%d %H:%M:%S} " + ) + if tot_items: + click.echo(f"Total size {tot_size / 1_000_000:.3f} MB belonging to {tot_items} items") + else: + click.echo("No cached items") + + +def cache_remove(app: str = "", hash: str = "") -> None: + rem_items = 0 + rem_size = 0 + for item in get_bench_cache_path("apps").iterdir(): + if not should_remove_item(item, app, hash): + continue + + rem_items += 1 + rem_size += item.stat().st_size + item.unlink(True) + click.echo(f"Removed {item.name}") + + if rem_items: + click.echo(f"Cleared {rem_size / 1_000_000:.3f} MB belonging to {rem_items} items") + else: + click.echo("No items removed") + + +def should_remove_item(item: Path, app: str, hash: str) -> bool: + if item.suffix not in [".tar", ".tgz"]: + return False + + name = item.name + if app and hash and name.startswith(f"{app}-{hash[:10]}."): + return True + + if app and name.startswith(f"{app}-"): + return True + + if hash and f"-{hash[:10]}." in name: + return True + + return False + + +def cache_clear() -> None: + cache_path = get_bench_cache_path("apps") + tot_items = len(os.listdir(cache_path)) + if not tot_items: + click.echo("No cached items") + return + + tot_size = get_dir_size(cache_path) + shutil.rmtree(cache_path) + + rem_items = tot_items - len(os.listdir(cache_path)) + rem_size = tot_size - get_dir_size(cache_path) + + if rem_items: + click.echo(f"Cleared {rem_size / 1_000_000:.3f} MB belonging to {rem_items} items") + + +def get_dir_size(p: Path) -> int: + return sum(i.stat(follow_symlinks=False).st_size for i in p.iterdir()) From c5ec4f752868c3885cd30a4610f8a368fd68019c Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 19 Jan 2024 19:18:13 +0530 Subject: [PATCH 09/11] chore: rename commit-hash to cache-key --- bench/app.py | 18 +++++++++--------- bench/commands/make.py | 20 ++++++++++++++------ bench/commands/utils.py | 8 ++++---- bench/utils/bench.py | 17 +++++++++-------- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/bench/app.py b/bench/app.py index 0eebdc2e..bd69152c 100755 --- a/bench/app.py +++ b/bench/app.py @@ -169,7 +169,7 @@ class App(AppMeta): branch: str = None, bench: "Bench" = None, soft_link: bool = False, - commit_hash = None, + cache_key = None, *args, **kwargs, ): @@ -177,7 +177,7 @@ class App(AppMeta): self.soft_link = soft_link self.required_by = None self.local_resolution = [] - self.commit_hash = commit_hash + self.cache_key = cache_key super().__init__(name, branch, *args, **kwargs) @step(title="Fetching App {repo}", success="App {repo} Fetched") @@ -314,15 +314,15 @@ class App(AppMeta): return Path(self.bench.name) / "apps" / self.app_name def get_app_cache_path(self, is_compressed=False) -> Path: - assert self.commit_hash is not None + assert self.cache_key is not None cache_path = get_bench_cache_path("apps") ext = "tgz" if is_compressed else "tar" - tarfile_name = f"{self.app_name}-{self.commit_hash[:10]}.{ext}" + tarfile_name = f"{self.app_name}-{self.cache_key[:10]}.{ext}" return cache_path / tarfile_name def get_cached(self) -> bool: - if not self.commit_hash: + if not self.cache_key: return False cache_path = self.get_app_cache_path() @@ -348,7 +348,7 @@ class App(AppMeta): return True def set_cache(self, compress_artifacts=False) -> bool: - if not self.commit_hash: + if not self.cache_key: return False app_path = self.get_app_path() @@ -359,7 +359,7 @@ class App(AppMeta): cache_path = self.get_app_cache_path(compress_artifacts) mode = "w:gz" if compress_artifacts else "w" - message = f"Caching ${self.app_name} app directory" + message = f"Caching {self.app_name} app directory" if compress_artifacts: message += " (compressed)" click.secho(message) @@ -481,7 +481,7 @@ def get_app( soft_link=False, init_bench=False, resolve_deps=False, - commit_hash=None, + cache_key=None, compress_artifacts=False, ): """bench get-app clones a Frappe App from remote (GitHub or any other git server), @@ -497,7 +497,7 @@ def get_app( from bench.utils.app import check_existing_dir bench = Bench(bench_path) - app = App(git_url, branch=branch, bench=bench, soft_link=soft_link, commit_hash=commit_hash) + app = App(git_url, branch=branch, bench=bench, soft_link=soft_link, cache_key=cache_key) git_url = app.url repo_name = app.repo branch = app.tag diff --git a/bench/commands/make.py b/bench/commands/make.py index 2c52ad04..846e7b49 100755 --- a/bench/commands/make.py +++ b/bench/commands/make.py @@ -151,9 +151,18 @@ def drop(path): default=False, help="Resolve dependencies before installing app", ) -@click.option("--commit-hash", default=None, help="Required for caching get-app artifacts.") -@click.option("--cache-artifacts", is_flag=True, default=False, help="Whether to cache get-app artifacts. Needs commit-hash.") -@click.option("--compress-artifacts", is_flag=True, default=False, help="Whether to gzip get-app artifacts that are to be cached.") +@click.option( + "--cache-key", + type=str, + default=None, + help="Caches get-app artifacts if provided (only first 10 chars is used)", +) +@click.option( + "--compress-artifacts", + is_flag=True, + default=False, + help="Whether to gzip get-app artifacts that are to be cached", +) def get_app( git_url, branch, @@ -163,8 +172,7 @@ def get_app( soft_link=False, init_bench=False, resolve_deps=False, - commit_hash=None, - cache_artifacts=False, + cache_key=None, compress_artifacts=False, ): "clone an app from the internet and set it up in your bench" @@ -178,7 +186,7 @@ def get_app( soft_link=soft_link, init_bench=init_bench, resolve_deps=resolve_deps, - commit_hash=commit_hash if cache_artifacts else None, + cache_key=cache_key, compress_artifacts=compress_artifacts, ) diff --git a/bench/commands/utils.py b/bench/commands/utils.py index 5b1e5c1f..0a7d97c5 100644 --- a/bench/commands/utils.py +++ b/bench/commands/utils.py @@ -186,11 +186,11 @@ def migrate_env(python, backup=True): help="Removes all items that match provided app name", ) @click.option( - "--remove-hash", + "--remove-key", default="", - help="Removes all items that matches provided commit-hash", + help="Removes all items that matches provided cache key", ) -def app_cache_helper(clear=False, remove_app="", remove_hash=""): +def app_cache_helper(clear=False, remove_app="", remove_key=""): from bench.utils.bench import cache_helper - cache_helper(clear, remove_app, remove_hash) + cache_helper(clear, remove_app, remove_key) diff --git a/bench/utils/bench.py b/bench/utils/bench.py index 188283b2..73462316 100644 --- a/bench/utils/bench.py +++ b/bench/utils/bench.py @@ -644,12 +644,12 @@ To switch to your required branch, run the following commands: bench switch-to-b sys.exit(1) -def cache_helper(clear=False, remove_app="", remove_hash="") -> None: - can_remove = bool(remove_hash or remove_app) +def cache_helper(clear=False, remove_app="", remove_key="") -> None: + can_remove = bool(remove_key or remove_app) if not clear and not can_remove: cache_list() elif can_remove: - cache_remove(remove_app, remove_hash) + cache_remove(remove_app, remove_key) elif clear: cache_clear() else: @@ -696,17 +696,18 @@ def cache_list() -> None: f"{created:%Y-%m-%d %H:%M:%S} " f"{accessed:%Y-%m-%d %H:%M:%S} " ) + if tot_items: click.echo(f"Total size {tot_size / 1_000_000:.3f} MB belonging to {tot_items} items") else: click.echo("No cached items") -def cache_remove(app: str = "", hash: str = "") -> None: +def cache_remove(app: str = "", key: str = "") -> None: rem_items = 0 rem_size = 0 for item in get_bench_cache_path("apps").iterdir(): - if not should_remove_item(item, app, hash): + if not should_remove_item(item, app, key): continue rem_items += 1 @@ -720,18 +721,18 @@ def cache_remove(app: str = "", hash: str = "") -> None: click.echo("No items removed") -def should_remove_item(item: Path, app: str, hash: str) -> bool: +def should_remove_item(item: Path, app: str, key: str) -> bool: if item.suffix not in [".tar", ".tgz"]: return False name = item.name - if app and hash and name.startswith(f"{app}-{hash[:10]}."): + if app and key and name.startswith(f"{app}-{key[:10]}."): return True if app and name.startswith(f"{app}-"): return True - if hash and f"-{hash[:10]}." in name: + if key and f"-{key[:10]}." in name: return True return False From 4e170a2042c114e46b978c83413e2267194d957b Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 19 Jan 2024 19:28:57 +0530 Subject: [PATCH 10/11] fix: remove rem check from app-cache --clear --- bench/utils/bench.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bench/utils/bench.py b/bench/utils/bench.py index 73462316..9f1daded 100644 --- a/bench/utils/bench.py +++ b/bench/utils/bench.py @@ -748,11 +748,8 @@ def cache_clear() -> None: tot_size = get_dir_size(cache_path) shutil.rmtree(cache_path) - rem_items = tot_items - len(os.listdir(cache_path)) - rem_size = tot_size - get_dir_size(cache_path) - - if rem_items: - click.echo(f"Cleared {rem_size / 1_000_000:.3f} MB belonging to {rem_items} items") + if tot_items: + click.echo(f"Cleared {tot_size / 1_000_000:.3f} MB belonging to {tot_items} items") def get_dir_size(p: Path) -> int: From a3d0c2cf9c925d28c0ac06661adab8f39df204fe Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 23 Jan 2024 13:40:21 +0530 Subject: [PATCH 11/11] chore: bump Jinja appease CI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d1011ff1..af7d0eaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "Click>=7.0", "GitPython~=3.1.30", "honcho", - "Jinja2~=3.0.3", + "Jinja2~=3.1.3", "python-crontab~=2.6.0", "requests", "semantic-version~=2.8.2",