templify-cli 0.2.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.
Files changed (23) hide show
  1. {templify_cli-0.2.0 → templify_cli-0.3.0}/PKG-INFO +155 -142
  2. {templify_cli-0.2.0 → templify_cli-0.3.0}/README.md +153 -140
  3. {templify_cli-0.2.0 → templify_cli-0.3.0}/pyproject.toml +113 -3
  4. {templify_cli-0.2.0 → templify_cli-0.3.0}/pyproject.toml.orig +44 -4
  5. templify_cli-0.3.0/src/templify/cli/__main__.py +4 -0
  6. templify_cli-0.3.0/src/templify/cli/cli.py +172 -0
  7. templify_cli-0.3.0/src/templify/cli/dependencies/data.py +304 -0
  8. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/cli/dependencies/template.py +87 -96
  9. templify_cli-0.3.0/src/templify/cli/help.py +32 -0
  10. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/filter_rules.py +1 -1
  11. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/readers.py +47 -23
  12. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/writers.py +314 -312
  13. templify_cli-0.2.0/src/templify/cli/dependencies/data.py +0 -202
  14. templify_cli-0.2.0/src/templify/cli/help.py +0 -61
  15. templify_cli-0.2.0/src/templify/cli/main.py +0 -230
  16. {templify_cli-0.2.0 → templify_cli-0.3.0}/LICENSE +0 -0
  17. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/__init__.py +0 -0
  18. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/cli/__init__.py +0 -0
  19. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/cli/dependencies/__init__.py +0 -0
  20. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/cli/dependencies/console.py +0 -0
  21. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/models.py +0 -0
  22. {templify_cli-0.2.0 → templify_cli-0.3.0}/src/templify/py.typed +0 -0
  23. {templify_cli-0.2.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.2.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
@@ -25,7 +25,7 @@ Requires-Dist: jinja2>=3.1
25
25
  Requires-Dist: openpyxl>=3.1.5
26
26
  Requires-Dist: pyspecification>=4.0.0
27
27
  Requires-Dist: rich>=13.0
28
- Requires-Dist: schemify-cli
28
+ Requires-Dist: schemify-cli>=0.1.0
29
29
  Requires-Dist: typer>=0.27.2
30
30
  Requires-Dist: typer-di>=0.1.5
31
31
  Requires-Dist: typer-swissknife>=0.1.0
@@ -40,10 +40,10 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  A command line tool that fills document templates with data.
42
42
 
43
- Point it at a **template** (Word, text, Markdown, HTML or JSON) and a **data file** (Excel or JSON). templify renders one file per row, or one file with every row in it. Before rendering, you can **order**, **filter** and **group** the data.
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
44
 
45
45
  ```bash
46
- templify m -t offer.docx -d employees.xlsx -o offers -k name
46
+ templify render --each -t offer.docx -d employees.xlsx -o offers -n name
47
47
  ```
48
48
 
49
49
  ```text
@@ -63,10 +63,9 @@ offers/
63
63
  - [Quick start](#quick-start)
64
64
  - [How it works](#how-it-works)
65
65
  - [Commands](#commands)
66
- - [`multiple-templates` (`m`)](#multiple-templates-m)
67
- - [`single-template` (`s`)](#single-template-s)
68
- - [`schema-template` (`js`) and `schema-list-template` (`jsl`)](#schema-template-js-and-schema-list-template-jsl)
69
- - [`filters-json-schema` (`fjs`)](#filters-json-schema-fjs)
66
+ - [`render` (`r`)](#render-r)
67
+ - [`filters` (`f`)](#filters-f)
68
+ - [Upgrading from 0.2](#upgrading-from-02)
70
69
  - [Data sources](#data-sources)
71
70
  - [Templates](#templates)
72
71
  - [Supported formats](#supported-formats)
@@ -91,8 +90,8 @@ offers/
91
90
  ## Features
92
91
 
93
92
  - **Many template formats**: `.docx`, `.txt`, `.md`, `.html`, `.htm` and `.json`. The writer is picked automatically from the template's extension.
94
- - **Many data sources**: Excel (`.xlsx`) and JSON (`.json`).
95
- - **Two output modes**: one file per row (`multiple-templates`), or every row in one file (`single-template`).
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`).
96
95
  - **Ordering** by any number of columns, ascending or descending.
