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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanky
3
- Version: 0.3.0.dev0
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("basic", expected_args=[], card_fields=[])
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 --model basic --into english::vocab
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 --model basic --into english::vocab
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`, into the anki deck `english::vocab`.
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
- A processor is registered with three things:
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(model, expected_args=[...], card_fields=[...])
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(model, expected_args=["lang"], card_fields=[...])
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("lang-vocab", expected_args=[], card_fields=["word"])
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("lang-vocab", expected_args=[], card_fields=["translation"])
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 --model lang-vocab --into french::vocab`
267
+ Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --into french::vocab`
265
268
 
266
- > Note that we are no longer using a *basic* model. 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.
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), "basic", "english::vocab")
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] --model MODEL [options]
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. Required.
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. Required.
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" --model basic -r
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("basic", expected_args=[], card_fields=[])
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 --model basic --into english::vocab
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 --model basic --into english::vocab
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`, into the anki deck `english::vocab`.
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
- A processor is registered with three things:
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(model, expected_args=[...], card_fields=[...])
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(model, expected_args=["lang"], card_fields=[...])
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("lang-vocab", expected_args=[], card_fields=["word"])
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("lang-vocab", expected_args=[], card_fields=["translation"])
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 --model lang-vocab --into french::vocab`
249
+ Then we would run the script like normal. `python3 my_script.py pipe words.xlsx --into french::vocab`
247
250
 
248
- > Note that we are no longer using a *basic* model. 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.
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), "basic", "english::vocab")
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] --model MODEL [options]
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. Required.
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. Required.
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" --model basic -r
392
+ hanky pipe-dir "french/" "*.csv" -r
385
393
  ```
386
394
 
387
395
  ```
@@ -16,7 +16,7 @@ classifiers = [
16
16
  ]
17
17
 
18
18
  keywords = ["anki"]
19
- version = "0.3.0.dev0"
19
+ version = "0.3.0.dev1"
20
20
 
21
21
  dependencies = [
22
22
  "anki",
@@ -6,7 +6,8 @@ from hanky.errors import HankyError
6
6
 
7
7
  def main():
8
8
  try:
9
- hanky = HankyPipeline()
9
+ # default to basic model
10
+ hanky = HankyPipeline("basic")
10
11
  hanky.run()
11
12
  except HankyError as e:
12
13
  print(f"error: {e}", file=sys.stderr)
@@ -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.add_argument(
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.add_argument(
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, ModelProcessor
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: dictionary representing configuration and kwargs arguments
45
- processors: dictionary of anki model names mapped to user defined callables
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: Dict[str, List[ModelProcessor]] = dict()
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 when adding a card of type model.
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
- model_name: the name of the card model
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
- if model_name not in self.processors:
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 decoracted function will be called every time a card of type model
216
- is added. The first argument to the decorated function will always be
217
- the card data as a dictionary of fields, value pairs.
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(model, func, expected_args, card_fields)
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(model_name)
280
+ model = col.models.by_name(self._model)
290
281
  if not model:
291
282
  raise ModelNotFoundError(
292
- f"Model '{model_name}' does not exist in your anki collection. Ensure it has been added before using it with hanky."
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 '{model_name}' yielded a "
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 transformers:
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
- model_name,
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 = model_name
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, model_name, deck_name, fail_fast=fail_fast, **model_args
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 ModelProcessor:
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 model processor
52
+ """Initializes a card processor
53
53
 
54
54
  Args:
55
55
  func: the user defined callable which processes each card
@@ -398,7 +398,7 @@ wheels = [
398
398
 
399
399
  [[package]]
400
400
  name = "hanky"
401
- version = "0.3.0.dev0"
401
+ version = "0.3.0.dev1"
402
402
  source = { editable = "." }
403
403
  dependencies = [
404
404
  { name = "anki" },
File without changes
File without changes
File without changes