Skip to content

Latest commit

 

History

History
1065 lines (882 loc) · 36.2 KB

onetag.org

File metadata and controls

1065 lines (882 loc) · 36.2 KB

ONETAG: (anki menu item)

what we want to do

it turns out that Anki opens files in a way that won’t allow a pipeline (as far as i can see), so the file itself needs to have the tags in it. or, maybe /dev/fdN could be used?

want a function that, given an argument, will ensure that 1) the current deck has only cards with TAG; 2) all cards in Anki with TAG are in that deck.

so, first, find all cards not in the current deck with TAG. then, move all those cards from their previous deck to the current deck.

XXX should we check we’re not moving too many cards?

then, search the current deck and find all cards that do not have the the designated tag. delete all those cards.

XXX same, should we check we’re not deleting too many cards?

remove the menu item (or, at least, the pre-canned tag)

after all this, delete the tags from all cards in the current deck.

ANKI-IMPORT: (shell command)

usage: run the import operation, which cons up TAG, and adds it to all the cards in the deck, then imports the cards into the named deck: ./anki-import turkish-2015 turkish-2015.html after the import, prime a menu item in Anki (with a shortcut, hopefully), that will cause onetag to run with the correct tag.

one function: given a file name and a deck name, import the contents of that file into that deck. need to have it tell how many fields it will find (maybe separate function for that?).

one function: given a search expression (such as “deck:current tag:fubar”), execute the search, and then execute a “move” to a designated deck (which must already exist).

one function: given a search expression, execute the search and print out the resulting cards.

one function: print the names of the existing decks.

one function: remove a given tag from a given (all?) deck(s?).

X11 anki

to get anki to work, need to run ./tools/build-ui.sh. for that, needed something from Qt designer, which required installing a bunch of Qt. then, to actually run it, needed to extend path, which i did by

bash
PATH=/sw/lib/qt4-mac/lib/python2.7/bin/:$PATH
./tools/build_ui.sh 

then, needed to copy all the files to ~/usr/lib/python/anki/ again (i.e., after running ./tools/build-ui.sh).

cool. now have an x11 version of anki (“% ./runanki”).

some bugs maybe in dae’s code/documentation

  • in “Import a text file into the collection”, should

deck[‘mid’] = m[‘id’]

possibly be

deck[‘mod’] = m[‘mod’]

?

  • also, i run this code, and my cards get added to the “Default” deck (rather than to the one i looked up using col.decks.id), or (it appears) whichever deck was most recently “selected”. here’s (more or less) DAE’s example:
import sys
sys.path.append("/usr/share/anki")
sys.path.append("/usr/local/share/anki")

from anki import Collection
from anki.importing import TextImporter
file = TEXT_FILE_PATH
collection = COLLECTION_FILE_PATH
deckName = DECK_NAME
col = Collection(collection)
ti = TextImporter(col, file)
# get note type for deck (could be a parameter)
m = col.models.byName("Basic")
# select deck
did = col.decks.byName(deckname)
# make sure we are importing to desired deck
col.decks.select(did)
if did != ti.model['did']:
   ti.model['did'] = did
   col.models.save(ti.model)
# import into the collection
ti.initMapping()
ti.run()
col.close()

consistency bug?

it seems my deck is always getting reset. i composed this message. but, then i upgraded to 2.1.35 (from, i think, 2.1.16):

/usr/local/share/anki/anki/__init__.py:version="2.1.16" # build scripts grep this line, so preserve formatting

so, i didn’t post the query yet

hi.

my normal usage is to add cards from my scripts on my laptop, launch anki on my laptop to synch to ankiweb, the launch IOS app, synch, study, synch.  repeat.  and, i seem (for maybe several years now?  only now am i trying to track it down) to lose my deck state, so that it's Groundhog Day every day, i seem to start with a fresh deck, end up with 40 non-new cards (i guess this is a good metric?).

so, probably either my work flow, or my scripts, or the combination, are triggering some sort of deck reset.

if up to this point, there's an obvious answer, i'm all ears!  but, if it's not that obvious (other than, "your decks, man!" :), i wonder if this question has an answer:

let there be three systems:
- LL -- laptop linux
- AW -- Ankiweb
- IP -- iphone

(oof, sorry) let Review(IP) be the result of reviewing the deck on IP.  let Modify(LL) be the result of adding/deleting cards on LL.  and, Sync(x) be the result of synching system x with AW.

