templify-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- templify/__init__.py +1 -0
- templify/cli/__init__.py +1 -0
- templify/cli/dependencies/__init__.py +1 -0
- templify/cli/dependencies/console.py +9 -0
- templify/cli/dependencies/data.py +196 -0
- templify/cli/dependencies/template.py +96 -0
- templify/cli/help.py +61 -0
- templify/cli/main.py +165 -0
- templify/filter_rules.py +366 -0
- templify/models.py +9 -0
- templify/py.typed +0 -0
- templify/readers.py +148 -0
- templify/transformers.py +120 -0
- templify/writers.py +312 -0
- templify_cli-0.1.0.dist-info/METADATA +980 -0
- templify_cli-0.1.0.dist-info/RECORD +19 -0
- templify_cli-0.1.0.dist-info/WHEEL +4 -0
- templify_cli-0.1.0.dist-info/entry_points.txt +3 -0
- templify_cli-0.1.0.dist-info/licenses/LICENSE +232 -0
templify/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""templify: fill document templates (docx, txt, md, html, json) with data (xlsx, json)."""
|
templify/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The templify command line interface."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Typer dependencies shared by the commands, injected with typer-di."""
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The rich console used for command output."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from typer_di import Depends
|
|
7
|
+
|
|
8
|
+
ConsoleDI = Annotated[Console, Depends(lambda: Console(color_system="truecolor"))]
|
|
9
|
+
"""Injects a truecolor rich console."""
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""The data pipeline options: `--data`, `--order`, `--filter` and `--group-by`."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from zipfile import BadZipFile
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from pyspecification import (
|
|
10
|
+
ArgumentError,
|
|
11
|
+
ExpressionWrapperDict,
|
|
12
|
+
Predicate,
|
|
13
|
+
PredicateCompiler,
|
|
14
|
+
PredicateDict,
|
|
15
|
+
RuleDoesNotExistError,
|
|
16
|
+
RuleKeyDoesNotExistError,
|
|
17
|
+
)
|
|
18
|
+
from typer_di import Depends
|
|
19
|
+
|
|
20
|
+
from templify.filter_rules import rules
|
|
21
|
+
from templify.models import Data
|
|
22
|
+
from templify.readers import readers
|
|
23
|
+
from templify.transformers import OrderByKey, filter_data, group_by_data, order_by_data
|
|
24
|
+
|
|
25
|
+
# -----------------------
|
|
26
|
+
# utils
|
|
27
|
+
# -----------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_extension(file: Path) -> str:
|
|
31
|
+
return file.suffix.lstrip(".")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# -----------------------
|
|
35
|
+
# read data
|
|
36
|
+
# -----------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check_if_extension_is_supported(file: Path) -> Path:
|
|
40
|
+
extension = _extract_extension(file)
|
|
41
|
+
|
|
42
|
+
if extension not in readers:
|
|
43
|
+
message = f"Extension {extension} is not supported"
|
|
44
|
+
raise typer.BadParameter(message)
|
|
45
|
+
|
|
46
|
+
return file
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read(
|
|
50
|
+
file: Annotated[
|
|
51
|
+
Path,
|
|
52
|
+
typer.Option(
|
|
53
|
+
"-d",
|
|
54
|
+
"--data",
|
|
55
|
+
help="Path to the data file",
|
|
56
|
+
exists=True,
|
|
57
|
+
file_okay=True,
|
|
58
|
+
dir_okay=False,
|
|
59
|
+
resolve_path=True,
|
|
60
|
+
callback=_check_if_extension_is_supported,
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
) -> Data:
|
|
64
|
+
try:
|
|
65
|
+
return readers[_extract_extension(file)](file)
|
|
66
|
+
except (ValueError, BadZipFile) as e:
|
|
67
|
+
msg = f"Could not read data file '{file.name}': {e}"
|
|
68
|
+
raise typer.BadParameter(msg) from e
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# -----------------------
|
|
72
|
+
# order data
|
|
73
|
+
# -----------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_order_by_keys(
|
|
77
|
+
keys: Annotated[
|
|
78
|
+
list[str],
|
|
79
|
+
typer.Option(
|
|
80
|
+
"--order",
|
|
81
|
+
help="Columns to order by",
|
|
82
|
+
default_factory=list,
|
|
83
|
+
rich_help_panel="Data Options",
|
|
84
|
+
),
|
|
85
|
+
],
|
|
86
|
+
) -> list[OrderByKey]:
|
|
87
|
+
return [OrderByKey(key) for key in keys]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _order(
|
|
91
|
+
data: Annotated[Data, Depends(_read)],
|
|
92
|
+
keys: Annotated[list[OrderByKey], Depends(_get_order_by_keys)],
|
|
93
|
+
) -> Data:
|
|
94
|
+
try:
|
|
95
|
+
return order_by_data(data, keys) if keys else data
|
|
96
|
+
except KeyError as e:
|
|
97
|
+
raise typer.BadParameter(str(e)) from e
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# -----------------------
|
|
101
|
+
# filter data
|
|
102
|
+
# -----------------------
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _get_compiler() -> PredicateCompiler:
|
|
106
|
+
return PredicateCompiler(
|
|
107
|
+
rules.rules,
|
|
108
|
+
lambda schema: Predicate(
|
|
109
|
+
lambda _: schema["operator"] == "all",
|
|
110
|
+
operator="logical",
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_filter_schema_dict(
|
|
116
|
+
json_string: Annotated[
|
|
117
|
+
str | None,
|
|
118
|
+
typer.Option(
|
|
119
|
+
"--filter",
|
|
120
|
+
help="JSON string containing the filter schema",
|
|
121
|
+
rich_help_panel="Data Options",
|
|
122
|
+
),
|
|
123
|
+
] = None,
|
|
124
|
+
) -> ExpressionWrapperDict | PredicateDict | None:
|
|
125
|
+
if json_string is None:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
return json.loads(json_string)
|
|
130
|
+
except json.JSONDecodeError as e:
|
|
131
|
+
raise typer.BadParameter(str(e)) from e
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _filter(
|
|
135
|
+
data: Annotated[Data, Depends(_order)],
|
|
136
|
+
compiler: Annotated[PredicateCompiler, Depends(_get_compiler)],
|
|
137
|
+
schema: Annotated[
|
|
138
|
+
ExpressionWrapperDict | PredicateDict | None, Depends(_get_filter_schema_dict)
|
|
139
|
+
],
|
|
140
|
+
) -> Data:
|
|
141
|
+
if schema is None:
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
return filter_data(data, compiler, schema)
|
|
146
|
+
except (RuleKeyDoesNotExistError, RuleDoesNotExistError, ArgumentError) as e:
|
|
147
|
+
raise typer.BadParameter(str(e)) from e
|
|
148
|
+
except (TypeError, ValueError, AttributeError) as e:
|
|
149
|
+
msg = f"Filter failed on a data value (empty cell or wrong type?): {e}"
|
|
150
|
+
raise typer.BadParameter(msg) from e
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# -----------------------
|
|
154
|
+
# group data
|
|
155
|
+
# -----------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _group(
|
|
159
|
+
data: Annotated[Data, Depends(_filter)],
|
|
160
|
+
group_keys: Annotated[
|
|
161
|
+
list[str],
|
|
162
|
+
typer.Option(
|
|
163
|
+
"--group-by",
|
|
164
|
+
help="Columns to group by, each group holds its rows under --group-items",
|
|
165
|
+
default_factory=list,
|
|
166
|
+
rich_help_panel="Data Options",
|
|
167
|
+
),
|
|
168
|
+
],
|
|
169
|
+
group_items_key: Annotated[
|
|
170
|
+
str,
|
|
171
|
+
typer.Option(
|
|
172
|
+
"--group-items",
|
|
173
|
+
help="Name of the variable that holds each group rows",
|
|
174
|
+
rich_help_panel="Data Options",
|
|
175
|
+
),
|
|
176
|
+
] = "items",
|
|
177
|
+
) -> Data:
|
|
178
|
+
if not group_keys:
|
|
179
|
+
return data
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
return group_by_data(data, group_keys, group_items_key)
|
|
183
|
+
except (KeyError, ValueError) as e:
|
|
184
|
+
raise typer.BadParameter(str(e)) from e
|
|
185
|
+
except TypeError as e:
|
|
186
|
+
msg = f"Can not group by unhashable values (like lists or objects): {e}"
|
|
187
|
+
raise typer.BadParameter(msg) from e
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# -----------------------
|
|
191
|
+
# DI
|
|
192
|
+
# -----------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
DataDI = Annotated[Data, Depends(_group)]
|
|
196
|
+
"""Injects the records read from `--data`, then ordered, filtered and grouped."""
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""The template and output options: `--template` and `--output`."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from jinja2 import TemplateError
|
|
9
|
+
from typer_di import Depends
|
|
10
|
+
|
|
11
|
+
from templify.writers import Writer, writers
|
|
12
|
+
|
|
13
|
+
# -----------------------
|
|
14
|
+
# template
|
|
15
|
+
# -----------------------
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Template:
|
|
20
|
+
"""A template file and the writer chosen from its extension."""
|
|
21
|
+
|
|
22
|
+
path: Path
|
|
23
|
+
writer: Writer
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def extension(self) -> str:
|
|
27
|
+
"""The template extension, lowercase and without the dot: `docx`."""
|
|
28
|
+
return self.path.suffix.lstrip(".").lower()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _check_if_extension_is_supported(file: Path) -> Path:
|
|
32
|
+
extension = file.suffix.lstrip(".")
|
|
33
|
+
|
|
34
|
+
if extension not in writers:
|
|
35
|
+
supported = ", ".join(writers.extensions)
|
|
36
|
+
message = f"Template extension '{extension}' is not supported, supported: {supported}"
|
|
37
|
+
raise typer.BadParameter(message)
|
|
38
|
+
|
|
39
|
+
return file
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_template(
|
|
43
|
+
template_path: Annotated[
|
|
44
|
+
Path,
|
|
45
|
+
typer.Option(
|
|
46
|
+
"-t",
|
|
47
|
+
"--template",
|
|
48
|
+
help=f"Path to the template file ({', '.join(writers.extensions)})",
|
|
49
|
+
exists=True,
|
|
50
|
+
file_okay=True,
|
|
51
|
+
dir_okay=False,
|
|
52
|
+
resolve_path=True,
|
|
53
|
+
callback=_check_if_extension_is_supported,
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
) -> Template:
|
|
57
|
+
try:
|
|
58
|
+
return Template(path=template_path, writer=writers.create(template_path))
|
|
59
|
+
except TemplateError as e:
|
|
60
|
+
msg = f"Template Error: {e}"
|
|
61
|
+
raise typer.BadParameter(msg) from e
|
|
62
|
+
except Exception as e:
|
|
63
|
+
msg = f"Could not load template '{template_path.name}': {e}"
|
|
64
|
+
raise typer.BadParameter(msg) from e
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# -----------------------
|
|
68
|
+
# output
|
|
69
|
+
# -----------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_output_dir(
|
|
73
|
+
output_path: Annotated[
|
|
74
|
+
Path,
|
|
75
|
+
typer.Option(
|
|
76
|
+
"-o",
|
|
77
|
+
"--output",
|
|
78
|
+
help="Path to the output directory",
|
|
79
|
+
file_okay=False,
|
|
80
|
+
dir_okay=True,
|
|
81
|
+
resolve_path=True,
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
) -> Path:
|
|
85
|
+
return output_path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# -----------------------
|
|
89
|
+
# DI
|
|
90
|
+
# -----------------------
|
|
91
|
+
|
|
92
|
+
TemplateDI = Annotated[Template, Depends(_get_template)]
|
|
93
|
+
"""Injects the `--template` file with the writer matching its extension."""
|
|
94
|
+
|
|
95
|
+
OutputDirDI = Annotated[Path, Depends(_get_output_dir)]
|
|
96
|
+
"""Injects the `--output` directory."""
|
templify/cli/help.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Long help texts of the commands."""
|
|
2
|
+
|
|
3
|
+
SINGLE_TEMPLATE_HELP_TEXT = (
|
|
4
|
+
"Generate single template file (docx, txt, md, html, json, ...)\n"
|
|
5
|
+
"\n"
|
|
6
|
+
"Usage:\n"
|
|
7
|
+
"\n"
|
|
8
|
+
f"| {'name':<10} | {'age':<4}\n"
|
|
9
|
+
"-------------------\n"
|
|
10
|
+
f"| {'Abdullah':<10} | {'32':<4}\n"
|
|
11
|
+
f"| {'Rawaa':<10} | {'33':<4}\n"
|
|
12
|
+
f"| {'Rama':<10} | {'25':<4}\n"
|
|
13
|
+
"\n"
|
|
14
|
+
"Template:\n"
|
|
15
|
+
"\n"
|
|
16
|
+
"{% for person in data %}\n"
|
|
17
|
+
"Hello my name is {{ person['name'] }}, my age is {{ person['age'] }}\n"
|
|
18
|
+
"{{ page_break }}\n"
|
|
19
|
+
"{% endfor %}\n"
|
|
20
|
+
"\n"
|
|
21
|
+
"Command:\n"
|
|
22
|
+
"\n"
|
|
23
|
+
"templify s \\ \n"
|
|
24
|
+
"-t <your_path_to_template_file> \\ \n"
|
|
25
|
+
"-d <your_path_to_data_file> \\ \n"
|
|
26
|
+
"-o <your_path_to_output_directory> \n"
|
|
27
|
+
"\n"
|
|
28
|
+
"it will generate a single file with three pages like this:\n"
|
|
29
|
+
"\n"
|
|
30
|
+
"Page 1: Hello my name is Abdullah, my age is 32\n"
|
|
31
|
+
"Page 2: Hello my name is Rawaa, my age is 33\n"
|
|
32
|
+
"Page 3: Hello my name is Rama, my age is 25\n"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
MULTIPLE_TEMPLATES_HELP_TEXT = (
|
|
36
|
+
"Generate multiple template files (docx, txt, md, html, json, ...)\n"
|
|
37
|
+
"\n"
|
|
38
|
+
"Usage:\n"
|
|
39
|
+
"\n"
|
|
40
|
+
f"| {'name':<10} | {'age':<4}\n"
|
|
41
|
+
"-------------------\n"
|
|
42
|
+
f"| {'Abdullah':<10} | {'32':<4}\n"
|
|
43
|
+
f"| {'Rawaa':<10} | {'33':<4}\n"
|
|
44
|
+
f"| {'Rama':<10} | {'25':<4}\n"
|
|
45
|
+
"\n"
|
|
46
|
+
"Template: Hello my name is {{ name }}, my age is {{ age }}\n"
|
|
47
|
+
"\n"
|
|
48
|
+
"Command:\n"
|
|
49
|
+
"\n"
|
|
50
|
+
"templify m \\ \n"
|
|
51
|
+
"-t <your_path_to_template_file> \\ \n"
|
|
52
|
+
"-d <your_path_to_data_file> \\ \n"
|
|
53
|
+
"-o <your_path_to_output_directory> \\ \n"
|
|
54
|
+
"-k name --index\n"
|
|
55
|
+
"\n"
|
|
56
|
+
"it will generate a multiple files like this:\n"
|
|
57
|
+
"\n"
|
|
58
|
+
"1 - Abdullah.docx `Hello my name is Abdullah, my age is 32`\n"
|
|
59
|
+
"2 - Rawaa.docx `Hello my name is Rawaa, my age is 33`\n"
|
|
60
|
+
"3 - Rama.docx `Hello my name is Rama, my age is 25`\n"
|
|
61
|
+
)
|
templify/cli/main.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# ruff: noqa: PLR0913
|
|
2
|
+
"""The templify command line application."""
|
|
3
|
+
|
|
4
|
+
from typing import Annotated, Any
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from jinja2 import TemplateError
|
|
8
|
+
from pyspecification import get_expression_json_schema
|
|
9
|
+
from rich.markup import escape
|
|
10
|
+
from rich.progress import track
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from typer_di import TyperDI
|
|
13
|
+
from typer_swissknife import command
|
|
14
|
+
|
|
15
|
+
from templify.cli.dependencies.console import ConsoleDI
|
|
16
|
+
from templify.cli.dependencies.data import DataDI
|
|
17
|
+
from templify.cli.dependencies.template import OutputDirDI, TemplateDI
|
|
18
|
+
from templify.filter_rules import rules
|
|
19
|
+
from templify.writers import InvalidOutputError, write_multiple, write_single
|
|
20
|
+
|
|
21
|
+
from .help import MULTIPLE_TEMPLATES_HELP_TEXT, SINGLE_TEMPLATE_HELP_TEXT
|
|
22
|
+
|
|
23
|
+
app = TyperDI(
|
|
24
|
+
name="templify",
|
|
25
|
+
help="A CLI application for templating documents",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@command(
|
|
31
|
+
app,
|
|
32
|
+
name="filters-json-schema",
|
|
33
|
+
aliases=("filters-schema", "fjs"),
|
|
34
|
+
help="List the filter rules, their arguments and description",
|
|
35
|
+
)
|
|
36
|
+
def print_filter_schema(console: ConsoleDI) -> None:
|
|
37
|
+
"""List the filter rules, their arguments and description."""
|
|
38
|
+
table = Table("Rule", "Arguments", "Description", box=None, header_style="bold")
|
|
39
|
+
expressions = get_expression_json_schema(rules.rules)["$defs"]["expression"]["oneOf"]
|
|
40
|
+
|
|
41
|
+
for expression in expressions:
|
|
42
|
+
if "name" not in expression.get("properties", {}):
|
|
43
|
+
continue # the all / any wrapper, not a rule
|
|
44
|
+
|
|
45
|
+
properties = expression["properties"]
|
|
46
|
+
arguments = properties["kwargs"]["properties"].items()
|
|
47
|
+
table.add_row(
|
|
48
|
+
properties["name"]["const"],
|
|
49
|
+
escape(", ".join(f"{name}: {_type_name(schema)}" for name, schema in arguments)),
|
|
50
|
+
expression.get("description", ""),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
console.print(table)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _type_name(schema: dict[str, Any]) -> str:
|
|
57
|
+
"""Return a short type name for a json schema: `string`, `date`, `list[string]`, ..."""
|
|
58
|
+
if schema.get("type") == "array":
|
|
59
|
+
return f"list[{_type_name(schema.get('items', {}))}]"
|
|
60
|
+
|
|
61
|
+
return schema.get("format") or schema.get("type") or "any"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@command(
|
|
65
|
+
app,
|
|
66
|
+
name="single-template",
|
|
67
|
+
aliases=("single", "s"),
|
|
68
|
+
no_args_is_help=True,
|
|
69
|
+
help=SINGLE_TEMPLATE_HELP_TEXT,
|
|
70
|
+
)
|
|
71
|
+
def generate_single_file(
|
|
72
|
+
console: ConsoleDI,
|
|
73
|
+
data: DataDI,
|
|
74
|
+
template: TemplateDI,
|
|
75
|
+
output_dir: OutputDirDI,
|
|
76
|
+
filename: Annotated[
|
|
77
|
+
str | None,
|
|
78
|
+
typer.Option(
|
|
79
|
+
"-f",
|
|
80
|
+
"--filename",
|
|
81
|
+
help="Output filename without extension [default: template filename]",
|
|
82
|
+
),
|
|
83
|
+
] = None,
|
|
84
|
+
data_variable: Annotated[
|
|
85
|
+
str,
|
|
86
|
+
typer.Option(
|
|
87
|
+
"-v",
|
|
88
|
+
"--data-variable",
|
|
89
|
+
help="Name of the variable that holds the data inside the template",
|
|
90
|
+
),
|
|
91
|
+
] = "data",
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Generate a single file from all the records."""
|
|
94
|
+
try:
|
|
95
|
+
filepath = write_single(
|
|
96
|
+
template.writer,
|
|
97
|
+
data,
|
|
98
|
+
output_dir=output_dir,
|
|
99
|
+
filename=filename or template.path.stem,
|
|
100
|
+
extension=template.extension,
|
|
101
|
+
data_variable=data_variable,
|
|
102
|
+
)
|
|
103
|
+
except (TemplateError, InvalidOutputError) as e:
|
|
104
|
+
msg = f"Template Error: {e}"
|
|
105
|
+
raise typer.BadParameter(msg) from e
|
|
106
|
+
else:
|
|
107
|
+
console.print(f"[green]File generated successfully:[/] {filepath}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@command(
|
|
111
|
+
app,
|
|
112
|
+
name="multiple-templates",
|
|
113
|
+
aliases=("multiple", "m"),
|
|
114
|
+
no_args_is_help=True,
|
|
115
|
+
help=MULTIPLE_TEMPLATES_HELP_TEXT,
|
|
116
|
+
)
|
|
117
|
+
def generate_multiple_files(
|
|
118
|
+
console: ConsoleDI,
|
|
119
|
+
data: DataDI,
|
|
120
|
+
template: TemplateDI,
|
|
121
|
+
output_dir: OutputDirDI,
|
|
122
|
+
filename_key: Annotated[
|
|
123
|
+
str | None,
|
|
124
|
+
typer.Option(
|
|
125
|
+
"-k",
|
|
126
|
+
"--filename-key",
|
|
127
|
+
help="Column used to name each generated file [default: template-<index>]",
|
|
128
|
+
),
|
|
129
|
+
] = None,
|
|
130
|
+
include_index: Annotated[ # noqa: FBT002
|
|
131
|
+
bool,
|
|
132
|
+
typer.Option(
|
|
133
|
+
"-i",
|
|
134
|
+
"--index",
|
|
135
|
+
help="Prefix each filename with its row index",
|
|
136
|
+
),
|
|
137
|
+
] = False,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Generate one file per record (or per group)."""
|
|
140
|
+
files = write_multiple(
|
|
141
|
+
template.writer,
|
|
142
|
+
data,
|
|
143
|
+
output_dir=output_dir,
|
|
144
|
+
extension=template.extension,
|
|
145
|
+
filename_key=filename_key,
|
|
146
|
+
include_index=include_index,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
for _ in track(files, description="Generating", total=len(data), console=console):
|
|
151
|
+
pass
|
|
152
|
+
except (TemplateError, InvalidOutputError) as e:
|
|
153
|
+
msg = f"Template Error: {e}"
|
|
154
|
+
raise typer.BadParameter(msg) from e
|
|
155
|
+
else:
|
|
156
|
+
console.print(f"[green]{len(data)} files generated successfully in:[/] {output_dir}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main() -> None:
|
|
160
|
+
"""Run the templify command line application."""
|
|
161
|
+
app()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|