diff --git a/CHANGES.rst b/CHANGES.rst index 8e69e64..513eddb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +0.14.0 (2021-09-30) +------------------- +- One can now use python -m docxtpl on command line + to generate a docx from a template and a json file as a context + Thanks to Lcrs123@github + 0.12.0 (2021-08-15) ------------------- - Code has be split into many files for better readability diff --git a/docs/index.rst b/docs/index.rst index 0c328c3..900c460 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -387,6 +387,30 @@ Then in your template, you will be able to use:: Euros price : {{ price_dollars|multiply_by(0.88) }} + +Command-line execution +---------------------- + +One can use `docxtpl` module directly on command line to generate a docx from a template and a json file as a context:: + + usage: python -m docxtpl [-h] [-o] [-q] template_path json_path output_filename + + Make docx file from existing template docx and json data. + + positional arguments: + template_path The path to the template docx file. + json_path The path to the json file with the data. + output_filename The filename to save the generated docx. + + optional arguments: + -h, --help show this help message and exit + -o, --overwrite If output file already exists, overwrites without asking + for confirmation + -q, --quiet Do not display unnecessary messages + + +See tests/module_execute.py for an example. + Examples -------- diff --git a/docxtpl/__init__.py b/docxtpl/__init__.py index 280de22..563b398 100644 --- a/docxtpl/__init__.py +++ b/docxtpl/__init__.py @@ -4,7 +4,7 @@ Created : 2015-03-12 @author: Eric Lapouyade """ -__version__ = '0.12.0' +__version__ = '0.14.0' # flake8: noqa from .inline_image import InlineImage diff --git a/docxtpl/__main__.py b/docxtpl/__main__.py new file mode 100644 index 0000000..0bc685c --- /dev/null +++ b/docxtpl/__main__.py @@ -0,0 +1,147 @@ +import argparse, json +from pathlib import Path + +from .template import DocxTemplate, TemplateError + +TEMPLATE_ARG = 'template_path' +JSON_ARG = 'json_path' +OUTPUT_ARG = 'output_filename' +OVERWRITE_ARG = 'overwrite' +QUIET_ARG = 'quiet' + + +def make_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + usage=f'python -m docxtpl [-h] [-o] [-q] {TEMPLATE_ARG} {JSON_ARG} {OUTPUT_ARG}', + description='Make docx file from existing template docx and json data.') + parser.add_argument(TEMPLATE_ARG, + type=str, + help='The path to the template docx file.') + parser.add_argument(JSON_ARG, + type=str, + help='The path to the json file with the data.') + parser.add_argument(OUTPUT_ARG, + type=str, + help='The filename to save the generated docx.') + parser.add_argument('-' + OVERWRITE_ARG[0], '--' + OVERWRITE_ARG, + action='store_true', + help='If output file already exists, overwrites without asking for confirmation') + parser.add_argument('-' + QUIET_ARG[0], '--' + QUIET_ARG, + action='store_true', + help='Do not display unnecessary messages') + return parser + + +def get_args(parser: argparse.ArgumentParser) -> dict: + try: + parsed_args = vars(parser.parse_args()) + return parsed_args + # Argument errors raise a SystemExit with code 2. Normal usage of the + # --help or -h flag raises a SystemExit with code 0. + except SystemExit as e: + if e.code == 0: + raise SystemExit from e + else: + raise RuntimeError(f'Correct usage is:\n{parser.usage}') from e + + +def is_argument_valid(arg_name: str, arg_value: str,overwrite: bool) -> bool: + # Basic checks for the arguments + if arg_name == TEMPLATE_ARG: + return Path(arg_value).is_file() and arg_value.endswith('.docx') + elif arg_name == JSON_ARG: + return Path(arg_value).is_file() and arg_value.endswith('.json') + elif arg_name == OUTPUT_ARG: + return arg_value.endswith('.docx') and check_exists_ask_overwrite( + arg_value, overwrite) + elif arg_name in [OVERWRITE_ARG, QUIET_ARG]: + return arg_value in [True, False] + + +def check_exists_ask_overwrite(arg_value:str, overwrite: bool) -> bool: + # If output file does not exist or command was run with overwrite option, + # returns True, else asks for overwrite confirmation. If overwrite is + # confirmed returns True, else raises FileExistsError. + if Path(arg_value).exists() and not overwrite: + try: + if input(f'File {arg_value} already exists, would you like to overwrite the existing file? (y/n)').lower() == 'y': + return True + else: + raise FileExistsError + except FileExistsError as e: + raise RuntimeError(f'File {arg_value} already exists, please choose a different name.') from e + else: + return True + + +def validate_all_args(parsed_args:dict) -> None: + overwrite = parsed_args[OVERWRITE_ARG] + # Raises AssertionError if any of the arguments is not validated + try: + for arg_name, arg_value in parsed_args.items(): + if not is_argument_valid(arg_name, arg_value,overwrite): + raise AssertionError + except AssertionError as e: + raise RuntimeError( + f'The specified {arg_name} "{arg_value}" is not valid.') from e + + +def get_json_data(json_path: Path) -> dict: + with open(json_path) as file: + try: + json_data = json.load(file) + return json_data + except json.JSONDecodeError as e: + print( + f'There was an error on line {e.lineno}, column {e.colno} while trying to parse file {json_path}') + raise RuntimeError('Failed to get json data.') from e + + +def make_docxtemplate(template_path: Path) -> DocxTemplate: + try: + return DocxTemplate(template_path) + except TemplateError as e: + raise RuntimeError('Could not create docx template.') from e + + +def render_docx(doc:DocxTemplate, json_data: dict) -> DocxTemplate: + try: + doc.render(json_data) + return doc + except TemplateError as e: + raise RuntimeError(f'An error ocurred while trying to render the docx') from e + + +def save_file(doc: DocxTemplate, parsed_args: dict) -> None: + try: + output_path = parsed_args[OUTPUT_ARG] + doc.save(output_path) + if not parsed_args[QUIET_ARG]: + print(f'Document successfully generated and saved at {output_path}') + except PermissionError as e: + print(f'{e.strerror}. Could not save file {e.filename}.') + raise RuntimeError('Failed to save file.') from e + + +def main() -> None: + parser = make_arg_parser() + # Everything is in a try-except block that cacthes a RuntimeError that is + # raised if any of the individual functions called cause an error + # themselves, terminating the main function. + parsed_args = get_args(parser) + try: + validate_all_args(parsed_args) + json_data = get_json_data(Path(parsed_args[JSON_ARG]).resolve()) + doc = make_docxtemplate(Path(parsed_args[TEMPLATE_ARG]).resolve()) + doc = render_docx(doc,json_data) + save_file(doc, parsed_args) + except RuntimeError as e: + print('Error: '+e.__str__()) + return + finally: + if not parsed_args[QUIET_ARG]: + print('Exiting program!') + + +if __name__ == '__main__': + main() diff --git a/tests/module_execute.py b/tests/module_execute.py new file mode 100644 index 0000000..f33f611 --- /dev/null +++ b/tests/module_execute.py @@ -0,0 +1,22 @@ +import sys, os +from pathlib import Path + +TEMPLATE_PATH = 'templates/module_execute_tpl.docx' +JSON_PATH = 'templates/module_execute.json' +OUTPUT_FILENAME = 'output/module_execute.docx' +OVERWRITE = '-o' +QUIET = '-q' + + +output_path = Path(OUTPUT_FILENAME) +if output_path.exists(): + output_path.unlink() + +os.chdir(Path(__file__).parent) +cmd = f'python -m docxtpl {TEMPLATE_PATH} {JSON_PATH} {OUTPUT_FILENAME} {OVERWRITE} {QUIET}' +print(f'Executing "{cmd}" ...') +os.system(cmd) + +if output_path.exists(): + print(f' --> File {output_path.resolve()} has been generated.') + diff --git a/tests/templates/module_execute.json b/tests/templates/module_execute.json new file mode 100644 index 0000000..535e175 --- /dev/null +++ b/tests/templates/module_execute.json @@ -0,0 +1,8 @@ +{"json_dict_var" : {"json_dict_var":"successfully inserted"}, +"json_array_var": ["json","array","var","successfully", "inserted"], +"json_string_var":"json_string_var successfully inserted", +"json_int_var":123, +"json_float_var":1.234, +"json_true_var":true, +"json_false_var":false, +"json_none_var":null} \ No newline at end of file diff --git a/tests/templates/module_execute_tpl.docx b/tests/templates/module_execute_tpl.docx new file mode 100644 index 0000000..88b6250 Binary files /dev/null and b/tests/templates/module_execute_tpl.docx differ