and, let the three systems start off in a fully consistent state.   

then, should the following two sequences result in the same (globally consistent) state?
- `Review(); Sync IP; Modify(); Sync LL; Sync IP`
- `Review(); Sync IP; Sync LL; Modify(); Sync LL; Sync IP`

i've added some debugging to try to track down what is happening.  and, looking over syncing topics, this [forum entry](https://forums.ankiweb.net/t/bug-sync-resetting-new-card-count/5245/2) seems like it might be similar.

okay, thanks.

random documentation

nice comparison of Python and Lisp.

it appears that the fnotes themselves maybe contain information about the destination deck. or, at least, that when importing, one of the rows in addNew is supposed to contain the deck name?

we could import, then use the _ids in the TextImporter object, or just the tags we used, to move the new cards into the deck we want. (assuming we figure that out! :)

git remote issues

github wants to get rid of password authenticated writes.

bash apollo2 (master): {49407} git remote -v
origin  https://github.com/greg-minshall/ankibits.git (fetch)
origin  https://github.com/greg-minshall/ankibits.git (push)

i guess, do this

bash apollo2 (master): {49412} git remote set-url origin [email protected]:greg-minshall/ankibits.git
bash apollo2 (master): {49413} git remote -v
origin  [email protected]:greg-minshall/ankibits.git (fetch)
origin  [email protected]:greg-minshall/ankibits.git (push)

seems to work

bash apollo2 (master): {49414} git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   onetag.org

no changes added to commit (use "git add" and/or "git commit -a")
bash apollo2 (master): {49415} git push
Everything up-to-date

code

parameters

i’m not sure how to get scalar parameters into variables. the following works, but is maybe not ideal.

/Users/minshall/Documents/Anki/test/collection.anki2
decktest
/Users/minshall/src/mine/ankiplugins/test.html
a

#+RESULTS[90f772dc0313b916f2f89b493f51aef5d5351cf8]: anki2 /Users/minshall/Documents/Anki/test/collection.anki2

a

#+RESULTS[fe3bb60a68b6853fa7d7b2e7bb50abe431ff3935]: deckname decktest

a

#+RESULTS[fc56904fc33ce7b967cb09b25e451de24614ee04]: imfile /Users/minshall/src/mine/ankiplugins/test.html

one might want to say “#+name: foo\nbar\n”, but then “:var a=foo” produces a value in a of “bar\n”, i.e., with a trailing newline.

also, one might want to say “:cache yes”, but then, the value passed is “nil” (in the case where the cache entry is valid).

utilities

collection_guard

this allows us to open an Anki collection and ensure that the collection is closed “no matter what” happens. (this does not include some extraordinary event, such as a core dump.) this comes from http://effbot.org/zone/python-with-statement.htm

class collection_guard:
    def __init__(self, ankipath):
        self.ankipath = ankipath
    def __enter__(self):
        self.col = Collection(self.ankipath)
        return self
    def __exit__(self, type, value, traceback):
        # the protocol is, do a commit() *before* exiting
        self.undo()
        if self.col is not None:
            self.col.close()
            self.col = None
        return False
    def undo(self):            # we're unhappy, so undo() our progress
        if self.col is not None:
            # XXX what's the difference btw col.undo() and .rollback()?
            self.col.rollback()
    def commit(self):
        if self.col is not None:
            self.col.save()

unescape

the following html unescape() function is from this post on stackoverflow.

but, some problems when printing out notes with non-ASCII characters in them.

EncodeError: 'ascii' codec can't encode character u'\xd6' in position 21: ordinal not in range(128)

this environment variable approach works. but isn’t optimal. when writing to the terminal, sys.stdout.encoding == ‘UTF-8’, but when writing to a file or pipe, it is set to ‘None’, which i guess (this always makes my head hurt) causes the error message. would like a way to generally change the encoding of standard out.

this appears to do the trick.

try:
    from html import unescape  # python 3.4+
except ImportError:
    from html.parser import HTMLParser  # python 3.x (<3.4)
    unescape = HTMLParser().unescape

recipe577058 (yes/no dialog)

# from http://code.activestate.com/recipes/577058/

