From cc6549505c75c1320039b6c22be587b79a3a5289 Mon Sep 17 00:00:00 2001 From: Philipp Stadler <hello@phstadler.com> Date: Sun, 21 Jul 2024 12:37:43 +0200 Subject: [PATCH 1/3] feat!: add special CSV deck field, comment support and skip fields by mapping to the empty string BREAKING CHANGE: `deck` in CSV is now not a field but a special value for the per-note deck pattern. Additionally, lines starting with hash in CSV are now ignored for better Anki compatibility. --- export_apkgs.py | 53 ++++++++++++------- test/fixtures/anki_text/.apkg-spec.yaml | 23 ++++++++ test/fixtures/anki_text/Tapirs.txt | 6 +++ test/fixtures/csv/.apkg-spec.yaml | 1 + .../csv_updated_content/.apkg-spec.yaml | 1 + test/fixtures/fields_static/.apkg-spec.yaml | 1 + test/test_export_apkgs.py | 43 +++++++++++++++ version.txt | 2 +- 8 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/anki_text/.apkg-spec.yaml create mode 100644 test/fixtures/anki_text/Tapirs.txt diff --git a/export_apkgs.py b/export_apkgs.py index eaedd2e..e0d9b7b 100755 --- a/export_apkgs.py +++ b/export_apkgs.py @@ -42,6 +42,7 @@ class ImportApkg(BaseModel): class ImportCsv(BaseModel): content_version: datetime + delimiter: str note_type: str file_patterns: list[str]|None deck_name_pattern: str @@ -109,6 +110,12 @@ class ApkgExporter: added_note_type_ids: list[int] added_card_types: set[str] resource_pseudo_usages: str + """ + Note GUID against deck. + + Set from the deck field in CSV. + """ + note_deck_patterns: dict[str, str] def __init__(self, spec: ApkgSpec, content_dir: Path, intermediate_dir: Path, args: argparse.Namespace) -> None: self.spec = spec @@ -121,6 +128,7 @@ class ApkgExporter: self.added_note_type_ids = [] self.added_card_types = set() self.resource_pseudo_usages = '' + self.note_deck_patterns = {} def export(self) -> Path: @@ -319,7 +327,6 @@ class ApkgExporter: if 'import_csv' in content: import_csv : ImportCsv = content['import_csv'] deck_name = import_csv.deck_name_pattern - fields_mapping = import_csv.fields_mapping ntid = name_to_ntid[import_csv.note_type] with fake_time(format_time(import_csv.content_version)): for pattern in import_csv.file_patterns or ['**/*.csv']: @@ -330,8 +337,7 @@ class ApkgExporter: tmp_deck, import_csv.tags, csv_path, - fields_mapping, - import_csv.fields_static) + import_csv) self.move_to_target_decks(col, tmp_deck, deck_name, csv_path) elif 'import_apkg' in content: import_apkg : ImportApkg = content['import_apkg'] @@ -377,23 +383,30 @@ class ApkgExporter: tmp_deck: int, tags: list[str], csv_path: Path, - fields_mapping: list[str], - fields_static: dict[str, str]): + opts: ImportCsv): with open(csv_path) as csv_file: - reader = csv.reader(csv_file, delimiter=';') + delimiter = opts.delimiter or "\t" + reader = csv.reader(csv_file, delimiter=delimiter) for row in reader: - note = Note(col, note_type_id) - note = self.new_note(col, row[fields_mapping.index('guid')], note_type_id) - for idx, field in enumerate(fields_mapping): - if idx >= len(row): - raise Exception(f'CSV row is missing {field} at index {idx}:\n{row}') - if field != 'guid': - note[field] = row[idx] - for field, value in fields_static.items(): - note[field] = value - for tag in tags: - note.add_tag(tag) - col.add_note(note, tmp_deck) + # hash lines are comments + if len(row) >= 1 and not row[0].startswith('#'): + note = Note(col, note_type_id) + guid = row[opts.fields_mapping.index('guid')] + note = self.new_note(col, guid, note_type_id) + for idx, field in enumerate(opts.fields_mapping): + if field != '': + if idx >= len(row): + raise Exception(f'CSV row is missing {field} at index {idx}:\n{row}') + # deck overrides the pattern per note + if field == 'deck': + self.note_deck_patterns[guid] = row[idx] + elif field != 'guid': + note[field] = row[idx] + for field, value in opts.fields_static.items(): + note[field] = value + for tag in tags: + note.add_tag(tag) + col.add_note(note, tmp_deck) def field_columns(self, col: Collection, note_type_id: int, mapping: list[str]) -> list[int]: """ @@ -462,7 +475,9 @@ class ApkgExporter: for card_id in col.find_cards('deck:"' + TEMP_DECK_NAME + '"'): card = col.get_card(card_id) card_type_name = card.template()["name"] - desired_deck_name = self.format_deck_name(deck_name_pattern, csv_path, card_type_name) + guid = card.note().guid + pattern = self.note_deck_patterns[guid] if guid in self.note_deck_patterns else deck_name_pattern + desired_deck_name = self.format_deck_name(pattern, csv_path, card_type_name) card.did = self.get_or_create_deck_id(col, desired_deck_name) col.update_card(card) diff --git a/test/fixtures/anki_text/.apkg-spec.yaml b/test/fixtures/anki_text/.apkg-spec.yaml new file mode 100644 index 0000000..ead10a8 --- /dev/null +++ b/test/fixtures/anki_text/.apkg-spec.yaml @@ -0,0 +1,23 @@ +content_version: 1.0.0 + +templates: +- q_a + +content: +- import_csv: + content_version: 2024-01-19 19:00:00+00:00 + note_type: Q/A Testnotetype + file_patterns: + - '*.txt' + delimiter: "\t" + # Not needed because the CSV contains the deck name + deck_name_pattern: 'UNUSED' + fields_mapping: + - guid + - deck + - Question + - Answer + fields_static: {} + tags: [] + +resource_paths: [] diff --git a/test/fixtures/anki_text/Tapirs.txt b/test/fixtures/anki_text/Tapirs.txt new file mode 100644 index 0000000..7b13e86 --- /dev/null +++ b/test/fixtures/anki_text/Tapirs.txt @@ -0,0 +1,6 @@ +#separator:tab +#html:true +#guid column:1 +#deck column:1 +#tags column:4 +x,/Dh9c@Q^ Tapirs::Species Which tapir has a prominent mane? Tapirus terrestris diff --git a/test/fixtures/csv/.apkg-spec.yaml b/test/fixtures/csv/.apkg-spec.yaml index 0c13501..5c2d695 100644 --- a/test/fixtures/csv/.apkg-spec.yaml +++ b/test/fixtures/csv/.apkg-spec.yaml @@ -9,6 +9,7 @@ content: note_type: Q/A Testnotetype file_patterns: - '*.csv' + delimiter: ';' # and to making one deck per card type deck_name_pattern: '{{card_type}}' fields_mapping: diff --git a/test/fixtures/csv_updated_content/.apkg-spec.yaml b/test/fixtures/csv_updated_content/.apkg-spec.yaml index 18e0338..1e15dd2 100644 --- a/test/fixtures/csv_updated_content/.apkg-spec.yaml +++ b/test/fixtures/csv_updated_content/.apkg-spec.yaml @@ -9,6 +9,7 @@ content: note_type: Q/A Testnotetype file_patterns: - '*.csv' + delimiter: ';' # and to making one deck per card type deck_name_pattern: '{{card_type}}' fields_mapping: diff --git a/test/fixtures/fields_static/.apkg-spec.yaml b/test/fixtures/fields_static/.apkg-spec.yaml index 455ae6e..b4b47f8 100644 --- a/test/fixtures/fields_static/.apkg-spec.yaml +++ b/test/fixtures/fields_static/.apkg-spec.yaml @@ -9,6 +9,7 @@ content: note_type: Q/A Testnotetype file_patterns: - '*.csv' + delimiter: ';' # and to making one deck per card type deck_name_pattern: '{{card_type}}' fields_mapping: diff --git a/test/test_export_apkgs.py b/test/test_export_apkgs.py index 7ff7af3..aeb5a36 100644 --- a/test/test_export_apkgs.py +++ b/test/test_export_apkgs.py @@ -17,6 +17,7 @@ CONTENT_PATH_ANKI = 'test/fixtures/anki' CONTENT_PATH_CSV = 'test/fixtures/csv' CONTENT_PATH_CSV_UPDATED_CONTENT = 'test/fixtures/csv_updated_content' CONTENT_PATH_FIELDS_STATIC = 'test/fixtures/fields_static' +CONTENT_PATH_ANKI_TEXT = 'test/fixtures/anki_text' TEMPLATES_PATH = 'test/fixtures/templates' TEMPLATES_PATH_UPDATED = 'test/fixtures/templates_updated' @@ -362,6 +363,48 @@ class TestExportApkgs(unittest.TestCase): ) + def test_anki_text(self): + """ + Anki text export with deck name + """ + with TemporaryDirectory() as temp_collection_dir: + col = Collection(str(Path(temp_collection_dir) / "test.anki2")) + with TemporaryDirectory() as first_export_dir: + args_first = MockArgs( + content=CONTENT_PATH_ANKI_TEXT, + templates_dir=TEMPLATES_PATH, + output_dir=first_export_dir, + dry_run=False) + package = export_package_from_spec( + Path(CONTENT_PATH_ANKI_TEXT) / '.apkg-spec.yaml', + args_first) + + # import the first time, everything is new + result1 = col.import_anki_package(ImportAnkiPackageRequest( + package_path=str(package), + options=ImportAnkiPackageOptions( + merge_notetypes=True, + update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER, + update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER, + with_scheduling=False, + with_deck_configs=False, + ) + )) + self.assertEqual(len(result1.log.new), 1) + self.assertEqual( + result1.log.new[0].fields, + # the empty one is resources + [ + 'Which tapir has a prominent mane?', + 'Tapirus terrestris', + '' + ] + ) + self.assertEqual( + len(col.find_cards('deck:"Tapirs::Species"')), + 1) + + def find_template(col: Collection, name: str): template = None for name_and_id in col.models.all_names_and_ids(): diff --git a/version.txt b/version.txt index 4a36342..fcdb2e1 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.0.0 +4.0.0 -- GitLab From 7197d2b1a6039e19af89038dc1c2b287dc061bb8 Mon Sep 17 00:00:00 2001 From: Philipp Stadler <hello@phstadler.com> Date: Sun, 21 Jul 2024 16:02:10 +0200 Subject: [PATCH 2/3] refactor: split export_apkgs.py into a module --- Dockerfile | 4 +- export_apkgs/__init__.py | 17 +++ export_apkgs/__main__.py | 21 +++ export_apkgs/config.py | 16 +++ export_apkgs/error.py | 2 + export_apkgs.py => export_apkgs/exporter.py | 120 ++---------------- export_apkgs/format.py | 5 + export_apkgs/model.py | 59 +++++++++ .../fixtures/resource_missing/.apkg-spec.yaml | 23 ++++ test/fixtures/resource_missing/content.csv | 1 + test/test_export_apkgs.py | 18 ++- 11 files changed, 172 insertions(+), 114 deletions(-) create mode 100644 export_apkgs/__init__.py create mode 100644 export_apkgs/__main__.py create mode 100644 export_apkgs/config.py create mode 100644 export_apkgs/error.py rename export_apkgs.py => export_apkgs/exporter.py (84%) mode change 100755 => 100644 create mode 100644 export_apkgs/format.py create mode 100644 export_apkgs/model.py create mode 100644 test/fixtures/resource_missing/.apkg-spec.yaml create mode 100644 test/fixtures/resource_missing/content.csv diff --git a/Dockerfile b/Dockerfile index f898dbc..f0e3eae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,5 +17,5 @@ to-static-jsonp COPY Pipfile Pipfile.lock /app/ RUN cd /app && pipenv install --system --deploy -COPY export_apkgs.py /app/export_apkgs.py -RUN echo "#!/bin/sh\n$(python-libfaketime)\n/app/export_apkgs.py \$@" > /bin/export_apkgs && chmod +x /bin/export_apkgs +COPY ./export_apkgs /app/export_apkgs +RUN echo "#!/bin/sh\n$(python-libfaketime)\npython3 -m /app/export_apkgs \$@" > /bin/export_apkgs && chmod +x /bin/export_apkgs diff --git a/export_apkgs/__init__.py b/export_apkgs/__init__.py new file mode 100644 index 0000000..8b04926 --- /dev/null +++ b/export_apkgs/__init__.py @@ -0,0 +1,17 @@ +from .config import load_apkg_spec +from .exporter import ApkgExporter +from pathlib import Path +from tempfile import TemporaryDirectory +import argparse + +def export_package_from_spec(spec_path: Path, args: argparse.Namespace) -> Path: + with TemporaryDirectory() as intermediate_dir: + package_directory = spec_path.parent + spec = load_apkg_spec(spec_path) + exporter = ApkgExporter( + spec=spec, + content_dir=package_directory, + intermediate_dir=Path(intermediate_dir), + args=args + ) + return exporter.export() diff --git a/export_apkgs/__main__.py b/export_apkgs/__main__.py new file mode 100644 index 0000000..51163e6 --- /dev/null +++ b/export_apkgs/__main__.py @@ -0,0 +1,21 @@ +from . import export_package_from_spec +from .config import APKG_SPEC_FILENAME +from pathlib import Path +import argparse + +parser = argparse.ArgumentParser( + prog="export_apkgs", + description="Generates apkgs from a spec in a specified directory") +parser.add_argument('-c', '--content', default='content', help="path to YAML or directory tree that contains .apkg-spec.yaml, each spec generates an APKG") +parser.add_argument('-t', '--templates-dir', required=True) +parser.add_argument('-o', '--output-dir', help="directory to put the output apkgs in") +parser.add_argument('-n', '--dry-run', action='store_true', required=False, help="Don't actually build Anki files, but print the files that would be generated") +args = parser.parse_args() +content = Path(args.content) +if content.suffix == '.yaml' or content.suffix == '.yml': + # directly specified a YAML as argument + export_package_from_spec(content, args) +else: + # got a directory or directory tree to find specs in + for apkg_spec_path in Path(args.content).glob(f'**/{APKG_SPEC_FILENAME}'): + export_package_from_spec(apkg_spec_path, args) diff --git a/export_apkgs/config.py b/export_apkgs/config.py new file mode 100644 index 0000000..c0e5a3f --- /dev/null +++ b/export_apkgs/config.py @@ -0,0 +1,16 @@ +from .model import ApkgSpec, TemplateSpec +from pathlib import Path +from yaml import load, CLoader + +APKG_SPEC_FILENAME = '.apkg-spec.yaml' +TEMPLATE_SPEC_FILENAME = '.template-spec.yaml' + +def load_template_spec(templates_dir: Path, template: str) -> TemplateSpec: + with open(templates_dir / template / TEMPLATE_SPEC_FILENAME) as spec_yaml: + spec_yaml = load(spec_yaml, Loader=CLoader) + return TemplateSpec(**spec_yaml) + +def load_apkg_spec(spec_path: Path) -> ApkgSpec: + with open(spec_path) as spec_yaml: + spec_yaml = load(spec_yaml, Loader=CLoader) + return ApkgSpec(**spec_yaml) diff --git a/export_apkgs/error.py b/export_apkgs/error.py new file mode 100644 index 0000000..a0b1667 --- /dev/null +++ b/export_apkgs/error.py @@ -0,0 +1,2 @@ +class MissingResourceException(Exception): + pass diff --git a/export_apkgs.py b/export_apkgs/exporter.py old mode 100755 new mode 100644 similarity index 84% rename from export_apkgs.py rename to export_apkgs/exporter.py index e0d9b7b..2358305 --- a/export_apkgs.py +++ b/export_apkgs/exporter.py @@ -1,103 +1,22 @@ -#!/usr/bin/env python3 +from .config import load_template_spec +from .error import MissingResourceException +from .format import format_time +from .model import * from anki.collection import Collection, ExportAnkiPackageOptions, ImportAnkiPackageOptions, ImportAnkiPackageRequest from anki.import_export_pb2 import ImportAnkiPackageUpdateCondition from anki.models import ChangeNotetypeRequest, NotetypeDict from anki.notes import Note -from datetime import datetime, timezone +from datetime import datetime from functools import reduce from itertools import chain from libfaketime import fake_time -from pathlib import Path -from pydantic import BaseModel, field_serializer, field_validator -from semver import Version -from tempfile import TemporaryDirectory -from typing import Literal -from yaml import load, CLoader from os import getenv +from pathlib import Path import argparse import csv -APKG_SPEC_FILENAME = '.apkg-spec.yaml' -TEMPLATE_SPEC_FILENAME = '.template-spec.yaml' - -parser = argparse.ArgumentParser( - prog="export_apkgs.py", - description="Generates apkgs from a spec in a specified directory") -parser.add_argument('-c', '--content', default='content', help="path to YAML or directory tree that contains .apkg-spec.yaml, each spec generates an APKG") -parser.add_argument('-t', '--templates-dir', required=True) -parser.add_argument('-o', '--output-dir', help="directory to put the output apkgs in") -parser.add_argument('-n', '--dry-run', action='store_true', required=False, help="Don't actually build Anki files, but print the files that would be generated") - -class NoteType(BaseModel): - id: datetime - name: str - fields: list[str] - -class CardType(BaseModel): - name: str - template: Path - -class ImportApkg(BaseModel): - note_type: str - -class ImportCsv(BaseModel): - content_version: datetime - delimiter: str - note_type: str - file_patterns: list[str]|None - deck_name_pattern: str - fields_mapping: list[str] - fields_static: dict[str, str] - tags: list[str] - -class ImportImages(BaseModel): - content_version: datetime - note_type: str - deck_name_pattern: str - fields_mapping: dict[str, str] - -class TemplateSpec(BaseModel): - template_version: datetime - note_type: NoteType - card_types: list[CardType] - resource_paths: list[Path] - -class ApkgSpec(BaseModel): - # semver of the content version, appended to the filename after a dash - content_version: str - templates: list[str] - content: list[ - dict[Literal['import_apkg'], ImportApkg]| - dict[Literal['import_csv'], ImportCsv]| - dict[Literal['import_images'], ImportImages] - ] - resource_paths: list[Path] - - @field_serializer('content_version') - def dump_content_version(self, ver: Version): - return str(ver) - - @field_validator('content_version') - def validate_content_version(cls, version): - Version.parse(version) - return version - - -def load_apkg_spec(spec_path: Path) -> ApkgSpec: - with open(spec_path) as spec_yaml: - spec_yaml = load(spec_yaml, Loader=CLoader) - return ApkgSpec(**spec_yaml) - -def load_template_spec(templates_dir: Path, template: str) -> TemplateSpec: - with open(templates_dir / template / TEMPLATE_SPEC_FILENAME) as spec_yaml: - spec_yaml = load(spec_yaml, Loader=CLoader) - return TemplateSpec(**spec_yaml) - -def format_time(time: datetime): - """ Formats the given time in a format suitable for faketime """ - return time.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M:%S') - TEMP_DECK_NAME = 'just_imported' + class ApkgExporter: spec: ApkgSpec content_dir: Path @@ -213,6 +132,8 @@ class ApkgExporter: self.add_resource(col, resources_path) elif resources_path.is_dir(): self.add_resources(col, resources_path.glob('*')) + else: + raise MissingResourceException(f'resource {resources_path} not found or is not a file or directory') def add_resource(self, col: Collection, resource_path: Path): actual_path = col.media.add_file(resource_path) @@ -532,26 +453,3 @@ class ApkgExporter: for key, value in replacements.items(): pattern = pattern.replace('{{' + key + '}}', value) return pattern - -def export_package_from_spec(spec_path: Path, args: argparse.Namespace) -> Path: - with TemporaryDirectory() as intermediate_dir: - package_directory = spec_path.parent - spec = load_apkg_spec(spec_path) - exporter = ApkgExporter( - spec=spec, - content_dir=package_directory, - intermediate_dir=Path(intermediate_dir), - args=args - ) - return exporter.export() - -if __name__ == "__main__": - args = parser.parse_args() - content = Path(args.content) - if content.suffix == '.yaml' or content.suffix == '.yml': - # directly specified a YAML as argument - export_package_from_spec(content, args) - else: - # got a directory or directory tree to find specs in - for apkg_spec_path in Path(args.content).glob(f'**/{APKG_SPEC_FILENAME}'): - export_package_from_spec(apkg_spec_path, args) diff --git a/export_apkgs/format.py b/export_apkgs/format.py new file mode 100644 index 0000000..b9c4f14 --- /dev/null +++ b/export_apkgs/format.py @@ -0,0 +1,5 @@ +from datetime import datetime, timezone + +def format_time(time: datetime): + """ Formats the given time in a format suitable for faketime """ + return time.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M:%S') diff --git a/export_apkgs/model.py b/export_apkgs/model.py new file mode 100644 index 0000000..3b362ea --- /dev/null +++ b/export_apkgs/model.py @@ -0,0 +1,59 @@ +from datetime import datetime +from pathlib import Path +from pydantic import BaseModel, field_serializer, field_validator +from semver import Version +from typing import Literal + +class NoteType(BaseModel): + id: datetime + name: str + fields: list[str] + +class CardType(BaseModel): + name: str + template: Path + +class ImportApkg(BaseModel): + note_type: str + +class ImportCsv(BaseModel): + content_version: datetime + delimiter: str + note_type: str + file_patterns: list[str]|None + deck_name_pattern: str + fields_mapping: list[str] + fields_static: dict[str, str] + tags: list[str] + +class ImportImages(BaseModel): + content_version: datetime + note_type: str + deck_name_pattern: str + fields_mapping: dict[str, str] + +class TemplateSpec(BaseModel): + template_version: datetime + note_type: NoteType + card_types: list[CardType] + resource_paths: list[Path] + +class ApkgSpec(BaseModel): + # semver of the content version, appended to the filename after a dash + content_version: str + templates: list[str] + content: list[ + dict[Literal['import_apkg'], ImportApkg]| + dict[Literal['import_csv'], ImportCsv]| + dict[Literal['import_images'], ImportImages] + ] + resource_paths: list[Path] + + @field_serializer('content_version') + def dump_content_version(self, ver: Version): + return str(ver) + + @field_validator('content_version') + def validate_content_version(cls, version): + Version.parse(version) + return version diff --git a/test/fixtures/resource_missing/.apkg-spec.yaml b/test/fixtures/resource_missing/.apkg-spec.yaml new file mode 100644 index 0000000..de7f1f4 --- /dev/null +++ b/test/fixtures/resource_missing/.apkg-spec.yaml @@ -0,0 +1,23 @@ +content_version: 1.0.0 + +templates: +- q_a + +content: +- import_csv: + content_version: 2024-01-19 19:00:00+00:00 + note_type: Q/A Testnotetype + file_patterns: + - '*.csv' + delimiter: ';' + # and to making one deck per card type + deck_name_pattern: '{{card_type}}' + fields_mapping: + - guid + - Question + - Answer + fields_static: {} + tags: [] + +resource_paths: +- missing.txt diff --git a/test/fixtures/resource_missing/content.csv b/test/fixtures/resource_missing/content.csv new file mode 100644 index 0000000..3c98112 --- /dev/null +++ b/test/fixtures/resource_missing/content.csv @@ -0,0 +1 @@ +test-csv-note-1;What is the scientific name of the only tapir living in Asia?;Tapirus indicus \ No newline at end of file diff --git a/test/test_export_apkgs.py b/test/test_export_apkgs.py index aeb5a36..65ceaf9 100644 --- a/test/test_export_apkgs.py +++ b/test/test_export_apkgs.py @@ -1,7 +1,8 @@ from anki.collection import Collection, ImportAnkiPackageRequest, ImportAnkiPackageOptions from anki.import_export_pb2 import ImportAnkiPackageUpdateCondition -from export_apkgs import export_package_from_spec from dataclasses import dataclass +from export_apkgs import export_package_from_spec +from export_apkgs.error import MissingResourceException from pathlib import Path from tempfile import TemporaryDirectory import unittest @@ -18,6 +19,7 @@ CONTENT_PATH_CSV = 'test/fixtures/csv' CONTENT_PATH_CSV_UPDATED_CONTENT = 'test/fixtures/csv_updated_content' CONTENT_PATH_FIELDS_STATIC = 'test/fixtures/fields_static' CONTENT_PATH_ANKI_TEXT = 'test/fixtures/anki_text' +CONTENT_PATH_RESOURCE_MISSING = 'test/fixtures/resource_missing' TEMPLATES_PATH = 'test/fixtures/templates' TEMPLATES_PATH_UPDATED = 'test/fixtures/templates_updated' @@ -403,6 +405,20 @@ class TestExportApkgs(unittest.TestCase): self.assertEqual( len(col.find_cards('deck:"Tapirs::Species"')), 1) + + def test_missing_resource(self): + """ + Referenced resource file does not exist + """ + with TemporaryDirectory() as first_export_dir: + args_first = MockArgs( + content=CONTENT_PATH_RESOURCE_MISSING, + templates_dir=TEMPLATES_PATH, + output_dir=first_export_dir, + dry_run=False) + self.assertRaises(MissingResourceException, lambda: export_package_from_spec( + Path(CONTENT_PATH_RESOURCE_MISSING) / '.apkg-spec.yaml', + args_first)) def find_template(col: Collection, name: str): -- GitLab From 8aed9327133200aa3a32d0848c3c647bdec8ce02 Mon Sep 17 00:00:00 2001 From: Philipp Stadler <hello@phstadler.com> Date: Sun, 21 Jul 2024 16:03:51 +0200 Subject: [PATCH 3/3] chore: bump version --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index fcdb2e1..1454f6e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.0.0 +4.0.1 -- GitLab