hanky 0.3.0.dev0__tar.gz → 0.3.0.dev1__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.
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/PKG-INFO +33 -25
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/README.md +32 -24
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/pyproject.toml +1 -1
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/__main__.py +2 -1
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/cli.py +13 -14
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/hanky.py +36 -48
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/processors.py +2 -2
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/uv.lock +1 -1
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/.gitignore +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/LICENSE +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/__init__.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/anki_utils.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/config.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/errors.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/fs.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/media.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev1}/src/hanky/report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hanky
|
|
3
|
-
Version: 0.3.0.
|
|
3
|
+
Version: 0.3.0.dev1
|
|
4
4
|
Summary: Simple library and command line tool for loading flash cards into anki.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Haeata-Ash/hanky
|
|
6
6
|
Project-URL: Issues, https://github.com/Haeata-Ash/hanky/issues
|
|
@@ -68,18 +68,18 @@ See [Card Processors & Pipelines](#card-processors--pipelines) or the [demo fold
|
|
|
68
68
|
from hanky import HankyPipeline
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
# instantiate the hanky app
|
|
72
|
-
hanky = HankyPipeline()
|
|
71
|
+
# instantiate the hanky app, creating cards with the "basic" model
|
|
72
|
+
hanky = HankyPipeline("basic")
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
@hanky.card_processor(
|
|
75
|
+
@hanky.card_processor(expected_args=[], card_fields=[])
|
|
76
76
|
def lowercase_card(card: dict):
|
|
77
77
|
"""Lower-case the text on every field of the card."""
|
|
78
78
|
return {field: value.lower() for field, value in card.items()}
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
# run the hanky cli application by running this python file, for example:
|
|
82
|
-
# python3 my_script.py pipe words.csv --
|
|
82
|
+
# python3 my_script.py pipe words.csv --into english::vocab
|
|
83
83
|
hanky.run()
|
|
84
84
|
|
|
85
85
|
```
|
|
@@ -100,10 +100,10 @@ Ephemeral,Lasting A VERY Short Time
|
|
|
100
100
|
We would run our new script like so
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
python3 my_script.py pipe words.csv --
|
|
103
|
+
python3 my_script.py pipe words.csv --into english::vocab
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
Here we tell hanky to **pipe** each word in `words.csv` through our `card_processors`, **transforming** each line of the csv with the `lowercase_card`, before finally adding each card, of note type `basic
|
|
106
|
+
Here we tell hanky to **pipe** each word in `words.csv` through our `card_processors`, **transforming** each line of the csv with the `lowercase_card`, before finally adding each card, of note type `basic` (the model we set on the pipeline), into the anki deck `english::vocab`. A single run can target a different model by passing `--model`.
|
|
107
107
|
|
|
108
108
|
This would leave us with a **english::vocab** deck containing the following cards:
|
|
109
109
|
|
|
@@ -155,7 +155,7 @@ over the file). Useful if you want different config for different scripts:
|
|
|
155
155
|
from hanky import HankyPipeline
|
|
156
156
|
from hanky.config import Config
|
|
157
157
|
|
|
158
|
-
hanky = HankyPipeline(config=Config(ALLOW_DUPLICATES=True))
|
|
158
|
+
hanky = HankyPipeline("basic", config=Config(ALLOW_DUPLICATES=True))
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
## Card Processors & Pipelines
|
|
@@ -168,17 +168,20 @@ Multiple processors can be registered on a `HankyPipeline` app to create a pipel
|
|
|
168
168
|
|
|
169
169
|
### The processor contract
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
Processors are model-agnostic: the Anki model/note-type is set once on the
|
|
172
|
+
pipeline itself (`HankyPipeline("basic")`), and every card the pipeline adds is
|
|
173
|
+
created with that model (unless overridden for a run with `--model`).
|
|
174
|
+
|
|
175
|
+
A processor is registered with two things:
|
|
172
176
|
|
|
173
177
|
```python
|
|
174
|
-
@hanky.card_processor(
|
|
178
|
+
@hanky.card_processor(expected_args=[...], card_fields=[...])
|
|
175
179
|
def my_processor(card: dict, **expected_args):
|
|
176
180
|
...
|
|
177
181
|
```
|
|
178
182
|
|
|
179
183
|
| Part | Meaning |
|
|
180
184
|
| --- | --- |
|
|
181
|
-
| `model` | The Anki model/note-type name. The processor runs on every card of this model. |
|
|
182
185
|
| `expected_args` | Names of CLI arguments the processor needs. They are passed in from the command line via `--args key=value` and arrive as keyword arguments. For example, you might have the same pipeline for different languages, so you would pass in `lang=german` or `lang=french`|
|
|
183
186
|
| `card_fields` | Fields that **must already be present** on the card when this processor runs. Hanky checks this and raises a clear error if one is missing. It lets a processor declare what an *earlier* step must have produced. |
|
|
184
187
|
|
|
@@ -186,7 +189,7 @@ When hanky calls your processors, the first argument is always the `card`; a pla
|
|
|
186
189
|
|
|
187
190
|
|
|
188
191
|
```python
|
|
189
|
-
@hanky.card_processor(
|
|
192
|
+
@hanky.card_processor(expected_args=["lang"], card_fields=[...])
|
|
190
193
|
def my_processor(card: dict, lang):
|
|
191
194
|
...
|
|
192
195
|
```
|
|
@@ -240,10 +243,10 @@ def scrape_wordreference(word: str, lang_pair: str) -> tuple[str, str]:
|
|
|
240
243
|
def generate_neural_speech(utf_8_str: str, voice: str) -> bytes:
|
|
241
244
|
...
|
|
242
245
|
|
|
243
|
-
hanky = HankyPipeline()
|
|
246
|
+
hanky = HankyPipeline("lang-vocab")
|
|
244
247
|
|
|
245
248
|
# Stage 1: requires `word` (this comes straight from the csv), produces `translation` + `example`.
|
|
246
|
-
@hanky.card_processor(
|
|
249
|
+
@hanky.card_processor(expected_args=[], card_fields=["word"])
|
|
247
250
|
def scrape_translation(card: dict):
|
|
248
251
|
translation, example = scrape_wordreference(card["word"], "enfr")
|
|
249
252
|
card["translation"] = translation
|
|
@@ -251,7 +254,7 @@ def scrape_translation(card: dict):
|
|
|
251
254
|
return card
|
|
252
255
|
|
|
253
256
|
# Stage 2: requires `translation` (from stage 1), attaches audio media.
|
|
254
|
-
@hanky.card_processor(
|
|
257
|
+
@hanky.card_processor(expected_args=[], card_fields=["translation"])
|
|
255
258
|
def add_audio(card: dict):
|
|
256
259
|
speech = generate_neural_speech(card["translation"], voice="Lea")
|
|
257
260
|
audio = CardMedia(speech, ".mp3")
|
|
@@ -261,9 +264,9 @@ def add_audio(card: dict):
|
|
|
261
264
|
hanky.run()
|
|
262
265
|
```
|
|
263
266
|
|
|
264
|
-
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --
|
|
267
|
+
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --into french::vocab`
|
|
265
268
|
|
|
266
|
-
> Note that
|
|
269
|
+
> Note that this pipeline was created with the *lang-vocab* model rather than *basic*. That means we are assuming that a model called *lang-vocab* has already been created in the anki ui. See the [Anki documentation for adding a note type](https://docs.ankiweb.net/editing.html#adding-a-note-type) to learn how this is done.
|
|
267
270
|
|
|
268
271
|
|
|
269
272
|
### Attaching media
|
|
@@ -322,7 +325,7 @@ DICTIONARY = {
|
|
|
322
325
|
# ...
|
|
323
326
|
}
|
|
324
327
|
|
|
325
|
-
hanky = HankyPipeline()
|
|
328
|
+
hanky = HankyPipeline("basic")
|
|
326
329
|
|
|
327
330
|
|
|
328
331
|
def random_word_cards(n):
|
|
@@ -331,7 +334,7 @@ def random_word_cards(n):
|
|
|
331
334
|
|
|
332
335
|
|
|
333
336
|
# add 20 cards straight from the generator, with no file involved
|
|
334
|
-
report = hanky.import_from_source(random_word_cards(20), "
|
|
337
|
+
report = hanky.import_from_source(random_word_cards(20), "english::vocab")
|
|
335
338
|
print(f"Added {report.added}, skipped {report.skipped}, failed {report.failed}.")
|
|
336
339
|
```
|
|
337
340
|
|
|
@@ -367,15 +370,20 @@ from simplest to most involved. Install their dependencies with
|
|
|
367
370
|
Both the `hanky` command and your own scripts share the same interface:
|
|
368
371
|
|
|
369
372
|
```
|
|
370
|
-
[hanky | python3 my_script.py] <operation> <file|dir> [pattern]
|
|
373
|
+
[hanky | python3 my_script.py] <operation> <file|dir> [pattern] [options]
|
|
371
374
|
```
|
|
372
375
|
|
|
376
|
+
Cards are created with the model set on the pipeline (`HankyPipeline("basic")`);
|
|
377
|
+
pass `-m/--model` to override it for a single run. The standalone `hanky`
|
|
378
|
+
command has no script of its own, so it uses Anki's built-in `Basic` model
|
|
379
|
+
unless you override it.
|
|
380
|
+
|
|
373
381
|
**`pipe`** — pipe cards from a single file into a deck:
|
|
374
382
|
|
|
375
383
|
```
|
|
376
|
-
hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
384
|
+
hanky pipe [-h] [-m MODEL] [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
377
385
|
file File to load from (.csv, .json, or a registered extension).
|
|
378
|
-
-m, --model Anki model/note-type name to create cards with.
|
|
386
|
+
-m, --model Override the Anki model/note-type name to create cards with.
|
|
379
387
|
--into Destination deck. Defaults to the filename without extension.
|
|
380
388
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
381
389
|
of skipping it and reporting it at the end.
|
|
@@ -385,10 +393,10 @@ hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
|
385
393
|
**`pipe-dir`** — pipe many files from a directory, deriving deck names from paths:
|
|
386
394
|
|
|
387
395
|
```
|
|
388
|
-
hanky pipe-dir [-h] -m MODEL [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
396
|
+
hanky pipe-dir [-h] [-m MODEL] [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
389
397
|
dir Directory to load from.
|
|
390
398
|
pattern Glob selecting files, e.g. "*.csv".
|
|
391
|
-
-m, --model Anki model/note-type name to create cards with.
|
|
399
|
+
-m, --model Override the Anki model/note-type name to create cards with.
|
|
392
400
|
-r, --recursive Also descend into sub-directories.
|
|
393
401
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
394
402
|
of skipping it and reporting it at the end.
|
|
@@ -399,7 +407,7 @@ For example, to load every CSV under `french/` while building deck names from
|
|
|
399
407
|
the folder structure:
|
|
400
408
|
|
|
401
409
|
```sh
|
|
402
|
-
hanky pipe-dir "french/" "*.csv"
|
|
410
|
+
hanky pipe-dir "french/" "*.csv" -r
|
|
403
411
|
```
|
|
404
412
|
|
|
405
413
|
```
|
|
@@ -50,18 +50,18 @@ See [Card Processors & Pipelines](#card-processors--pipelines) or the [demo fold
|
|
|
50
50
|
from hanky import HankyPipeline
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
# instantiate the hanky app
|
|
54
|
-
hanky = HankyPipeline()
|
|
53
|
+
# instantiate the hanky app, creating cards with the "basic" model
|
|
54
|
+
hanky = HankyPipeline("basic")
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
@hanky.card_processor(
|
|
57
|
+
@hanky.card_processor(expected_args=[], card_fields=[])
|
|
58
58
|
def lowercase_card(card: dict):
|
|
59
59
|
"""Lower-case the text on every field of the card."""
|
|
60
60
|
return {field: value.lower() for field, value in card.items()}
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
# run the hanky cli application by running this python file, for example:
|
|
64
|
-
# python3 my_script.py pipe words.csv --
|
|
64
|
+
# python3 my_script.py pipe words.csv --into english::vocab
|
|
65
65
|
hanky.run()
|
|
66
66
|
|
|
67
67
|
```
|
|
@@ -82,10 +82,10 @@ Ephemeral,Lasting A VERY Short Time
|
|
|
82
82
|
We would run our new script like so
|
|
83
83
|
|
|
84
84
|
```sh
|
|
85
|
-
python3 my_script.py pipe words.csv --
|
|
85
|
+
python3 my_script.py pipe words.csv --into english::vocab
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Here we tell hanky to **pipe** each word in `words.csv` through our `card_processors`, **transforming** each line of the csv with the `lowercase_card`, before finally adding each card, of note type `basic
|
|
88
|
+
Here we tell hanky to **pipe** each word in `words.csv` through our `card_processors`, **transforming** each line of the csv with the `lowercase_card`, before finally adding each card, of note type `basic` (the model we set on the pipeline), into the anki deck `english::vocab`. A single run can target a different model by passing `--model`.
|
|
89
89
|
|
|
90
90
|
This would leave us with a **english::vocab** deck containing the following cards:
|
|
91
91
|
|
|
@@ -137,7 +137,7 @@ over the file). Useful if you want different config for different scripts:
|
|
|
137
137
|
from hanky import HankyPipeline
|
|
138
138
|
from hanky.config import Config
|
|
139
139
|
|
|
140
|
-
hanky = HankyPipeline(config=Config(ALLOW_DUPLICATES=True))
|
|
140
|
+
hanky = HankyPipeline("basic", config=Config(ALLOW_DUPLICATES=True))
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
## Card Processors & Pipelines
|
|
@@ -150,17 +150,20 @@ Multiple processors can be registered on a `HankyPipeline` app to create a pipel
|
|
|
150
150
|
|
|
151
151
|
### The processor contract
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
Processors are model-agnostic: the Anki model/note-type is set once on the
|
|
154
|
+
pipeline itself (`HankyPipeline("basic")`), and every card the pipeline adds is
|
|
155
|
+
created with that model (unless overridden for a run with `--model`).
|
|
156
|
+
|
|
157
|
+
A processor is registered with two things:
|
|
154
158
|
|
|
155
159
|
```python
|
|
156
|
-
@hanky.card_processor(
|
|
160
|
+
@hanky.card_processor(expected_args=[...], card_fields=[...])
|
|
157
161
|
def my_processor(card: dict, **expected_args):
|
|
158
162
|
...
|
|
159
163
|
```
|
|
160
164
|
|
|
161
165
|
| Part | Meaning |
|
|
162
166
|
| --- | --- |
|
|
163
|
-
| `model` | The Anki model/note-type name. The processor runs on every card of this model. |
|
|
164
167
|
| `expected_args` | Names of CLI arguments the processor needs. They are passed in from the command line via `--args key=value` and arrive as keyword arguments. For example, you might have the same pipeline for different languages, so you would pass in `lang=german` or `lang=french`|
|
|
165
168
|
| `card_fields` | Fields that **must already be present** on the card when this processor runs. Hanky checks this and raises a clear error if one is missing. It lets a processor declare what an *earlier* step must have produced. |
|
|
166
169
|
|
|
@@ -168,7 +171,7 @@ When hanky calls your processors, the first argument is always the `card`; a pla
|
|
|
168
171
|
|
|
169
172
|
|
|
170
173
|
```python
|
|
171
|
-
@hanky.card_processor(
|
|
174
|
+
@hanky.card_processor(expected_args=["lang"], card_fields=[...])
|
|
172
175
|
def my_processor(card: dict, lang):
|
|
173
176
|
...
|
|
174
177
|
```
|
|
@@ -222,10 +225,10 @@ def scrape_wordreference(word: str, lang_pair: str) -> tuple[str, str]:
|
|
|
222
225
|
def generate_neural_speech(utf_8_str: str, voice: str) -> bytes:
|
|
223
226
|
...
|
|
224
227
|
|
|
225
|
-
hanky = HankyPipeline()
|
|
228
|
+
hanky = HankyPipeline("lang-vocab")
|
|
226
229
|
|
|
227
230
|
# Stage 1: requires `word` (this comes straight from the csv), produces `translation` + `example`.
|
|
228
|
-
@hanky.card_processor(
|
|
231
|
+
@hanky.card_processor(expected_args=[], card_fields=["word"])
|
|
229
232
|
def scrape_translation(card: dict):
|
|
230
233
|
translation, example = scrape_wordreference(card["word"], "enfr")
|
|
231
234
|
card["translation"] = translation
|
|
@@ -233,7 +236,7 @@ def scrape_translation(card: dict):
|
|
|
233
236
|
return card
|
|
234
237
|
|
|
235
238
|
# Stage 2: requires `translation` (from stage 1), attaches audio media.
|
|
236
|
-
@hanky.card_processor(
|
|
239
|
+
@hanky.card_processor(expected_args=[], card_fields=["translation"])
|
|
237
240
|
def add_audio(card: dict):
|
|
238
241
|
speech = generate_neural_speech(card["translation"], voice="Lea")
|
|
239
242
|
audio = CardMedia(speech, ".mp3")
|
|
@@ -243,9 +246,9 @@ def add_audio(card: dict):
|
|
|
243
246
|
hanky.run()
|
|
244
247
|
```
|
|
245
248
|
|
|
246
|
-
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --
|
|
249
|
+
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --into french::vocab`
|
|
247
250
|
|
|
248
|
-
> Note that
|
|
251
|
+
> Note that this pipeline was created with the *lang-vocab* model rather than *basic*. That means we are assuming that a model called *lang-vocab* has already been created in the anki ui. See the [Anki documentation for adding a note type](https://docs.ankiweb.net/editing.html#adding-a-note-type) to learn how this is done.
|
|
249
252
|
|
|
250
253
|
|
|
251
254
|
### Attaching media
|
|
@@ -304,7 +307,7 @@ DICTIONARY = {
|
|
|
304
307
|
# ...
|
|
305
308
|
}
|
|
306
309
|
|
|
307
|
-
hanky = HankyPipeline()
|
|
310
|
+
hanky = HankyPipeline("basic")
|
|
308
311
|
|
|
309
312
|
|
|
310
313
|
def random_word_cards(n):
|
|
@@ -313,7 +316,7 @@ def random_word_cards(n):
|
|
|
313
316
|
|
|
314
317
|
|
|
315
318
|
# add 20 cards straight from the generator, with no file involved
|
|
316
|
-
report = hanky.import_from_source(random_word_cards(20), "
|
|
319
|
+
report = hanky.import_from_source(random_word_cards(20), "english::vocab")
|
|
317
320
|
print(f"Added {report.added}, skipped {report.skipped}, failed {report.failed}.")
|
|
318
321
|
```
|
|
319
322
|
|
|
@@ -349,15 +352,20 @@ from simplest to most involved. Install their dependencies with
|
|
|
349
352
|
Both the `hanky` command and your own scripts share the same interface:
|
|
350
353
|
|
|
351
354
|
```
|
|
352
|
-
[hanky | python3 my_script.py] <operation> <file|dir> [pattern]
|
|
355
|
+
[hanky | python3 my_script.py] <operation> <file|dir> [pattern] [options]
|
|
353
356
|
```
|
|
354
357
|
|
|
358
|
+
Cards are created with the model set on the pipeline (`HankyPipeline("basic")`);
|
|
359
|
+
pass `-m/--model` to override it for a single run. The standalone `hanky`
|
|
360
|
+
command has no script of its own, so it uses Anki's built-in `Basic` model
|
|
361
|
+
unless you override it.
|
|
362
|
+
|
|
355
363
|
**`pipe`** — pipe cards from a single file into a deck:
|
|
356
364
|
|
|
357
365
|
```
|
|
358
|
-
hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
366
|
+
hanky pipe [-h] [-m MODEL] [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
359
367
|
file File to load from (.csv, .json, or a registered extension).
|
|
360
|
-
-m, --model Anki model/note-type name to create cards with.
|
|
368
|
+
-m, --model Override the Anki model/note-type name to create cards with.
|
|
361
369
|
--into Destination deck. Defaults to the filename without extension.
|
|
362
370
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
363
371
|
of skipping it and reporting it at the end.
|
|
@@ -367,10 +375,10 @@ hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
|
367
375
|
**`pipe-dir`** — pipe many files from a directory, deriving deck names from paths:
|
|
368
376
|
|
|
369
377
|
```
|
|
370
|
-
hanky pipe-dir [-h] -m MODEL [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
378
|
+
hanky pipe-dir [-h] [-m MODEL] [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
371
379
|
dir Directory to load from.
|
|
372
380
|
pattern Glob selecting files, e.g. "*.csv".
|
|
373
|
-
-m, --model Anki model/note-type name to create cards with.
|
|
381
|
+
-m, --model Override the Anki model/note-type name to create cards with.
|
|
374
382
|
-r, --recursive Also descend into sub-directories.
|
|
375
383
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
376
384
|
of skipping it and reporting it at the end.
|
|
@@ -381,7 +389,7 @@ For example, to load every CSV under `french/` while building deck names from
|
|
|
381
389
|
the folder structure:
|
|
382
390
|
|
|
383
391
|
```sh
|
|
384
|
-
hanky pipe-dir "french/" "*.csv"
|
|
392
|
+
hanky pipe-dir "french/" "*.csv" -r
|
|
385
393
|
```
|
|
386
394
|
|
|
387
395
|
```
|
|
@@ -20,6 +20,17 @@ def _add_fail_fast(subparser):
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _add_model_override(subparser):
|
|
24
|
+
subparser.add_argument(
|
|
25
|
+
"-m",
|
|
26
|
+
"--model",
|
|
27
|
+
dest="model",
|
|
28
|
+
required=False,
|
|
29
|
+
default=None,
|
|
30
|
+
help="Override the name of the anki model to create cards with.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
23
34
|
def _add_processor_args(subparser):
|
|
24
35
|
subparser.add_argument(
|
|
25
36
|
"--args",
|
|
@@ -45,13 +56,7 @@ def make_parser(has_card_processors=False):
|
|
|
45
56
|
"pipe", help="Pipe card(s) from a file into an anki deck."
|
|
46
57
|
)
|
|
47
58
|
pipe.add_argument("file", help="Path of the file to load from")
|
|
48
|
-
pipe
|
|
49
|
-
"-m",
|
|
50
|
-
"--model",
|
|
51
|
-
dest="model",
|
|
52
|
-
required=True,
|
|
53
|
-
help="Name of the anki model to create cards with.",
|
|
54
|
-
)
|
|
59
|
+
_add_model_override(pipe)
|
|
55
60
|
pipe.add_argument(
|
|
56
61
|
"--into",
|
|
57
62
|
dest="deck",
|
|
@@ -71,13 +76,7 @@ def make_parser(has_card_processors=False):
|
|
|
71
76
|
"pattern",
|
|
72
77
|
help="Glob pattern used to decide which files to load. For example, '*.csv'",
|
|
73
78
|
)
|
|
74
|
-
pipe_dir
|
|
75
|
-
"-m",
|
|
76
|
-
"--model",
|
|
77
|
-
dest="model",
|
|
78
|
-
required=True,
|
|
79
|
-
help="Name of the anki model to create cards with.",
|
|
80
|
-
)
|
|
79
|
+
_add_model_override(pipe_dir)
|
|
81
80
|
pipe_dir.add_argument(
|
|
82
81
|
"-r",
|
|
83
82
|
"--recursive",
|
|
@@ -14,7 +14,7 @@ from typing import (
|
|
|
14
14
|
from anki.collection import Collection
|
|
15
15
|
|
|
16
16
|
from hanky.anki_utils import add_card, add_deck, add_media, backup_collection
|
|
17
|
-
from hanky.processors import CardProcessingException,
|
|
17
|
+
from hanky.processors import CardProcessingException, CardProcessor
|
|
18
18
|
from hanky.cli import make_parser
|
|
19
19
|
from hanky.config import Config
|
|
20
20
|
from hanky.errors import (
|
|
@@ -35,31 +35,40 @@ class HankyPipeline:
|
|
|
35
35
|
interactions with the anki collection and exposes a simplified interface
|
|
36
36
|
for adding transformation logic. Optionally runnable as a CLI application.
|
|
37
37
|
|
|
38
|
+
Cards are added using the anki model/note type the pipeline was constructed
|
|
39
|
+
with (a single CLI run can override it via --model).
|
|
40
|
+
|
|
38
41
|
Keeps track of 'card processor' functions/callables which enrich or
|
|
39
42
|
transform their data before adding the card to the database.
|
|
40
43
|
|
|
41
44
|
Keeps track of 'loader' functions which read possibly incomplete card data from files.
|
|
42
45
|
|
|
43
46
|
Attributes:
|
|
44
|
-
config:
|
|
45
|
-
processors:
|
|
47
|
+
config: the pipeline's Config object, lazy loaded if not provided
|
|
48
|
+
processors: list of user defined card processor callables, in registration order
|
|
46
49
|
loaders: dictionary of file extensions mapped to a function which reads card data
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
|
-
def __init__(self, config: Optional[Config] = None):
|
|
52
|
+
def __init__(self, model: str, *, config: Optional[Config] = None):
|
|
50
53
|
"""Initializes a HankyPipeline application object
|
|
51
54
|
|
|
52
55
|
Note: if no config object is provided, hanky will try load from the default config location,
|
|
53
56
|
before finally using the default configuration values if the file does not exist.
|
|
54
57
|
|
|
55
58
|
Args:
|
|
59
|
+
model: the name of the anki model/note type to create cards with
|
|
56
60
|
config: custom config object
|
|
57
61
|
"""
|
|
62
|
+
if not isinstance(model, str):
|
|
63
|
+
raise TypeError(
|
|
64
|
+
"'model' must be the name of an anki model/note type (a string)"
|
|
65
|
+
)
|
|
66
|
+
|
|
58
67
|
self._config = config
|
|
59
68
|
|
|
60
69
|
self._col: Optional[Collection] = None
|
|
61
|
-
|
|
62
|
-
self.processors:
|
|
70
|
+
self._model = model
|
|
71
|
+
self.processors: List[CardProcessor] = list()
|
|
63
72
|
self.loaders: Dict[str, Callable[[str], Iterator[dict]]] = dict()
|
|
64
73
|
for ext, spec in DEFAULT_LOADERS.items():
|
|
65
74
|
self.register_loader(
|
|
@@ -167,12 +176,11 @@ class HankyPipeline:
|
|
|
167
176
|
|
|
168
177
|
def register_card_processor(
|
|
169
178
|
self,
|
|
170
|
-
model_name: str,
|
|
171
179
|
processor: Callable[[dict], dict],
|
|
172
180
|
expected_args: List[str] = [],
|
|
173
181
|
card_fields: List[str] = [],
|
|
174
182
|
) -> None:
|
|
175
|
-
"""Adds a python callable to be called
|
|
183
|
+
"""Adds a python callable to be called as a processor during a pipeline run.
|
|
176
184
|
|
|
177
185
|
The callable will be called with the first argument being the card
|
|
178
186
|
data (a dictionary mapping field names to values) BEFORE the card
|
|
@@ -183,8 +191,7 @@ class HankyPipeline:
|
|
|
183
191
|
order in which they were registered.
|
|
184
192
|
|
|
185
193
|
Args:
|
|
186
|
-
|
|
187
|
-
processor: the callable to apply to the cards of the given model type
|
|
194
|
+
processor: the callable to apply to each card
|
|
188
195
|
expected_args: list of arguments expected by the callable
|
|
189
196
|
card_fields: list of fields expected to be present in the card
|
|
190
197
|
when the callable is applied
|
|
@@ -192,15 +199,9 @@ class HankyPipeline:
|
|
|
192
199
|
Returns:
|
|
193
200
|
None
|
|
194
201
|
"""
|
|
195
|
-
|
|
196
|
-
self.processors[model_name] = []
|
|
197
|
-
self.processors[model_name].append(
|
|
198
|
-
ModelProcessor(processor, expected_args, card_fields)
|
|
199
|
-
)
|
|
202
|
+
self.processors.append(CardProcessor(processor, expected_args, card_fields))
|
|
200
203
|
|
|
201
|
-
def card_processor(
|
|
202
|
-
self, model: str, expected_args: List[str], card_fields: List[str]
|
|
203
|
-
):
|
|
204
|
+
def card_processor(self, expected_args: List[str], card_fields: List[str]):
|
|
204
205
|
"""Decorator which automatically registers a card processor function
|
|
205
206
|
|
|
206
207
|
A card processor takes a card (dictionary of field, value pairs) and any
|
|
@@ -212,14 +213,14 @@ class HankyPipeline:
|
|
|
212
213
|
- perform a mathmatical operation then write back the answer as a string
|
|
213
214
|
- set a field of a card which is currently missing
|
|
214
215
|
|
|
215
|
-
The
|
|
216
|
-
is
|
|
217
|
-
the card data as a dictionary of
|
|
216
|
+
The decorated function will be called on every card each time the pipeline
|
|
217
|
+
is run, whether via the CLI or the import_from_* methods. The first argument
|
|
218
|
+
to the decorated function will always be the card data as a dictionary of
|
|
219
|
+
fields, value pairs.
|
|
218
220
|
|
|
219
221
|
The decorated function must return a dictionary.
|
|
220
222
|
|
|
221
223
|
Args:
|
|
222
|
-
model_name: the name of the card model
|
|
223
224
|
expected_args: list of named arguments expected by the card processor.
|
|
224
225
|
They will be passed in as key word arguments.
|
|
225
226
|
card_fields: list of fields expected to be present in the card
|
|
@@ -230,18 +231,11 @@ class HankyPipeline:
|
|
|
230
231
|
"""
|
|
231
232
|
|
|
232
233
|
def decorator(func):
|
|
233
|
-
self.register_card_processor(
|
|
234
|
+
self.register_card_processor(func, expected_args, card_fields)
|
|
234
235
|
return func
|
|
235
236
|
|
|
236
237
|
return decorator
|
|
237
238
|
|
|
238
|
-
def get_model_processors(self, model_name: str) -> List[ModelProcessor]:
|
|
239
|
-
"""Get all card processors for a particular model"""
|
|
240
|
-
if model_name in self.processors:
|
|
241
|
-
return self.processors[model_name]
|
|
242
|
-
|
|
243
|
-
return []
|
|
244
|
-
|
|
245
239
|
def get_loader(self, suffix) -> Callable:
|
|
246
240
|
"""Get the loader function for a particular file extension"""
|
|
247
241
|
suffix = suffix.lower()
|
|
@@ -257,7 +251,6 @@ class HankyPipeline:
|
|
|
257
251
|
def import_from_source(
|
|
258
252
|
self,
|
|
259
253
|
source: Iterable[Mapping],
|
|
260
|
-
model_name: str,
|
|
261
254
|
deck_name: str,
|
|
262
255
|
fail_fast: bool = False,
|
|
263
256
|
**model_args,
|
|
@@ -271,7 +264,6 @@ class HankyPipeline:
|
|
|
271
264
|
Args:
|
|
272
265
|
source: any iterable yielding dictionaries (mappings) of card
|
|
273
266
|
field names to values
|
|
274
|
-
model_name: The anki model/card type of the cards
|
|
275
267
|
deck_name: the name of the destination deck
|
|
276
268
|
fail_fast: raise on the first bad card instead of collecting it
|
|
277
269
|
**model_args: arguments to provide to the card processor functions
|
|
@@ -280,16 +272,15 @@ class HankyPipeline:
|
|
|
280
272
|
A :class:`LoadReport` describing what was added, skipped and failed.
|
|
281
273
|
|
|
282
274
|
Raises:
|
|
283
|
-
ModelNotFoundError: the model does not exist in the collection
|
|
275
|
+
ModelNotFoundError: the pipeline's model does not exist in the collection
|
|
284
276
|
Exception: if ``fail_fast`` is set, whatever a bad card raised
|
|
285
277
|
"""
|
|
286
|
-
transformers = self.get_model_processors(model_name)
|
|
287
278
|
|
|
288
279
|
with self.session() as col:
|
|
289
|
-
model = col.models.by_name(
|
|
280
|
+
model = col.models.by_name(self._model)
|
|
290
281
|
if not model:
|
|
291
282
|
raise ModelNotFoundError(
|
|
292
|
-
f"Model '{
|
|
283
|
+
f"Model '{self._model}' does not exist in your anki collection. Ensure it has been added before using it with hanky."
|
|
293
284
|
)
|
|
294
285
|
|
|
295
286
|
add_deck(col, deck_name)
|
|
@@ -301,12 +292,12 @@ class HankyPipeline:
|
|
|
301
292
|
try:
|
|
302
293
|
if not isinstance(item, Mapping):
|
|
303
294
|
raise ValueError(
|
|
304
|
-
f"Card source for model '{
|
|
295
|
+
f"Card source for model '{self._model}' yielded a "
|
|
305
296
|
f"{type(item).__name__}, expected a dictionary (mapping)."
|
|
306
297
|
)
|
|
307
298
|
card = dict(item)
|
|
308
299
|
media: List[CardMedia] = []
|
|
309
|
-
for t in
|
|
300
|
+
for t in self.processors:
|
|
310
301
|
card, new_media = t(card, **model_args)
|
|
311
302
|
media += new_media
|
|
312
303
|
|
|
@@ -319,7 +310,7 @@ class HankyPipeline:
|
|
|
319
310
|
if add_card(
|
|
320
311
|
col,
|
|
321
312
|
deck_name,
|
|
322
|
-
|
|
313
|
+
self._model,
|
|
323
314
|
allow_duplicates=self.config.ALLOW_DUPLICATES,
|
|
324
315
|
**card,
|
|
325
316
|
):
|
|
@@ -329,7 +320,7 @@ class HankyPipeline:
|
|
|
329
320
|
except Exception as e:
|
|
330
321
|
# inject model info into exception which processor doesn't know
|
|
331
322
|
if isinstance(e, CardProcessingException):
|
|
332
|
-
e.model =
|
|
323
|
+
e.model = self._model
|
|
333
324
|
if fail_fast:
|
|
334
325
|
raise
|
|
335
326
|
errors.append(CardError(card=item, error=str(e)))
|
|
@@ -339,7 +330,6 @@ class HankyPipeline:
|
|
|
339
330
|
def import_from_file(
|
|
340
331
|
self,
|
|
341
332
|
fpath: str,
|
|
342
|
-
model_name: str,
|
|
343
333
|
deck_name: Optional[str] = None,
|
|
344
334
|
fail_fast: bool = False,
|
|
345
335
|
**model_args,
|
|
@@ -351,7 +341,6 @@ class HankyPipeline:
|
|
|
351
341
|
|
|
352
342
|
Args:
|
|
353
343
|
fpath: the path to the file
|
|
354
|
-
model_name: The anki model/card type of the cards in the file
|
|
355
344
|
deck_name: Optionally the name of the deck. Defaults to the
|
|
356
345
|
filename without its extension.
|
|
357
346
|
fail_fast: raise on the first bad card instead of collecting it
|
|
@@ -368,13 +357,12 @@ class HankyPipeline:
|
|
|
368
357
|
|
|
369
358
|
source = self.get_loader(path.suffix)(str(path))
|
|
370
359
|
report = self.import_from_source(
|
|
371
|
-
source,
|
|
360
|
+
source, deck_name, fail_fast=fail_fast, **model_args
|
|
372
361
|
)
|
|
373
362
|
return report.with_source(str(path))
|
|
374
363
|
|
|
375
364
|
def import_from_dir(
|
|
376
365
|
self,
|
|
377
|
-
model: str,
|
|
378
366
|
root_dir: str,
|
|
379
367
|
glob_pattern: str,
|
|
380
368
|
recursive=False,
|
|
@@ -402,7 +390,6 @@ class HankyPipeline:
|
|
|
402
390
|
french::grammar::passe_compose
|
|
403
391
|
|
|
404
392
|
Args:
|
|
405
|
-
model_name: The anki model/card type of the cards which will be loaded
|
|
406
393
|
root_dir: The root directory in which to find the files
|
|
407
394
|
glob_pattern: A glob pattern such as '*.csv' to match the desired files
|
|
408
395
|
recursive: whether or not to descend into sub directories, defaults to false
|
|
@@ -448,7 +435,6 @@ class HankyPipeline:
|
|
|
448
435
|
|
|
449
436
|
report += self.import_from_file(
|
|
450
437
|
str(abs_path),
|
|
451
|
-
model,
|
|
452
438
|
deck_name=full_deck,
|
|
453
439
|
fail_fast=fail_fast,
|
|
454
440
|
**model_args,
|
|
@@ -488,12 +474,15 @@ def _run_app(app: HankyPipeline, args: Optional[Sequence[str]] = None):
|
|
|
488
474
|
except AttributeError:
|
|
489
475
|
pass
|
|
490
476
|
|
|
477
|
+
# TODO: shouldn't really mutate _model like this
|
|
478
|
+
if parsed_args.model is not None:
|
|
479
|
+
app._model = parsed_args.model
|
|
480
|
+
|
|
491
481
|
report = LoadReport()
|
|
492
482
|
if parsed_args.operation == "pipe":
|
|
493
483
|
print(f"Loading into deck {parsed_args.deck} from file {parsed_args.file}")
|
|
494
484
|
report = app.import_from_file(
|
|
495
485
|
parsed_args.file,
|
|
496
|
-
parsed_args.model,
|
|
497
486
|
deck_name=parsed_args.deck,
|
|
498
487
|
fail_fast=parsed_args.fail_fast,
|
|
499
488
|
**model_args,
|
|
@@ -502,7 +491,6 @@ def _run_app(app: HankyPipeline, args: Optional[Sequence[str]] = None):
|
|
|
502
491
|
elif parsed_args.operation == "pipe-dir":
|
|
503
492
|
print(f"Loading from dirrectory {parsed_args.dir}")
|
|
504
493
|
report = app.import_from_dir(
|
|
505
|
-
parsed_args.model,
|
|
506
494
|
parsed_args.dir,
|
|
507
495
|
parsed_args.pattern,
|
|
508
496
|
parsed_args.is_rec,
|
|
@@ -30,7 +30,7 @@ class CardProcessingException(Exception):
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class
|
|
33
|
+
class CardProcessor:
|
|
34
34
|
"""The wrapper for user defined functions which processes cards.
|
|
35
35
|
|
|
36
36
|
Wraps a python callable which takes a dictionary representing an anki card,
|
|
@@ -49,7 +49,7 @@ class ModelProcessor:
|
|
|
49
49
|
expected_args: List[str],
|
|
50
50
|
card_fields: List[str],
|
|
51
51
|
):
|
|
52
|
-
"""Initializes a
|
|
52
|
+
"""Initializes a card processor
|
|
53
53
|
|
|
54
54
|
Args:
|
|
55
55
|
func: the user defined callable which processes each card
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|