mirror of
https://github.com/dtomlinson91/tembo.git
synced 2025-12-22 11:55:45 +00:00
346 lines
12 KiB
Python
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",
|
|
# )
|
|
# )
|