import tempfile
from fastcore.test import *Core API
Each call opens the default collection, does its work, and closes up, so there’s no session state to manage. fb variants take plain front/back strings and scalar ids.
os.environ['FASTANKI_DIR'] = tempfile.mkdtemp()This notebook runs against a throwaway data folder; without that override, everything below uses ~/.fastanki.
Cards and notes
add_cloze_card
def add_cloze_card(
text:str, # Cloze text with `{c1::hidden}` deletions
back_extra:str='', # Extra info shown on the back of every card
deck:str='Default', # Deck name (`::` for nesting; created if needed)
tags:str=None, # Space-separated tags
):Add a Cloze card ({c1::hidden} syntax), returning the new note id.
add_fb_card
def add_fb_card(
front:str, # Front (question) text
back:str, # Back (answer) text
deck:str='Default', # Deck name (`::` for nesting; created if needed)
tags:str=None, # Space-separated tags
):Add a Basic card, returning the new note id.
add_card
def add_card(
model:str='Basic', # Notetype name (the tool description lists each notetype's fields)
deck:str='Default', # Deck name (`::` for nesting; created if needed)
tags:str=None, # Space-separated tags
fields:dict=None, # Field name -> value, e.g. {'Front':'2+2', 'Back':'4'}
):Add a card of any notetype, returning the new note id. Field names must match the notetype.
nid = add_fb_card('What is the capital of France?', 'Paris', tags='geo')
note = add_card(deck='Spanish::Vocab', tags='spanish', fields={'Front':'hola','Back':'hello'})
note1784759309644
czid = add_cloze_card('Minus times {{c1::minus}} is {{c2::plus}}', tags='maths')
test_eq(type(czid), int)To put an image or sound on a card, first add_media the file, then cite the returned name in a field with <img src="name"> or [sound:name]. The name can differ from what you passed (see fastanki.media for the rules), so always use the returned one:
add_media
def add_media(
path:str, # File to copy into the collection's media folder
fname:str=None, # Name to store it under (default: the file's own name)
):Add a media file to the collection, returning the filename to cite in fields: or [sound:name].
p = Path(tempfile.mkdtemp())/'chart.png'
p.write_bytes(b'png bytes here')
name = add_media(p)
test_eq(name, 'chart.png')
imgid = add_fb_card(f'What trend does this show? <img src="{name}">', 'up and to the right')
test_eq((Path(os.environ['FASTANKI_DIR'])/'collection.media'/name).read_bytes(), b'png bytes here')Finding, updating, removing
find_note_ids
def find_note_ids(
deck:str=None, # Deck name (matches subdecks too)
tag:str=None, # Tag to match
added_days:int=None, # Only notes added in the last this-many days
fields:dict=None, # Field name -> case-insensitive substring, e.g. {'Front':'hello'}
):Ids of notes matching all given criteria.
find_notes
def find_notes(
deck:str=None, # Deck name (matches subdecks too)
tag:str=None, # Tag to match
added_days:int=None, # Only notes added in the last this-many days
fields:dict=None, # Field name -> case-insensitive substring, e.g. {'Front':'hello'}
):Notes matching all given criteria.
find_card_ids
def find_card_ids(
deck:str=None, # Deck name (matches subdecks too)
tag:str=None, # Tag to match
added_days:int=None, # Only cards added in the last this-many days
is_due:bool=None, # Only cards due for review
fields:dict=None, # Field name -> case-insensitive substring, e.g. {'Front':'hello'}
):Ids of cards matching all given criteria.
find_cards
def find_cards(
deck:str=None, # Deck name (matches subdecks too)
tag:str=None, # Tag to match
added_days:int=None, # Only cards added in the last this-many days
is_due:bool=None, # Only cards due for review
fields:dict=None, # Field name -> case-insensitive substring, e.g. {'Front':'hello'}
):Cards matching all given criteria.
get_note
def get_note(
note_id:int, # Id of the note to retrieve
):Retrieve a note by id.
test_eq(find_note_ids(tag='geo'), [nid])
test_eq(find_note_ids(deck='Spanish'), [note])
test_eq(find_note_ids(fields={'Front':'capital'}), [nid])
test_eq(len(find_cards()), 5)
get_note(nid)Front: What is the capital of France? | Back: Paris | 🏷 geo
del_note
def del_note(
notes:list, # Note ids (or `Note` objects) to delete, along with their cards
):Delete note(s) (and their cards) by Note or id, singly or in a list.
update_fb_note
def update_fb_note(
note_id:int, # Id of the Basic note to update
front:str='', # New Front text (empty leaves it unchanged)
back:str='', # New Back text (empty leaves it unchanged)
tags:str=None, # Space-separated tags, replacing all existing tags
add_tags:str=None, # Space-separated tags to add, keeping existing ones
):Update a Basic note’s front/back and/or tags.
update_note
def update_note(
note, tags:NoneType=None, add_tags:NoneType=None, **fields
):Update fields and/or tags of a Note or note id; tags replaces, add_tags appends.
n2 = update_note(note, Back='hello!', add_tags='greeting')
test_eq(n2['Back'], 'hello!')
test_eq(n2.tags, ['spanish','greeting'])
test_eq(update_fb_note(nid, back='Paris, France').fields['Back'], 'Paris, France')
test_eq(del_note([nid, czid]), 2)
test_eq(find_note_ids(tag='geo'), [])Syncing
sync
def sync(
user:str=None, # AnkiWeb email (only needed the first time)
passw:str=None, # AnkiWeb password (only the first time; a host key is saved after)
endpoint:str=None, # Sync server URL (defaults to AnkiWeb)
upload:bool=False, # Force-upload the local collection, replacing the server copy
media:bool=True, # Also sync media files
):Sync the default collection with AnkiWeb. Pass credentials the first time; they’re saved after that.
The first sync of a fresh collection is a full one: by default that’s a download (the server copy wins), and replacing a non-empty server copy with a fresh empty collection is refused unless the server side is empty too. Pass upload=True deliberately to push your local copy wholesale.
sync(user=os.environ['ANKI_USER'], passw=os.environ['ANKI_PASS']) # first time
sync() # after thatTool use
anki_tools
def anki_tools():Call self as a function.
anki_tools()&`[add_card, add_fb_card, add_cloze_card, add_media, find_notes, find_note_ids, find_cards, find_card_ids, get_note, del_note, update_fb_note, sync]`
add_card handles any notetype via a fields dict, and rejects unknown field names with a clear error:
czid = add_card(model='Cloze', fields={'Text':'{{c1::pi}} ~ 3.14'})
test_eq(get_note(czid)['Text'], '{{c1::pi}} ~ 3.14')
test_fail(lambda: add_card(fields={'Nope':'x'}), contains='Nope') # unknown field -> clear error