Skip to content

jinja

trestle.core.commands.author.jinja ¤

Trestle Commands.

logger ¤

Classes¤

JinjaCmd (CommandPlusDocs) ¤

Transform an input template to an output document using jinja templating.

Source code in trestle/core/commands/author/jinja.py
class JinjaCmd(CommandPlusDocs):
    """Transform an input template to an output document using jinja templating."""

    max_recursion_depth = 2

    name = 'jinja'

    def _init_arguments(self):
        self.add_argument('-i', '--input', help='Input jinja template, relative to trestle root', required=True)
        self.add_argument('-o', '--output', help='Output template, relative to trestle root.', required=True)
        self.add_argument(
            '-lut',
            '--look-up-table',
            help='Key-value pair table, stored as yaml, to be passed to jinja as variables',
            required=False
        )
        self.add_argument(
            '-elp',
            '--external-lut-prefix',
            help='Prefix paths for LUT, to maintain compatibility with other templating systems',
            required=False
        )
        self.add_argument(
            '-nc',
            '--number-captions',
            help='Add incremental numbering to table and image captions, in the form Table n - ... and Figure n - ...',
            action='store_true'
        )
        self.add_argument(
            '-pf',
            '--param-formatting',
            help='Add given text to each parameter, '
            'use dot to specify location of parameter (i.e. foo.bar will wrap param with foo and bar)',
            required=False
        )
        self.add_argument(
            '-ssp', '--system-security-plan', help='An optional SSP to be passed', default=None, required=False
        )
        self.add_argument('-p', '--profile', help='An optional profile to be passed', default=None, required=False)
        self.add_argument(
            '-dp',
            '--docs-profile',
            help='Output profile controls to separate markdown files',
            action='store_true',
            required=False
        )

    def _run(self, args: argparse.Namespace):
        try:
            log.set_log_level_from_args(args)
            logger.debug(f'Starting {self.name} command')
            input_path = pathlib.Path(args.input)
            output_path = pathlib.Path(args.output)
            if args.system_security_plan and args.docs_profile:
                raise TrestleIncorrectArgsError('Output to multiple files is possible with profile only.')

            if args.docs_profile and not args.profile:
                raise TrestleIncorrectArgsError('Profile must be provided to output to multiple files.')

            lut = {}
            if args.look_up_table:
                lut_table = pathlib.Path(args.look_up_table)
                lookup_table_path = pathlib.Path.cwd() / lut_table
                lut = JinjaCmd.load_LUT(lookup_table_path, args.external_lut_prefix)

            if args.system_security_plan:
                return JinjaCmd.jinja_ify(
                    pathlib.Path(args.trestle_root),
                    input_path,
                    output_path,
                    args.system_security_plan,
                    args.profile,
                    lut,
                    number_captions=args.number_captions,
                    parameters_formatting=args.param_formatting
                )
            elif args.profile and args.docs_profile:
                return JinjaCmd.jinja_multiple_md(
                    pathlib.Path(args.trestle_root),
                    input_path,
                    output_path,
                    args.profile,
                    lut,
                    parameters_formatting=args.param_formatting
                )

        except Exception as e:  # pragma: no cover
            return handle_generic_command_exception(e, logger, 'Error while generating markdown via Jinja template')

    @staticmethod
    def load_LUT(path: pathlib.Path, prefix: Optional[str]) -> Dict[str, Any]:  # noqa: N802
        """Load a Yaml lookup table from file."""
        yaml = YAML()
        lut = yaml.load(path.open('r', encoding=const.FILE_ENCODING))
        if prefix:
            prefixes = prefix.split('.')
            while prefixes:
                old_lut = lut
                lut[prefixes.pop(-1)] = old_lut

        return lut

    @staticmethod
    def jinja_ify(
        trestle_root: pathlib.Path,
        r_input_file: pathlib.Path,
        r_output_file: pathlib.Path,
        ssp: Optional[str],
        profile: Optional[str],
        lut: Dict[str, Any],
        number_captions: Optional[bool] = False,
        parameters_formatting: Optional[str] = None
    ) -> int:
        """Run jinja over an input file with additional booleans."""
        template_folder = pathlib.Path.cwd()
        jinja_env = Environment(
            loader=FileSystemLoader(template_folder),
            extensions=[MDSectionInclude, MDCleanInclude, MDDatestamp],
            trim_blocks=True,
            autoescape=True
        )
        template = jinja_env.get_template(str(r_input_file))
        # create boolean dict
        if operator.xor(bool(ssp), bool(profile)):
            raise TrestleIncorrectArgsError('Both SSP and profile should be provided or not at all')

        if ssp:
            # name lookup
            ssp_data, _ = load_validate_model_name(trestle_root, ssp, SystemSecurityPlan)
            lut['ssp'] = ssp_data
            profile_path = ModelUtils.full_path_for_top_level_model(trestle_root, profile, Profile)
            profile_resolver = ProfileResolver()
            resolved_catalog = profile_resolver.get_resolved_profile_catalog(
                trestle_root, profile_path, False, False, parameters_formatting, ParameterRep.ASSIGNMENT_FORM
            )

            ssp_writer = SSPMarkdownWriter(trestle_root)
            ssp_writer.set_ssp(ssp_data)
            ssp_writer.set_catalog(resolved_catalog)
            lut['catalog'] = resolved_catalog
            lut['catalog_interface'] = CatalogInterface(resolved_catalog)
            lut['control_interface'] = ControlInterface()
            lut['control_writer'] = DocsControlWriter()
            lut['ssp_md_writer'] = ssp_writer

            output = JinjaCmd.render_template(template, lut, template_folder)

            output_file = trestle_root / r_output_file
            if number_captions:
                output_file.open('w', encoding=const.FILE_ENCODING).write(_number_captions(output))
            else:
                output_file.open('w', encoding=const.FILE_ENCODING).write(output)

            return CmdReturnCodes.SUCCESS.value

    @staticmethod
    def jinja_multiple_md(
        trestle_root: pathlib.Path,
        r_input_file: pathlib.Path,
        r_output_file: pathlib.Path,
        profile_name: Optional[str],
        lut: Dict[str, Any],
        parameters_formatting: Optional[str] = None
    ) -> int:
        """Output profile as multiple markdown files using Jinja."""
        template_folder = pathlib.Path.cwd()

        # Output to multiple markdown files
        profile, profile_path = ModelUtils.load_top_level_model(trestle_root, profile_name, Profile)
        profile_resolver = ProfileResolver()
        resolved_catalog = profile_resolver.get_resolved_profile_catalog(
            trestle_root, profile_path, False, False, parameters_formatting, ParameterRep.ASSIGNMENT_FORM
        )
        catalog_interface = CatalogInterface(resolved_catalog)

        # Generate a single markdown page for each control per each group
        for group in catalog_interface.get_all_groups_from_catalog():
            for control in catalog_interface.get_sorted_controls_in_group(group.id):
                _, group_title, _ = catalog_interface.get_group_info_by_control(control.id)
                group_dir = r_output_file
                control_path = catalog_interface.get_control_path(control.id)
                for sub_dir in control_path:
                    group_dir = group_dir / sub_dir
                    if not group_dir.exists():
                        group_dir.mkdir(parents=True, exist_ok=True)

                control_writer = DocsControlWriter()

                jinja_env = Environment(
                    loader=FileSystemLoader(template_folder),
                    extensions=[MDSectionInclude, MDCleanInclude, MDDatestamp],
                    trim_blocks=True,
                    autoescape=True
                )
                template = jinja_env.get_template(str(r_input_file))
                lut['catalog_interface'] = catalog_interface
                lut['control_interface'] = ControlInterface()
                lut['control_writer'] = control_writer
                lut['control'] = control
                lut['profile'] = profile
                lut['group_title'] = group_title
                output = JinjaCmd.render_template(template, lut, template_folder)

                output_file = trestle_root / group_dir / pathlib.Path(control.id + const.MARKDOWN_FILE_EXT)
                output_file.open('w', encoding=const.FILE_ENCODING).write(output)

        return CmdReturnCodes.SUCCESS.value

    @staticmethod
    def render_template(template: Template, lut: Dict[str, Any], template_folder: pathlib.Path) -> str:
        """Render template."""
        new_output = template.render(**lut)
        output = ''
        # This recursion allows nesting within expressions (e.g. an expression can contain jinja templates).
        error_countdown = JinjaCmd.max_recursion_depth
        while new_output != output and error_countdown > 0:
            error_countdown = error_countdown - 1
            output = new_output
            random_name = uuid.uuid4()  # Should be random and not used.
            dict_loader = DictLoader({str(random_name): new_output})
            jinja_env = Environment(
                loader=ChoiceLoader([dict_loader, FileSystemLoader(template_folder)]),
                extensions=[MDCleanInclude, MDSectionInclude, MDDatestamp],
                autoescape=True,
                trim_blocks=True
            )
            template = jinja_env.get_template(str(random_name))
            new_output = template.render(**lut)

        return output
