diff --git a/pyproject.toml b/pyproject.toml index e1961cc..1621517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ packages = ["rmtree", "rmtree.struct"] [project] name = "rmtree" -version = "1.0.6" +version = "2.0.1" requires-python = ">=3.10" readme = "README.md" license = { file = "LICENSE" } @@ -25,4 +25,4 @@ dependencies = [ [project.scripts] -rmtree = "rmtree.main:main" +rmtree = "rmtree.__main__:main" diff --git a/rmtree/__main__.py b/rmtree/__main__.py index 40e2b01..63fc000 100644 --- a/rmtree/__main__.py +++ b/rmtree/__main__.py @@ -1,4 +1,48 @@ -from .main import main +import argparse +import logging +from pathlib import Path + +from tqdm import tqdm + +from rmtree.debug import test_assertion +from rmtree.logger import setup_logging +from rmtree.struct.file import list_files + +logger = logging.getLogger(__name__) + + +def main(args=None): + parser = argparse.ArgumentParser("rmtree", description="Process the file tree of the reMarkable tablet.") + parser.add_argument("src", type=Path, help="The source folder.") + parser.add_argument("dst", type=Path, nargs="?", help="The folder where the files are exported to.") + parser.add_argument("--debug", "-d", action="store_true", help="Set all loggers to DEBUG (including rmscene).") + parser.add_argument("--test-compatibility", "-t", action="store_true", + help="Test if the files from the reMarkable are compatible with this program.") + parser.add_argument("--ignore-assertion", "-ia", action="store_true", + help="Continue despite assertion errors. Output correctness is not guaranteed.") + parser.add_argument("--ignore-compatibility", "-ic", action="store_true", + help="Continue despite compatibility errors. WARNING: the output will not be correct.") + args = parser.parse_args(args) + + if not (args.dst or args.test_compatibility): + parser.error("Please specify the 'dst' argument.") + + setup_logging(args.debug, args.test_compatibility) + is_compatible, are_assertions_correct = test_assertion(args.src) + + if ((not args.test_compatibility) + and (are_assertions_correct or args.ignore_assertion) + and is_compatible or args.ignore_compatibility): + # export all the files + files = list_files(args.src) + progress = tqdm(files.items()) + for uuid, f in progress: + progress.set_description(str(f)) + try: + f.export(args.dst.joinpath(f.get_path(files))) + except Exception as e: + logger.error(f"Failed to export {f.get_name()}: {e}") + if __name__ == "__main__": main() diff --git a/rmtree/debug.py b/rmtree/debug.py index 8f4f653..041e059 100644 --- a/rmtree/debug.py +++ b/rmtree/debug.py @@ -1,12 +1,15 @@ +import logging import os -import typing as tp from pathlib import Path +from typing import Tuple, Dict -from rmtree.struct.content import ContentFile +from rmtree.struct.content import FileType, ContentFile from rmtree.struct.file import ID_PATTERN from rmtree.struct.metadata import Metadata from rmtree.struct.page import PageRM, PageVersion +logger = logging.getLogger(__name__) + """ Here is a list and partial description of the files in the /home/root/.local/share/remarkable/xochitl/ of the reMarkable. `[uuid]` represents an uuid v4. @@ -29,7 +32,7 @@ know_folder_extensions = ["thumbnails", "highlights", "textconversion", "RM_FOLDER"] -def count_extension(src: Path) -> tp.Tuple[tp.Dict[str, int], tp.Dict[str, int]]: +def count_extension(src: Path) -> Tuple[Dict[str, int], Dict[str, int]]: """ Count the number of files of each extension in the `src` folder :param src: the source folder @@ -58,25 +61,23 @@ def count_extension(src: Path) -> tp.Tuple[tp.Dict[str, int], tp.Dict[str, int]] return file_extension, folder_extension -def test_assertion(src: Path, custom_print=print) -> tp.Tuple[int, int]: - custom_print(f"===== Testing compatibility and assertion on {src} =====") - custom_print() - custom_print("Be aware of the following:") - custom_print("\t- Compatibility refers to the constraint that I decided to impose (mainly the\n" +def test_assertion(src: Path) -> Tuple[bool, bool]: + logger.info(f"===== Testing compatibility and assertion on {src} =====\n") + logger.info("Be aware of the following:") + logger.info("\t- Compatibility refers to the constraint that I decided to impose (mainly the\n" "\t version constraint on .rm and .content files). Any error regarding this is\n" "\t considered 'wrong input from the user'/'software limitation' and not a bug.\n" "\t- Assertion refers to the hypothesis made as there is no official API for\n" "\t the reMarkable file structure. Errors regarding these assertions can be\n" - "\t considered bugs. Please report them on GitHub with as much information as possible.") - custom_print() + "\t considered bugs. Please report them on GitHub with as much information as possible.\n") extensions = count_extension(src) - custom_print("Extension of detected files:") - custom_print("\n".join([f"\t- {ext} " + logger.info("Extension of detected files:") + logger.info("\n".join([f"\t- {ext} " f"{'' if ext in know_file_extensions else '(unknown, please consider submitting an issue)'}: {c}" for ext, c in extensions[0].items()])) - custom_print("Extension of detected folder:") - custom_print("\n".join([f"\t- {ext} " + logger.info("Extension of detected folder:") + logger.info("\n".join([f"\t- {ext} " f"{'' if ext in know_folder_extensions else '(unknown, please consider submitting an issue)'}: {c}" for ext, c in extensions[1].items()])) @@ -98,32 +99,38 @@ def exists(name: str): continue # otherwise there should be a metadata and a content - if not (exists(uuid + ".metadata") and exists(uuid + ".content")): + if not exists(uuid + ".metadata"): errors[uuid] = {"type": "assert", - "reason": "Either the metadata or content file is missing."} + "reason": "The metadata is missing."} continue # verify assertion on the metadata file metadata = Metadata(src, uuid) - if not metadata.test_assertion(uuid_list): + r = metadata.test_assertion(uuid_list) + if r is not True: errors[uuid] = {"type": "assert", - "reason": "The metadata assertion are not verified."} + "reason": r} continue - # verify assertion on the content file content = metadata.get_associated_content() - if isinstance(content, ContentFile) and content.get_version() not in [1, 2]: + # .content is optional for folders + if metadata.get_file_type() == FileType.DOCUMENT and content is None: errors[uuid] = {"name": metadata.get_name(), "type": "compatibility", - "reason": "This software is only compatible with content version 1 and 2."} - continue - elif not content.test_assertion(): - errors[uuid] = {"name": metadata.get_name(), "type": "assert", - "reason": "The content assertion are not verified."} + "reason": "The .content is missing."} continue - # check rm file version + # verify assertion on the .content + if content is not None: + r = content.test_assertion() + if r is not True: + errors[uuid] = {"name": metadata.get_name(), "type": "compatibility", + "reason": r} + continue + + # check assertion specific to documents if isinstance(content, ContentFile): - not_compatible_pages: list[tp.Tuple[int, str, PageVersion]] = [] + # check rm file version + not_compatible_pages: list[Tuple[int, str, PageVersion]] = [] for n, page in enumerate(content.get_pages()): if isinstance(page, PageRM) and not page.test_assertion(): not_compatible_pages.append((n + 1, page.page_uuid, page.get_version())) @@ -132,36 +139,38 @@ def exists(name: str): errors[uuid] = {"name": metadata.get_name(), "pages": not_compatible_pages, "type": "compatibility", "reason": "This software is only compatible with rm file version 6"} - # [uuid]/ folder only contains rm files - if exists(uuid): - for filename in os.listdir(os.path.join(src, uuid)): - if not (filename.endswith(".rm") or filename.endswith("-metadata.json")): - errors[uuid] = {"name": metadata.get_name(), "type": "assert", - "reason": "Unknown files in the [uuid]/ folder"} + # [uuid]/ folder only contains rm files + if exists(uuid): + for filename in os.listdir(os.path.join(src, uuid)): + if not (filename.endswith(".rm") or filename.endswith("-metadata.json")): + errors[uuid] = {"name": metadata.get_name(), "type": "assert", + "reason": "Unknown files in the [uuid]/ folder"} # print compatibilities errors compatibility_errors = {uuid: error for uuid, error in errors.items() if error["type"] == "compatibility"} if len(compatibility_errors) > 0: - custom_print() - custom_print( + logger.info( "The following are compatibility errors. This software is explicitly not compatible with those files.\n" "You can look at the README.md to find more information:\n" "https://github.com/Seb-sti1/rmtree?tab=readme-ov-file#how-to-check-compatibility-and-update-my-files-to-v6.") for uuid, error in compatibility_errors.items(): if "pages" in error: - custom_print( + logger.info( f"\t- {error['name']} (page n°{', '.join([str(pages[0]) for pages in error['pages']])}) ({uuid}):" f" This software is only compatible with rm file version 6") else: - custom_print(f"\t- {error['name']} ({uuid}): {error['reason']}") + logger.info(f"\t- {error['name']} ({uuid}): {error['reason']}") # print assertion errors assertion_errors = {uuid: error for uuid, error in errors.items() if error["type"] == "assert"} if len(assertion_errors) > 0: - custom_print() - custom_print( + logger.info( "The following are assertion errors. You can report them at https://github.com/Seb-sti1/rmtree/issues.") for uuid, error in assertion_errors.items(): - custom_print(f"\t- {error['name'] if 'name' in error else 'Unknown name'} ({uuid}): {error['reason']}") + logger.info(f"\t- {error['name'] if 'name' in error else 'Unknown name'} ({uuid}): {error['reason']}") - return len(compatibility_errors), len(assertion_errors) + if len(compatibility_errors) > 0: + logger.warning(f"{len(compatibility_errors)} detected compatibility errors.") + if len(assertion_errors) > 0: + logger.warning(f"{len(assertion_errors)} detected assertion errors.") + return len(compatibility_errors) == 0, len(assertion_errors) == 0 diff --git a/rmtree/logger.py b/rmtree/logger.py new file mode 100644 index 0000000..3b4ba07 --- /dev/null +++ b/rmtree/logger.py @@ -0,0 +1,31 @@ +import logging +import sys + + +class ColorFormatter(logging.Formatter): + COLORS = { + logging.DEBUG: "\033[36m", # Cyan + logging.INFO: "\033[0m", # Default terminal color + logging.WARNING: "\033[33m", # Dark yellow / amber + logging.ERROR: "\033[31m", # Red + logging.CRITICAL: "\033[1;31m", # Bold red + } + RESET = "\033[0m" + + def format(self, record): + prefix = "" + if logging.getLogger("rmtree").level == logging.DEBUG: + prefix = f"[{record.name}] " + color = self.COLORS.get(record.levelno, self.RESET) + return f"{prefix}{color}{record.getMessage()}{self.RESET}" + + +def setup_logging(debug: bool, test_compatibility: bool) -> None: + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(ColorFormatter()) + logging.basicConfig(handlers=[handler], level=logging.DEBUG if debug else logging.INFO) + logging.getLogger("rmtree").setLevel(logging.DEBUG if debug else logging.INFO) + logging.getLogger("rmtree.debug").setLevel(logging.DEBUG if debug else + logging.INFO if test_compatibility else + logging.WARNING) + logging.getLogger("rmscene").setLevel(logging.DEBUG if debug else logging.CRITICAL) diff --git a/rmtree/main.py b/rmtree/main.py deleted file mode 100644 index 5d7aadf..0000000 --- a/rmtree/main.py +++ /dev/null @@ -1,68 +0,0 @@ -import argparse -import logging -from pathlib import Path - -from tqdm import tqdm - -from rmtree.debug import test_assertion -from rmtree.struct.file import list_files - -logger = logging.getLogger(__name__) - - -def get_verbosity(verbose: int): - if verbose == 0: - return logging.CRITICAL - if verbose == 1: - return logging.WARNING - if verbose == 2: - return logging.INFO - if verbose >= 3: - return logging.DEBUG - - -def main(args=None): - # set up the arg parser - parser = argparse.ArgumentParser("rmtree", description='Process the file tree of the reMarkable tablet.') - parser.add_argument('src', type=Path, help='The source folder.') - parser.add_argument('dst', type=Path, help='The folder where the files are exported to.', - nargs='?') - parser.add_argument('--verbose', '-v', action='count', default=0, help='Increase verbosity.') - parser.add_argument('--dependencies-verbosity', type=int, default=0, - help='Set verbosity of the dependencies.') - parser.add_argument('--test-compatibility', '-t', action='store_true', default=False, - help='Test if the files from the reMarkable are compatible with this program.') - parser.add_argument('--ignore-assertion', '-i', action='store_true', default=False, - help='If the program should continue despite assertion errors.' - 'There will be no guarantee the output is correct.') - - args = parser.parse_args(args) - - if not (args.dst or args.test_compatibility): - parser.error("Please specify 'dst' argument.") - - # set up the logging - logging.basicConfig(level=logging.DEBUG if args.test_compatibility else get_verbosity(args.verbose)) - deps_verbosity = logging.DEBUG if args.test_compatibility else get_verbosity(args.dependencies_verbosity) - logging.getLogger("rmscene").setLevel(deps_verbosity) - logging.getLogger("rmc").setLevel(deps_verbosity) - - count_compatibility_errors, count_assertion_errors = test_assertion(args.src, - print if args.test_compatibility else - lambda *args: None) - print(f"{count_compatibility_errors} detected compatibility errors.") - print(f"{count_assertion_errors} detected assertion errors.") - - if ((not args.test_compatibility) - and (count_assertion_errors == 0 or args.ignore_assertion) - and count_compatibility_errors == 0): - # export all the files - files = list_files(args.src) - progress = tqdm(files.items()) - for uuid, f in progress: - progress.set_description(str(f)) - f.export(args.dst.joinpath(f.get_path(files))) - - -if __name__ == "__main__": - main() diff --git a/rmtree/struct/content.py b/rmtree/struct/content.py index 6151591..dd1c141 100644 --- a/rmtree/struct/content.py +++ b/rmtree/struct/content.py @@ -1,9 +1,8 @@ from __future__ import annotations import json -import typing as tp from pathlib import Path -from typing import Optional +from typing import Optional, Iterator, Tuple, Dict, Literal, Union from pypdf import PageObject, PdfReader @@ -22,7 +21,7 @@ def valid_type(file_type: str) -> bool: class Content: - def __init__(self, src: Path, uuid: str, raw): + def __init__(self, src: Path, uuid: str, raw: Dict): self.src = src self.uuid = uuid self.raw = raw @@ -30,29 +29,30 @@ def __init__(self, src: Path, uuid: str, raw): @staticmethod def from_file(src: Path, uuid: str, file_type: FileType) -> Optional[Content]: path = src.joinpath(uuid + ".content") - with open(str(path), "r") as f: + if not path.exists(): + return None + with path.open() as f: raw = json.load(f) - if file_type == FileType.FOLDER: return ContentFolder(src, uuid, raw) elif file_type == FileType.DOCUMENT: return ContentFile(src, uuid, raw) return None - def get_version(self) -> tp.Optional[int]: - raise NotImplemented("This is an abstract class.") + def get_version(self) -> Optional[int]: + raise NotImplementedError("This is an abstract class.") - def test_assertion(self) -> bool: + def test_assertion(self) -> Union[Literal[True], str]: # if the file is a document, the content file needs to have: # - the formatVersion # - the pageCount # - the pages info - raise NotImplemented("This is an abstract class") + raise NotImplementedError("This is an abstract class") class ContentFile(Content): - def __init__(self, src: Path, uuid: str, raw: tp.Dict): + def __init__(self, src: Path, uuid: str, raw: Dict): super().__init__(src, uuid, raw) def get_version(self) -> int: @@ -61,15 +61,14 @@ def get_version(self) -> int: def get_pages_count(self) -> int: return len(self.raw["cPages"]["pages"] if self.get_version() == 2 else self.raw["pages"]) - def iterate_pages(self, bg_pdf: tp.Optional[PdfReader]) -> tp.Iterator[tp.Tuple[tp.Optional[Page], - tp.Optional[PageObject]]]: + def iterate_pages(self, bg_pdf: Optional[PdfReader]) -> Iterator[Tuple[Optional[Page], Optional[PageObject]]]: for i in range(max(self.get_pages_count(), 0 if bg_pdf is None else len(bg_pdf.pages))): page = None bg = None if self.get_version() == 1: if i < len(self.raw["pages"]): page_def = self.raw["pages"][i] - page = Page.from_file(self.src, self.uuid, page_def["id"], page_def) + page = Page.from_file(self.src, self.uuid, page_def, page_def) if bg_pdf is not None and i < len(bg_pdf.pages): bg = bg_pdf.pages[i] @@ -81,7 +80,7 @@ def iterate_pages(self, bg_pdf: tp.Optional[PdfReader]) -> tp.Iterator[tp.Tuple[ bg = bg_pdf.pages[page.bg_pdf_page_idx] yield page, bg - def get_pages(self) -> tp.Iterator[Page]: + def get_pages(self) -> Iterator[Page]: pages_data = self.raw["cPages"]["pages"] if self.get_version() == 2 else self.raw["pages"] for page_def in pages_data: @@ -89,20 +88,25 @@ def get_pages(self) -> tp.Iterator[Page]: yield Page.from_file(self.src, self.uuid, page_def if self.get_version() == 1 else page_def["id"], page_def) - def test_assertion(self) -> bool: + def test_assertion(self) -> Union[Literal[True], str]: if self.get_version() == 1: - return all([p in self.raw for p in ["formatVersion", "pageCount", "pages"]]) + if not all([p in self.raw for p in ["formatVersion", "pageCount", "pages"]]): + return "Missing attributes in .content file." elif self.get_version() == 2: - return all([p in self.raw for p in ["formatVersion", "pageCount", "cPages"]]) \ - and "pages" in self.raw["cPages"] + if not all([p in self.raw for p in ["formatVersion", "pageCount", "cPages"]]) \ + and "pages" in self.raw["cPages"]: + return "Missing attributes in .content file." + else: + return "This software is only compatible with content version 1 and 2." + return True class ContentFolder(Content): - def __init__(self, src: Path, uuid: str, raw: tp.Dict): + def __init__(self, src: Path, uuid: str, raw: Dict): super().__init__(src, uuid, raw) - def get_version(self) -> tp.Optional[int]: + def get_version(self) -> Optional[int]: return None - def test_assertion(self) -> bool: + def test_assertion(self) -> Union[Literal[True], str]: return True diff --git a/rmtree/struct/file.py b/rmtree/struct/file.py index 5db08e5..96ff304 100644 --- a/rmtree/struct/file.py +++ b/rmtree/struct/file.py @@ -7,12 +7,14 @@ import traceback import typing as tp from pathlib import Path +from typing import Optional from pypdf import PdfReader, PdfWriter, Transformation +from pypdf.annotations import FreeText -from rmtree.struct.content import Content, ContentFile +from rmtree.struct.content import ContentFile, FileType, ContentFolder from rmtree.struct.metadata import Metadata -from rmtree.struct.page import PageEmpty, PageRM +from rmtree.struct.page import PageEmpty, PageRM, PageVersion logger = logging.getLogger(__name__) @@ -28,15 +30,13 @@ def replace_invalid_char(string: str) -> str: class File: @staticmethod def from_metadata(metadata: Metadata) -> File: - content = metadata.get_associated_content() - if isinstance(content, ContentFile): - return Notebook(metadata, content) + if metadata.get_file_type() == FileType.FOLDER: + return Folder(metadata) else: - return Folder(metadata, content) + return Notebook(metadata) - def __init__(self, metadata: Metadata, content: Content): + def __init__(self, metadata: Metadata): self.metadata = metadata - self.content = content def get_uuid(self): return self.metadata.uuid @@ -68,7 +68,7 @@ def export(self, output_path: Path): :param output_path: A folder where to save the file :return: """ - raise NotImplemented("This is an abstract class.") + raise NotImplementedError("This is an abstract class.") def __str__(self): return f"{self.metadata.get_name()} ({self.get_uuid()})" @@ -79,9 +79,25 @@ def __repr__(self): class Notebook(File): - def __init__(self, metadata: Metadata, content: ContentFile): - super().__init__(metadata, content) - self.content = content # fix type hints + def __init__(self, metadata: Metadata): + super().__init__(metadata) + self.content: ContentFile = metadata.get_associated_content() + + @staticmethod + def __add_annotation__(doc: PdfWriter, page_number: int, content: str, + width=400, height=40) -> None: + annotation = FreeText( + text=content, + rect=(0, 0, width, height), + font="Arial", + italic=True, + font_size="20pt", + font_color="ffffff", + border_color=None, + background_color=None, + ) + annotation.flags = 4 + doc.add_annotation(page_number=page_number, annotation=annotation) def export(self, output_path: Path): fullpath = os.path.join(output_path, replace_invalid_char(self.metadata.get_name())) @@ -97,13 +113,13 @@ def export(self, output_path: Path): background_pdf = PdfReader(background_pdf_path) if os.path.exists(background_pdf_path) else None output_pdf = PdfWriter() for page, background_page in self.content.iterate_pages(background_pdf): - if isinstance(page, PageRM): + if isinstance(page, PageRM) and page.get_version() == PageVersion.V6: try: # get the svg as a pdf svg_pdf_p, (x_shift, y_shift, w_svg, h_svg) = page.export() # get size of the background_page - w_bg = 0 if background_page is None else background_page.mediabox.width - h_bg = 0 if background_page is None else background_page.mediabox.height + w_bg = 0 if background_page is None else background_page.cropbox.width + h_bg = 0 if background_page is None else background_page.cropbox.height # add a blank page that can contains both svg and background pdf width, height = max(w_svg, w_bg), max(h_svg, h_bg) new_page = output_pdf.add_blank_page(width, height) @@ -125,10 +141,27 @@ def export(self, output_path: Path): new_page.merge_transformed_page(svg_pdf_p, Transformation().translate(x_svg, y_svg)) except Exception: + output_pdf.add_blank_page(400, 500) + self.__add_annotation__(output_pdf, + len(output_pdf.pages) - 1, + f"An error occurred while exporting this page.\n\n\n" + f"{traceback.format_exc()}", + 400, + 500) logger.warning(f"Failed to export {page.get_page_uuid()} of {self.get_uuid()}:") traceback.print_exc() - elif background_page is not None: - output_pdf.add_page(background_page) + else: + if background_page is not None: + output_pdf.add_page(background_page) + else: + output_pdf.add_blank_page(400, 500) + if isinstance(page, PageRM): # if there is a non v6 page + self.__add_annotation__(output_pdf, + len(output_pdf.pages) - 1, + f"This page uses a rm v{page.get_version()}." + f" It is incompatible with this software.\n" + f"Please go to this page, draw and remove a stroke in order" + f" to update the page to v6.") if len(output_pdf.pages) > 0: output_pdf.write(fullpath + ".pdf") @@ -142,8 +175,9 @@ class Folder(File): This a simple folder """ - def __init__(self, metadata: Metadata, content: Content): - super().__init__(metadata, content) + def __init__(self, metadata: Metadata): + super().__init__(metadata) + self.content: Optional[ContentFolder] = metadata.get_associated_content() def export(self, output_path: Path): fullpath = os.path.join(output_path, self.metadata.get_name()) @@ -164,7 +198,6 @@ def list_files(src: Path) -> tp.Dict[str, File]: files = {} for uuid in uuid_list: - if os.path.exists(src.joinpath(uuid + ".metadata")) \ - and os.path.exists(src.joinpath(uuid + ".content")): + if os.path.exists(src.joinpath(uuid + ".metadata")): files[uuid] = File.from_metadata(Metadata(src, uuid)) return files diff --git a/rmtree/struct/metadata.py b/rmtree/struct/metadata.py index d35f5b8..2cf388a 100644 --- a/rmtree/struct/metadata.py +++ b/rmtree/struct/metadata.py @@ -1,6 +1,6 @@ import json from pathlib import Path -from typing import Optional +from typing import Optional, Union, Literal from rmtree.struct.content import Content, FileType @@ -28,10 +28,14 @@ def get_file_type(self) -> FileType: def get_associated_content(self) -> Optional[Content]: return Content.from_file(self.src, self.uuid, self.get_file_type()) - def test_assertion(self, uuid_list: list[str]) -> bool: + def test_assertion(self, uuid_list: list[str]) -> Union[Literal[True], str]: # the metadata contains is type, visibleName and parent # - type is one of FileType valid type # - parent is in uuid - return all([p in self.raw for p in ["type", "visibleName", "parent"]]) \ - and self.raw["parent"] in uuid_list or self.raw["parent"] in ["", "trash"] \ - and FileType.valid_type(self.raw["type"]) + if not all([p in self.raw for p in ["type", "visibleName", "parent"]]): + return "Missing attributes in .metadata file." + if not (self.raw["parent"] in uuid_list or self.raw["parent"] in ["", "trash"]): + return "Can't find parent of this file." + if not FileType.valid_type(self.raw["type"]): + return "This document has an unknown type." + return True \ No newline at end of file diff --git a/rmtree/struct/page.py b/rmtree/struct/page.py index c2abad2..51d4e75 100644 --- a/rmtree/struct/page.py +++ b/rmtree/struct/page.py @@ -4,8 +4,8 @@ import logging import os import re -import typing as tp from pathlib import Path +from typing import Dict, Tuple, Union, Literal import cairosvg from pypdf import PdfReader, PageObject @@ -33,7 +33,7 @@ class Page: Represents a page of a notebook """ - def __init__(self, src: Path, file_uuid: str, page_uuid: str, definition: tp.Dict): + def __init__(self, src: Path, file_uuid: str, page_uuid: str, definition: Dict): self.path = src.joinpath(file_uuid, page_uuid + ".rm") self.file_uuid = file_uuid self.page_uuid = page_uuid @@ -48,7 +48,7 @@ def __init__(self, src: Path, file_uuid: str, page_uuid: str, definition: tp.Dic self.bg_pdf_page_idx = definition["redir"]["value"] @staticmethod - def from_file(src: Path, file_uuid: str, page_uuid: str, definition: tp.Dict) -> Page: + def from_file(src: Path, file_uuid: str, page_uuid: str, definition: Dict) -> Page: if src.joinpath(file_uuid, page_uuid + ".rm").exists(): return PageRM(src, file_uuid, page_uuid, definition) else: @@ -57,12 +57,12 @@ def from_file(src: Path, file_uuid: str, page_uuid: str, definition: tp.Dict) -> def get_page_uuid(self) -> str: return self.page_uuid - def test_assertion(self) -> bool: - raise NotImplemented("This is an abstract class") + def test_assertion(self) -> Union[Literal[True], str]: + raise NotImplementedError("This is an abstract class") class PageRM(Page): - def __init__(self, src: Path, file_uuid: str, page_uuid: str, definition: tp.Dict): + def __init__(self, src: Path, file_uuid: str, page_uuid: str, definition: Dict): super().__init__(src, file_uuid, page_uuid, definition) self.version = self.__compute_version__() @@ -79,7 +79,7 @@ def __compute_version__(self) -> PageVersion: def get_version(self) -> PageVersion: return self.version - def export(self) -> (PageObject, (float, float, float, float)): + def export(self) -> Tuple[PageObject, Tuple[float, float, float, float]]: """ Use rmc to convert a rm binary file to a svg and then to a pdf @@ -121,13 +121,15 @@ def export(self) -> (PageObject, (float, float, float, float)): return svg_pdf.pages[0], (x_shift, y_shift, w, h) - def test_assertion(self) -> bool: - return self.version == PageVersion.V6 + def test_assertion(self) -> Union[Literal[True], str]: + if self.version != PageVersion.V6: + return "This software is only compatible with rm file version 6." + return True class PageEmpty(Page): - def __init__(self, src: Path = None, file_uuid: str = None, page_uuid: str = None, definition: tp.Dict = None): + def __init__(self, src: Path, file_uuid: str, page_uuid: str, definition: Dict): super().__init__(src, file_uuid, page_uuid, definition) - def test_assertion(self) -> bool: + def test_assertion(self) -> Union[Literal[True], str]: return True