def query_yes_no(question, default="yes", helpeval=None, helplocals=None):
    import sys
    """Ask a yes/no question via raw_input() and return their answer.

    "question" is a string that is presented to the user.
    "default" is the presumed answer if the user just hits <Enter>.
    It must be "yes" (the default), "no" or None (meaning
    an answer is required of the user).

    The "answer" return value is one of "yes" or "no".
    """
    valid = {"yes":"yes",   "y":"yes",  "ye":"yes",
             "no":"no",     "n":"no"}
    nprompt = "n"
    yprompt = "y"
    qprompt = ""
    if helpeval:
        qprompt = "/?"
    if default == "yes":
        yprompt = "Y"
    elif default == "no":
        nprompt = "N"
    elif default != None:
        raise ValueError("invalid default answer: '%s'" % default)
    prompt = " [%s/%s%s] " % (yprompt, nprompt, qprompt)
    # for some reason, a blank line here generates an error
    while 1:
        sys.stdout.write(question + prompt)
        # handle C-c at this point
        choice = input().lower()
        if default is not None and choice == '':
            return default
        elif choice in list(valid.keys()):
            return valid[choice]
        elif choice == "?":
            if helpeval:
                if debugging:
                    print("%s (globals=%s)" % (helpeval, helplocals))
                    print(pcards)
                eval(helpeval, helplocals)
        sys.stdout.write("Please respond with 'yes' or 'no' "\
                             "(or 'y' or 'n').\n")

myparse, myargs, myargsdeck

import argparse

<<collection_guard>>
<<consankipath>>

def myparse(parser, argv=None, deckmustexist=True):
    """parse the arguments; set up ankipath and, optionally, check if deck exists"""
    import argparse
    import codecs
    import sys
    import json

    global debugging, verbosity, args

    args = parser.parse_args(argv)
    debugging = args.debugging
    verbosity = args.verbosity

    if args.tags:
        args.tags = json.loads(args.tags)

    if debugging > 1:
        print(args)

    ankipath = consankipath(args.path, args.user)
    if debugging:
        print(ankipath)

    # dir(): http://stackoverflow.com/a/191029
    if ('deckname' in dir(args)) & deckmustexist:
        with collection_guard(ankipath) as cg:
            if cg.col.decks.byName(args.deckname) == None:
                import sys
                print("error: deckname %s does not exist" % args.deckname)
                sys.exit(3)

    # sigh.  we *also* make sure stdout uses utf-8 encoding
    # (to avoid the errors mentioned above at unescape())
    # https://stackoverflow.com/a/52372390
    sys.stdout.reconfigure(encoding='utf-8')

    return [args, ankipath]
def myargs():
    import argparse
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-p", "--path", action="store",
                        default="~/.local/share/Anki2", metavar="pathname",
                        help="pathname to directory holding Anki collections")
    parser.add_argument("-u", "--user", action="store",
                        default="User 1", metavar="username",
                        help="Anki username of collection")
    parser.add_argument("--debugging", action="count", default=0,
                        help="increase level of (debugging) verbosity")
    parser.add_argument("-v", "--verbosity", action="count",
                        default=0,
                        help="increase level of verbosity")
    parser.add_argument("--json", action="store_true",
                        help="output in json format")
    parser.add_argument("--tags",
                        help="json object to use to tag entries")
    return parser

# for convenience
<<myparse>>
<<myargs>>

def myargsdeck():
    parser = argparse.ArgumentParser(parents=[myargs()], add_help=False)
    parser.add_argument("-d", "--deck", action="store", dest='deckname',
                        default='Default',
                        help="name of deck in Anki user's collection")
    return parser
def consankipath(path, user):
    import os

    unexpandedpath = ''.join([path, '/', user, '/', 'collection.anki2'])
    # https://docs.python.org/2/library/os.path.html#os.path.expanduser
    ankipath = os.path.expanduser(os.path.expandvars(unexpandedpath))
    if debugging:
        print(ankipath)
    return ankipath
def abspath(path):
    import os

    return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))

pager

invoking a sub-process from python; but, for us, we need to use popen.

def pager(text):
    """display text on the terminal (via less)"""
    import os
    import shlex
    import subprocess

    # figure out pager to use
    # http://stackoverflow.com/a/4907053
    try:
        pager = os.environ['PAGER']
    except KeyError:
        # does "less" exist?
    args = shlex.split("less")
    p = subprocess.Popen(args, stdin=subprocess.PIPE)
    p.communicate(text)
    p.stdin.close()
    p.wait()

ankidecks [–user username] [–path pathname]

list the decks in the collection. the optional argument username argument specifies the “username” of the Anki collection.