max_recursion_depth ¤
name ¤
Methods¤
jinja_ify(trestle_root, r_input_file, r_output_file, ssp, profile, lut, number_captions=False, parameters_formatting=None) staticmethod ¤

Run jinja over an input file with additional booleans.

Source code in trestle/core/commands/author/jinja.py
@staticmethod
def jinja_ify(
    trestle_root: pathlib.Path,
    r_input_file: pathlib.Path,
    r_output_file: pathlib.Path,
    ssp: Optional[str],
    profile: Optional[str],
    lut: Dict[str, Any],
    number_captions: Optional[bool] = False,
    parameters_formatting: Optional[str] = None
) -> int:
    """Run jinja over an input file with additional booleans."""
    template_folder = pathlib.Path.cwd()
    jinja_env = Environment(
        loader=FileSystemLoader(template_folder),
        extensions=[MDSectionInclude, MDCleanInclude, MDDatestamp],
        trim_blocks=True,
        autoescape=True
    )
    template = jinja_env.get_template(str(r_input_file))
    # create boolean dict
    if operator.xor(bool(ssp), bool(profile)):
        raise TrestleIncorrectArgsError('Both SSP and profile should be provided or not at all')

    if ssp:
        # name lookup
        ssp_data, _ = load_validate_model_name(trestle_root, ssp, SystemSecurityPlan)
        lut['ssp'] = ssp_data
        profile_path = ModelUtils.full_path_for_top_level_model(trestle_root, profile, Profile)
        profile_resolver = ProfileResolver()
        resolved_catalog = profile_resolver.get_resolved_profile_catalog(
            trestle_root, profile_path, False, False, parameters_formatting, ParameterRep.ASSIGNMENT_FORM
        )

        ssp_writer = SSPMarkdownWriter(trestle_root)
        ssp_writer.set_ssp(ssp_data)
        ssp_writer.set_catalog(resolved_catalog)
        lut['catalog'] = resolved_catalog
        lut['catalog_interface'] = CatalogInterface(resolved_catalog)
        lut['control_interface'] = ControlInterface()
        lut['control_writer'] = DocsControlWriter()
        lut['ssp_md_writer'] = ssp_writer

        output = JinjaCmd.render_template(template, lut, template_folder)

        output_file = trestle_root / r_output_file
        if number_captions:
            output_file.open('w', encoding=const.FILE_ENCODING).write(_number_captions(output))
        else:
            output_file.open('w', encoding=const.FILE_ENCODING).write(output)

        return CmdReturnCodes.SUCCESS.value
