Files
tembo/tembo/journal/pages.py
2021-10-24 21:56:04 +01:00

346 lines
12 KiB
Python

from __future__ import annotations
from abc import ABCMeta, abstractmethod
import pathlib
import re
from typing import Tuple, Literal
import jinja2
import pendulum
from tembo import logger, CONFIG
from tembo.exceptions import MismatchedTokenError
class PageCreator:
@abstractmethod
def create_page(
self,
base_path: str,
page_path: str,
filename: str,
extension: str,
name: str,
user_input: Tuple[str, ...] | Tuple[()],
template_filename: str | None = None,
) -> Page:
raise NotImplementedError
@staticmethod
def _convert_to_path(
base_path: str, page_path: str, filename: str, extension: str
) -> pathlib.Path:
# check if Tembo base path exists
if not pathlib.Path(base_path).expanduser().exists():
logger.critical("Tembo base path of %s does not exist - exiting", base_path)
raise SystemExit(1)
path_to_file = (
pathlib.Path(base_path).expanduser()
/ pathlib.Path(page_path).expanduser()
/ filename
)
try:
# check for existing `.` in filename extension
extension = extension[1:] if extension[0] == "." else extension
except IndexError:
# return paths without a file
return path_to_file
# return path with a file
return path_to_file.with_suffix(f".{extension}")
def _load_template(self, base_path: str, template_filename: str) -> str:
if CONFIG.template_path is not None:
# check for overriden template_path
template_path = self._convert_to_path("", CONFIG.template_path, "", "")
else:
# default template_path is base_path / templates
template_path = self._convert_to_path(base_path, ".templates", "", "")
# load the template folder
file_loader = jinja2.FileSystemLoader(template_path)
env = jinja2.Environment(loader=file_loader, autoescape=True)
# load the template contents
loaded_template = env.get_template(template_filename)
return loaded_template.render()
class ScopedPageCreator(PageCreator):
def __init__(self) -> None:
self.base_path = ""
self.page_path = ""
self.filename = ""
self.extension = ""
# TODO: rename these to input tokens + more sensible
self.path_input_tokens: Tuple[int, int] = (0, 0)
self.template_input_tokens: Tuple[int, int] = (0, 0)
def create_page(
self,
base_path: str,
page_path: str,
filename: str,
extension: str,
name: str,
user_input: Tuple[str, ...] | Tuple[()],
template_filename: str | None = None,
) -> Page:
self.base_path = base_path
self.page_path = page_path
self.filename = filename
self.extension = extension
# get the path of the scoped page
path = self._convert_to_path(
self.base_path, self.page_path, self.filename, self.extension
)
# substitute tokens in the filepath
path = pathlib.Path(
self._substitute_tokens(str(path), user_input, name, "path")
)
if sum(self.path_input_tokens) > 0:
_highest_input_token_in_path = max(self.path_input_tokens)
else:
_highest_input_token_in_path = 0
# get the template file
if template_filename is not None:
# load the template file contents and substitute tokens
template_contents = self._load_template(self.base_path, template_filename)
template_contents = self._substitute_tokens(
template_contents, user_input, name, "template"
)
if sum(self.template_input_tokens) > 0:
_highest_input_token_in_template = max(self.template_input_tokens)
else:
_highest_input_token_in_template = 0
else:
template_contents = ""
self.__check_input_token_mismatch(
_highest_input_token_in_path, _highest_input_token_in_template
)
return ScopedPage(path, template_contents)
def __check_input_token_mismatch(
self, _highest_input_token_in_path: int, _highest_input_token_in_template: int
) -> None:
_highest_input_token_count = max(
_highest_input_token_in_path, _highest_input_token_in_template
)
if _highest_input_token_in_path < _highest_input_token_count:
logger.critical(
"Your config/template specifies %s input tokens, you gave %s "
"- exiting",
_highest_input_token_count,
self.path_input_tokens[0],
)
raise SystemExit(1)
if _highest_input_token_in_path > _highest_input_token_count:
logger.warning(
"Your config/template specifies %s input tokens, you gave %s",
_highest_input_token_count,
self.path_input_tokens[0],
)
if _highest_input_token_in_template < _highest_input_token_count:
logger.critical(
"Your config/template specifies %s input tokens, you gave %s "
"- exiting",
_highest_input_token_count,
self.template_input_tokens[0],
)
raise SystemExit(1)
if _highest_input_token_in_template > _highest_input_token_count:
logger.warning(
"Your config/template specifies %s input tokens, you gave %s",
_highest_input_token_count,
self.template_input_tokens[0],
)
def _substitute_tokens(
self,
tokenified_string: str,
user_input: Tuple[str, ...] | Tuple[()],
name: str,
input_token_type: Literal["path", "template"],
) -> str:
"""For a tokened string, substitute input, name and date tokens."""
tokenified_string = self.__substitute_input_tokens(
tokenified_string, user_input, input_token_type
)
tokenified_string = self.__substitute_name_tokens(tokenified_string, name)
tokenified_string = self.__substitute_date_tokens(tokenified_string)
return tokenified_string
@staticmethod
def __substitute_name_tokens(tokenified_string: str, name: str) -> str:
# find any {name} tokens and substitute for the name value
name_extraction = re.findall(r"(\{name\})", tokenified_string)
for extracted_input in name_extraction:
tokenified_string = tokenified_string.replace(extracted_input, name)
return tokenified_string
# @staticmethod
def __substitute_input_tokens(
self,
tokenified_string: str,
user_input: Tuple[str, ...] | Tuple[()],
input_token_type: Literal["path", "template"],
) -> str:
"""Find `{inputN}` tokens in string."""
input_extraction = re.findall(r"(\{input\d*\})", tokenified_string)
# if there is no user input
if len(user_input) == 0:
if len(input_extraction) > 0:
# if the regex matches, save the number of input tokens found
if input_token_type == "path": # noqa: bandit 105
# TODO: change this to a dict instead of tuple
self.path_input_tokens = (len(input_extraction), 0)
if input_token_type == "template": # noqa: bandit 105
self.template_input_tokens = (len(input_extraction), 0)
# if there aren't any tokens in the string, return the string
return tokenified_string
# if there is user input
if len(user_input) > 0:
# save the number of input tokens, and the number of user inputs
if input_token_type == "path": # noqa: bandit 105
self.path_input_tokens = (len(input_extraction), len(user_input))
elif input_token_type == "template": # noqa: bandit 105
self.template_input_tokens = (len(input_extraction), len(user_input))
# sbustitute the input token for the user's input
for extracted_input, input_value in zip(input_extraction, user_input):
tokenified_string = tokenified_string.replace(
extracted_input, input_value
)
return tokenified_string
@staticmethod
def __substitute_date_tokens(tokenified_string: str) -> str:
"""Find any {d:%d-%M-%Y} tokens."""
# extract the full token string
date_extraction_token = re.findall(r"(\{d\:[^}]*\})", tokenified_string)
for extracted_token in date_extraction_token:
# extract the inner %d-%M-%Y only
strftime_value = re.match(r"\{d\:([^\}]*)\}", extracted_token)
if strftime_value is not None:
strftime_value = strftime_value.group(1)
if isinstance(strftime_value, str):
tokenified_string = tokenified_string.replace(
extracted_token, pendulum.now().strftime(strftime_value)
)
return tokenified_string
class Page(metaclass=ABCMeta):
@abstractmethod
def __init__(self, path: pathlib.Path, page_content: str) -> None:
raise NotImplementedError
@abstractmethod
def save_to_disk(self, dry_run: bool) -> None:
raise NotImplementedError
class ScopedPage(Page):
"""A Page that uses substitute tokens."""
def __init__(self, path: pathlib.Path, page_content: str) -> None:
self.path = path
self.page_content = page_content
def __str__(self) -> str:
return f"ScopedPage({self.path})"
def save_to_disk(self, dry_run: bool = False) -> None:
if dry_run:
logger.info("%s will be created", self.path)
raise SystemExit(0)
# create the parent directories
scoped_note_file = pathlib.Path(self.path)
scoped_note_file.parents[0].mkdir(parents=True, exist_ok=True)
if not scoped_note_file.exists():
with scoped_note_file.open("w", encoding="utf-8") as scoped_page:
scoped_page.write(self.page_content)
else:
logger.info("%s already exists - skipping.", self.path)
raise SystemExit(0)
if __name__ == "__main__":
c = ScopedPageCreator()
# # raises error
# # print(c._substitute_tokens("scratchpad/{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file.md", None))
# print(
# c._substitute_tokens(
# "scratchpad/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file-{input0}.md", ("last",)
# )
# )
# print(
# c.create_page(
# "~/tembo",
# "{name}",
# "{input0}-{input1}-file",
# "md",
# "scratchpad",
# ("first", "second"),
# )
# )
# print(
# c.create_page(
# "~/tembo",
# "{name}/{d:MMMM-YY}",
# "{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file",
# "md",
# "scratchpad",
# ("first",),
# )
# )
# print(
# c.create_page(
# "~/tembo",
# "{name}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}",
# "file",
# "md",
# "scratchpad",
# None,
# )
# )
# print(
# c.create_page(
# "~/tembo",
# "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}",
# "file-{input0}-{name}",
# ".md",
# "meeting",
# ("last",),
# "scratchpad.md.tpl",
# )
# )
test_page_with_template = c.create_page(
"~/tembo",
"{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}",
"file-{input0}-{name}",
".md",
"meeting",
("last",),
"scratchpad.md.tpl",
)
print(test_page_with_template)
test_page_with_template.save_to_disk(False)
# print(
# c.create_page(
# "~/tembo",
# "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}",
# "file-{input0}-{name}",
# ".md",
# "meeting",
# ("last",),
# "scratchpad_templates/scratchpad.md.tpl",
# )
# )