the optional pathname (typically something like ~/Documents/Anki/) is the pathname where Anki collections are stored.

import sys
sys.path.append("/usr/share/anki")
sys.path.append("/usr/local/share/anki")

from anki import Collection

def pdecks(col):
    for i, val in enumerate(col.decks.all_names_and_ids(
            skip_empty_default=False,
            include_filtered=True)):
        print(val.name)
import argparse

<<collection_guard>>
<<decks>>
<<myargs>>

def main():
    # scope rules (LEGB): http://stackoverflow.com/a/292502
    parser = argparse.ArgumentParser(description=
                                     "list the decks in an Anki collection",
                                     parents=[myargs()])
    args, ankipath = myparse(parser)

    with collection_guard(ankipath) as cg:
        pdecks(cg.col)
        cg.commit()

if __name__ == "__main__":
    main()

ankicards [–user username] [–path pathname]

list out the notes from a given deck (the name of which is required).

import sys
import time
import json

sys.path.append("/usr/share/anki")
sys.path.append("/usr/local/share/anki")

from anki import Collection

<<unescape>>

def date(t):
    return time.strftime("%F", time.gmtime(t))

def datetime(t):
    return time.strftime("%FT%H:%M:%SZ", time.gmtime(t))

def fcard(col, id, decknamep=False):
    card = col.getCard(id)
    if debugging:
        print(card)
    card = col.getCard(id)
    note = card.note()
    values = list(note.values())

    verbosenesses = {"utc": datetime(time.time())}
    # http://stackoverflow.com/a/60211
    type = { 0: "new",
             1: "learning",
             2: "due" }[card.type]
    verbosenesses["type"] = "%s (%s)" % (card.type, type)
    if (card.queue != card.type):
        queue = { -3: "/sched-buried",
                  -2: "/user-buried",
                  -1: "/suspended",
                  0: "",
                  1: "",
                  2: "" }[card.queue]
        verbosenesses["queue"] = "%s (%s)" % (card.queue, queue)
    else:
        verbosenesses["queue"] = card.queue
    # computing "due" is a bit tricky
    next = time.time()
    posn = card.due
    if card.type in (1,2):
        posn = float("inf")
        if card.odid or card.queue < 0:
            next = float("inf")
        else:
            if card.queue in (2,3):
                next = time.time()+((card.due - col.sched.today)*86400)
            else:
                next = card.due
        if next:
            verbosenesses["due"] = next
    if next < float("inf"): # if not never
        next = date(next)
    verbosenesses["due"] = next
    if decknamep or verbosity:
        verbosenesses["deckname"] = col.decks.get(card.did, default=False)['name']
    if (verbosity):
        verbosenesses["cid"] = card.id
        # a lot from anki's stats.py
        # ~/src/import/anki/dae/git/anki/anki/stats.py (older version)
        first = col.db.scalar(
            "select min(id) from revlog where cid = ?", card.id)
        last = col.db.scalar(
            "select max(id) from revlog where cid = ?", card.id)
        verbosenesses["added"] = date(card.id/1000)
        if (first):
            verbosenesses["first"] = date(first/1000)
        else:
            verbosenesses["first"] = float("inf")
        if (last):
            verbosenesses["last"] = date(last/1000)
        else:
            verbosenesses["last"] = float("inf")
        verbosenesses["odid"] = card.odid
        verbosenesses["type"] = "%s (%s)" % (card.type, type)
        verbosenesses["position"] = posn
        verbosenesses["interval"] = card.ivl * 86400
        verbosenesses["ease"] = card.factor/10.0
        verbosenesses["reviews"] = card.reps
        verbosenesses["lapses"] = card.lapses
        if (verbosity > 1):     # i think these are always the same
            verbosenesses["ctype"] = card.template()["name"]
            verbosenesses["ntype"] = card.model()["name"]
        verbosenesses["did"] = card.did
        verbosenesses["deckname"] = col.decks.get(card.did, default=False)['name']
        verbosenesses["nid"] = card.nid

    verbosenesses["front"] = unescape(values[0])
    verbosenesses["back"] = unescape(values[1])

    for k in verbosenesses.keys():
        verbosenesses[k] = str(verbosenesses[k])
    return verbosenesses

def pcards(col, ids, decknamep=False):
    if debugging:
        print(ids)
    didheader = False
    for i, id in enumerate(ids):
        fc = fcard(col, id, decknamep)
        if args.tags:
            fc = args.tags | fc
        if args.json:
            print(json.dumps(fc, ensure_ascii=False))
        else:
            if not didheader:
                print("\t".join(fc.keys()))    # header line
                didheader = True
            print("\t".join(fc.values()))  # actual line