jinja_multiple_md(trestle_root, r_input_file, r_output_file, profile_name, lut, parameters_formatting=None) staticmethod ¤

Output profile as multiple markdown files using Jinja.

Source code in trestle/core/commands/author/jinja.py
@staticmethod
def jinja_multiple_md(
    trestle_root: pathlib.Path,
    r_input_file: pathlib.Path,
    r_output_file: pathlib.Path,
    profile_name: Optional[str],
    lut: Dict[str, Any],
    parameters_formatting: Optional[str] = None
) -> int:
    """Output profile as multiple markdown files using Jinja."""
    template_folder = pathlib.Path.cwd()

    # Output to multiple markdown files
    profile, profile_path = ModelUtils.load_top_level_model(trestle_root, profile_name, Profile)
    profile_resolver = ProfileResolver()
    resolved_catalog = profile_resolver.get_resolved_profile_catalog(
        trestle_root, profile_path, False, False, parameters_formatting, ParameterRep.ASSIGNMENT_FORM
    )
    catalog_interface = CatalogInterface(resolved_catalog)

    # Generate a single markdown page for each control per each group
    for group in catalog_interface.get_all_groups_from_catalog():
        for control in catalog_interface.get_sorted_controls_in_group(group.id):
            _, group_title, _ = catalog_interface.get_group_info_by_control(control.id)
            group_dir = r_output_file
            control_path = catalog_interface.get_control_path(control.id)
            for sub_dir in control_path:
                group_dir = group_dir / sub_dir
                if not group_dir.exists():
                    group_dir.mkdir(parents=True, exist_ok=True)

            control_writer = DocsControlWriter()

            jinja_env = Environment(
                loader=FileSystemLoader(template_folder),
                extensions=[MDSectionInclude, MDCleanInclude, MDDatestamp],
                trim_blocks=True,
                autoescape=True
            )
            template = jinja_env.get_template(str(r_input_file))
            lut['catalog_interface'] = catalog_interface
            lut['control_interface'] = ControlInterface()
            lut['control_writer'] = control_writer
            lut['control'] = control
            lut['profile'] = profile
            lut['group_title'] = group_title
            output = JinjaCmd.render_template(template, lut, template_folder)

            output_file = trestle_root / group_dir / pathlib.Path(control.id + const.MARKDOWN_FILE_EXT)
            output_file.open('w', encoding=const.FILE_ENCODING).write(output)

    return CmdReturnCodes.SUCCESS.value
