1import os
2from typing import TextIO, Sequence
3
4from jinja2 import FileSystemLoader, Template
5from jinja2.sandbox import SandboxedEnvironment
6
7from colander_data_converter.base.models import ColanderFeed
8from colander_data_converter.exporters.exporter import BaseExporter
9
10
[docs]
11class TemplateExporter(BaseExporter):
12 """
13 Template-based exporter using Jinja2_ templating engine.
14
15 This exporter allows for flexible data export by using Jinja2_ templates to format
16 the output. It supports both file-based templates loaded from the filesystem and
17 pre-compiled :py:obj:`~jinja2.Template` object. The implementation uses a sandboxed environment
18 for security when processing templates.
19
20 The exporter streams the template output, making it memory-efficient for large
21 datasets by processing data in chunks rather than loading everything into memory.
22
23 .. _Jinja2: https://jinja.palletsprojects.com/
24
25 """
26
[docs]
27 def __init__(
28 self,
29 feed: ColanderFeed,
30 template_search_path: str | os.PathLike[str] | Sequence[str | os.PathLike[str]],
31 template_name: str,
32 template: Template = None,
33 **loader_options,
34 ):
35 """
36 Initialize the TemplateExporter with feed data and template configuration.
37
38 This constructor sets up the Jinja2 templating environment and loads the specified
39 template. If a pre-compiled :py:obj:`~jinja2.Template` object is provided, it will be used directly.
40 Otherwise, the template will be loaded from the filesystem using the provided
41 search path and template name.
42
43 Args:
44 feed (~colander_data_converter.base.models.ColanderFeed): The data feed containing entities to
45 be exported. This feed will be passed to the template as the :py:obj:`feed` variable.
46 template_search_path (str | os.PathLike[str] | Sequence[str | os.PathLike[str]]):
47 Path or sequence of paths where template files are located. Can be a single
48 path string, PathLike object, or sequence of paths for multiple search locations.
49 template_name (str): The name of the template file to load from the search path.
50 Should include the file extension (e.g., "template.j2", "export.html").
51 template (~jinja2.Template): A pre-compiled Jinja2 Template object. If provided,
52 :py:obj:`template_search_path` and :py:obj:`template_name` are ignored. Defaults to None.
53 **loader_options: Additional keyword arguments passed to the :py:obj:`~jinja2.FileSystemLoader`.
54
55 Note:
56 The exporter uses a :py:obj:`~jinja2.sandbox.SandboxedEnvironment` for security, which restricts
57 access to potentially dangerous operations in templates. Auto-reload is
58 enabled by default for development convenience.
59
60 Warning:
61 When a pre-compiled Template object is provided via the :py:obj:`template` parameter,
62 it will NOT be executed in a sandboxed environment. This means the template
63 can access all Python built-ins and potentially execute dangerous operations.
64 Only use trusted templates when providing pre-compiled Template objects.
65 """
66 self.feed = feed
67 self.template = template
68 if not self.template:
69 self.template_search_path = template_search_path
70 self.template_name = template_name
71 self.loader = FileSystemLoader(self.template_search_path, **loader_options)
72 self.environment = SandboxedEnvironment(loader=self.loader, auto_reload=True)
73 self.template: Template = self.environment.get_template(self.template_name)
74
[docs]
75 def export(self, output: TextIO, **kwargs):
76 """
77 Export data by rendering the template and writing output to the provided stream.
78
79 This method uses Jinja2's streaming to render the template in chunks,
80 making it memory-efficient for large datasets. The feed data is passed to the
81 template as the 'feed' variable, and any additional keyword arguments are also
82 made available as template variables.
83
84 Args:
85 output (io.TextIO): A text-based output stream where the rendered template
86 will be written. This can be a file object, StringIO,
87 or any object implementing the TextIO interface.
88 **kwargs: Additional keyword arguments that will be passed as variables
89 to the template context. These can be used within the template
90 to customize the output or provide additional data.
91
92 Raises:
93 :py:obj:`jinja2.TemplateError`: If there are errors in template syntax or rendering
94 :py:obj:`jinja2.TemplateNotFound`: If the specified template file cannot be found
95 IOError: If there are issues writing to the output stream
96
97 Warning:
98 If this exporter was initialized with a pre-compiled Template object,
99 the template will NOT execute in a sandboxed environment and may have
100 access to dangerous Python operations. Ensure only trusted templates
101 are used in such cases.
102 """
103 for chunk in self.template.stream(feed=self.feed, **kwargs):
104 output.write(chunk)