def dopcards(col, deckname, decknamep=False):
    pcards(col, col.findCards("deck:%s" % deckname), decknamep)
<<collection_guard>>
<<myargsdeck>>
<<cards>>

def main():
    parser = argparse.ArgumentParser(parents=[myargsdeck()],
                description="list the notes in one deck in an Anki collection")
    args, ankipath = myparse(parser)

    with collection_guard(ankipath) as cg:
        dopcards(cg.col, args.deckname)
        cg.commit()

if __name__ == "__main__":
    main()

ankinotes [–user username] [–path pathname] [{-d|–deck} deckname]

list out the notes from a given deck (the name of which is required).

import sys
sys.path.append("/usr/share/anki")
sys.path.append("/usr/local/share/anki")

from anki import Collection

<<unescape>>

def pnotes(col, deckname):
    ids = col.findNotes("deck:%s" % deckname)
    if debugging:
        print(ids)
    for i, id in enumerate(ids):
        note = col.getNote(id)
        values = list(note.values())
        print(unescape("%s\t%s" % (values[0], values[1])))
<<collection_guard>>
<<myargsdeck>>
<<notes>>

def main():
    parser = argparse.ArgumentParser(parents=[myargsdeck()],
                description="list the notes in one deck in an Anki collection")
    args, ankipath = myparse(parser)

    with collection_guard(ankipath) as cg:
        pnotes(cg.col, args.deckname)
        cg.commit()

if __name__ == "__main__":
    main()

ankitags [{-u|–user} username] [{-p|–path} pathname] [{-d|–deck} deckname]

list the tags that exist in a given deck, along with the number of notes with each tag.

import sys
sys.path.append("/usr/share/anki")
sys.path.append("/usr/local/share/anki")

from anki import Collection

def ptags(col, deckname):
    ids = col.findNotes("deck:%s" % deckname)
    if debugging:
        print(ids)
    # https://docs.python.org/2/library/stdtypes.html#dict
    tags = dict()
    for i, id in enumerate(ids):
        note = col.getNote(id)
        if debugging:
            print(note.stringTags())
        for s in note.stringTags().split():
            if debugging:
                print(s)
            # "s not in tags": http://stackoverflow.com/a/18300596
            if s not in tags:
                tags[s] = 1
            else:
                tags[s] += 1
    for t in iter(tags):
        print(t, tags[t])
<<collection_guard>>
<<myargsdeck>>
<<tags>>

def main():
    parser = argparse.ArgumentParser(parents=[myargsdeck()],
                description="list the notes in one deck in an Anki collection")
    args, ankipath = myparse(parser)

    with collection_guard(ankipath) as cg:
        ptags(cg.col, args.deckname)
        cg.commit()

if __name__ == "__main__":
    main()

ankisearch [{-p|–path} pathname] [{-u|–user} username] [{-d|–deck} deckname] query

search a given deck

import sys
sys.path.append("/usr/share/anki")
sys.path.append("/usr/local/share/anki")

from anki import Collection

<<unescape>>

# XXX here (and elsewhere) check that deck exists
# XXX graceful error message if user, database file doesn't exist
def psearch(col, deckname, query):
    ids = []
    ids = col.findNotes("".join(["deck:", deckname, " ", query]))
    if debugging:
        print(ids)
    for i, id in enumerate(ids):
        note = col.getNote(id)
        values = list(note.values())
        print(unescape("%s\t%s" % (values[0], values[1])))
import sys

<<collection_guard>>
<<myargsdeck>>
<<search>>

def main():
    parser = argparse.ArgumentParser(parents=[myargsdeck()],
                    description="search the notes in one deck in an Anki collection")
    parser.add_argument("query", nargs=argparse.REMAINDER, action="store",
                        metavar="query",
                        help="query terms for search [e.g., 'tag:foo aspirin']")
    args, ankipath = myparse(parser)
    # "not args.query": http://stackoverflow.com/a/53522
    if ('query' not in args) | (not args.query):
        print("required 'query' term missing")
        parser.print_usage()
        sys.exit()

    with collection_guard(ankipath) as cg:
        psearch(cg.col, args.deckname, " ".join(args.query))
        cg.commit()