load_LUT(path, prefix) staticmethod ¤

Load a Yaml lookup table from file.

Source code in trestle/core/commands/author/jinja.py
@staticmethod
def load_LUT(path: pathlib.Path, prefix: Optional[str]) -> Dict[str, Any]:  # noqa: N802
    """Load a Yaml lookup table from file."""
    yaml = YAML()
    lut = yaml.load(path.open('r', encoding=const.FILE_ENCODING))
    if prefix:
        prefixes = prefix.split('.')
        while prefixes:
            old_lut = lut
            lut[prefixes.pop(-1)] = old_lut

    return lut
render_template(template, lut, template_folder) staticmethod ¤

Render template.

Source code in trestle/core/commands/author/jinja.py
@staticmethod
def render_template(template: Template, lut: Dict[str, Any], template_folder: pathlib.Path) -> str:
    """Render template."""
    new_output = template.render(**lut)
    output = ''
    # This recursion allows nesting within expressions (e.g. an expression can contain jinja templates).
    error_countdown = JinjaCmd.max_recursion_depth
    while new_output != output and error_countdown > 0:
        error_countdown = error_countdown - 1
        output = new_output
        random_name = uuid.uuid4()  # Should be random and not used.
        dict_loader = DictLoader({str(random_name): new_output})
        jinja_env = Environment(
            loader=ChoiceLoader([dict_loader, FileSystemLoader(template_folder)]),
            extensions=[MDCleanInclude, MDSectionInclude, MDDatestamp],
            autoescape=True,
            trim_blocks=True
        )
        template = jinja_env.get_template(str(random_name))
        new_output = template.render(**lut)

    return output

handler: python