97
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.
98
97
  - **Grouping** by one or more columns, e.g. one file per department listing its employees.
@@ -148,7 +147,7 @@ You joined {{ department }} on {{ hired.strftime("%d/%m/%Y") }}.
148
147
  **3. Run templify:**
149
148
 
150
149
  ```bash
151
- templify m -t welcome.txt -d employees.xlsx -o welcome -k name
150
+ templify render --each -t welcome.txt -d employees.xlsx -o welcome -n name
152
151
  ```
153
152
 
154
153
  `welcome/Rama.txt`:
@@ -162,21 +161,23 @@ You joined IT on 10/02/2024.
162
161
 
163
162
  ## How it works
164
163
 
165
- Every command runs the same pipeline:
164
+ `render` runs one pipeline:
166
165
 
167
166
  ```text
168
167
  read data ──► order ──► filter ──► group ──► render template ──► save file(s)
169
- (-d) (--order) (--filter) (--group-by) (-t) (-o)
168
+ (-d / -s) (--order) (--filter) (--group-by) (-t) (-o)
170
169
  ```
171
170
 
172
- 1. **Read**: the data file becomes a list of rows, each row a set of `column: value` pairs.
171
+ 1. **Read**: the [data source](#data-sources) becomes a list of rows, each row a set of `column: value` pairs.
173
172
  2. **Order**: rows are sorted by `--order` columns (optional).
174
173
  3. **Filter**: rows not matching `--filter` are dropped (optional).
175
174
  4. **Group**: rows are grouped by `--group-by` columns (optional).
176
175
  5. **Render**: the writer chosen from the template extension renders the rows.
177
176
  6. **Save**:
178
- - `multiple-templates` saves one file per row, or per group when grouping.
179
- - `single-template` saves one file containing every row.
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.
180
181
 
181
182
  ## Commands
182
183
 
@@ -184,83 +185,62 @@ Every command runs the same pipeline:
184
185
  templify --help
185
186
  ```
186
187
 
187
- | Command | Aliases | What it does |
188
- | --------------------- | --------------------- | -------------------------------------------- |
189
- | `multiple-templates` | `multiple`, `m` | Render **one file per row** (or per group) |
190
- | `single-template` | `single`, `s` | Render **one file** with all the rows |
191
- | `schema-template` | `schema`, `js` | Ask for one object from a JSON Schema, render **one file** |
192
- | `schema-list-template`| `schema-list`, `jsl` | Ask for a list from a JSON Schema, render **one file** |
193
- | `filters-json-schema` | `filters-schema`, `fjs` | List the filter rules and their arguments |
194
-
195
- ### `multiple-templates` (`m`)
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 |
196
192
 
197
- Renders the template once **per row**. Each row's columns are the template variables: `{{ name }}`, `{{ salary }}`, ...
193
+ ### `render` (`r`)
198
194
 
199
195
  ```bash
200
- templify m -t <template> -d <data> -o <output-dir> [OPTIONS]
196
+ templify render -t <template> (-d <data> | -s <schema.json>) [--each | --object] [-o <output-dir>] [OPTIONS]
201
197
  ```
202
198
 
203
- | Option | Short | Description | Default |
204
- | ------------------ | ----- | -------------------------------------------------------------------- | ---------------- |
205
- | `--template` | `-t` | Template file. **Required.** | |
206
- | `--data` | `-d` | Data file. **Required.** | |
207
- | `--output` | `-o` | Output directory, created if missing. **Required.** | |
208
- | `--filename-key` | `-k` | Column whose value names each file | `template-<n>` |
209
- | `--index` | `-i` | Prefix each filename with its row number: `1 - Rama.docx` | off |
210
- | `--order` | | Column to order by, `-column` for descending. Repeatable | |
211
- | `--filter` | | JSON [filter schema](#filter-schema) | |
212
- | `--group-by` | | Column to [group](#grouping) by. Repeatable | |
213
- | `--group-items` | | Variable name holding each group's rows | `items` |
199
+ The mode decides what the template sees and how many files are written:
214
200
 
215
- Example:
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 }}` |
216
206
 
217
- ```bash
218
- templify m -t offer.docx -d employees.xlsx -o offers -k name --index
219
- ```
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` |
220
223
 
221
- ```text
222
- offers/
223
- ├── 1 - Abdullah.docx
224
- ├── 2 - Rawaa.docx
225
- ├── 3 - Rama.docx
226
- └── 4 - Omar.docx
227
- ```
228
-
229
- ### `single-template` (`s`)
230
-
231
- Renders the template **once**, with every row in a list variable (`data` by default). The template loops over it.
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`.
232
226
 
233
- ```bash
234
- templify s -t <template> -d <data> -o <output-dir> [OPTIONS]
235
- ```
227
+ #### All rows in one file (default)
236
228
 
237
- | Option | Short | Description | Default |
238
- | ----------------- | ----- | ---------------------------------------------------------- | ---------------------- |
239
- | `--template` | `-t` | Template file. **Required.** | |
240
- | `--data` | `-d` | Data file. **Required.** | |
241
- | `--output` | `-o` | Output directory, created if missing. **Required.** | |
242
- | `--filename` | `-f` | Output filename **without** extension | the template's name |
243
- | `--data-variable` | `-v` | Name of the list variable inside the template | `data` |
244
- | `--order` | | Column to order by, `-column` for descending. Repeatable | |
245
- | `--filter` | | JSON [filter schema](#filter-schema) | |
246
- | `--group-by` | | Column to [group](#grouping) by. Repeatable | |
247
- | `--group-items` | | Variable name holding each group's rows | `items` |
248
-
249
- Example with `team.md`:
229
+ `team.md`:
250
230
 
251
231
  ```jinja
252
232
  # Team report
253
233
 
254
234
  | # | Name | Department | Salary |
255
235
  |---|------|------------|-------:|
256
- {% for e in data -%}
236
+ {% for e in objects -%}
257
237
  | {{ loop.index }} | {{ e.name }} | {{ e.department }} | {{ "{:,}".format(e.salary) }} |
258
238
  {% endfor %}
259
- **Total salaries:** {{ data | sum(attribute="salary") }}
239
+ **Total salaries:** {{ objects | sum(attribute="salary") }}
260
240
  ```
261
241
 
262
242
  ```bash
263
- templify s -t team.md -d employees.xlsx -o reports --order -salary
243
+ templify render -t team.md -d employees.xlsx -o reports --order -salary
264
244
  ```
265
245
 
266
246
  `reports/team.md`:
@@ -281,44 +261,44 @@ templify s -t team.md -d employees.xlsx -o reports --order -salary
281
261
  Rename the output and the loop variable:
282
262
 
283
263
  ```bash
284
- templify s -t team.md -d employees.xlsx -o reports -f "2024 team" -v employees
264
+ templify render -t team.md -d employees.xlsx -o reports -f "2024 team" -v employees
285
265
  ```
286
266
 
287
267
  The template then loops with `{% for e in employees %}`, and the file is saved as `reports/2024 team.md`.
288
268
 
289
- ### `schema-template` (`js`) and `schema-list-template` (`jsl`)
290
-
291
- No data file: [schemify](https://github.com/aaltatan/schemify-cli) asks for the data matching a
292
- JSON Schema file, then it is rendered into **one file**. `schema-template` asks for one object,
293
- `schema-list-template` for a list of them.
294
-
295
- | Option | Short | Description | Default |
296
- | ----------------- | ----- | --------------------------------------------------- | ------------------- |
297
- | `--json-schema` | `--js`| JSON Schema file. **Required.** | |
298
- | `--template` | `-t` | Template file. **Required.** | |
299
- | `--output` | `-o` | Output directory, created if missing. **Required.** | |
300
- | `--filename` | `-f` | Output filename **without** extension | the template's name |
301
- | `--data-variable` | `-v` | Name of the variable holding the data | `data` |
269
+ #### One file per row (`--each`)
302
270
 
303
271
  ```bash
304
- templify js --js person.schema.json -t letter.docx -o out # Hello {{ data.name }}
305
- templify jsl --js person.schema.json -t team.md -o out # {% for p in data %}...{% endfor %}
272
+ templify render -t offer.docx -d employees.xlsx -o offers --each -n name --numbered
306
273
  ```
307
274
 
308
- `schema-template` fills one object:
275
+ ```text
276
+ offers/
277
+ ├── 1 - Abdullah.docx
278
+ ├── 2 - Rawaa.docx
279
+ ├── 3 - Rama.docx
280
+ └── 4 - Omar.docx
281
+ ```
282
+
283
+ ![templify render --each](https://raw.githubusercontent.com/aaltatan/templify/main/docs/demo/each.gif)
309
284
 
310
- ![templify schema-template](https://raw.githubusercontent.com/aaltatan/templify/main/docs/demo/schema.gif)
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
+ ```
311
291
 
312
- `schema-list-template` keeps asking for items until you answer **No**:
292
+ With `-s`, [schemify](https://github.com/aaltatan/schemify-cli) asks for the fields:
313
293
 
314
- ![templify schema-list-template](https://raw.githubusercontent.com/aaltatan/templify/main/docs/demo/schema-list.gif)
294
+ ![templify render --object](https://raw.githubusercontent.com/aaltatan/templify/main/docs/demo/schema.gif)
315
295
 
316
- ### `filters-json-schema` (`fjs`)
296
+ ### `filters` (`f`)
317
297
 
318
298
  Lists every filter rule you can use in `--filter`, with its arguments (in `args` order) and description:
319
299
 
320
300
  ```bash
321
- templify fjs
301
+ templify filters
322
302
  ```
323
303
 
324
304
  ```text
@@ -336,15 +316,44 @@ templify fjs
336
316
 
337
317
  The argument names are also the `kwargs` names: `{"key": "salary", "value": 1400}`.
338
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
+
339
335
  ## Data sources
340
336
 
341
- The reader is chosen from the data file extension, ignoring case.
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
+ ![templify render -s](https://raw.githubusercontent.com/aaltatan/templify/main/docs/demo/schema-list.gif)
343
+
344
+ Data files for the default mode and `--each` hold a **list of objects**:
342
345
 
343
346
  | Extension | Format |
344
347
  | --------- | --------------------------------------------------------------------------- |
345
348
  | `.xlsx` | Excel workbook: the **active sheet**, first row = column names |
346
349
  | `.json` | A JSON **list of objects**: `[{"name": "Rama", ...}, ...]` |
347
350
 
351
+ Data files for `--object` hold a **single object**:
352
+
353
+ | Extension | Format |
354
+ | --------- | --------------------------------------------------------------------------- |
355
+ | `.json` | A JSON **object**: `{"name": "Rama", ...}` |
356
+
348
357
  **Excel notes**
349
358
 
350
359
  - The first row holds the column names, and each following row is one record.
@@ -395,7 +404,7 @@ All templates use [Jinja2](https://jinja.palletsprojects.com/en/stable/templates
395
404
  | ------------------------------------------ | ---------------------------------------- |
396
405
  | `{{ name }}` | Print a variable |
397
406
  | `{{ e.name }}` or `{{ e["name"] }}` | Print a field of a row |
398
- | `{% for e in data %} ... {% endfor %}` | Loop |
407
+ | `{% for e in objects %} ... {% endfor %}` | Loop |
399
408
  | `{{ loop.index }}` | 1-based loop counter |
400
409
  | `{% if salary > 1400 %} ... {% endif %}` | Condition |
401
410
  | `{{ name \| upper }}` | Filter: `upper`, `lower`, `title`, `length`, `sum`, `default`, `tojson`, ... |
@@ -435,24 +444,24 @@ docxtpl adds special tags for Word structure:
435
444
 
436
445
  | Tag | Use |
437
446
  | ----------------------------------- | ------------------------------------------------------- |
438
- | `{%p for e in data %}` ... `{%p endfor %}` | Loop over **paragraphs**: each tag sits on its own paragraph |
439
- | `{%tr for e in data %}` ... `{%tr endfor %}` | Loop over **table rows**: one row per record |
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 |
440
449
  | `{%tc ... %}` | Loop over table cells |
441
450
  | `{%r ... %}` | Tag applied to a run |
442
451
 
443
452
  **One Word file, one page per employee** (`all-offers.docx`, three paragraphs):
444
453
 
445
454
  ```text
446
- {%p for e in data %}
455
+ {%p for e in objects %}
447
456
  {{ e.name }} - {{ e.department }}{{ page_break }}
448
457
  {%p endfor %}
449
458
  ```
450
459
 
451
460
  ```bash
452
- templify s -t all-offers.docx -d employees.xlsx -o offers
461
+ templify render -t all-offers.docx -d employees.xlsx -o offers
453
462
  ```
454
463
 
455
- **A Word table with one row per employee**: create a 2-row table. Put `{%tr for e in data %}` and `{%tr endfor %}` in rows of their own, around a row containing `{{ e.name }}`, `{{ e.salary }}`, ...
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 }}`, ...
456
465
 
457
466
  See the [docxtpl documentation](https://docxtpl.readthedocs.io/) for images, rich text and more.
458
467
 
@@ -474,7 +483,7 @@ Always write values through the `tojson` filter. It adds quotes and escapes char
474
483
  ```
475
484
 
476
485
  ```bash
477
- templify m -t profile.json -d employees.json -o profiles -k name
486
+ templify render --each -t profile.json -d employees.json -o profiles -n name
478
487
  ```
479
488
 
480
489
  `profiles/Omar.json`:
@@ -491,11 +500,11 @@ templify m -t profile.json -d employees.json -o profiles -k name
491
500
  Dump the whole (ordered / filtered / grouped) dataset:
492
501
 
493
502
  ```jinja
494
- {{ data | tojson(indent=2) }}
503
+ {{ objects | tojson(indent=2) }}
495
504
  ```
496
505
 
497
506
  ```bash
498
- templify s -t all.json -d employees.xlsx -o export -f employees --order name
507
+ templify render -t all.json -d employees.xlsx -o export -f employees --order name
499
508
  ```
500
509
 
501
510
  > `tojson` sorts object keys alphabetically.
@@ -526,7 +535,7 @@ Invalid value: Template Error: Rendered output is not valid JSON: Expecting ','
526
535
  ```
527
536
 
528
537
  ```bash
529
- templify m -t department.html -d employees.xlsx -o departments --group-by department -k department
538
+ templify render --each -t department.html -d employees.xlsx -o departments --group-by department -n department
530
539
  ```
531
540
 
532
541
  > Values are **not** HTML-escaped, so `<b>` in your data stays bold. If the data isn't trusted, escape it with `{{ value | e }}`.
@@ -535,15 +544,15 @@ templify m -t department.html -d employees.xlsx -o departments --group-by depart
535
544
 
536
545
  | Situation | Result |
537
546
  | ------------------------------------------- | --------------------------------------------------- |
538
- | No `-k` | `template-1.txt`, `template-2.txt`, ... |
547
+ | No `-n` | `template-1.txt`, `template-2.txt`, ... |
539
548
  | `-k name` | `Rama.txt` |
540
549
  | `-k name -i` | `3 - Rama.txt` |
541
- | The `-k` column is empty or missing for a row | That row falls back to `template-<n>.txt` |
550
+ | The `-n` column is empty or missing for a row | That row falls back to `template-<n>.txt` |
542
551
  | The value has invalid characters: `a/b:c?` | They become `_`: `a_b_c_.txt` |
543
552
  | The value is a number: `-k id` | `42.txt` |
544
553
  | The file already exists | A short random suffix is added: `Rama-3f9a1c2e.txt`. **Nothing is ever overwritten** |
545
554
  | The output folder doesn't exist | It is created, including parent folders |
546
- | `single-template` without `-f` | Named after the template: `team.md` → `team.md` |
555
+ | `render` without `-f` | Named after the template: `team.md` → `team.md` |
547
556
 
548
557
  Output files are written as **UTF-8**, with the template's line endings kept as they are.
549
558
 
@@ -553,10 +562,10 @@ Output files are written as **UTF-8**, with the template's line endings kept as
553
562
 
554
563
  ```bash
555
564
  # highest salary first
556
- templify s -t names.txt -d employees.xlsx -o out --order -salary
565
+ templify render -t names.txt -d employees.xlsx -o out --order -salary
557
566
 
558
567
  # by department A→Z, then by salary high→low inside each department
559
- templify s -t names.txt -d employees.xlsx -o out --order department --order -salary
568
+ templify render -t names.txt -d employees.xlsx -o out --order department --order -salary
560
569
  ```
561
570
 
562
571
  ```text
@@ -596,17 +605,17 @@ Ordering by a column that doesn't exist fails with the list of available columns
596
605
 
597
606
  ```bash
598
607
  # IT only
599
- templify s -t names.txt -d employees.xlsx -o out \
608
+ templify render -t names.txt -d employees.xlsx -o out \
600
609
  --filter '{"name": "string__eq", "args": ["department", "IT"], "kwargs": {}, "inverse": false}'
601
610
  # → Rawaa Rama
602
611
 
603
612
  # everyone except IT
604
- templify s -t names.txt -d employees.xlsx -o out \
613
+ templify render -t names.txt -d employees.xlsx -o out \
605
614
  --filter '{"name": "string__eq", "args": ["department", "IT"], "kwargs": {}, "inverse": true}'
606
615
  # → Abdullah Omar
607
616
 
608
617
  # same rule, arguments by name
609
- templify s -t names.txt -d employees.xlsx -o out \
618
+ templify render -t names.txt -d employees.xlsx -o out \
610
619
  --filter '{"name": "int__gt", "args": [], "kwargs": {"key": "salary", "value": 1400}, "inverse": false}'
611
620
  # → Rawaa Rama
612
621
  ```
@@ -620,7 +629,7 @@ templify s -t names.txt -d employees.xlsx -o out \
620
629
 
621
630
  ```bash
622
631
  # in Homs AND earning at least 1300
623
- templify s -t names.txt -d employees.xlsx -o out --filter '{
632
+ templify render -t names.txt -d employees.xlsx -o out --filter '{
624
633
  "operator": "all",
625
634
  "expressions": [
626
635
  {"name": "string__eq", "args": ["city", "Homs"], "kwargs": {}, "inverse": false},
@@ -630,7 +639,7 @@ templify s -t names.txt -d employees.xlsx -o out --filter '{
630
639
  # → Rama
631
640
 
632
641
  # in Aleppo OR earning more than 1700
633
- templify s -t names.txt -d employees.xlsx -o out --filter '{
642
+ templify render -t names.txt -d employees.xlsx -o out --filter '{
634
643
  "operator": "any",
635
644
  "expressions": [
636
645
  {"name": "string__eq", "args": ["city", "Aleppo"], "kwargs": {}, "inverse": false},
@@ -644,7 +653,7 @@ Expressions **nest** to any depth:
644
653
 
645
654
  ```bash
646
655
  # IT AND (in Damascus OR hired since 2024)
647
- templify s -t names.txt -d employees.xlsx -o out --filter '{
656
+ templify render -t names.txt -d employees.xlsx -o out --filter '{
648
657
  "operator": "all",
649
658
  "expressions": [
650
659
  {"name": "string__eq", "args": ["department", "IT"], "kwargs": {}, "inverse": false},
@@ -724,12 +733,12 @@ They compare the **day only** and accept every form a date comes in:
724
733
 
725
734
  ```bash
726
735
  # hired in 2024 or later (Excel dates)
727
- templify s -t names.txt -d employees.xlsx -o out \
736
+ templify render -t names.txt -d employees.xlsx -o out \
728
737
  --filter '{"name": "date__ge", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false}'
729
738
  # → Rama Omar
730
739
 
731
740
  # hired before 2024 (JSON string dates)
732
- templify s -t names.txt -d employees.json -o out \
741
+ templify render -t names.txt -d employees.json -o out \
733
742
  --filter '{"name": "date__lt", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false}'
734
743
  # → Abdullah Rawaa
735
744
 
@@ -753,7 +762,7 @@ templify s -t names.txt -d employees.json -o out \
753
762
  `--group-by <column>` merges rows that share the same value into **one group**. Each group is a new row holding:
754
763
 
755
764
  - the group-by column(s), with the shared value
756
- - `items` (rename it with `--group-items`), the list of the original rows in that group
765
+ - `items` (rename it with `--items-var`), the list of the original rows in that group
757
766
 
758
767
  ```text
759
768
  rows: groups (--group-by department):
@@ -765,10 +774,10 @@ rows: groups (--group-by department):
765
774
 
766
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.
767
776
 
768
- **One file per group**, with `multiple-templates`: `-k` can name files after the group column.
777
+ **One file per group**, with `render --each`: `-n` can name files after the group column.
769
778
 
770
779
  ```bash
771
- templify m -t department.html -d employees.xlsx -o departments --group-by department -k department
780
+ templify render --each -t department.html -d employees.xlsx -o departments --group-by department -n department
772
781
  ```
773
782
 
774
783
  ```text
@@ -777,20 +786,20 @@ departments/
777
786
  └── IT.html
778
787
  ```
779
788
 
780
- **One report of all groups**, with `single-template`: loop over groups, then over each group's rows.
789
+ **One report of all groups**, with `render`: loop over groups, then over each group's rows.
781
790
 
782
791
  `by-city.txt`:
783
792
 
784
793
  ```jinja
785
- {% for group in data -%}
794
+ {% for group in objects -%}
786
795
  {{ group.city }} / {{ group.department }}:
787
796
  {%- for e in group.people %} {{ e.name }}{% endfor %}
788
797
  {% endfor %}
789
798
  ```
790
799
 
791
800
  ```bash
792
- templify s -t by-city.txt -d employees.xlsx -o out \
793
- --order city --group-by city --group-by department --group-items people
801
+ templify render -t by-city.txt -d employees.xlsx -o out \
802
+ --order city --group-by city --group-by department --items-var people
794
803
  ```
795
804
 
796
805
  ```text
@@ -814,7 +823,7 @@ Useful group-level expressions:
814
823
  | Problem | Message |
815
824
  | -------------------------------------------------------- | --------------------------------------------------- |
816
825
  | The column doesn't exist | `Key 'country' not found in data, Available keys: ...` |
817
- | `--group-items` equals a group-by column | `Items key 'city' can not be one of the group by keys` |
826
+ | `--items-var` equals a group-by column | `Items key 'city' can not be one of the group by keys` |
818
827
  | The column holds lists or objects (JSON) | `Can not group by unhashable values ...` |
819
828
 
820
829
  ## Cookbook
@@ -822,20 +831,20 @@ Useful group-level expressions:
822
831
  **Offer letters in Word, one per employee, numbered:**
823
832
 
824
833
  ```bash
825
- templify m -t offer.docx -d employees.xlsx -o offers -k name -i
834
+ templify render --each -t offer.docx -d employees.xlsx -o offers -n name --numbered
826
835
  ```
827
836
 
828
837
  **Only employees hired this year, newest first:**
829
838
 
830
839
  ```bash
831
- templify m -t offer.docx -d employees.xlsx -o new-hires -k name --order -hired \
840
+ templify render --each -t offer.docx -d employees.xlsx -o new-hires -n name --order -hired \
832
841
  --filter '{"name": "date__ge", "args": ["hired", "2024-01-01"], "kwargs": {}, "inverse": false}'
833
842
  ```
834
843
 
835
844
  **Emails only for people who have an email address:**
836
845
 
837
846
  ```bash
838
- templify m -t email.txt -d employees.xlsx -o emails -k email \
847
+ templify render --each -t email.txt -d employees.xlsx -o emails -n email \
839
848
  --filter '{"name": "is_null", "args": ["email"], "kwargs": {}, "inverse": true}'
840
849
  ```
841
850
 
@@ -853,39 +862,42 @@ Total: {{ items | sum(attribute="salary") }}
853
862
  ```
854
863
 
855
864
  ```bash
856
- templify m -t department.md -d employees.xlsx -o reports -k department --group-by department --order -salary
865
+ templify render --each -t department.md -d employees.xlsx -o reports -n department --group-by department --order -salary
857
866
  ```
858
867
 
859
868
  **Convert Excel to JSON:**
860
869
 
861
870
  ```bash
862
- templify s -t all.json -d employees.xlsx -o export -f employees
871
+ templify render -t all.json -d employees.xlsx -o export -f employees
863
872
  ```
864
873
 
865
- with `all.json` containing `{{ data | tojson(indent=2) }}`.
874
+ with `all.json` containing `{{ objects | tojson(indent=2) }}`.
866
875
 
867
876
  **One JSON file per record, for an API import:**
868
877
 
869
878
  ```bash
870
- templify m -t profile.json -d employees.xlsx -o api -k email
879
+ templify render --each -t profile.json -d employees.xlsx -o api -n email
871
880
  ```
872
881
 
873
882
  **A single printable Word file, one page per employee:**
874
883
 
875
884
  ```bash
876
- templify s -t all-offers.docx -d employees.xlsx -o print --order name
885
+ templify render -t all-offers.docx -d employees.xlsx -o print --order name
877
886
  ```
878
887
 
879
- **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 `-k full_title`.
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`.
880
889
 
881
890
  ## Errors
882
891
 
883
- templify reports problems as short messages and exits with code `2`:
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`:
884
893
 
885
894
  | Problem | Example message |
886
895
  | ------------------------------------------------ | ---------------------------------------------------------------------- |
887
896
  | Unsupported template extension | `Template extension 'pdf' is not supported, supported: docx, htm, ...` |
888
- | Unsupported data extension | `Extension csv is not supported` |
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` |
889
901
  | File doesn't exist / is a folder | `File '...' does not exist.` |
890
902
  | Jinja syntax error in the template | `Template Error: unexpected '}'` |
891
903
  | Corrupted `.docx` template | `Could not load template 't.docx': ...` |
@@ -903,7 +915,7 @@ Each extension point is a registry filled with a decorator. Add your code, and t
903
915
 
904
916
  ### A new data source
905
917
 
906
- `src/templify/readers.py`: a function that takes a path and returns a list of dicts.
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`.
907
919
 
908
920
  ```python
909
921
  import csv
@@ -939,8 +951,9 @@ class PptxWriter:
939
951
  def __init__(self, template_path: Path) -> None:
940
952
  self.template_path = template_path
941
953
 
942
- def write(self, context: Context, filepath: Path) -> None:
943
- ... # render context into the template and save to filepath
954
+ def write(
955
+ self, context: Context, filepath: Path
956
+ ) -> None: ... # render context into the template and save to filepath
944
957
  ```
945
958
 
946
959
  A format that needs validation can extend `TextWriter` and override `render`, the way `JsonWriter` does.
@@ -985,7 +998,7 @@ The demo GIFs are recorded with [VHS](https://github.com/charmbracelet/vhs) (`sc
985
998
  or `brew install vhs`) and PowerShell (`pwsh`), from the repository root:
986
999
 
987
1000
  ```bash
988
- vhs docs/demo/schema.tape && vhs docs/demo/schema-list.tape
1001
+ vhs docs/demo/each.tape && vhs docs/demo/schema.tape && vhs docs/demo/schema-list.tape
989
1002
  ```
990
1003
 
991
1004
  ### Project structure
@@ -993,7 +1006,7 @@ vhs docs/demo/schema.tape && vhs docs/demo/schema-list.tape
993
1006
  ```text
994
1007
  src/templify/
995
1008
  ├── models.py # Data / DataItem types
996
- ├── readers.py # data sources: ReaderRegistry, read_excel, read_json
1009
+ ├── readers.py # data sources: readers / object_readers, read_excel, read_json
997
1010
  ├── writers.py # Writer protocol, WriterRegistry, Docx/Text/Json writers,
998
1011
  │ # write_single / write_multiple, file naming
999
1012
  ├── transformers.py # order_by_data, filter_data, group_by_data