if __name__ == "__main__":
    main()

ankiimport

<<recipe577058>>

def str2list(expected):
    """given a string representation of a list (of strings), make a list"""

    if expected:
        import re
        # make sure this looks good
        if re.match('\A\[[\w,]+\]\Z', expected):
            list = re.split('[\[\],]', expected)[1:-1]
            if debugging:
                print("str2list(%s) returns %s" % (expected, list))
            return list
    return expected             # i.e., None



# two functions: one that shows the mapping, allows one to proceed or
# cancel (returns True or False); a second shows the results of the
# import, allows one to accept or abort (returns True or False)

# XXX allow user to specify expected mapping, abort (with message) if
# different; don't query if same
def checkmapping(ti, expected):
    """check the mapping (of note fields to card contents)

if the user specified an expected mapping, check that, aborting (with
an error message) if it doesn't match.  if no expected mapping was
specified, display the mapping to the user, giving him/her the
opportunity to cancel the import

    """
    if expected:
        mapping = []            # build up the actual mapping
        for num in range(len(ti.mapping)):
            if ti.mapping[num] == '_tags':
                mapping = mapping + ['tags']
            elif ti.mapping[num]:
                mapping = mapping + [ti.mapping[num].lower()]
            else:
                mapping = mapping + ["ignored"]
        if mapping != expected: # if it doesn't match the expected mapping
            print("expected mapping (%s) not equal to computed (%s)" % \
                (expected, mapping))
            abort()             # abort
    else:
        # from showMapping in aqt/importing.py
        for num in range(len(ti.mapping)):
            intro = "Field %d of file is:" % (num+1)
            if ti.mapping[num] == "_tags":
                where = "mapped to Tags"
            elif ti.mapping[num]:
                where = "mapped to %s" % ti.mapping[num]
            else:
                where = "<ignored>"
            print("%s%s" % (intro, where))
        if query_yes_no("proceed with import?", default=None) == 'no':
            abort()
import sys
sys.path.append("/usr/share/anki")
sys.path.append("/usr/local/share/anki")

from anki import Collection
from anki.importing import TextImporter

<<abspath>>
<<cards>>
<<collection_guard>>
<<myargsdeck>>
<<unescape>>
<<user_interface>>

def interactivep():
  # https://stackoverflow.com/a/2356420/1527747
  import __main__ as main
  return not hasattr(main, '__file__')

def splitin(string):
    # https://stackoverflow.com/a/79985/1527747
    import shlex
    ankiimportMain(shlex.split(string))

def abort(rc=3):
    import sys
    sys.exit(rc)

# the official ternary operator
# http://stackoverflow.com/a/394814
# is too ugly
def plurality(n, singular, plural):
    if abs(n) == 1:
        return singular
    else:
        return plural

def logcards(col, why, prefix, ids):
    """send a list of cards (given by IDS) to the log; WHY is text explaining why"""
    if logfile:
        # http://stackoverflow.com/q/1987626
        print(why, file=logfile)
        for id in ids:
            print("%s%s" % (prefix, fcard(col, id)), file=logfile)

def superset(col, deckname, nids):
    """make sure all cards with ids in IDS are in Anki deck DECKNAME"""
    snids = "%s" % ",".join(str(i) for i in nids) # XXX ids we added [ids2str]    
    did = col.decks.byName(deckname)['id']
    ids = []
    ids = col.findCards("-deck:%s nid:%s" % (deckname, snids))
    if ids:
        if debugging:
            print("superset ids: %s" % ids)
        if logfile and verbosity:
            print("superset ids: %s" % ids, file=logfile)
        n = len(ids)
        print("will move %s %s into deck %s" % \
            (n, plurality(len(ids),"card", "cards"), deckname))
        # XXX allow user to see what cards will be moved
        if query_yes_no("proceed with import?", default=None,
                        helpeval="pcards(col, ids, True)",
                        helplocals=dict(pcards=pcards, col=col,
                                        ids=ids)) == 'no':
            abort()
        logcards(col, "cards moved to deck %s" % deckname, "  ", ids)
        col.decks.setDeck(ids, did)
        col.sched.resetCards(ids)

