diff --git a/.gitignore b/.gitignore index 2dc658c..34243a4 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,6 @@ dmypy.json # Cython debug symbols cython_debug/ +# custom .vscode/ +**/__pycache__ diff --git a/.pythonversion b/.python-version similarity index 100% rename from .pythonversion rename to .python-version diff --git a/TODO.todo b/TODO.todo index 1287bf5..0f5c56b 100644 --- a/TODO.todo +++ b/TODO.todo @@ -2,32 +2,49 @@ Functionality: ☐ Handle case where there are no scopes in the config and command is invoked. ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` ☐ Should be a `tembo new --list` to list all possible names. + ☐ When template not found, raise a Tembo error + ☐ Convert spaces to underscores in filepath + currently raises jinja2.exceptions.TemplateNotFound on line 62, in _load_template + ☐ Add update notification? + check pypi for latest version and compare to current ✔ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in @done(21-10-24 05:31) +Bug: + ☐ tokens + Say we have input0 and input3 in file path + and we have input1 and input2 in template + it only recognises this as 2 inputs total, not four. + passing in tembo new meeting a b will work and input0=input1, input3=input2 + VSCode: ☐ Look at Logging: - ☐ How to raise + debug an exception? - ☐ Document how to raise a logger.critical instead of exception - in a try, except you can just do logger.critical(exec_info=1) to print the stack -Documentation: - ☐ Document usage of Panaetius in a module - Using the logger, initialising with the config path etc - ✘ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens @cancelled(21-10-24 05:32) - ☐ Uses `strftime` tokens: - ☐ Document latest typing. + Documentation: + ☐ Document usage of Panaetius in a module + Using the logger, initialising with the config path etc + ✘ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens @cancelled(21-10-24 05:32) + ☐ Uses `strftime` tokens: + ☐ Document latest typing. ☐ Using from `__future__` with `|` ☐ `using Tuple[str, ...]` ☐ `Sequence` vs `Collection` - ☐ Document how to do docstrings in python. Don't document `__init__` do it in class. - Should update the default gist to hide the `__init__` messages - ☐ Document using jinja2 briefly and link to Tembo (link to ) - Tembo: - ☐ Document creating new Tembo config + ☐ Document how to do docstrings in python. Don't document `__init__` do it in class. + Should update the default gist to hide the `__init__` messages + ☐ Document using jinja2 briefly and link to Tembo (link to ) + + Logging: + ☐ How to raise + debug an exception? + ☐ Document how to raise a logger.critical instead of exception + in a try, except you can just do logger.critical(exec_info=1) to print the stack + + Tembo: + ☐ Document creating new Tembo config ☐ ~/tembo needs creating ☐ ~/tembo/.config ☐ ~/tembo/.templates ☐ ~/tembo/logs - ☐ Document how to overwrite these with ENV vars + ☐ Document how to overwrite these with ENV vars + ☐ have a git repo with all the above already configured and walk user through + clone the repo, delete .git, git init, configure and add git origin diff --git a/tembo/cli.py b/tembo/cli.py index b26acec..e5810e6 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -46,7 +46,6 @@ def new(scope, inputs, dry_run): template_filename=str(user_scope["template_filename"]) ) scoped_page.save_to_disk(dry_run=dry_run) - tembo.logger.info("Saved %s to disk", scoped_page.path) raise SystemExit(0) if not _name_found and len(tembo.CONFIG.scopes) > 0: tembo.logger.warning("Command %s not found in config.yml - exiting", scope) @@ -62,5 +61,6 @@ run.add_command(new) if __name__ == "__main__": # BUG: fix this bug where input tokens are mismatched - new(["meeting", "robs presentation", "meeting on gcp"]) + # new(["meeting", "robs presentation", "meeting on gcp"]) + new(["meeting", "a", "b", "c", "d"]) # new(["meeting", "robs presentation"]) diff --git a/tembo/exceptions.py b/tembo/exceptions.py index c8c6ad5..a71ea99 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -1 +1,5 @@ """Tembo exceptions.""" + + +class MismatchedTokenError(Exception): + pass diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index ba4a6e9..8609377 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -68,6 +68,7 @@ class ScopedPageCreator(PageCreator): self.page_path = "" self.filename = "" self.extension = "" + self._all_input_tokens: list[str] = [] def create_page( self, @@ -83,12 +84,22 @@ class ScopedPageCreator(PageCreator): self.page_path = page_path self.filename = filename self.extension = extension + + # verify the user input length matches the number of input tokens in the + # config/templates + self._all_input_tokens = self._get_input_tokens(template_filename) + self._verify_input_tokens(user_input) + # 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 = pathlib.Path( + self._substitute_tokens(str(path), user_input, name) + ) + # get the template file if template_filename is not None: # load the template file contents and substitute tokens @@ -98,15 +109,45 @@ class ScopedPageCreator(PageCreator): ) else: template_contents = "" + return ScopedPage(path, template_contents) + def _get_input_tokens(self, template_filename: str | None) -> list[str]: + path = str( + pathlib.Path( + self.base_path, self.page_path, self.filename, self.extension + ).expanduser() + ) + if template_filename is not None: + template_contents = self._load_template(self.base_path, template_filename) + else: + template_contents = "" + # get the input tokens from both the path and the template + all_input_tokens = [] + for tokenified_string in (path, template_contents): + all_input_tokens.extend(re.findall(r"(\{input\d*\})", tokenified_string)) + return sorted(all_input_tokens) + + def _verify_input_tokens(self, user_input: Tuple[str, ...] | Tuple[()]) -> None: + if len(self._all_input_tokens) != len(user_input): + logger.critical( + "Your config/template specifies %s input tokens, you gave %s", + len(self._all_input_tokens), + len(user_input), + ) + raise SystemExit(1) + def _substitute_tokens( self, tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()], name: str, ) -> str: - # for a tokened string, substitute input, name and date tokens + """For a tokened string, substitute input, name and date tokens.""" + # TODO: fn to get tokens from file and template + # tokenified_string = self.__substitute_input_tokens( + # tokenified_string, user_input, input_token_type + # ) tokenified_string = self.__substitute_input_tokens( tokenified_string, user_input ) @@ -114,6 +155,15 @@ class ScopedPageCreator(PageCreator): tokenified_string = self.__substitute_date_tokens(tokenified_string) return tokenified_string + def __substitute_input_tokens( + self, + tokenified_string: str, + user_input: Tuple[str, ...] | Tuple[()], + ) -> str: + for input_value, extracted_token in zip(user_input, self._all_input_tokens): + tokenified_string = tokenified_string.replace(extracted_token, input_value) + return tokenified_string + @staticmethod def __substitute_name_tokens(tokenified_string: str, name: str) -> str: # find any {name} tokens and substitute for the name value @@ -122,49 +172,11 @@ class ScopedPageCreator(PageCreator): tokenified_string = tokenified_string.replace(extracted_input, name) return tokenified_string - @staticmethod - def __substitute_input_tokens( - tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()] - ) -> str: - # find {inputN} tokens in string - input_extraction = re.findall(r"(\{input\d*\})", tokenified_string) - if len(user_input) == 0: - # if there's no user input, but the regex matches, raise error - if len(input_extraction) > 0: - logger.critical( - "Your config/template specifies %s input tokens, you gave 0 " - "- exiting", - len(input_extraction), - ) - raise SystemExit(1) - # if there aren't any tokens in the string, return the string - return tokenified_string - # if there is user input, check the number of tokens match the number passed in - if ( - len(input_extraction) > 0 - and len(input_extraction) != len(user_input) - and len(user_input) > 0 - ): - # if there are input matches and they don't equal the number of input - # tokens, raise error - logger.critical( - "Your config/template specifies %s input tokens, you gave %s - exiting", - len(input_extraction), - len(user_input), - ) - raise SystemExit(1) - # if the length of both the input matches and the number of tokens match then - # substitute each token with the user's input - if len(user_input) > 0: - 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 + """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 @@ -208,9 +220,10 @@ class ScopedPage(Page): if not scoped_note_file.exists(): with scoped_note_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) + logger.info("Saved %s to disk", self.path) else: logger.info("%s already exists - skipping.", self.path) - raise SystemExit(0) + raise SystemExit(0) if __name__ == "__main__": diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 317d9d6..0000000 Binary files a/tests/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc b/tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc deleted file mode 100644 index 6bfba60..0000000 Binary files a/tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc and /dev/null differ