From d1c7e3d83bc6d4fd053114022578d3e869ea906a Mon Sep 17 00:00:00 2001 From: Martin Bernstorff Date: Sat, 23 Dec 2023 13:07:17 +0000 Subject: [PATCH 1/4] feat: use context manager to temp deck file deletion. Fixes #423 From f2bc39b66be8104d83f5735206c7c64eac10392e Mon Sep 17 00:00:00 2001 From: Martin Bernstorff Date: Sat, 23 Dec 2023 13:11:59 +0000 Subject: [PATCH 2/4] implement --- .../data_access/ankiconnect_gateway.py | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/personal_mnemonic_medium/data_access/ankiconnect_gateway.py b/personal_mnemonic_medium/data_access/ankiconnect_gateway.py index cf58a945..85a2dc1a 100644 --- a/personal_mnemonic_medium/data_access/ankiconnect_gateway.py +++ b/personal_mnemonic_medium/data_access/ankiconnect_gateway.py @@ -4,7 +4,7 @@ import logging import traceback import urllib.request -from collections.abc import Mapping, Sequence +from collections.abc import Iterator, Mapping, Sequence from dataclasses import dataclass from enum import Enum from pathlib import Path @@ -18,6 +18,22 @@ log = logging.getLogger(__name__) +import shutil +from contextlib import contextmanager + + +@contextmanager +def tempdir(tmp_path: Path) -> Iterator[Path]: + """Context manager for a temporary directory that is deleted after use.""" + try: + yield tmp_path + except: + # If there's an error, ensure the directory is deleted before the error is propagated. + shutil.rmtree(str(tmp_path)) + raise + else: + shutil.rmtree(str(tmp_path)) + class AnkiField(pydantic.BaseModel): value: str @@ -83,20 +99,22 @@ def update_model(self, model: genanki.Model) -> None: ) def import_package(self, package: genanki.Package) -> None: - apkg_name = ( - f"{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.apkg" - ) - write_path = self.tmp_write_dir / apkg_name - package.write_to_file(write_path) # type: ignore - - read_path = self.tmp_read_dir / apkg_name - try: - self._invoke(AnkiConnectCommand.IMPORT_PACKAGE, path=str(read_path)) - log.info(f"Imported from {read_path}!") - write_path.unlink() - except Exception: - log.error(f"""Unable to sync from {read_path}.""") - traceback.print_exc() + with tempdir(self.tmp_write_dir) as tmp_write_dir: + apkg_name = ( + f"{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.apkg" + ) + write_path = tmp_write_dir / apkg_name + package.write_to_file(write_path) # type: ignore + + read_path = self.tmp_read_dir / apkg_name + try: + self._invoke( + AnkiConnectCommand.IMPORT_PACKAGE, path=str(read_path) + ) + log.info(f"Imported from {read_path}!") + except Exception as e: + log.error(f"""Unable to sync from {read_path}, {e}""") + traceback.print_exc() def delete_notes(self, note_ids: Sequence[int]) -> None: if len(note_ids) > self.max_deletions_per_run: From 9dc5beffc0a3501e5f6006da925ed0a577a1f278 Mon Sep 17 00:00:00 2001 From: Martin Bernstorff Date: Sat, 23 Dec 2023 13:16:47 +0000 Subject: [PATCH 3/4] fix: do not delete entire dir --- personal_mnemonic_medium/data_access/ankiconnect_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/personal_mnemonic_medium/data_access/ankiconnect_gateway.py b/personal_mnemonic_medium/data_access/ankiconnect_gateway.py index 85a2dc1a..59fe2779 100644 --- a/personal_mnemonic_medium/data_access/ankiconnect_gateway.py +++ b/personal_mnemonic_medium/data_access/ankiconnect_gateway.py @@ -99,7 +99,7 @@ def update_model(self, model: genanki.Model) -> None: ) def import_package(self, package: genanki.Package) -> None: - with tempdir(self.tmp_write_dir) as tmp_write_dir: + with tempdir(self.tmp_write_dir / "tmp_apkg_dir") as tmp_write_dir: apkg_name = ( f"{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.apkg" ) From 0978523972239e54064be61c0c6e15fea9690bfa Mon Sep 17 00:00:00 2001 From: Martin Bernstorff Date: Sat, 23 Dec 2023 13:22:06 +0000 Subject: [PATCH 4/4] fix: create a subdir, so it can never delete an existing dir --- .../data_access/ankiconnect_gateway.py | 9 ++++---- .../data_access/test_ankiconnect_gateway.py | 21 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/personal_mnemonic_medium/data_access/ankiconnect_gateway.py b/personal_mnemonic_medium/data_access/ankiconnect_gateway.py index 59fe2779..147e41a0 100644 --- a/personal_mnemonic_medium/data_access/ankiconnect_gateway.py +++ b/personal_mnemonic_medium/data_access/ankiconnect_gateway.py @@ -26,6 +26,7 @@ def tempdir(tmp_path: Path) -> Iterator[Path]: """Context manager for a temporary directory that is deleted after use.""" try: + tmp_path.mkdir(parents=True, exist_ok=True) yield tmp_path except: # If there's an error, ensure the directory is deleted before the error is propagated. @@ -99,14 +100,14 @@ def update_model(self, model: genanki.Model) -> None: ) def import_package(self, package: genanki.Package) -> None: - with tempdir(self.tmp_write_dir / "tmp_apkg_dir") as tmp_write_dir: + subdir = "tmp_apkg_dir" + with tempdir(self.tmp_write_dir / subdir) as tmp_write_subdir: apkg_name = ( f"{datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.apkg" ) - write_path = tmp_write_dir / apkg_name - package.write_to_file(write_path) # type: ignore + package.write_to_file(tmp_write_subdir / apkg_name) # type: ignore - read_path = self.tmp_read_dir / apkg_name + read_path = self.tmp_read_dir / subdir / apkg_name try: self._invoke( AnkiConnectCommand.IMPORT_PACKAGE, path=str(read_path) diff --git a/personal_mnemonic_medium/data_access/test_ankiconnect_gateway.py b/personal_mnemonic_medium/data_access/test_ankiconnect_gateway.py index 5947a052..95dfd15f 100644 --- a/personal_mnemonic_medium/data_access/test_ankiconnect_gateway.py +++ b/personal_mnemonic_medium/data_access/test_ankiconnect_gateway.py @@ -32,15 +32,6 @@ class TestAnkiConnectGateway: output_path = Path("/output") def test_import_get_delete_happy_path(self): - gateway = AnkiConnectGateway( - ankiconnect_url=ANKICONNECT_URL, - base_deck="Test deck", - tmp_read_dir=(get_host_home_dir() / "ankidecks"), - tmp_write_dir=self.output_path, - max_deletions_per_run=1, - max_wait_seconds=0, - ) - # Delete all .apkg in the output directory for f in self.output_path.glob("*.apkg"): f.unlink() @@ -68,10 +59,20 @@ def test_import_get_delete_happy_path(self): ) ) - package = genanki.Package(deck_or_decks=deck) + tmp_read_dir = get_host_home_dir() / "ankidecks" + gateway = AnkiConnectGateway( + ankiconnect_url=ANKICONNECT_URL, + base_deck="Test deck", + tmp_read_dir=tmp_read_dir, + tmp_write_dir=self.output_path, + max_deletions_per_run=1, + max_wait_seconds=0, + ) # Phase 1: Importing + package = genanki.Package(deck_or_decks=deck) gateway.import_package(package=package) + assert self.output_path.is_dir() assert len(list(Path("/output").glob("*.apkg"))) == 0 # Phase 2: Getting