hanky 0.3.0.dev0__tar.gz → 0.3.0.dev2__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.dev2}/PKG-INFO +28 -25
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/README.md +27 -24
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/pyproject.toml +1 -1
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/__main__.py +2 -1
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/cli.py +13 -14
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/hanky.py +36 -48
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/processors.py +2 -2
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/uv.lock +1 -1
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/.gitignore +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/LICENSE +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/__init__.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/anki_utils.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/config.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/errors.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/fs.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/src/hanky/media.py +0 -0
- {hanky-0.3.0.dev0 → hanky-0.3.0.dev2}/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.dev2
|
|
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 gave to the pipeline), into the anki deck `english::vocab`.
|
|
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,16 @@ Multiple processors can be registered on a `HankyPipeline` app to create a pipel
|
|
|
168
168
|
|
|
169
169
|
### The processor contract
|
|
170
170
|
|
|
171
|
-
A processor is registered with
|
|
171
|
+
A processor is registered with two things:
|
|
172
172
|
|
|
173
173
|
```python
|
|
174
|
-
@hanky.card_processor(
|
|
174
|
+
@hanky.card_processor(expected_args=[...], card_fields=[...])
|
|
175
175
|
def my_processor(card: dict, **expected_args):
|
|
176
176
|
...
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
| Part | Meaning |
|
|
180
180
|
| --- | --- |
|
|
181
|
-
| `model` | The Anki model/note-type name. The processor runs on every card of this model. |
|
|
182
181
|
| `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
182
|
| `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
183
|
|
|
@@ -186,7 +185,7 @@ When hanky calls your processors, the first argument is always the `card`; a pla
|
|
|
186
185
|
|
|
187
186
|
|
|
188
187
|
```python
|
|
189
|
-
@hanky.card_processor(
|
|
188
|
+
@hanky.card_processor(expected_args=["lang"], card_fields=[...])
|
|
190
189
|
def my_processor(card: dict, lang):
|
|
191
190
|
...
|
|
192
191
|
```
|
|
@@ -240,10 +239,10 @@ def scrape_wordreference(word: str, lang_pair: str) -> tuple[str, str]:
|
|
|
240
239
|
def generate_neural_speech(utf_8_str: str, voice: str) -> bytes:
|
|
241
240
|
...
|
|
242
241
|
|
|
243
|
-
hanky = HankyPipeline()
|
|
242
|
+
hanky = HankyPipeline("lang-vocab")
|
|
244
243
|
|
|
245
244
|
# Stage 1: requires `word` (this comes straight from the csv), produces `translation` + `example`.
|
|
246
|
-
@hanky.card_processor(
|
|
245
|
+
@hanky.card_processor(expected_args=[], card_fields=["word"])
|
|
247
246
|
def scrape_translation(card: dict):
|
|
248
247
|
translation, example = scrape_wordreference(card["word"], "enfr")
|
|
249
248
|
card["translation"] = translation
|
|
@@ -251,7 +250,7 @@ def scrape_translation(card: dict):
|
|
|
251
250
|
return card
|
|
252
251
|
|
|
253
252
|
# Stage 2: requires `translation` (from stage 1), attaches audio media.
|
|
254
|
-
@hanky.card_processor(
|
|
253
|
+
@hanky.card_processor(expected_args=[], card_fields=["translation"])
|
|
255
254
|
def add_audio(card: dict):
|
|
256
255
|
speech = generate_neural_speech(card["translation"], voice="Lea")
|
|
257
256
|
audio = CardMedia(speech, ".mp3")
|
|
@@ -261,9 +260,9 @@ def add_audio(card: dict):
|
|
|
261
260
|
hanky.run()
|
|
262
261
|
```
|
|
263
262
|
|
|
264
|
-
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --
|
|
263
|
+
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --into french::vocab`
|
|
265
264
|
|
|
266
|
-
> Note that
|
|
265
|
+
> 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
266
|
|
|
268
267
|
|
|
269
268
|
### Attaching media
|
|
@@ -322,7 +321,7 @@ DICTIONARY = {
|
|
|
322
321
|
# ...
|
|
323
322
|
}
|
|
324
323
|
|
|
325
|
-
hanky = HankyPipeline()
|
|
324
|
+
hanky = HankyPipeline("basic")
|
|
326
325
|
|
|
327
326
|
|
|
328
327
|
def random_word_cards(n):
|
|
@@ -331,7 +330,7 @@ def random_word_cards(n):
|
|
|
331
330
|
|
|
332
331
|
|
|
333
332
|
# add 20 cards straight from the generator, with no file involved
|
|
334
|
-
report = hanky.import_from_source(random_word_cards(20), "
|
|
333
|
+
report = hanky.import_from_source(random_word_cards(20), "english::vocab")
|
|
335
334
|
print(f"Added {report.added}, skipped {report.skipped}, failed {report.failed}.")
|
|
336
335
|
```
|
|
337
336
|
|
|
@@ -367,15 +366,19 @@ from simplest to most involved. Install their dependencies with
|
|
|
367
366
|
Both the `hanky` command and your own scripts share the same interface:
|
|
368
367
|
|
|
369
368
|
```
|
|
370
|
-
[hanky | python3 my_script.py] <operation> <file|dir> [pattern]
|
|
369
|
+
[hanky | python3 my_script.py] <operation> <file|dir> [pattern] [options]
|
|
371
370
|
```
|
|
372
371
|
|
|
372
|
+
Note that cards are created with the model set on the pipeline (`HankyPipeline("my-model")`).
|
|
373
|
+
Since standalone `hanky` command has no script of its own, it uses Anki's built-in
|
|
374
|
+
`Basic` model unless you override it.
|
|
375
|
+
|
|
373
376
|
**`pipe`** — pipe cards from a single file into a deck:
|
|
374
377
|
|
|
375
378
|
```
|
|
376
|
-
hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
379
|
+
hanky pipe [-h] [-m MODEL] [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
377
380
|
file File to load from (.csv, .json, or a registered extension).
|
|
378
|
-
-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.
|
|
379
382
|
--into Destination deck. Defaults to the filename without extension.
|
|
380
383
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
381
384
|
of skipping it and reporting it at the end.
|
|
@@ -385,10 +388,10 @@ hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
|
385
388
|
**`pipe-dir`** — pipe many files from a directory, deriving deck names from paths:
|
|
386
389
|
|
|
387
390
|
```
|
|
388
|
-
hanky pipe-dir [-h] -m MODEL [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
391
|
+
hanky pipe-dir [-h] [-m MODEL] [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
389
392
|
dir Directory to load from.
|
|
390
393
|
pattern Glob selecting files, e.g. "*.csv".
|
|
391
|
-
-m, --model Anki model/note-type name to create cards with.
|
|
394
|
+
-m, --model Override the Anki model/note-type name to create cards with.
|
|
392
395
|
-r, --recursive Also descend into sub-directories.
|
|
393
396
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
394
397
|
of skipping it and reporting it at the end.
|
|
@@ -399,7 +402,7 @@ For example, to load every CSV under `french/` while building deck names from
|
|
|
399
402
|
the folder structure:
|
|
400
403
|
|
|
401
404
|
```sh
|
|
402
|
-
hanky pipe-dir "french/" "*.csv"
|
|
405
|
+
hanky pipe-dir "french/" "*.csv" -r
|
|
403
406
|
```
|
|
404
407
|
|
|
405
408
|
```
|
|
@@ -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 gave to the pipeline), into the anki deck `english::vocab`.
|
|
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,16 @@ Multiple processors can be registered on a `HankyPipeline` app to create a pipel
|
|
|
150
150
|
|
|
151
151
|
### The processor contract
|
|
152
152
|
|
|
153
|
-
A processor is registered with
|
|
153
|
+
A processor is registered with two things:
|
|
154
154
|
|
|
155
155
|
```python
|
|
156
|
-
@hanky.card_processor(
|
|
156
|
+
@hanky.card_processor(expected_args=[...], card_fields=[...])
|
|
157
157
|
def my_processor(card: dict, **expected_args):
|
|
158
158
|
...
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
| Part | Meaning |
|
|
162
162
|
| --- | --- |
|
|
163
|
-
| `model` | The Anki model/note-type name. The processor runs on every card of this model. |
|
|
164
163
|
| `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
164
|
| `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
165
|
|
|
@@ -168,7 +167,7 @@ When hanky calls your processors, the first argument is always the `card`; a pla
|
|
|
168
167
|
|
|
169
168
|
|
|
170
169
|
```python
|
|
171
|
-
@hanky.card_processor(
|
|
170
|
+
@hanky.card_processor(expected_args=["lang"], card_fields=[...])
|
|
172
171
|
def my_processor(card: dict, lang):
|
|
173
172
|
...
|
|
174
173
|
```
|
|
@@ -222,10 +221,10 @@ def scrape_wordreference(word: str, lang_pair: str) -> tuple[str, str]:
|
|
|
222
221
|
def generate_neural_speech(utf_8_str: str, voice: str) -> bytes:
|
|
223
222
|
...
|
|
224
223
|
|
|
225
|
-
hanky = HankyPipeline()
|
|
224
|
+
hanky = HankyPipeline("lang-vocab")
|
|
226
225
|
|
|
227
226
|
# Stage 1: requires `word` (this comes straight from the csv), produces `translation` + `example`.
|
|
228
|
-
@hanky.card_processor(
|
|
227
|
+
@hanky.card_processor(expected_args=[], card_fields=["word"])
|
|
229
228
|
def scrape_translation(card: dict):
|
|
230
229
|
translation, example = scrape_wordreference(card["word"], "enfr")
|
|
231
230
|
card["translation"] = translation
|
|
@@ -233,7 +232,7 @@ def scrape_translation(card: dict):
|
|
|
233
232
|
return card
|
|
234
233
|
|
|
235
234
|
# Stage 2: requires `translation` (from stage 1), attaches audio media.
|
|
236
|
-
@hanky.card_processor(
|
|
235
|
+
@hanky.card_processor(expected_args=[], card_fields=["translation"])
|
|
237
236
|
def add_audio(card: dict):
|
|
238
237
|
speech = generate_neural_speech(card["translation"], voice="Lea")
|
|
239
238
|
audio = CardMedia(speech, ".mp3")
|
|
@@ -243,9 +242,9 @@ def add_audio(card: dict):
|
|
|
243
242
|
hanky.run()
|
|
244
243
|
```
|
|
245
244
|
|
|
246
|
-
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --
|
|
245
|
+
Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --into french::vocab`
|
|
247
246
|
|
|
248
|
-
> Note that
|
|
247
|
+
> 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
248
|
|
|
250
249
|
|
|
251
250
|
### Attaching media
|
|
@@ -304,7 +303,7 @@ DICTIONARY = {
|
|
|
304
303
|
# ...
|
|
305
304
|
}
|
|
306
305
|
|
|
307
|
-
hanky = HankyPipeline()
|
|
306
|
+
hanky = HankyPipeline("basic")
|
|
308
307
|
|
|
309
308
|
|
|
310
309
|
def random_word_cards(n):
|
|
@@ -313,7 +312,7 @@ def random_word_cards(n):
|
|
|
313
312
|
|
|
314
313
|
|
|
315
314
|
# add 20 cards straight from the generator, with no file involved
|
|
316
|
-
report = hanky.import_from_source(random_word_cards(20), "
|
|
315
|
+
report = hanky.import_from_source(random_word_cards(20), "english::vocab")
|
|
317
316
|
print(f"Added {report.added}, skipped {report.skipped}, failed {report.failed}.")
|
|
318
317
|
```
|
|
319
318
|
|
|
@@ -349,15 +348,19 @@ from simplest to most involved. Install their dependencies with
|
|
|
349
348
|
Both the `hanky` command and your own scripts share the same interface:
|
|
350
349
|
|
|
351
350
|
```
|
|
352
|
-
[hanky | python3 my_script.py] <operation> <file|dir> [pattern]
|
|
351
|
+
[hanky | python3 my_script.py] <operation> <file|dir> [pattern] [options]
|
|
353
352
|
```
|
|
354
353
|
|
|
354
|
+
Note that cards are created with the model set on the pipeline (`HankyPipeline("my-model")`).
|
|
355
|
+
Since standalone `hanky` command has no script of its own, it uses Anki's built-in
|
|
356
|
+
`Basic` model unless you override it.
|
|
357
|
+
|
|
355
358
|
**`pipe`** — pipe cards from a single file into a deck:
|
|
356
359
|
|
|
357
360
|
```
|
|
358
|
-
hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
361
|
+
hanky pipe [-h] [-m MODEL] [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
359
362
|
file File to load from (.csv, .json, or a registered extension).
|
|
360
|
-
-m, --model Anki model/note-type name to create cards with.
|
|
363
|
+
-m, --model Override the Anki model/note-type name to create cards with.
|
|
361
364
|
--into Destination deck. Defaults to the filename without extension.
|
|
362
365
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
363
366
|
of skipping it and reporting it at the end.
|
|
@@ -367,10 +370,10 @@ hanky pipe [-h] -m MODEL [--into DECK] [--fail-fast] [--args K=V ...] file
|
|
|
367
370
|
**`pipe-dir`** — pipe many files from a directory, deriving deck names from paths:
|
|
368
371
|
|
|
369
372
|
```
|
|
370
|
-
hanky pipe-dir [-h] -m MODEL [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
373
|
+
hanky pipe-dir [-h] [-m MODEL] [-r] [--fail-fast] [--args K=V ...] dir pattern
|
|
371
374
|
dir Directory to load from.
|
|
372
375
|
pattern Glob selecting files, e.g. "*.csv".
|
|
373
|
-
-m, --model Anki model/note-type name to create cards with.
|
|
376
|
+
-m, --model Override the Anki model/note-type name to create cards with.
|
|
374
377
|
-r, --recursive Also descend into sub-directories.
|
|
375
378
|
--fail-fast Stop and raise on the first card that can't be added, instead
|
|
376
379
|
of skipping it and reporting it at the end.
|
|
@@ -381,7 +384,7 @@ For example, to load every CSV under `french/` while building deck names from
|
|
|
381
384
|
the folder structure:
|
|
382
385
|
|
|
383
386
|
```sh
|
|
384
|
-
hanky pipe-dir "french/" "*.csv"
|
|
387
|
+
hanky pipe-dir "french/" "*.csv" -r
|
|
385
388
|
```
|
|
386
389
|
|
|
387
390
|
```
|
|
@@ -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
|