templify-cli 0.1.0__tar.gz → 0.3.0__tar.gz
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_cli-0.1.0 → templify_cli-0.3.0}/PKG-INFO +996 -945
- {templify_cli-0.1.0 → templify_cli-0.3.0}/README.md +993 -943
- {templify_cli-0.1.0 → templify_cli-0.3.0}/pyproject.toml +114 -3
- templify_cli-0.3.0/pyproject.toml.orig +115 -0
- templify_cli-0.3.0/src/templify/cli/__main__.py +4 -0
- templify_cli-0.3.0/src/templify/cli/cli.py +172 -0
- templify_cli-0.3.0/src/templify/cli/dependencies/data.py +304 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/cli/dependencies/template.py +87 -96
- templify_cli-0.3.0/src/templify/cli/help.py +32 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/filter_rules.py +382 -366
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/readers.py +47 -23
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/writers.py +314 -312
- templify_cli-0.1.0/pyproject.toml.orig +0 -80
- templify_cli-0.1.0/src/templify/cli/dependencies/data.py +0 -196
- templify_cli-0.1.0/src/templify/cli/help.py +0 -61
- templify_cli-0.1.0/src/templify/cli/main.py +0 -165
- {templify_cli-0.1.0 → templify_cli-0.3.0}/LICENSE +0 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/__init__.py +0 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/cli/__init__.py +0 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/cli/dependencies/__init__.py +0 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/cli/dependencies/console.py +0 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/models.py +0 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/py.typed +0 -0
- {templify_cli-0.1.0 → templify_cli-0.3.0}/src/templify/transformers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: templify-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Fill Word, text, Markdown, HTML and JSON templates with Excel or JSON data, from the command line.
|
|
5
5
|
Keywords: template,templating,jinja2,docx,word,excel,mail-merge,document-generation,cli
|
|
6
6
|
Author: aaltatan
|
|
@@ -23,8 +23,9 @@ Classifier: Typing :: Typed
|
|
|
23
23
|
Requires-Dist: docxtpl>=0.20.2
|
|
24
24
|
Requires-Dist: jinja2>=3.1
|
|
25
25
|
Requires-Dist: openpyxl>=3.1.5
|
|
26
|
-
Requires-Dist: pyspecification>=
|
|
26
|
+
Requires-Dist: pyspecification>=4.0.0
|
|
27
27
|
Requires-Dist: rich>=13.0
|
|
28
|
+
Requires-Dist: schemify-cli>=0.1.0
|
|
28
29
|
Requires-Dist: typer>=0.27.2
|
|
29
30
|
Requires-Dist: typer-di>=0.1.5
|
|
30
31
|
Requires-Dist: typer-swissknife>=0.1.0
|
|
@@ -35,946 +36,996 @@ Project-URL: Repository, https://github.com/aaltatan/templify
|
|
|
35
36
|
Project-URL: Issues, https://github.com/aaltatan/templify/issues
|
|
36
37
|
Description-Content-Type: text/markdown
|
|
37
38
|
|
|
38
|
-
# templify
|
|
39
|
-
|
|
40
|
-
A command line tool that fills document templates with data.
|
|
41
|
-
|
|
42
|
-
Point it at a **template** (Word, text, Markdown, HTML or JSON) and a **data
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
templify
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
```text
|
|
49
|
-
offers/
|
|
50
|
-
├── Abdullah.docx
|
|
51
|
-
├── Rawaa.docx
|
|
52
|
-
├── Rama.docx
|
|
53
|
-
└── Omar.docx
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## Table of contents
|
|
59
|
-
|
|
60
|
-
- [Features](#features)
|
|
61
|
-
- [Installation](#installation)
|
|
62
|
-
- [Quick start](#quick-start)
|
|
63
|
-
- [How it works](#how-it-works)
|
|
64
|
-
- [Commands](#commands)
|
|
65
|
-
- [`
|
|
66
|
-
- [`
|
|
67
|
-
- [
|
|
68
|
-
- [Data sources](#data-sources)
|
|
69
|
-
- [Templates](#templates)
|
|
70
|
-
- [Supported formats](#supported-formats)
|
|
71
|
-
- [Jinja2 in 60 seconds](#jinja2-in-60-seconds)
|
|
72
|
-
- [Built-in variables](#built-in-variables)
|
|
73
|
-
- [Word (`.docx`) templates](#word-docx-templates)
|
|
74
|
-
- [JSON templates](#json-templates)
|
|
75
|
-
- [HTML templates](#html-templates)
|
|
76
|
-
- [Output files](#output-files)
|
|
77
|
-
- [Ordering](#ordering)
|
|
78
|
-
- [Filtering](#filtering)
|
|
79
|
-
- [Filter schema](#filter-schema)
|
|
80
|
-
- [Filter rules reference](#filter-rules-reference)
|
|
81
|
-
- [Grouping](#grouping)
|
|
82
|
-
- [Cookbook](#cookbook)
|
|
83
|
-
- [Errors](#errors)
|
|
84
|
-
- [Extending templify](#extending-templify)
|
|
85
|
-
- [Development](#development)
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## Features
|
|
90
|
-
|
|
91
|
-
- **Many template formats**: `.docx`, `.txt`, `.md`, `.html`, `.htm` and `.json`. The writer is picked automatically from the template's extension.
|
|
92
|
-
- **Many data sources**: Excel (`.xlsx`)
|
|
93
|
-
- **
|
|
94
|
-
- **Ordering** by any number of columns, ascending or descending.
|
|
95
|
-
- **Filtering** with a JSON filter language: 50+ rules for strings, numbers, decimals, dates, datetimes and UUIDs. Rules combine with `all` / `any`, nest, and can be inverted.
|
|
96
|
-
- **Grouping** by one or more columns, e.g. one file per department listing its employees.
|
|
97
|
-
- **Safe output**: existing files are never overwritten, invalid filename characters are replaced, and missing folders are created.
|
|
98
|
-
- **Validated JSON output**: a JSON template that renders invalid JSON is rejected before anything is saved.
|
|
99
|
-
- **Clear errors** for bad templates, bad data, unknown columns and bad filters, instead of Python tracebacks.
|
|
100
|
-
- **Extensible**: add a new data reader, template writer or filter rule with one decorator.
|
|
101
|
-
|
|
102
|
-
## Installation
|
|
103
|
-
|
|
104
|
-
Requires **Python 3.12+**.
|
|
105
|
-
|
|
106
|
-
Install it as a global command from PyPI with [uv](https://docs.astral.sh/uv/):
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
uv tool install templify-cli
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
or with [pipx](https://pipx.pypa.io/):
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
pipx install templify-cli
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Or run it from a clone without installing:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
uv sync
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
uv run templify --help
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## Quick start
|
|
129
|
-
|
|
130
|
-
**1. Prepare your data**, e.g. `employees.xlsx`. The first row holds the column names:
|
|
131
|
-
|
|
132
|
-
| name | department | city | salary | hired | email |
|
|
133
|
-
| -------- | ---------- | -------- | -----: | ---------- | -------------------- |
|
|
134
|
-
| Abdullah | Sales | Homs | 1200 | 2021-04-01 | abdullah@example.com |
|
|
135
|
-
| Rawaa | IT | Damascus | 1800 | 2023-09-15 | rawaa@example.com |
|
|
136
|
-
| Rama | IT | Homs | 1500 | 2024-02-10 | rama@example.com |
|
|
137
|
-
| Omar | Sales | Aleppo | 1100 | 2024-11-03 | |
|
|
138
|
-
|
|
139
|
-
**2. Write a template** that uses the column names as variables, e.g. `welcome.txt`:
|
|
140
|
-
|
|
141
|
-
```jinja
|
|
142
|
-
Hello {{ name }},
|
|
143
|
-
You joined {{ department }} on {{ hired.strftime("%d/%m/%Y") }}.
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
**3. Run templify:**
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
templify
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
`welcome/Rama.txt`:
|
|
153
|
-
|
|
154
|
-
```text
|
|
155
|
-
Hello Rama,
|
|
156
|
-
You joined IT on 10/02/2024.
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
> The same data file is used in every example below.
|
|
160
|
-
|
|
161
|
-
## How it works
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
```text
|
|
166
|
-
read data ──► order ──► filter ──► group ──► render template ──► save file(s)
|
|
167
|
-
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
1. **Read**: the data
|
|
171
|
-
2. **Order**: rows are sorted by `--order` columns (optional).
|
|
172
|
-
3. **Filter**: rows not matching `--filter` are dropped (optional).
|
|
173
|
-
4. **Group**: rows are grouped by `--group-by` columns (optional).
|
|
174
|
-
5. **Render**: the writer chosen from the template extension renders the rows.
|
|
175
|
-
6. **Save**:
|
|
176
|
-
-
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
| `
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
| `--
|
|
204
|
-
| `--
|
|
205
|
-
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
| `--
|
|
209
|
-
| `--
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
```
|
|
248
|
-
# Team report
|
|
249
|
-
|
|
250
|
-
| # | Name | Department | Salary |
|
|
251
|
-
|---|------|------------|-------:|
|
|
252
|
-
|
|
253
|
-
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
`
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
|
353
|
-
|
|
|
354
|
-
| `.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
|
406
|
-
|
|
|
407
|
-
| `{
|
|
408
|
-
| `{%
|
|
409
|
-
| `{
|
|
410
|
-
| `{
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
{%
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
`
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
{{
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
"
|
|
594
|
-
"
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}'
|
|
609
|
-
# → Rawaa
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
"
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
]
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
templify
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
`
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
```bash
|
|
800
|
-
templify
|
|
801
|
-
--
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
**
|
|
837
|
-
|
|
838
|
-
```bash
|
|
839
|
-
templify
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
A
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
```
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
```
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
39
|
+
# templify
|
|
40
|
+
|
|
41
|
+
A command line tool that fills document templates with data.
|
|
42
|
+
|
|
43
|
+
Point it at a **template** (Word, text, Markdown, HTML or JSON) and a **data source**: an Excel or JSON file, or a JSON Schema that templify asks you to fill. templify renders one file per row, one file with every row in it, or one file from a single object. Before rendering, you can **order**, **filter** and **group** the data.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
templify render --each -t offer.docx -d employees.xlsx -o offers -n name
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
offers/
|
|
51
|
+
├── Abdullah.docx
|
|
52
|
+
├── Rawaa.docx
|
|
53
|
+
├── Rama.docx
|
|
54
|
+
└── Omar.docx
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Table of contents
|
|
60
|
+
|
|
61
|
+
- [Features](#features)
|
|
62
|
+
- [Installation](#installation)
|
|
63
|
+
- [Quick start](#quick-start)
|
|
64
|
+
- [How it works](#how-it-works)
|
|
65
|
+
- [Commands](#commands)
|
|
66
|
+
- [`render` (`r`)](#render-r)
|
|
67
|
+
- [`filters` (`f`)](#filters-f)
|
|
68
|
+
- [Upgrading from 0.2](#upgrading-from-02)
|
|
69
|
+
- [Data sources](#data-sources)
|
|
70
|
+
- [Templates](#templates)
|
|
71
|
+
- [Supported formats](#supported-formats)
|
|
72
|
+
- [Jinja2 in 60 seconds](#jinja2-in-60-seconds)
|
|
73
|
+
- [Built-in variables](#built-in-variables)
|
|
74
|
+
- [Word (`.docx`) templates](#word-docx-templates)
|
|
75
|
+
- [JSON templates](#json-templates)
|
|
76
|
+
- [HTML templates](#html-templates)
|
|
77
|
+
- [Output files](#output-files)
|
|
78
|
+
- [Ordering](#ordering)
|
|
79
|
+
- [Filtering](#filtering)
|
|
80
|
+
- [Filter schema](#filter-schema)
|
|
81
|
+
- [Filter rules reference](#filter-rules-reference)
|
|
82
|
+
- [Grouping](#grouping)
|
|
83
|
+
- [Cookbook](#cookbook)
|
|
84
|
+
- [Errors](#errors)
|
|
85
|
+
- [Extending templify](#extending-templify)
|
|
86
|
+
- [Development](#development)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Features
|
|
91
|
+
|
|
92
|
+
- **Many template formats**: `.docx`, `.txt`, `.md`, `.html`, `.htm` and `.json`. The writer is picked automatically from the template's extension.
|
|
93
|
+
- **Many data sources**: Excel (`.xlsx`), JSON (`.json`), or answers asked from a JSON Schema (`-s`).
|
|
94
|
+
- **One command, three modes**: every row in one file (`render`), one file per row (`render --each`), or one object in one file (`render --object`).
|
|
95
|
+
- **Ordering** by any number of columns, ascending or descending.
|
|
96
|
+
- **Filtering** with a JSON filter language: 50+ rules for strings, numbers, decimals, dates, datetimes and UUIDs. Rules combine with `all` / `any`, nest, and can be inverted.
|
|
97
|
+
- **Grouping** by one or more columns, e.g. one file per department listing its employees.
|
|
98
|
+
- **Safe output**: existing files are never overwritten, invalid filename characters are replaced, and missing folders are created.
|
|
99
|
+
- **Validated JSON output**: a JSON template that renders invalid JSON is rejected before anything is saved.
|
|
100
|
+
- **Clear errors** for bad templates, bad data, unknown columns and bad filters, instead of Python tracebacks.
|
|
101
|
+
- **Extensible**: add a new data reader, template writer or filter rule with one decorator.
|
|
102
|
+
|
|
103
|
+
## Installation
|
|
104
|
+
|
|
105
|
+
Requires **Python 3.12+**.
|
|
106
|
+
|
|
107
|
+
Install it as a global command from PyPI with [uv](https://docs.astral.sh/uv/):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv tool install templify-cli
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
or with [pipx](https://pipx.pypa.io/):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pipx install templify-cli
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or run it from a clone without installing:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv sync
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uv run templify --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Quick start
|
|
130
|
+
|
|
131
|
+
**1. Prepare your data**, e.g. `employees.xlsx`. The first row holds the column names:
|
|
132
|
+
|
|
133
|
+
| name | department | city | salary | hired | email |
|
|
134
|
+
| -------- | ---------- | -------- | -----: | ---------- | -------------------- |
|
|
135
|
+
| Abdullah | Sales | Homs | 1200 | 2021-04-01 | abdullah@example.com |
|
|
136
|
+
| Rawaa | IT | Damascus | 1800 | 2023-09-15 | rawaa@example.com |
|
|
137
|
+
| Rama | IT | Homs | 1500 | 2024-02-10 | rama@example.com |
|
|
138
|
+
| Omar | Sales | Aleppo | 1100 | 2024-11-03 | |
|
|
139
|
+
|
|
140
|
+
**2. Write a template** that uses the column names as variables, e.g. `welcome.txt`:
|
|
141
|
+
|
|
142
|
+
```jinja
|
|
143
|
+
Hello {{ name }},
|
|
144
|
+
You joined {{ department }} on {{ hired.strftime("%d/%m/%Y") }}.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**3. Run templify:**
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
templify render --each -t welcome.txt -d employees.xlsx -o welcome -n name
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`welcome/Rama.txt`:
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
Hello Rama,
|
|
157
|
+
You joined IT on 10/02/2024.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> The same data file is used in every example below.
|
|
161
|
+
|
|
162
|
+
## How it works
|
|
163
|
+
|
|
164
|
+
`render` runs one pipeline:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
read data ──► order ──► filter ──► group ──► render template ──► save file(s)
|
|
168
|
+
(-d / -s) (--order) (--filter) (--group-by) (-t) (-o)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
1. **Read**: the [data source](#data-sources) becomes a list of rows, each row a set of `column: value` pairs.
|
|
172
|
+
2. **Order**: rows are sorted by `--order` columns (optional).
|
|
173
|
+
3. **Filter**: rows not matching `--filter` are dropped (optional).
|
|
174
|
+
4. **Group**: rows are grouped by `--group-by` columns (optional).
|
|
175
|
+
5. **Render**: the writer chosen from the template extension renders the rows.
|
|
176
|
+
6. **Save**:
|
|
177
|
+
- by default, one file containing every row.
|
|
178
|
+
- with `--each`, one file per row, or per group when grouping.
|
|
179
|
+
|
|
180
|
+
With `--object`, the data is a **single object** instead of a list, so steps 2 to 4 don't apply and that object is rendered into one file.
|
|
181
|
+
|
|
182
|
+
## Commands
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
templify --help
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Command | Alias | What it does |
|
|
189
|
+
| --------- | ----- | --------------------------------------------------- |
|
|
190
|
+
| `render` | `r` | Render a template with data, in one of three modes |
|
|
191
|
+
| `filters` | `f` | List the filter rules and their arguments |
|
|
192
|
+
|
|
193
|
+
### `render` (`r`)
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
templify render -t <template> (-d <data> | -s <schema.json>) [--each | --object] [-o <output-dir>] [OPTIONS]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The mode decides what the template sees and how many files are written:
|
|
200
|
+
|
|
201
|
+
| Mode | Data | Writes | The template sees |
|
|
202
|
+
| ----------- | ------------------- | ------------------------------- | ------------------------------------------------------- |
|
|
203
|
+
| *(default)* | a list of rows | **one file** with all the rows | the rows as a list, `objects`: `{% for e in objects %}` |
|
|
204
|
+
| `--each` | a list of rows | **one file per row** (or group) | each row's columns: `{{ name }}`, `{{ salary }}` |
|
|
205
|
+
| `--object` | a **single object** | **one file** | the object as `object`: `{{ object.name }}` |
|
|
206
|
+
|
|
207
|
+
| Option | Short | Modes | Description | Default |
|
|
208
|
+
| ------------- | ----- | ------------------- | --------------------------------------------------------------- | -------------------- |
|
|
209
|
+
| `--template` | `-t` | all | Template file. **Required.** | |
|
|
210
|
+
| `--data` | `-d` | all | Data file. Exactly one of `-d` / `-s` is **required** | |
|
|
211
|
+
| `--schema` | `-s` | all | JSON Schema file, [ask for the data](#data-sources) matching it | |
|
|
212
|
+
| `--output` | `-o` | all | Output directory, created if missing | current directory |
|
|
213
|
+
| `--each` | | | One file per row (or per group) | off |
|
|
214
|
+
| `--object` | | | The data is a single object | off |
|
|
215
|
+
| `--filename` | `-f` | default, `--object` | Output filename **without** extension | the template's name |
|
|
216
|
+
| `--var` | `-v` | default, `--object` | Name of the variable holding the data | `objects` / `object` |
|
|
217
|
+
| `--name-by` | `-n` | `--each` | Column whose value names each file | `template-<n>` |
|
|
218
|
+
| `--numbered` | | `--each` | Prefix each filename with its row number: `1 - Rama.docx` | off |
|
|
219
|
+
| `--order` | | default, `--each` | Column to [order](#ordering) by, `-column` for descending. Repeatable | |
|
|
220
|
+
| `--filter` | | default, `--each` | JSON [filter schema](#filter-schema) | |
|
|
221
|
+
| `--group-by` | | default, `--each` | Column to [group](#grouping) by. Repeatable | |
|
|
222
|
+
| `--items-var` | | default, `--each` | Variable name holding each group's rows | `items` |
|
|
223
|
+
|
|
224
|
+
An option used in a mode that doesn't take it is an error, never silently ignored:
|
|
225
|
+
`--name-by only works with --each`.
|
|
226
|
+
|
|
227
|
+
#### All rows in one file (default)
|
|
228
|
+
|
|
229
|
+
`team.md`:
|
|
230
|
+
|
|
231
|
+
```jinja
|
|
232
|
+
# Team report
|
|
233
|
+
|
|
234
|
+
| # | Name | Department | Salary |
|
|
235
|
+
|---|------|------------|-------:|
|
|
236
|
+
{% for e in objects -%}
|
|
237
|
+
| {{ loop.index }} | {{ e.name }} | {{ e.department }} | {{ "{:,}".format(e.salary) }} |
|
|
238
|
+
{% endfor %}
|
|
239
|
+
**Total salaries:** {{ objects | sum(attribute="salary") }}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
templify render -t team.md -d employees.xlsx -o reports --order -salary
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`reports/team.md`:
|
|
247
|
+
|
|
248
|
+
```markdown
|
|
249
|
+
# Team report
|
|
250
|
+
|
|
251
|
+
| # | Name | Department | Salary |
|
|
252
|
+
|---|------|------------|-------:|
|
|
253
|
+
| 1 | Rawaa | IT | 1,800 |
|
|
254
|
+
| 2 | Rama | IT | 1,500 |
|
|
255
|
+
| 3 | Abdullah | Sales | 1,200 |
|
|
256
|
+
| 4 | Omar | Sales | 1,100 |
|
|
257
|
+
|
|
258
|
+
**Total salaries:** 5600
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Rename the output and the loop variable:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
templify render -t team.md -d employees.xlsx -o reports -f "2024 team" -v employees
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The template then loops with `{% for e in employees %}`, and the file is saved as `reports/2024 team.md`.
|
|
268
|
+
|
|
269
|
+
#### One file per row (`--each`)
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
templify render -t offer.docx -d employees.xlsx -o offers --each -n name --numbered
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
offers/
|
|
277
|
+
├── 1 - Abdullah.docx
|
|
278
|
+
├── 2 - Rawaa.docx
|
|
279
|
+
├── 3 - Rama.docx
|
|
280
|
+
└── 4 - Omar.docx
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+

|
|
284
|
+
|
|
285
|
+
#### One object (`--object`)
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
templify render -t letter.docx -d rama.json --object # Hello {{ object.name }}
|
|
289
|
+
templify render -t letter.docx -s person.schema.json --object # asks for the fields first
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
With `-s`, [schemify](https://github.com/aaltatan/schemify-cli) asks for the fields:
|
|
293
|
+
|
|
294
|
+

|
|
295
|
+
|
|
296
|
+
### `filters` (`f`)
|
|
297
|
+
|
|
298
|
+
Lists every filter rule you can use in `--filter`, with its arguments (in `args` order) and description:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
templify filters
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```text
|
|
305
|
+
Rule Arguments Description
|
|
306
|
+
is_true key: string Keep records where `key` is `true`.
|
|
307
|
+
is_null key: string Keep records where `key` is empty (`null`).
|
|
308
|
+
int__eq key: string, value: integer Keep records where `key` equals `value`.
|
|
309
|
+
...
|
|
310
|
+
string__regex key: string, pattern: string Keep records where `key` matches the regular expression `pattern`.
|
|
311
|
+
string__is_in key: string, value: list[string] Keep records where `key` is one of the `value` list.
|
|
312
|
+
...
|
|
313
|
+
date__ge key: string, value: date Keep records where `key` is greater than or equal to `value`, comparing the day only.
|
|
314
|
+
...
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The argument names are also the `kwargs` names: `{"key": "salary", "value": 1400}`.
|
|
318
|
+
|
|
319
|
+
### Upgrading from 0.2
|
|
320
|
+
|
|
321
|
+
| 0.2 | 0.3 |
|
|
322
|
+
| ---------------------------------------- | ----------------------------------------- |
|
|
323
|
+
| `templify single-template` / `s` | `templify render` |
|
|
324
|
+
| `templify multiple-templates` / `m` | `templify render --each` |
|
|
325
|
+
| `templify schema-template` / `js` | `templify render --object -s ...` |
|
|
326
|
+
| `templify schema-list-template` / `jsl` | `templify render -s ...` |
|
|
327
|
+
| `templify filters-json-schema` / `fjs` | `templify filters` |
|
|
328
|
+
| `-k` / `--filename-key` | `-n` / `--name-by` |
|
|
329
|
+
| `-i` / `--index` | `--numbered` |
|
|
330
|
+
| `--group-items` | `--items-var` |
|
|
331
|
+
| `-v` / `--data-variable`, default `data` | `-v` / `--var`, default `objects` |
|
|
332
|
+
| `-o` required | `-o` optional, default current directory |
|
|
333
|
+
| Template errors exit with code 2 | exit with code 1 (usage errors stay 2) |
|
|
334
|
+
|
|
335
|
+
## Data sources
|
|
336
|
+
|
|
337
|
+
`render` takes **exactly one** data source:
|
|
338
|
+
|
|
339
|
+
- `-d` / `--data`: a data file. The reader is chosen from its extension, ignoring case.
|
|
340
|
+
- `-s` / `--schema`: a JSON Schema file. [schemify](https://github.com/aaltatan/schemify-cli) asks for the data matching it: a list (keep answering until you say **No**), or one object with `--object`.
|
|
341
|
+
|
|
342
|
+

|
|
343
|
+
|
|
344
|
+
Data files for the default mode and `--each` hold a **list of objects**:
|
|
345
|
+
|
|
346
|
+
| Extension | Format |
|
|
347
|
+
| --------- | --------------------------------------------------------------------------- |
|
|
348
|
+
| `.xlsx` | Excel workbook: the **active sheet**, first row = column names |
|
|
349
|
+
| `.json` | A JSON **list of objects**: `[{"name": "Rama", ...}, ...]` |
|
|
350
|
+
|
|
351
|
+
Data files for `--object` hold a **single object**:
|
|
352
|
+
|
|
353
|
+
| Extension | Format |
|
|
354
|
+
| --------- | --------------------------------------------------------------------------- |
|
|
355
|
+
| `.json` | A JSON **object**: `{"name": "Rama", ...}` |
|
|
356
|
+
|
|
357
|
+
**Excel notes**
|
|
358
|
+
|
|
359
|
+
- The first row holds the column names, and each following row is one record.
|
|
360
|
+
- Fully blank rows are skipped.
|
|
361
|
+
- Formula cells give their **last calculated value**, not the formula text. A file created by a script and never opened in Excel has no calculated values yet, so those cells come back empty.
|
|
362
|
+
- Values keep their type: numbers stay numbers, dates become Python `datetime`s (so `{{ hired.strftime("%Y") }}` works), and empty cells become `None`.
|
|
363
|
+
|
|
364
|
+
**JSON notes**
|
|
365
|
+
|
|
366
|
+
- Must be a list of objects. An object, a list of numbers and so on are rejected with a clear error.
|
|
367
|
+
- Read as UTF-8, so Arabic and other non-Latin text works.
|
|
368
|
+
- JSON has no date type, so dates are strings (`"2024-02-10"`). The [`date__*` filter rules](#dates) understand both forms.
|
|
369
|
+
|
|
370
|
+
`employees.json`:
|
|
371
|
+
|
|
372
|
+
```json
|
|
373
|
+
[
|
|
374
|
+
{
|
|
375
|
+
"name": "Abdullah",
|
|
376
|
+
"department": "Sales",
|
|
377
|
+
"city": "Homs",
|
|
378
|
+
"salary": 1200,
|
|
379
|
+
"hired": "2021-04-01",
|
|
380
|
+
"email": "abdullah@example.com"
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Templates
|
|
386
|
+
|
|
387
|
+
### Supported formats
|
|
388
|
+
|
|
389
|
+
The writer is chosen from the **template extension**, ignoring case. The output file gets the same extension.
|
|
390
|
+
|
|
391
|
+
| Extension | Writer | Engine |
|
|
392
|
+
| ----------------------------- | ------------ | ------------------------------------------------ |
|
|
393
|
+
| `.docx` | `DocxWriter` | [docxtpl](https://docxtpl.readthedocs.io/) (Jinja2 inside Word) |
|
|
394
|
+
| `.txt`, `.md`, `.html`, `.htm` | `TextWriter` | [Jinja2](https://jinja.palletsprojects.com/) |
|
|
395
|
+
| `.json` | `JsonWriter` | Jinja2 + validates that the output is JSON |
|
|
396
|
+
|
|
397
|
+
Any other extension is rejected, and the error lists the supported ones.
|
|
398
|
+
|
|
399
|
+
### Jinja2 in 60 seconds
|
|
400
|
+
|
|
401
|
+
All templates use [Jinja2](https://jinja.palletsprojects.com/en/stable/templates/) syntax:
|
|
402
|
+
|
|
403
|
+
| Syntax | Meaning |
|
|
404
|
+
| ------------------------------------------ | ---------------------------------------- |
|
|
405
|
+
| `{{ name }}` | Print a variable |
|
|
406
|
+
| `{{ e.name }}` or `{{ e["name"] }}` | Print a field of a row |
|
|
407
|
+
| `{% for e in objects %} ... {% endfor %}` | Loop |
|
|
408
|
+
| `{{ loop.index }}` | 1-based loop counter |
|
|
409
|
+
| `{% if salary > 1400 %} ... {% endif %}` | Condition |
|
|
410
|
+
| `{{ name \| upper }}` | Filter: `upper`, `lower`, `title`, `length`, `sum`, `default`, `tojson`, ... |
|
|
411
|
+
| `{{ email or "no email" }}` | Fallback for empty values |
|
|
412
|
+
| `{{ hired.strftime("%d/%m/%Y") }}` | Format an Excel date |
|
|
413
|
+
| `{{ "{:,.2f}".format(salary) }}` | Format a number: `1,200.00` |
|
|
414
|
+
| `{# comment #}` | Comment, not rendered |
|
|
415
|
+
| `{%- ... -%}` | Trim the whitespace around a tag |
|
|
416
|
+
| `{% include "header.txt" %}` | Include another file from the template's folder (text formats) |
|
|
417
|
+
|
|
418
|
+
A variable that doesn't exist renders as an empty string instead of failing.
|
|
419
|
+
|
|
420
|
+
### Built-in variables
|
|
421
|
+
|
|
422
|
+
These are always available, in both modes:
|
|
423
|
+
|
|
424
|
+
| Variable | Value | In `.docx` it becomes |
|
|
425
|
+
| ---------------- | ------ | --------------------- |
|
|
426
|
+
| `{{ new_line }}` | `\n` | a line break |
|
|
427
|
+
| `{{ tab }}` | `\t` | a tab |
|
|
428
|
+
| `{{ page_break }}` | `\f` | a page break |
|
|
429
|
+
|
|
430
|
+
> If your data has a column with one of these names, the built-in value wins.
|
|
431
|
+
|
|
432
|
+
### Word (`.docx`) templates
|
|
433
|
+
|
|
434
|
+
Write Jinja2 tags directly in the Word document. Keep each tag in the **same formatting** (don't bold half of `{{ name }}`), otherwise Word splits it into pieces and Jinja can't read it.
|
|
435
|
+
|
|
436
|
+
`offer.docx`:
|
|
437
|
+
|
|
438
|
+
```text
|
|
439
|
+
Dear {{ name }},
|
|
440
|
+
Welcome to the {{ department }} team in {{ city }}. Your salary is {{ salary }}$.
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
docxtpl adds special tags for Word structure:
|
|
444
|
+
|
|
445
|
+
| Tag | Use |
|
|
446
|
+
| ----------------------------------- | ------------------------------------------------------- |
|
|
447
|
+
| `{%p for e in objects %}` ... `{%p endfor %}` | Loop over **paragraphs**: each tag sits on its own paragraph |
|
|
448
|
+
| `{%tr for e in objects %}` ... `{%tr endfor %}` | Loop over **table rows**: one row per record |
|
|
449
|
+
| `{%tc ... %}` | Loop over table cells |
|
|
450
|
+
| `{%r ... %}` | Tag applied to a run |
|
|
451
|
+
|
|
452
|
+
**One Word file, one page per employee** (`all-offers.docx`, three paragraphs):
|
|
453
|
+
|
|
454
|
+
```text
|
|
455
|
+
{%p for e in objects %}
|
|
456
|
+
{{ e.name }} - {{ e.department }}{{ page_break }}
|
|
457
|
+
{%p endfor %}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
templify render -t all-offers.docx -d employees.xlsx -o offers
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**A Word table with one row per employee**: create a 2-row table. Put `{%tr for e in objects %}` and `{%tr endfor %}` in rows of their own, around a row containing `{{ e.name }}`, `{{ e.salary }}`, ...
|
|
465
|
+
|
|
466
|
+
See the [docxtpl documentation](https://docxtpl.readthedocs.io/) for images, rich text and more.
|
|
467
|
+
|
|
468
|
+
### JSON templates
|
|
469
|
+
|
|
470
|
+
A `.json` template is rendered like text, then **validated**. If the result isn't valid JSON, nothing is saved and you get an error that points at the problem.
|
|
471
|
+
|
|
472
|
+
Always write values through the `tojson` filter. It adds quotes and escapes characters like `"` for you, and turns `None` into `null` and `True` into `true`.
|
|
473
|
+
|
|
474
|
+
`profile.json`:
|
|
475
|
+
|
|
476
|
+
```jinja
|
|
477
|
+
{
|
|
478
|
+
"name": {{ name | tojson }},
|
|
479
|
+
"department": {{ department | tojson }},
|
|
480
|
+
"email": {{ email | tojson }},
|
|
481
|
+
"senior": {{ (salary > 1400) | tojson }}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
templify render --each -t profile.json -d employees.json -o profiles -n name
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`profiles/Omar.json`:
|
|
490
|
+
|
|
491
|
+
```json
|
|
492
|
+
{
|
|
493
|
+
"name": "Omar",
|
|
494
|
+
"department": "Sales",
|
|
495
|
+
"email": null,
|
|
496
|
+
"senior": false
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Dump the whole (ordered / filtered / grouped) dataset:
|
|
501
|
+
|
|
502
|
+
```jinja
|
|
503
|
+
{{ objects | tojson(indent=2) }}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
templify render -t all.json -d employees.xlsx -o export -f employees --order name
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
> `tojson` sorts object keys alphabetically.
|
|
511
|
+
|
|
512
|
+
**Why use `tojson`?** With `"name": "{{ name }}"`, a name like `Ra"ma` produces broken JSON:
|
|
513
|
+
|
|
514
|
+
```text
|
|
515
|
+
Invalid value: Template Error: Rendered output is not valid JSON: Expecting ',' delimiter: line 1 column 14 (char 13)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### HTML templates
|
|
519
|
+
|
|
520
|
+
`department.html`:
|
|
521
|
+
|
|
522
|
+
```jinja
|
|
523
|
+
<!DOCTYPE html>
|
|
524
|
+
<html>
|
|
525
|
+
<body>
|
|
526
|
+
<h1>{{ department }} department</h1>
|
|
527
|
+
<ul>
|
|
528
|
+
{%- for e in items %}
|
|
529
|
+
<li>{{ e.name }} ({{ e.city }})</li>
|
|
530
|
+
{%- endfor %}
|
|
531
|
+
</ul>
|
|
532
|
+
<p>{{ items | length }} employees</p>
|
|
533
|
+
</body>
|
|
534
|
+
</html>
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
templify render --each -t department.html -d employees.xlsx -o departments --group-by department -n department
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
> Values are **not** HTML-escaped, so `<b>` in your data stays bold. If the data isn't trusted, escape it with `{{ value | e }}`.
|
|
542
|
+
|
|
543
|
+
## Output files
|
|
544
|
+
|
|
545
|
+
| Situation | Result |
|
|
546
|
+
| ------------------------------------------- | --------------------------------------------------- |
|
|
547
|
+
| No `-n` | `template-1.txt`, `template-2.txt`, ... |
|
|
548
|
+
| `-k name` | `Rama.txt` |
|
|
549
|
+
| `-k name -i` | `3 - Rama.txt` |
|
|
550
|
+
| The `-n` column is empty or missing for a row | That row falls back to `template-<n>.txt` |
|
|
551
|
+
| The value has invalid characters: `a/b:c?` | They become `_`: `a_b_c_.txt` |
|
|
552
|
+
| The value is a number: `-k id` | `42.txt` |
|
|
553
|
+
| The file already exists | A short random suffix is added: `Rama-3f9a1c2e.txt`. **Nothing is ever overwritten** |
|
|
554
|
+
| The output folder doesn't exist | It is created, including parent folders |
|
|
555
|
+
| `render` without `-f` | Named after the template: `team.md` → `team.md` |
|
|
556
|
+
|
|
557
|
+
Output files are written as **UTF-8**, with the template's line endings kept as they are.
|
|
558
|
+
|
|
559
|
+
## Ordering
|
|
560
|
+
|
|
561
|
+
`--order <column>` sorts ascending, and `--order -<column>` sorts descending. Repeat it to sort by several columns. The first one has the highest priority.
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
# highest salary first
|
|
565
|
+
templify render -t names.txt -d employees.xlsx -o out --order -salary
|
|
566
|
+
|
|
567
|
+
# by department A→Z, then by salary high→low inside each department
|
|
568
|
+
templify render -t names.txt -d employees.xlsx -o out --order department --order -salary
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
```text
|
|
572
|
+
Rawaa Rama Abdullah Omar
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Ordering by a column that doesn't exist fails with the list of available columns.
|
|
576
|
+
|
|
577
|
+
## Filtering
|
|
578
|
+
|
|
579
|
+
`--filter` takes a JSON string that describes which rows to **keep**.
|
|
580
|
+
|
|
581
|
+
> **Quoting on the command line**
|
|
582
|
+
>
|
|
583
|
+
> - **bash / zsh / PowerShell 7.3+**: wrap the JSON in single quotes: `--filter '{"name": ...}'`
|
|
584
|
+
> - **cmd.exe**: escape the inner quotes: `--filter "{\"name\": ...}"`
|
|
585
|
+
|
|
586
|
+
### Filter schema
|
|
587
|
+
|
|
588
|
+
**A single rule** (a *predicate*) has exactly four keys:
|
|
589
|
+
|
|
590
|
+
```json
|
|
591
|
+
{
|
|
592
|
+
"name": "string__eq",
|
|
593
|
+
"args": ["department", "IT"],
|
|
594
|
+
"kwargs": {},
|
|
595
|
+
"inverse": false
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
| Key | Meaning |
|
|
600
|
+
| --------- | ----------------------------------------------------------------------------------- |
|
|
601
|
+
| `name` | Rule name, see the [rules reference](#filter-rules-reference) |
|
|
602
|
+
| `args` | Positional arguments: **the column first**, then the value(s) |
|
|
603
|
+
| `kwargs` | The same arguments by name, e.g. `{"key": "salary", "value": 1400}`. Use `{}` otherwise |
|
|
604
|
+
| `inverse` | `true` negates the rule: "NOT equal", "does NOT contain", ... |
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
# IT only
|
|
608
|
+
templify render -t names.txt -d employees.xlsx -o out \
|
|
609
|
+
--filter '{"name": "string__eq", "args": ["department", "IT"], "kwargs": {}, "inverse": false}'
|
|
610
|
+
# → Rawaa Rama
|
|
611
|
+
|
|
612
|
+
# everyone except IT
|
|
613
|
+
templify render -t names.txt -d employees.xlsx -o out \
|
|
614
|
+
--filter '{"name": "string__eq", "args": ["department", "IT"], "kwargs": {}, "inverse": true}'
|
|
615
|
+
# → Abdullah Omar
|
|
616
|
+
|
|
617
|
+
# same rule, arguments by name
|
|
618
|
+
templify render -t names.txt -d employees.xlsx -o out \
|
|
619
|
+
--filter '{"name": "int__gt", "args": [], "kwargs": {"key": "salary", "value": 1400}, "inverse": false}'
|
|
620
|
+
# → Rawaa Rama
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Combine rules** with an *expression*: exactly two keys, `operator` and `expressions`.
|
|
624
|
+
|
|
625
|
+
| `operator` | Keeps a row when |
|
|
626
|
+
| ---------- | --------------------------------- |
|
|
627
|
+
| `all` | **every** expression matches (AND) |
|
|
628
|
+
| `any` | **at least one** matches (OR) |
|
|
629
|
+
|
|
630
|
+
```bash
|
|
631
|
+
# in Homs AND earning at least 1300
|
|
632
|
+
templify render -t names.txt -d employees.xlsx -o out --filter '{
|
|
633
|
+
"operator": "all",
|
|
634
|
+
"expressions": [
|
|
635
|
+
{"name": "string__eq", "args": ["city", "Homs"], "kwargs": {}, "inverse": false},
|
|
636
|
+
{"name": "int__ge", "args": ["salary", 1300], "kwargs": {}, "inverse": false}
|
|
637
|
+
]
|
|
638
|
+
}'
|
|
639
|
+
# → Rama
|
|
640
|
+
|
|
641
|
+
# in Aleppo OR earning more than 1700
|
|
642
|
+
templify render -t names.txt -d employees.xlsx -o out --filter '{
|
|
643
|
+
"operator": "any",
|
|
644
|
+
"expressions": [
|
|
645
|
+
{"name": "string__eq", "args": ["city", "Aleppo"], "kwargs": {}, "inverse": false},
|
|
646
|
+
{"name": "int__gt", "args": ["salary", 1700], "kwargs": {}, "inverse": false}
|
|
647
|
+
]
|
|
648
|
+
}'
|
|
649
|
+
# → Rawaa Omar
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Expressions **nest** to any depth:
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
# IT AND (in Damascus OR hired since 2024)
|
|
656
|
+
templify render -t names.txt -d employees.xlsx -o out --filter '{
|
|
657
|
+
"operator": "all",
|
|
658
|
+
"expressions": [
|
|
659
|
+
{"name": "string__eq", "args": ["department", "IT"], "kwargs": {}, "inverse": false},
|
|
660
|
+
{
|
|
661
|
+
"operator": "any",
|
|
662
|
+
"expressions": [
|
|
663
|
+
{"name": "string__eq", "args": ["city", "Damascus"], "kwargs": {}, "inverse": false},
|
|
664
|
+
{"name": "date__ge", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false}
|
|
665
|
+
]
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
}'
|
|
669
|
+
# → Rawaa Rama
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
> **Empty cells**: a comparison rule on an empty cell (e.g. `string__endswith` on a missing email) stops with
|
|
673
|
+
> `Filter failed on a data value (empty cell or wrong type?)`. Skip empty cells first by adding
|
|
674
|
+
> `{"name": "is_null", "args": ["email"], "kwargs": {}, "inverse": true}` to an `all` expression.
|
|
675
|
+
|
|
676
|
+
### Filter rules reference
|
|
677
|
+
|
|
678
|
+
The first argument is always the **column**. `value` is converted to the rule's type, so `"5"` works for `int__eq`.
|
|
679
|
+
|
|
680
|
+
#### Boolean & null
|
|
681
|
+
|
|
682
|
+
| Rule | Arguments | Keeps rows where |
|
|
683
|
+
| --------- | ---------- | -------------------------- |
|
|
684
|
+
| `is_true` | `[column]` | the value is `true` |
|
|
685
|
+
| `is_null` | `[column]` | the value is empty / `null` |
|
|
686
|
+
|
|
687
|
+
#### Numbers: `int__*`, `float__*`, `decimal__*`
|
|
688
|
+
|
|
689
|
+
| Suffix | Arguments | Keeps rows where |
|
|
690
|
+
| ------ | ----------------- | ---------------- |
|
|
691
|
+
| `eq` | `[column, value]` | `column == value` |
|
|
692
|
+
| `gt` | `[column, value]` | `column > value` |
|
|
693
|
+
| `ge` | `[column, value]` | `column >= value` |
|
|
694
|
+
| `lt` | `[column, value]` | `column < value` |
|
|
695
|
+
| `le` | `[column, value]` | `column <= value` |
|
|
696
|
+
|
|
697
|
+
For example: `int__ge`, `float__lt` and `decimal__eq`. For `decimal__*`, the value can be a string (`"10.50"`), an integer, or a float rounded to 2 decimals.
|
|
698
|
+
|
|
699
|
+
#### Strings: `string__*`
|
|
700
|
+
|
|
701
|
+
| Rule | Arguments | Keeps rows where |
|
|
702
|
+
| --------------------- | ------------------ | ---------------------------------------- |
|
|
703
|
+
| `string__eq` | `[column, text]` | equals `text` |
|
|
704
|
+
| `string__contains` | `[column, text]` | contains `text` |
|
|
705
|
+
| `string__icontains` | `[column, text]` | contains `text`, ignoring case |
|
|
706
|
+
| `string__startswith` | `[column, text]` | starts with `text` |
|
|
707
|
+
| `string__istartswith` | `[column, text]` | starts with `text`, ignoring case |
|
|
708
|
+
| `string__endswith` | `[column, text]` | ends with `text` |
|
|
709
|
+
| `string__iendswith` | `[column, text]` | ends with `text`, ignoring case |
|
|
710
|
+
| `string__is_in` | `[column, [a, b]]` | is one of the list |
|
|
711
|
+
| `string__iis_in` | `[column, [a, b]]` | is one of the list, ignoring case |
|
|
712
|
+
| `string__regex` | `[column, pattern]` | matches the [regular expression](https://docs.python.org/3/library/re.html) |
|
|
713
|
+
| `string__length_eq` | `[column, n]` | length `== n` (also `_gt`, `_ge`, `_lt`, `_le`) |
|
|
714
|
+
|
|
715
|
+
```bash
|
|
716
|
+
# lives in Homs or Aleppo
|
|
717
|
+
--filter '{"name": "string__is_in", "args": ["city", ["Homs", "Aleppo"]], "kwargs": {}, "inverse": false}'
|
|
718
|
+
# → Abdullah Rama Omar
|
|
719
|
+
|
|
720
|
+
# name starts with "Ra"
|
|
721
|
+
--filter '{"name": "string__regex", "args": ["name", "^Ra"], "kwargs": {}, "inverse": false}'
|
|
722
|
+
# → Rawaa Rama
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
#### Dates: `date__*` <a id="dates"></a>
|
|
726
|
+
|
|
727
|
+
`date__eq`, `date__gt`, `date__ge`, `date__lt`, `date__le`, with arguments `[column, "YYYY-MM-DD"]`.
|
|
728
|
+
|
|
729
|
+
They compare the **day only** and accept every form a date comes in:
|
|
730
|
+
|
|
731
|
+
- Excel date cells, where the time part is ignored
|
|
732
|
+
- ISO strings from JSON: `"2024-02-10"` or `"2024-02-10T08:30:00"`
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
# hired in 2024 or later (Excel dates)
|
|
736
|
+
templify render -t names.txt -d employees.xlsx -o out \
|
|
737
|
+
--filter '{"name": "date__ge", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false}'
|
|
738
|
+
# → Rama Omar
|
|
739
|
+
|
|
740
|
+
# hired before 2024 (JSON string dates)
|
|
741
|
+
templify render -t names.txt -d employees.json -o out \
|
|
742
|
+
--filter '{"name": "date__lt", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false}'
|
|
743
|
+
# → Abdullah Rawaa
|
|
744
|
+
|
|
745
|
+
# hired during 2024: combine two rules
|
|
746
|
+
--filter '{"operator": "all", "expressions": [
|
|
747
|
+
{"name": "date__ge", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false},
|
|
748
|
+
{"name": "date__le", "args": ["hired", "2024-12-31"], "kwargs": {}, "inverse": false}
|
|
749
|
+
]}'
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
#### Datetimes: `datetime__*`
|
|
753
|
+
|
|
754
|
+
`datetime__eq`, `datetime__gt`, `datetime__ge`, `datetime__lt`, `datetime__le`, with arguments `[column, "YYYY-MM-DDTHH:MM:SS"]`. They compare down to the second. The column must hold real datetimes, i.e. Excel date cells. For JSON string dates, use `date__*`.
|
|
755
|
+
|
|
756
|
+
#### UUIDs: `uuid__*`
|
|
757
|
+
|
|
758
|
+
`uuid__eq`, `uuid__gt`, `uuid__ge`, `uuid__lt`, `uuid__le`, with arguments `[column, "uuid-string"]`.
|
|
759
|
+
|
|
760
|
+
## Grouping
|
|
761
|
+
|
|
762
|
+
`--group-by <column>` merges rows that share the same value into **one group**. Each group is a new row holding:
|
|
763
|
+
|
|
764
|
+
- the group-by column(s), with the shared value
|
|
765
|
+
- `items` (rename it with `--items-var`), the list of the original rows in that group
|
|
766
|
+
|
|
767
|
+
```text
|
|
768
|
+
rows: groups (--group-by department):
|
|
769
|
+
{name: Abdullah, department: Sales} {department: Sales, items: [Abdullah, Omar]}
|
|
770
|
+
{name: Rawaa, department: IT} ──► {department: IT, items: [Rawaa, Rama]}
|
|
771
|
+
{name: Rama, department: IT}
|
|
772
|
+
{name: Omar, department: Sales}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Groups appear in the order their first row appears. Grouping runs **after** ordering and filtering, so `--order` also sorts the rows inside every group, and `--filter` decides which rows get grouped.
|
|
776
|
+
|
|
777
|
+
**One file per group**, with `render --each`: `-n` can name files after the group column.
|
|
778
|
+
|
|
779
|
+
```bash
|
|
780
|
+
templify render --each -t department.html -d employees.xlsx -o departments --group-by department -n department
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
```text
|
|
784
|
+
departments/
|
|
785
|
+
├── Sales.html
|
|
786
|
+
└── IT.html
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**One report of all groups**, with `render`: loop over groups, then over each group's rows.
|
|
790
|
+
|
|
791
|
+
`by-city.txt`:
|
|
792
|
+
|
|
793
|
+
```jinja
|
|
794
|
+
{% for group in objects -%}
|
|
795
|
+
{{ group.city }} / {{ group.department }}:
|
|
796
|
+
{%- for e in group.people %} {{ e.name }}{% endfor %}
|
|
797
|
+
{% endfor %}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
```bash
|
|
801
|
+
templify render -t by-city.txt -d employees.xlsx -o out \
|
|
802
|
+
--order city --group-by city --group-by department --items-var people
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
```text
|
|
806
|
+
Aleppo / Sales: Omar
|
|
807
|
+
Damascus / IT: Rawaa
|
|
808
|
+
Homs / Sales: Abdullah
|
|
809
|
+
Homs / IT: Rama
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Useful group-level expressions:
|
|
813
|
+
|
|
814
|
+
```jinja
|
|
815
|
+
{{ items | length }} {# rows in the group #}
|
|
816
|
+
{{ items | sum(attribute="salary") }} {# total of a column #}
|
|
817
|
+
{{ items | map(attribute="name") | join(", ") }} {# "Rawaa, Rama" #}
|
|
818
|
+
{{ (items | sum(attribute="salary")) / (items | length) }} {# average #}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
**Grouping errors**
|
|
822
|
+
|
|
823
|
+
| Problem | Message |
|
|
824
|
+
| -------------------------------------------------------- | --------------------------------------------------- |
|
|
825
|
+
| The column doesn't exist | `Key 'country' not found in data, Available keys: ...` |
|
|
826
|
+
| `--items-var` equals a group-by column | `Items key 'city' can not be one of the group by keys` |
|
|
827
|
+
| The column holds lists or objects (JSON) | `Can not group by unhashable values ...` |
|
|
828
|
+
|
|
829
|
+
## Cookbook
|
|
830
|
+
|
|
831
|
+
**Offer letters in Word, one per employee, numbered:**
|
|
832
|
+
|
|
833
|
+
```bash
|
|
834
|
+
templify render --each -t offer.docx -d employees.xlsx -o offers -n name --numbered
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**Only employees hired this year, newest first:**
|
|
838
|
+
|
|
839
|
+
```bash
|
|
840
|
+
templify render --each -t offer.docx -d employees.xlsx -o new-hires -n name --order -hired \
|
|
841
|
+
--filter '{"name": "date__ge", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false}'
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Emails only for people who have an email address:**
|
|
845
|
+
|
|
846
|
+
```bash
|
|
847
|
+
templify render --each -t email.txt -d employees.xlsx -o emails -n email \
|
|
848
|
+
--filter '{"name": "is_null", "args": ["email"], "kwargs": {}, "inverse": true}'
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
**A Markdown salary report per department:**
|
|
852
|
+
|
|
853
|
+
`department.md`:
|
|
854
|
+
|
|
855
|
+
```jinja
|
|
856
|
+
# {{ department }}
|
|
857
|
+
|
|
858
|
+
{% for e in items -%}
|
|
859
|
+
- {{ e.name }}: {{ e.salary }}
|
|
860
|
+
{% endfor %}
|
|
861
|
+
Total: {{ items | sum(attribute="salary") }}
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
```bash
|
|
865
|
+
templify render --each -t department.md -d employees.xlsx -o reports -n department --group-by department --order -salary
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
**Convert Excel to JSON:**
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
templify render -t all.json -d employees.xlsx -o export -f employees
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
with `all.json` containing `{{ objects | tojson(indent=2) }}`.
|
|
875
|
+
|
|
876
|
+
**One JSON file per record, for an API import:**
|
|
877
|
+
|
|
878
|
+
```bash
|
|
879
|
+
templify render --each -t profile.json -d employees.xlsx -o api -n email
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**A single printable Word file, one page per employee:**
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
templify render -t all-offers.docx -d employees.xlsx -o print --order name
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
**Name files with two columns**: templify names files from one column, so add a combined column to your data (an Excel formula like `=A2&" - "&B2` works once the file is saved in Excel), then use `-n full_title`.
|
|
889
|
+
|
|
890
|
+
## Errors
|
|
891
|
+
|
|
892
|
+
templify reports problems as short messages. Bad input (options, data, template files) exits with code `2`; a template that fails while rendering exits with code `1`:
|
|
893
|
+
|
|
894
|
+
| Problem | Example message |
|
|
895
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------- |
|
|
896
|
+
| Unsupported template extension | `Template extension 'pdf' is not supported, supported: docx, htm, ...` |
|
|
897
|
+
| Unsupported data extension | `Extension 'csv' is not supported for being a data file` |
|
|
898
|
+
| Both or neither of `-d` / `-s` | `Give exactly one data source: -d / --data or -s / --schema` |
|
|
899
|
+
| Option used in the wrong mode | `--name-by only works with --each` |
|
|
900
|
+
| `--each` with `--object` | `--each and --object can not be used together` |
|
|
901
|
+
| File doesn't exist / is a folder | `File '...' does not exist.` |
|
|
902
|
+
| Jinja syntax error in the template | `Template Error: unexpected '}'` |
|
|
903
|
+
| Corrupted `.docx` template | `Could not load template 't.docx': ...` |
|
|
904
|
+
| Corrupted `.xlsx` / JSON not a list of objects | `Could not read data file 'data.json': JSON data must be a list of objects` |
|
|
905
|
+
| JSON template rendered invalid JSON | `Template Error: Rendered output is not valid JSON: ...` |
|
|
906
|
+
| `--order` / `--group-by` column doesn't exist | `Key 'salary' not found in data, Available keys: name, age` |
|
|
907
|
+
| `--filter` isn't valid JSON | `Expecting property name enclosed in double quotes: ...` |
|
|
908
|
+
| Unknown filter rule | `Rule 'nope' does not exist. Available rules: is_true, is_null, ...` |
|
|
909
|
+
| Invalid value for a rule | `Argument 'value' with value 'soon' failed to process, ...` |
|
|
910
|
+
| Filter hit an empty cell or wrong type | `Filter failed on a data value (empty cell or wrong type?): ...` |
|
|
911
|
+
|
|
912
|
+
## Extending templify
|
|
913
|
+
|
|
914
|
+
Each extension point is a registry filled with a decorator. Add your code, and the CLI picks it up automatically, including `--help` and the error messages.
|
|
915
|
+
|
|
916
|
+
### A new data source
|
|
917
|
+
|
|
918
|
+
`src/templify/readers.py`: a function that takes a path and returns a list of dicts, registered on `readers`. A reader for `render --object` returns one dict and is registered on `object_readers`.
|
|
919
|
+
|
|
920
|
+
```python
|
|
921
|
+
import csv
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
@readers.reader("csv")
|
|
925
|
+
def read_csv(filepath: Path) -> Data:
|
|
926
|
+
with filepath.open(encoding="utf-8", newline="") as f:
|
|
927
|
+
return list(csv.DictReader(f))
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### A new template format
|
|
931
|
+
|
|
932
|
+
`src/templify/writers.py`: a class built from the template path that implements the `Writer` protocol, i.e. a `write(context, filepath)` method.
|
|
933
|
+
|
|
934
|
+
```python
|
|
935
|
+
class Writer(Protocol):
|
|
936
|
+
def write(self, context: Context, filepath: Path) -> None: ...
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
Text-based formats can simply be added to `TextWriter`:
|
|
940
|
+
|
|
941
|
+
```python
|
|
942
|
+
@writers.writer("txt", "md", "html", "htm", "xml", "csv")
|
|
943
|
+
class TextWriter: ...
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
A format with its own engine gets its own class:
|
|
947
|
+
|
|
948
|
+
```python
|
|
949
|
+
@writers.writer("pptx")
|
|
950
|
+
class PptxWriter:
|
|
951
|
+
def __init__(self, template_path: Path) -> None:
|
|
952
|
+
self.template_path = template_path
|
|
953
|
+
|
|
954
|
+
def write(
|
|
955
|
+
self, context: Context, filepath: Path
|
|
956
|
+
) -> None: ... # render context into the template and save to filepath
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
A format that needs validation can extend `TextWriter` and override `render`, the way `JsonWriter` does.
|
|
960
|
+
|
|
961
|
+
### A new filter rule
|
|
962
|
+
|
|
963
|
+
`src/templify/filter_rules.py`: the first two parameters are the row and the column. A `Process` marker on a parameter converts the value coming from the filter JSON.
|
|
964
|
+
|
|
965
|
+
```python
|
|
966
|
+
@rules.rule()
|
|
967
|
+
def float__between(d: dict[str, Any], key: str, low: FloatValue, high: FloatValue) -> bool:
|
|
968
|
+
return low <= d[key] <= high
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
```bash
|
|
972
|
+
--filter '{"name": "float__between", "args": ["salary", 1200, 1600], "kwargs": {}, "inverse": false}'
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
## Development
|
|
976
|
+
|
|
977
|
+
```bash
|
|
978
|
+
uv sync
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
Run the tests. Coverage (lines and branches) is printed after every run:
|
|
982
|
+
|
|
983
|
+
```bash
|
|
984
|
+
uv run pytest
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
Lint and format:
|
|
988
|
+
|
|
989
|
+
```bash
|
|
990
|
+
uvx ruff check src tests
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
```bash
|
|
994
|
+
uvx ruff format src tests
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
The demo GIFs are recorded with [VHS](https://github.com/charmbracelet/vhs) (`scoop install vhs`
|
|
998
|
+
or `brew install vhs`) and PowerShell (`pwsh`), from the repository root:
|
|
999
|
+
|
|
1000
|
+
```bash
|
|
1001
|
+
vhs docs/demo/each.tape && vhs docs/demo/schema.tape && vhs docs/demo/schema-list.tape
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Project structure
|
|
1005
|
+
|
|
1006
|
+
```text
|
|
1007
|
+
src/templify/
|
|
1008
|
+
├── models.py # Data / DataItem types
|
|
1009
|
+
├── readers.py # data sources: readers / object_readers, read_excel, read_json
|
|
1010
|
+
├── writers.py # Writer protocol, WriterRegistry, Docx/Text/Json writers,
|
|
1011
|
+
│ # write_single / write_multiple, file naming
|
|
1012
|
+
├── transformers.py # order_by_data, filter_data, group_by_data
|
|
1013
|
+
├── filter_rules.py # every --filter rule
|
|
1014
|
+
└── cli/
|
|
1015
|
+
├── main.py # the commands
|
|
1016
|
+
├── help.py # long --help texts
|
|
1017
|
+
└── dependencies/
|
|
1018
|
+
├── data.py # -d, --order, --filter, --group-by pipeline
|
|
1019
|
+
├── template.py # -t (picks the writer) and -o
|
|
1020
|
+
└── console.py # rich console
|
|
1021
|
+
tests/
|
|
1022
|
+
├── test_readers.py
|
|
1023
|
+
├── test_writers.py
|
|
1024
|
+
├── test_transformers.py
|
|
1025
|
+
├── test_filter_rules.py
|
|
1026
|
+
└── test_cli.py # end-to-end user scenarios
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
## License
|
|
1030
|
+
|
|
1031
|
+
templify is free software, released under the [GNU General Public License v3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) (see `LICENSE`).
|