def subset(col, deckname, nids):
    """make sure only cards with tag TAG are in Anki deck DECKNAME"""
    # XXX just deletes the cards; nicer might be to stash them somewhere
    snids = "%s" % ",".join(str(i) for i in nids) # XXX ids we added [ids2str]
    ids = []
    ids = col.findCards("deck:%s -nid:%s" % (deckname, snids))
    if ids:
        if debugging:
            print("subset ids: %s" % ids)
        if logfile and verbosity:
            print("subset ids: %s" % ids, file=logfile)
        n = len(ids)
        print("will delete %s %s from deck %s" % \
            (n, plurality(len(ids),"card", "cards"), deckname))
        # XXX allow user to see what cards will be deleted
        if query_yes_no("proceed with import?", default=None,
                        helpeval="pcards(col, ids, False)",
                        helplocals=dict(pcards=pcards, col=col,
                                        ids=ids)) == 'no':
            abort()
        logcards(col, "cards deleted from deck %s" % deckname, "  ", ids)
        # not sure about notes=True, but it makes sense for how we use it
        col.remCards(ids, notes=True)

# get foreign notes: these aren't (yet) real Anki notes, just a
# represenation that has been read in.
def getfnotes(ti):
    # now, get the notes
    fnotes = ti.foreignNotes()
    return fnotes

def add2col(col, deckname, ti, fnotes):
    # XXX should we remember previously selected deck (and reselect it
    # when we're done here)?
    did = col.decks.byName(deckname)['id']
    if debugging:
        print("did %s" % did)
    if did != ti.model['did']:
        ti.model['did'] = did
        col.models.save(ti.model)
    col.decks.select(did)
    ti.importNotes(fnotes)
    # XXX ids we added [ids2str]
    return ti._ids

def doimport(col, deckname, ifilepath, mapping, dosubset, dosuperset):
    ti = TextImporter(col, ifilepath)
    ti.allowHTML = True
    ti.initMapping()
    checkmapping(ti, mapping)    # this may abort
    # first, get anki read in the notes (to an intermediate form)
    fnotes = getfnotes(ti)
    # now, add these notes to the designated deck
    nids = add2col(col, deckname, ti, fnotes)
    if ti.log:
        # XXX don't print out all those lines; summarize (print
        # out first 3 lines), give option for paging through
        # everything (though, --logfilter helps)
        for txt in ti.log:
            if not logfilterpattern.search(txt):
                utxt = unescape(txt)
                print(utxt)
                if logfile:
                    print(utxt, file=logfile)
    del ti                      # no longer to be used
    if dosuperset:
        # now, move any notes from any *other* decks with one of our ids to this deck
        superset(col, deckname, nids)
    if dosubset:
        # now, delete any notes in deck that are not one of our ids
        subset(col, deckname, nids)
    # done!

# http://ankisrs.net/docs/addons.html#the-database
def ankiimportMain(argv=None):
    import codecs
    import re
    import time

    global logfilterpattern, logfile

    parser = argparse.ArgumentParser(parents=[myargsdeck()],
                description="import an HTML file into a deck in an Anki collection")
    parser.add_argument("-R", "--superset", action="store_true", default=False,
                        help="ensure the Anki deck is a supeRset of the input file")
    parser.add_argument("-B", "--subset", action="store_true", default=False,
                        help="ensure the Anki deck is a suBset of the input file")
    parser.add_argument("-m", "--mapping", action='store', default=None,
                        help="expected mapping of fields, e.g., '[front,back,tag]'")
    parser.add_argument("--logfilter", action='store',
                        default="First field matched",
                        help="regular expression of messages to *not* write to log")
    parser.add_argument("-l", "--logfile", type=argparse.FileType('a'), default=None,
                        help="file in which to log actions during import (appended)")
    parser.add_argument("inputfile", type=str)

    args, ankipath = myparse(parser, argv)

    if args.logfile:
        logfile = args.logfile
        logfile.reconfigure(encoding='utf-8')
        print("import started %s" % time.strftime("%x %X"), file=logfile)
        print("arguments: %s" % args, file=logfile)

    # https://wiki.python.org/moin/HandlingExceptions
    try:
        logfilterpattern = re.compile(args.logfilter)
    except:
        print("invalid regular expression in \"--logfilter %s\"" % args.logfilter)
        abort()

    ifilepath = abspath(args.inputfile)
    with collection_guard(ankipath) as cg:
        doimport(cg.col, args.deckname, ifilepath, str2list(args.mapping),
                 dosubset=args.subset, dosuperset=args.superset)
        cg.commit()

# https://stackoverflow.com/a/2356420/1527747
import __main__ as main
if not interactivep():
    ankiimportMain()