def test_basepath_is_none(): """Blah""" with pytest.raises(click.Abort): Anki(None) with pytest.raises(click.Abort): Anki('/non/existing/path')
def tag(query, add_tags, remove_tags): """List tags or add/remove tags from matching notes. If neither of the options --add-tags or --remove-tags are supplied, then this command simply lists all tags. """ with Anki(cfg['base']) as a: if add_tags is None and remove_tags is None: a.list_tags() return n_notes = len(list(a.find_notes(query))) if n_notes == 0: click.echo('No matching notes!') raise click.Abort() click.echo(f'The operation will be applied to {n_notes} matched notes:') a.list_notes(query) click.echo('') if add_tags is not None: click.echo(f'Add tags: {click.style(add_tags, fg="green")}') if remove_tags is not None: click.echo(f'Remove tags: {click.style(remove_tags, fg="red")}') if not click.confirm(click.style('Continue?', fg='blue')): raise click.Abort() if add_tags is not None: a.change_tags(query, add_tags) if remove_tags is not None: a.change_tags(query, remove_tags, add=False)
def tag(query, add_tags, remove_tags): """Add or remove tags from notes that match the query.""" if add_tags is None and remove_tags is None: click.echo(f'Please specify either -a and/or -r to add/remove tags!') return with Anki(cfg['base']) as a: n_notes = len(list(a.find_notes(query))) if n_notes == 0: click.echo(f'No matching notes!') raise click.Abort() click.echo(f'The operation will be applied to {n_notes} matched notes:') a.list_notes(query) click.echo('') if add_tags is not None: click.echo(f'Add tags: {click.style(add_tags, fg="green")}') if remove_tags is not None: click.echo(f'Remove tags: {click.style(remove_tags, fg="red")}') if not click.confirm(click.style('Continue?', fg='blue')): raise click.Abort() if add_tags is not None: a.change_tags(query, add_tags) if remove_tags is not None: a.change_tags(query, remove_tags, add=False)
def add(tags, model, deck): """Add notes interactively from terminal. Examples: \b # Add notes to deck "MyDeck" with tags 'my-tag' and 'new-tag' apy add -t "my-tag new-tag" -d MyDeck \b # Ask for the model and the deck for each new card apy add -m ASK -d ask """ with Anki(cfg['base']) as a: notes = a.add_notes_with_editor(tags, model, deck) decks = [a.col.decks.name(c.did) for n in notes for c in n.n.cards()] n_notes = len(notes) n_decks = len(decks) if a.n_decks > 1: if n_notes == 1: click.echo(f'Added note to deck: {decks[0]}') elif n_decks > 1: click.echo(f'Added {n_notes} notes to {n_decks} different decks') else: click.echo(f'Added {n_notes} notes to deck: {decks[0]}') else: click.echo(f'Added {n_notes} notes') if click.confirm('Review added notes?'): for i, note in enumerate(notes): note.review(i, n_notes, remove_actions=['Abort'])
def list_notes(query): """List notes that match a given query.""" from apy.anki import Anki with Anki(cfg['base']) as a: for note in a.find_notes(query): note.print_short()
def info(): """Print some basic statistics.""" from apy.anki import Anki from apy.config import cfg_file if cfg_file.exists(): click.echo(f"Config file: {cfg_file}") for key in cfg.keys(): click.echo(f"Config loaded: {key}") else: click.echo(f"Config file: Not found") with Anki(cfg['base']) as a: click.echo(f"Collecton path: {a.col.path}") click.echo(f"Scheduler version: {a.col.schedVer()}") click.echo(f"Number of notes: {a.col.noteCount()}") click.echo(f"Number of cards: {a.col.cardCount()}") click.echo( f"Number of cards (due): {len(a.col.findNotes('is:due'))}") click.echo( f"Number of marked cards: {len(a.col.findNotes('tag:marked'))}") click.echo(f"Number of decks: {a.col.decks.count()}") for d in sorted(a.deck_names): click.echo(f" - {d}") models = sorted(a.model_names) click.echo(f"Number of models: {len(models)}") for m in models: click.echo(f" - {m}")
def get_empty(): """Create empty Anki collection""" (fd, name) = tempfile.mkstemp(suffix=".anki2") os.close(fd) os.unlink(name) a = Anki(path=name) return a
def add_single(fields, tags=None, preset=None, model_name=None, deck=None): """Add a single note from command line arguments. Examples: \b # Add a note to the default deck apy add-single myfront myback \b # Add a cloze deletion note to the default deck apy add-single -m Cloze "cloze {{c1::deletion}}" "extra text" \b # Add a note to deck "MyDeck" with tags 'my-tag' and 'new-tag' apy add-single -t "my-tag new-tag" -d MyDeck myfront myback """ with Anki(**cfg) as a: tags_preset = ' '.join(cfg['presets'][preset]['tags']) if not tags: tags = tags_preset else: tags += ' ' + tags_preset if not model_name: model_name = cfg['presets'][preset]['model'] a.add_notes_single(fields, tags, model_name, deck)
def add_from_file(file, tags): """Add notes from Markdown file. For input file syntax specification, see docstring for markdown_file_to_notes() in convert.py. """ with Anki(cfg['base']) as a: notes = a.add_notes_from_file(file, tags) decks = [a.col.decks.name(c.did) for n in notes for c in n.n.cards()] n_notes = len(notes) n_decks = len(decks) if a.n_decks > 1: if n_notes == 1: click.echo(f'Added note to deck: {decks[0]}') elif n_decks > 1: click.echo(f'Added {n_notes} notes to {n_decks} different decks') else: click.echo(f'Added {n_notes} notes to deck: {decks[0]}') else: click.echo(f'Added {n_notes} notes') if click.confirm('Review added notes?'): for i, note in enumerate(notes): note.review(i, n_notes, remove_actions=['Abort'])
def edit_css(model_name, sync_after): """Edit the CSS template for the specified model.""" with Anki(cfg['base']) as a: a.edit_model_css(model_name) if a.modified and sync_after: a.sync() a.modified = False
def review(query): """Review marked notes.""" with Anki(cfg['base']) as a: notes = list(a.find_notes(query)) number_of_notes = len(notes) for i, note in enumerate(notes): if not note.review(i, number_of_notes): break
def test_rename_model(): """Test that we can rename models""" with Anki(path=testCol, debug=True) as a: assert 'MyTest' in a.model_names a.rename_model('MyTest', 'NewModelName') assert 'NewModelName' in a.model_names assert 'MyTest' not in a.model_names
def add_from_file(file, tags): """Add notes from Markdown file. For input file syntax specification, see docstring for markdown_file_to_notes() in convert.py. """ with Anki(**cfg) as a: notes = a.add_notes_from_file(file, tags) _added_notes_postprocessing(a, notes)
def review(query): """Review marked notes.""" from apy.anki import Anki with Anki(cfg['base']) as a: notes = list(a.find_notes(query)) number_of_notes = len(notes) for i, note in enumerate(notes): if not _review_note(a, note, i, number_of_notes): break
def test_decks(): """Test empty collection""" with Anki(path=testCol, debug=True) as a: assert a.col.decks.count() == 2 assert a.col.decks.current()['name'] == 'NewDeck' assert list(a.deck_names) == ['Default', 'NewDeck'] notes = a.add_notes_from_file(testDir + '/' + 'data/deck.md') assert a.col.decks.name(notes[0].n.cards()[0].did) == 'Default' assert a.col.decks.name(notes[1].n.cards()[0].did) == 'NewDeck'
def list_cards(query): """List cards that match a given query.""" from apy.anki import Anki from apy.convert import html_to_screen, clean_html with Anki(cfg['base']) as a: for cid in a.find_cards(query): c = a.col.getCard(cid) question = html_to_screen(clean_html(c.q())).replace('\n', ' ') # answer = html_to_screen(clean_html(c.a())).replace('\n', ' ') click.echo(f'lapses: {c.lapses:2d} ease: {c.factor/10}% Q: ' + question[:80])
def test_change_tags(): """Test empty collection""" with Anki(path=testCol, debug=True) as a: a.add_notes_from_file(testDir + '/' + 'data/deck.md') query = 'tag:test' n_original = len(list(a.find_notes(query))) a.change_tags(query, 'testendret') assert len(list(a.find_notes('tag:testendret'))) == n_original a.change_tags(query, 'test', add=False) assert len(list(a.find_notes(query))) == 0
def info(): """Print some basic statistics.""" if cfg_file.exists(): click.echo(f"Config file: {cfg_file}") for key in cfg.keys(): click.echo(f"Config loaded: {key}") else: click.echo("Config file: Not found") with Anki(**cfg) as a: click.echo(f"Collecton path: {a.col.path}") click.echo(f"Scheduler version: {a.col.schedVer()}") if a.col.decks.count() > 1: click.echo("Decks:") for name in sorted(a.deck_names): click.echo(f" - {name}") sum_notes = a.col.noteCount() sum_cards = a.col.cardCount() sum_due = len(a.col.findNotes('is:due')) sum_marked = len(a.col.findNotes('tag:marked')) sum_flagged = len(a.col.findNotes('-flag:0')) sum_new = len(a.col.findNotes('is:new')) sum_susp = len(a.col.findNotes('is:suspended')) click.echo( f"\n{'Model':24s} {'notes':>7s} {'cards':>7s} " f"{'due':>7s} {'new':>7s} {'susp.':>7s} {'marked':>7s} {'flagged':>7s}" ) click.echo("-" * 80) models = sorted(a.model_names) for m in models: nnotes = len(set(a.col.findNotes(f'"note:{m}"'))) ncards = len(a.find_cards(f'"note:{m}"')) ndue = len(a.find_cards(f'"note:{m}" is:due')) nmarked = len(a.find_cards(f'"note:{m}" tag:marked')) nflagged = len(a.find_cards(f'"note:{m}" -flag:0')) nnew = len(a.find_cards(f'"note:{m}" is:new')) nsusp = len(a.find_cards(f'"note:{m}" is:suspended')) name = m[:24] click.echo(f"{name:24s} {nnotes:7d} {ncards:7d} " f"{ndue:7d} {nnew:7d} {nsusp:7d} " f"{nmarked:7d} {nflagged:7d}") click.echo("-" * 80) click.echo(f"{'Sum':24s} {sum_notes:7d} {sum_cards:7d} " f"{sum_due:7d} {sum_new:7d} {sum_susp:7d} " f"{sum_marked:7d} {sum_flagged:7d}") click.echo("-" * 80)
def add(tags, model_name, deck): """Add notes interactively from terminal. Examples: \b # Add notes to deck "MyDeck" with tags 'my-tag' and 'new-tag' apy add -t "my-tag new-tag" -d MyDeck \b # Ask for the model and the deck for each new card apy add -m ASK -d ask """ with Anki(**cfg) as a: notes = a.add_notes_with_editor(tags, model_name, deck) _added_notes_postprocessing(a, notes)
def tag(query, add_tags, remove_tags): """Add/Remove tags to/from notes that match QUERY. The default QUERY is "tag:marked OR -flag:0". This default can be customized in the config file `~/.config/apy/apy.json`, e.g. with \b { "query": "tag:marked OR tag:leech" } If neither of the options --add-tags or --remove-tags are supplied, then this command simply lists all tags. """ if query: query = " ".join(query) else: query = cfg["query"] with Anki(**cfg) as a: if add_tags is None and remove_tags is None: a.list_tags() return n_notes = len(list(a.find_notes(query))) if n_notes == 0: click.echo('No matching notes!') raise click.Abort() click.echo( f'The operation will be applied to {n_notes} matched notes:') a.list_notes(query) click.echo('') if add_tags is not None: click.echo(f'Add tags: {click.style(add_tags, fg="green")}') if remove_tags is not None: click.echo(f'Remove tags: {click.style(remove_tags, fg="red")}') if not click.confirm(click.style('Continue?', fg='blue')): raise click.Abort() if add_tags is not None: a.change_tags(query, add_tags) if remove_tags is not None: a.change_tags(query, remove_tags, add=False)
def list_cards(query, verbose): """List cards that match QUERY. The default QUERY is "tag:marked OR -flag:0". This default can be customized in the config file `~/.config/apy/apy.json`, e.g. with \b { "query": "tag:marked OR tag:leech" } """ if query: query = " ".join(query) else: query = cfg["query"] with Anki(**cfg) as a: a.list_cards(query, verbose)
def review(query): """Review/Edit notes that match QUERY. The default QUERY is "tag:marked OR -flag:0". This default can be customized in the config file `~/.config/apy/apy.json`, e.g. with \b { "query": "tag:marked OR tag:leech" } """ if query: query = " ".join(query) else: query = cfg["query"] with Anki(**cfg) as a: notes = list(a.find_notes(query)) number_of_notes = len(notes) for i, note in enumerate(notes): if not note.review(i, number_of_notes): break
def info(): """Print some basic statistics.""" if cfg_file.exists(): click.echo(f"Config file: {cfg_file}") for key in cfg.keys(): click.echo(f"Config loaded: {key}") else: click.echo(f"Config file: Not found") with Anki(cfg['base']) as a: click.echo(f"Collecton path: {a.col.path}") click.echo(f"Scheduler version: {a.col.schedVer()}") if a.col.decks.count() > 1: click.echo("Decks:") for name in sorted(a.deck_names): click.echo(f" - {name}") sum_notes = a.col.noteCount() sum_cards = a.col.cardCount() sum_due = len(a.col.findNotes('is:due')) sum_marked = len(a.col.findNotes('tag:marked')) click.echo(f"\n{'Model':26s} {'notes':>8s} {'cards':>8s} " f"{'due':>8s} {'marked':>8s}") click.echo("-"*62) models = sorted(a.model_names) for m in models: nnotes = len(a.col.findNotes(f"note:'{m}'")) ncards = len(a.find_cards(f"note:'{m}'")) ndue = len(a.find_cards(f"note:'{m}' is:due")) nmarked = len(a.find_cards(f"note:'{m}' tag:marked")) click.echo(f"{m:26s} {nnotes:8d} {ncards:8d} " f"{ndue:8d} {nmarked:8d}") click.echo("-"*62) click.echo(f"{'Sum':26s} {sum_notes:8d} {sum_cards:8d} " f"{sum_due:8d} {sum_marked:8d}") click.echo("-"*62)
def reposition(position, query): """Reposition cards that match QUERY. Sets the new position to POSITION and shifts other cards. Note that repositioning only works with new cards! """ query = " ".join(query) with Anki(**cfg) as a: cids = list(a.find_cards(query)) if not cids: click.echo(f'No matching cards for query: {query}!') raise click.Abort() for cid in cids: card = a.col.getCard(cid) if card.type != 0: click.echo('Can only reposition new cards!') raise click.Abort() a.col.sched.sortCards(cids, position, 1, False, True) a.modified = True
def sync(): """Synchronize collection with AnkiWeb.""" with Anki(cfg['base']) as a: a.sync()
def list_cards(query, verbose): """List cards that match a given query.""" with Anki(cfg['base']) as a: a.list_cards(query, verbose)
def rename(old_name, new_name): """Rename model from old_name to new_name.""" with Anki(cfg['base']) as a: a.rename_model(old_name, new_name)
def rename(old_name, new_name): """Rename model from old_name to new_name.""" with Anki(**cfg) as a: a.rename_model(old_name, new_name)
def check_media(): """Check media""" with Anki(cfg['base']) as a: a.check_media()
def sync(): """Synchronize collection with AnkiWeb.""" with Anki(**cfg) as a: a.sync()