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_source: str = 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: 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: Path or sequence of paths where template files are located. Can be a single
47 path string, PathLike object, or sequence of paths for multiple search locations.
48 template_name: The name of the template file to load from the search path.
49 Should include the file extension (e.g., "template.j2", "export.html").
50 template_source: The source code of the Jinja2 template. If provided,
51 :py:obj:`template_search_path` and :py:obj:`template_name` are ignored. Defaults to None.
52 **loader_options: Additional keyword arguments passed to the :py:obj:`~jinja2.FileSystemLoader`.
53
54 Note:
55 The exporter uses a :py:obj:`~jinja2.sandbox.SandboxedEnvironment` for security, which restricts
56 access to potentially dangerous operations in templates. Auto-reload is
57 enabled by default for development convenience.
58 """
59 self.feed = feed
60 self.template_search_path = template_search_path
61 self.template_name = template_name
62 if template_source:
63 self.environment = SandboxedEnvironment()
64 self.template = self.environment.from_string(template_source)
65 else:
66 self.loader = FileSystemLoader(self.template_search_path, **loader_options)
67 self.environment = SandboxedEnvironment(loader=self.loader, auto_reload=True)
68 self.template: Template = self.environment.get_template(self.template_name)
69
[docs]
70 def export(self, output: TextIO, **kwargs):
71 """
72 Export data by rendering the template and writing output to the provided stream.
73
74 This method uses Jinja2's streaming to render the template in chunks,
75 making it memory-efficient for large datasets. The feed data is passed to the
76 template as the 'feed' variable, and any additional keyword arguments are also
77 made available as template variables.
78
79 Args:
80 output: A text-based output stream where the rendered template
81 will be written. This can be a file object, StringIO,
82 or any object implementing the TextIO interface.
83 **kwargs: Additional keyword arguments that will be passed as variables
84 to the template context. These can be used within the template
85 to customize the output or provide additional data.
86
87 Raises:
88 ~jinja2.TemplateError: If there are errors in template syntax or rendering
89 ~jinja2.TemplateNotFound: If the specified template file cannot be found
90 IOError: If there are issues writing to the output stream
91 """
92 for chunk in self.template.stream(feed=self.feed, **kwargs):
93 output.write(chunk)