anki/anki/collection.py
2019-03-04 17:25:19 +10:00

936 lines
32 KiB
Python

# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import pprint
import re
import time
import os
import random
import stat
import datetime
import copy
import traceback
import json
from anki.lang import _, ngettext
from anki.utils import ids2str, fieldChecksum, \
intTime, splitFields, joinFields, maxID, devMode, stripHTMLMedia
from anki.hooks import runFilter, runHook
from anki.models import ModelManager
from anki.media import MediaManager
from anki.decks import DeckManager
from anki.tags import TagManager
from anki.consts import *
from anki.errors import AnkiError
from anki.sound import stripSounds
import anki.latex # sets up hook
import anki.cards
import anki.notes
import anki.template
import anki.find
defaultConf = {
# review options
'activeDecks': [1],
'curDeck': 1,
'newSpread': NEW_CARDS_DISTRIBUTE,
'collapseTime': 1200,
'timeLim': 0,
'estTimes': True,
'dueCounts': True,
# other config
'curModel': None,
'nextPos': 1,
'sortType': "noteFld",
'sortBackwards': False,
'addToCur': True, # add new to currently selected deck?
'dayLearnFirst': False,
}
# this is initialized by storage.Collection
class _Collection:
def __init__(self, db, server=False, log=False):
self._debugLog = log
self.db = db
self.path = db._path
self._openLog()
self.log(self.path, anki.version)
self.server = server
self._lastSave = time.time()
self.clearUndo()
self.media = MediaManager(self, server)
self.models = ModelManager(self)
self.decks = DeckManager(self)
self.tags = TagManager(self)
self.load()
if not self.crt:
d = datetime.datetime.today()
d -= datetime.timedelta(hours=4)
d = datetime.datetime(d.year, d.month, d.day)
d += datetime.timedelta(hours=4)
self.crt = int(time.mktime(d.timetuple()))
self._loadScheduler()
if not self.conf.get("newBury", False):
self.conf['newBury'] = True
self.setMod()
def name(self):
n = os.path.splitext(os.path.basename(self.path))[0]
return n
# Scheduler
##########################################################################
defaultSchedulerVersion = 1
supportedSchedulerVersions = (1, 2)
def schedVer(self):
ver = self.conf.get("schedVer", self.defaultSchedulerVersion)
if ver in self.supportedSchedulerVersions:
return ver
else:
raise Exception("Unsupported scheduler version")
def _loadScheduler(self):
ver = self.schedVer()
if ver == 1:
from anki.sched import Scheduler
elif ver == 2:
from anki.schedv2 import Scheduler
self.sched = Scheduler(self)
def changeSchedulerVer(self, ver):
if ver == self.schedVer():
return
if ver not in self.supportedSchedulerVersions:
raise Exception("Unsupported scheduler version")
self.modSchema(check=True)
from anki.schedv2 import Scheduler
v2Sched = Scheduler(self)
if ver == 1:
v2Sched.moveToV1()
else:
v2Sched.moveToV2()
self.conf['schedVer'] = ver
self.setMod()
self._loadScheduler()
# DB-related
##########################################################################
def load(self):
(self.crt,
self.mod,
self.scm,
self.dty, # no longer used
self._usn,
self.ls,
self.conf,
models,
decks,
dconf,
tags) = self.db.first("""
select crt, mod, scm, dty, usn, ls,
conf, models, decks, dconf, tags from col""")
self.conf = json.loads(self.conf)
self.models.load(models)
self.decks.load(decks, dconf)
self.tags.load(tags)
def setMod(self):
"""Mark DB modified.
DB operations and the deck/tag/model managers do this automatically, so this
is only necessary if you modify properties of this object or the conf dict."""
self.db.mod = True
def flush(self, mod=None):
"Flush state to DB, updating mod time."
self.mod = intTime(1000) if mod is None else mod
self.db.execute(
"""update col set
crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.crt, self.mod, self.scm, self.dty,
self._usn, self.ls, json.dumps(self.conf))
def save(self, name=None, mod=None):
"Flush, commit DB, and take out another write lock."
# let the managers conditionally flush
self.models.flush()
self.decks.flush()
self.tags.flush()
# and flush deck + bump mod if db has been changed
if self.db.mod:
self.flush(mod=mod)
self.db.commit()
self.lock()
self.db.mod = False
self._markOp(name)
self._lastSave = time.time()
def autosave(self):
"Save if 5 minutes has passed since last save. True if saved."
if time.time() - self._lastSave > 300:
self.save()
return True
def lock(self):
# make sure we don't accidentally bump mod time
mod = self.db.mod
self.db.execute("update col set mod=mod")
self.db.mod = mod
def close(self, save=True):
"Disconnect from DB."
if self.db:
if save:
self.save()
else:
self.db.rollback()
if not self.server:
self.db.setAutocommit(True)
self.db.execute("pragma journal_mode = delete")
self.db.setAutocommit(False)
self.db.close()
self.db = None
self.media.close()
self._closeLog()
def reopen(self):
"Reconnect to DB (after changing threads, etc)."
import anki.db
if not self.db:
self.db = anki.db.DB(self.path)
self.media.connect()
self._openLog()
def rollback(self):
self.db.rollback()
self.load()
self.lock()
def modSchema(self, check):
"Mark schema modified. Call this first so user can abort if necessary."
if not self.schemaChanged():
if check and not runFilter("modSchema", True):
raise AnkiError("abortSchemaMod")
self.scm = intTime(1000)
self.setMod()
def schemaChanged(self):
"True if schema changed since last sync."
return self.scm > self.ls
def usn(self):
return self._usn if self.server else -1
def beforeUpload(self):
"Called before a full upload."
tbls = "notes", "cards", "revlog"
for t in tbls:
self.db.execute("update %s set usn=0 where usn=-1" % t)
# we can save space by removing the log of deletions
self.db.execute("delete from graves")
self._usn += 1
self.models.beforeUpload()
self.tags.beforeUpload()
self.decks.beforeUpload()
self.modSchema(check=False)
self.ls = self.scm
# ensure db is compacted before upload
self.db.setAutocommit(True)
self.db.execute("vacuum")
self.db.execute("analyze")
self.close()
# Object creation helpers
##########################################################################
def getCard(self, id):
return anki.cards.Card(self, id)
def getNote(self, id):
return anki.notes.Note(self, id=id)
# Utils
##########################################################################
def nextID(self, type, inc=True):
type = "next"+type.capitalize()
id = self.conf.get(type, 1)
if inc:
self.conf[type] = id+1
return id
def reset(self):
"Rebuild the queue and reload data after DB modified."
self.sched.reset()
# Deletion logging
##########################################################################
def _logRem(self, ids, type):
self.db.executemany("insert into graves values (%d, ?, %d)" % (
self.usn(), type), ([x] for x in ids))
# Notes
##########################################################################
def noteCount(self):
return self.db.scalar("select count() from notes")
def newNote(self, forDeck=True):
"Return a new note with the current model."
return anki.notes.Note(self, self.models.current(forDeck))
def addNote(self, note):
"Add a note to the collection. Return number of new cards."
# check we have card models available, then save
cms = self.findTemplates(note)
if not cms:
return 0
note.flush()
# deck conf governs which of these are used
due = self.nextID("pos")
# add cards
ncards = 0
for template in cms:
self._newCard(note, template, due)
ncards += 1
return ncards
def remNotes(self, ids):
self.remCards(self.db.list("select id from cards where nid in "+
ids2str(ids)))
def _remNotes(self, ids):
"Bulk delete notes by ID. Don't call this directly."
if not ids:
return
strids = ids2str(ids)
# we need to log these independently of cards, as one side may have
# more card templates
runHook("remNotes", self, ids)
self._logRem(ids, REM_NOTE)
self.db.execute("delete from notes where id in %s" % strids)
# Card creation
##########################################################################
def findTemplates(self, note):
"Return (active), non-empty templates."
model = note.model()
avail = self.models.availOrds(model, joinFields(note.fields))
return self._tmplsFromOrds(model, avail)
def _tmplsFromOrds(self, model, avail):
ok = []
if model['type'] == MODEL_STD:
for t in model['tmpls']:
if t['ord'] in avail:
ok.append(t)
else:
# cloze - generate temporary templates from first
for ord in avail:
t = copy.copy(model['tmpls'][0])
t['ord'] = ord
ok.append(t)
return ok
def genCards(self, nids):
"Generate cards for non-empty templates, return ids to remove."
# build map of (nid,ord) so we don't create dupes
snids = ids2str(nids)
have = {}
dids = {}
dues = {}
for id, nid, ord, did, due, odue, odid in self.db.execute(
"select id, nid, ord, did, due, odue, odid from cards where nid in "+snids):
# existing cards
if nid not in have:
have[nid] = {}
have[nid][ord] = id
# if in a filtered deck, add new cards to original deck
if odid != 0:
did = odid
# and their dids
if nid in dids:
if dids[nid] and dids[nid] != did:
# cards are in two or more different decks; revert to
# model default
dids[nid] = None
else:
# first card or multiple cards in same deck
dids[nid] = did
# save due
if odid != 0:
due = odue
if nid not in dues:
dues[nid] = due
# build cards for each note
data = []
ts = maxID(self.db)
now = intTime()
rem = []
usn = self.usn()
for nid, mid, flds in self.db.execute(
"select id, mid, flds from notes where id in "+snids):
model = self.models.get(mid)
avail = self.models.availOrds(model, flds)
did = dids.get(nid) or model['did']
due = dues.get(nid)
# add any missing cards
for t in self._tmplsFromOrds(model, avail):
doHave = nid in have and t['ord'] in have[nid]
if not doHave:
# check deck is not a cram deck
did = t['did'] or did
if self.decks.isDyn(did):
did = 1
# if the deck doesn't exist, use default instead
did = self.decks.get(did)['id']
# use sibling due# if there is one, else use a new id
if due is None:
due = self.nextID("pos")
data.append((ts, nid, did, t['ord'],
now, usn, due))
ts += 1
# note any cards that need removing
if nid in have:
for ord, id in list(have[nid].items()):
if ord not in avail:
rem.append(id)
# bulk update
self.db.executemany("""
insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""",
data)
return rem
# type 0 - when previewing in add dialog, only non-empty
# type 1 - when previewing edit, only existing
# type 2 - when previewing in models dialog, all templates
def previewCards(self, note, type=0, did=None):
if type == 0:
cms = self.findTemplates(note)
elif type == 1:
cms = [c.template() for c in note.cards()]
else:
cms = note.model()['tmpls']
if not cms:
return []
cards = []
for template in cms:
cards.append(self._newCard(note, template, 1, flush=False, did=did))
return cards
def _newCard(self, note, template, due, flush=True, did=None):
"Create a new card."
card = anki.cards.Card(self)
card.nid = note.id
card.ord = template['ord']
card.did = self.db.scalar("select did from cards where nid = ? and ord = ?", card.nid, card.ord)
# Use template did (deck override) if valid, otherwise did in argument, otherwise model did
if not card.did:
if template['did'] and str(template['did']) in self.decks.decks:
card.did = template['did']
elif did:
card.did = did
else:
card.did = note.model()['did']
# if invalid did, use default instead
deck = self.decks.get(card.did)
if deck['dyn']:
# must not be a filtered deck
card.did = 1
else:
card.did = deck['id']
card.due = self._dueForDid(card.did, due)
if flush:
card.flush()
return card
def _dueForDid(self, did, due):
conf = self.decks.confForDid(did)
# in order due?
if conf['new']['order'] == NEW_CARDS_DUE:
return due
else:
# random mode; seed with note ts so all cards of this note get the
# same random number
r = random.Random()
r.seed(due)
return r.randrange(1, max(due, 1000))
# Cards
##########################################################################
def isEmpty(self):
return not self.db.scalar("select 1 from cards limit 1")
def cardCount(self):
return self.db.scalar("select count() from cards")
def remCards(self, ids, notes=True):
"Bulk delete cards by ID."
if not ids:
return
sids = ids2str(ids)
nids = self.db.list("select nid from cards where id in "+sids)
# remove cards
self._logRem(ids, REM_CARD)
self.db.execute("delete from cards where id in "+sids)
# then notes
if not notes:
return
nids = self.db.list("""
select id from notes where id in %s and id not in (select nid from cards)""" %
ids2str(nids))
self._remNotes(nids)
def emptyCids(self):
rem = []
for m in self.models.all():
rem += self.genCards(self.models.nids(m))
return rem
def emptyCardReport(self, cids):
rep = ""
for ords, cnt, flds in self.db.all("""
select group_concat(ord+1), count(), flds from cards c, notes n
where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
rep += _("Empty card numbers: %(c)s\nFields: %(f)s\n\n") % dict(
c=ords, f=flds.replace("\x1f", " / "))
return rep
# Field checksums and sorting fields
##########################################################################
def _fieldData(self, snids):
return self.db.execute(
"select id, mid, flds from notes where id in "+snids)
def updateFieldCache(self, nids):
"Update field checksums and sort cache, after find&replace, etc."
snids = ids2str(nids)
r = []
for (nid, mid, flds) in self._fieldData(snids):
fields = splitFields(flds)
model = self.models.get(mid)
if not model:
# note points to invalid model
continue
r.append((stripHTMLMedia(fields[self.models.sortIdx(model)]),
fieldChecksum(fields[0]),
nid))
# apply, relying on calling code to bump usn+mod
self.db.executemany("update notes set sfld=?, csum=? where id=?", r)
# Q/A generation
##########################################################################
def renderQA(self, ids=None, type="card"):
# gather metadata
if type == "card":
where = "and c.id in " + ids2str(ids)
elif type == "note":
where = "and f.id in " + ids2str(ids)
elif type == "model":
where = "and m.id in " + ids2str(ids)
elif type == "all":
where = ""
else:
raise Exception()
return [self._renderQA(row)
for row in self._qaData(where)]
def _renderQA(self, data, qfmt=None, afmt=None):
"Returns hash of id, question, answer."
# data is [cid, nid, mid, did, ord, tags, flds, cardFlags]
# unpack fields and create dict
flist = splitFields(data[6])
fields = {}
model = self.models.get(data[2])
for (name, (idx, conf)) in list(self.models.fieldMap(model).items()):
fields[name] = flist[idx]
fields['Tags'] = data[5].strip()
fields['Type'] = model['name']
fields['Deck'] = self.decks.name(data[3])
fields['Subdeck'] = fields['Deck'].split('::')[-1]
fields['CardFlag'] = self._flagNameFromCardFlags(data[7])
if model['type'] == MODEL_STD:
template = model['tmpls'][data[4]]
else:
template = model['tmpls'][0]
fields['Card'] = template['name']
fields['c%d' % (data[4]+1)] = "1"
# render q & a
d = dict(id=data[0])
qfmt = qfmt or template['qfmt']
afmt = afmt or template['afmt']
for (type, format) in (("q", qfmt), ("a", afmt)):
if type == "q":
format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format)
format = format.replace("<%cloze:", "<%%cq:%d:" % (
data[4]+1))
else:
format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format)
format = format.replace("<%cloze:", "<%%ca:%d:" % (
data[4]+1))
fields['FrontSide'] = stripSounds(d['q'])
fields = runFilter("mungeFields", fields, model, data, self)
html = anki.template.render(format, fields)
d[type] = runFilter(
"mungeQA", html, type, fields, model, data, self)
# empty cloze?
if type == 'q' and model['type'] == MODEL_CLOZE:
if not self.models._availClozeOrds(model, data[6], False):
d['q'] += ("<p>" + _(
"Please edit this note and add some cloze deletions. (%s)") % (
"<a href=%s#cloze>%s</a>" % (HELP_SITE, _("help"))))
return d
def _qaData(self, where=""):
"Return [cid, nid, mid, did, ord, tags, flds, cardFlags] db query"
return self.db.execute("""
select c.id, f.id, f.mid, c.did, c.ord, f.tags, f.flds, c.flags
from cards c, notes f
where c.nid == f.id
%s""" % where)
def _flagNameFromCardFlags(self, flags):
flag = flags & 0b111
if not flag:
return ""
return "flag%d" % flag
# Finding cards
##########################################################################
def findCards(self, query, order=False):
return anki.find.Finder(self).findCards(query, order)
def findNotes(self, query):
return anki.find.Finder(self).findNotes(query)
def findReplace(self, nids, src, dst, regex=None, field=None, fold=True):
return anki.find.findReplace(self, nids, src, dst, regex, field, fold)
def findDupes(self, fieldName, search=""):
return anki.find.findDupes(self, fieldName, search)
# Stats
##########################################################################
def cardStats(self, card):
from anki.stats import CardStats
return CardStats(self, card).report()
def stats(self):
from anki.stats import CollectionStats
return CollectionStats(self)
# Timeboxing
##########################################################################
def startTimebox(self):
self._startTime = time.time()
self._startReps = self.sched.reps
def timeboxReached(self):
"Return (elapsedTime, reps) if timebox reached, or False."
if not self.conf['timeLim']:
# timeboxing disabled
return False
elapsed = time.time() - self._startTime
if elapsed > self.conf['timeLim']:
return (self.conf['timeLim'], self.sched.reps - self._startReps)
# Undo
##########################################################################
def clearUndo(self):
# [type, undoName, data]
# type 1 = review; type 2 = checkpoint
self._undo = None
def undoName(self):
"Undo menu item name, or None if undo unavailable."
if not self._undo:
return None
return self._undo[1]
def undo(self):
if self._undo[0] == 1:
return self._undoReview()
else:
self._undoOp()
def markReview(self, card):
old = []
if self._undo:
if self._undo[0] == 1:
old = self._undo[2]
self.clearUndo()
wasLeech = card.note().hasTag("leech") or False
self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech]
def _undoReview(self):
data = self._undo[2]
wasLeech = self._undo[3]
c = data.pop()
if not data:
self.clearUndo()
# remove leech tag if it didn't have it before
if not wasLeech and c.note().hasTag("leech"):
c.note().delTag("leech")
c.note().flush()
# write old data
c.flush()
# and delete revlog entry
last = self.db.scalar(
"select id from revlog where cid = ? "
"order by id desc limit 1", c.id)
self.db.execute("delete from revlog where id = ?", last)
# restore any siblings
self.db.execute(
"update cards set queue=type,mod=?,usn=? where queue=-2 and nid=?",
intTime(), self.usn(), c.nid)
# and finally, update daily counts
n = 1 if c.queue == 3 else c.queue
type = ("new", "lrn", "rev")[n]
self.sched._updateStats(c, type, -1)
self.sched.reps -= 1
return c.id
def _markOp(self, name):
"Call via .save()"
if name:
self._undo = [2, name]
else:
# saving disables old checkpoint, but not review undo
if self._undo and self._undo[0] == 2:
self.clearUndo()
def _undoOp(self):
self.rollback()
self.clearUndo()
# DB maintenance
##########################################################################
def basicCheck(self):
"Basic integrity check for syncing. True if ok."
# cards without notes
if self.db.scalar("""
select 1 from cards where nid not in (select id from notes) limit 1"""):
return
# notes without cards or models
if self.db.scalar("""
select 1 from notes where id not in (select distinct nid from cards)
or mid not in %s limit 1""" % ids2str(self.models.ids())):
return
# invalid ords
for m in self.models.all():
# ignore clozes
if m['type'] != MODEL_STD:
continue
if self.db.scalar("""
select 1 from cards where ord not in %s and nid in (
select id from notes where mid = ?) limit 1""" %
ids2str([t['ord'] for t in m['tmpls']]),
m['id']):
return
return True
def fixIntegrity(self):
"Fix possible problems and rebuild caches."
problems = []
self.save()
oldSize = os.stat(self.path)[stat.ST_SIZE]
if self.db.scalar("pragma integrity_check") != "ok":
return (_("Collection is corrupt. Please see the manual."), False)
# note types with a missing model
ids = self.db.list("""
select id from notes where mid not in """ + ids2str(self.models.ids()))
if ids:
problems.append(
ngettext("Deleted %d note with missing note type.",
"Deleted %d notes with missing note type.", len(ids))
% len(ids))
self.remNotes(ids)
# for each model
for m in self.models.all():
for t in m['tmpls']:
if t['did'] == "None":
t['did'] = None
problems.append(_("Fixed AnkiDroid deck override bug."))
self.models.save(m)
if m['type'] == MODEL_STD:
# model with missing req specification
if 'req' not in m:
self.models._updateRequired(m)
problems.append(_("Fixed note type: %s") % m['name'])
# cards with invalid ordinal
ids = self.db.list("""
select id from cards where ord not in %s and nid in (
select id from notes where mid = ?)""" %
ids2str([t['ord'] for t in m['tmpls']]),
m['id'])
if ids:
problems.append(
ngettext("Deleted %d card with missing template.",
"Deleted %d cards with missing template.",
len(ids)) % len(ids))
self.remCards(ids)
# notes with invalid field count
ids = []
for id, flds in self.db.execute(
"select id, flds from notes where mid = ?", m['id']):
if (flds.count("\x1f") + 1) != len(m['flds']):
ids.append(id)
if ids:
problems.append(
ngettext("Deleted %d note with wrong field count.",
"Deleted %d notes with wrong field count.",
len(ids)) % len(ids))
self.remNotes(ids)
# delete any notes with missing cards
ids = self.db.list("""
select id from notes where id not in (select distinct nid from cards)""")
if ids:
cnt = len(ids)
problems.append(
ngettext("Deleted %d note with no cards.",
"Deleted %d notes with no cards.", cnt) % cnt)
self._remNotes(ids)
# cards with missing notes
ids = self.db.list("""
select id from cards where nid not in (select id from notes)""")
if ids:
cnt = len(ids)
problems.append(
ngettext("Deleted %d card with missing note.",
"Deleted %d cards with missing note.", cnt) % cnt)
self.remCards(ids)
# cards with odue set when it shouldn't be
ids = self.db.list("""
select id from cards where odue > 0 and (type=1 or queue=2) and not odid""")
if ids:
cnt = len(ids)
problems.append(
ngettext("Fixed %d card with invalid properties.",
"Fixed %d cards with invalid properties.", cnt) % cnt)
self.db.execute("update cards set odue=0 where id in "+
ids2str(ids))
# cards with odid set when not in a dyn deck
dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)]
ids = self.db.list("""
select id from cards where odid > 0 and did in %s""" % ids2str(dids))
if ids:
cnt = len(ids)
problems.append(
ngettext("Fixed %d card with invalid properties.",
"Fixed %d cards with invalid properties.", cnt) % cnt)
self.db.execute("update cards set odid=0, odue=0 where id in "+
ids2str(ids))
# tags
self.tags.registerNotes()
# field cache
for m in self.models.all():
self.updateFieldCache(self.models.nids(m))
# new cards can't have a due position > 32 bits
self.db.execute("""
update cards set due = 1000000, mod = ?, usn = ? where due > 1000000
and type = 0""", intTime(), self.usn())
# new card position
self.conf['nextPos'] = self.db.scalar(
"select max(due)+1 from cards where type = 0") or 0
# reviews should have a reasonable due #
ids = self.db.list(
"select id from cards where queue = 2 and due > 100000")
if ids:
problems.append("Reviews had incorrect due date.")
self.db.execute(
"update cards set due = ?, ivl = 1, mod = ?, usn = ? where id in %s"
% ids2str(ids), self.sched.today, intTime(), self.usn())
# v2 sched had a bug that could create decimal intervals
curs = self.db.cursor()
curs.execute("update cards set ivl=round(ivl),due=round(due) where ivl!=round(ivl) or due!=round(due)")
if curs.rowcount:
problems.append("Fixed %d cards with v2 scheduler bug." % curs.rowcount)
curs.execute("update revlog set ivl=round(ivl),lastIvl=round(lastIvl) where ivl!=round(ivl) or lastIvl!=round(lastIvl)")
if curs.rowcount:
problems.append("Fixed %d review history entries with v2 scheduler bug." % curs.rowcount)
# and finally, optimize
self.optimize()
newSize = os.stat(self.path)[stat.ST_SIZE]
txt = _("Database rebuilt and optimized.")
ok = not problems
problems.append(txt)
# if any problems were found, force a full sync
if not ok:
self.modSchema(check=False)
self.save()
return ("\n".join(problems), ok)
def optimize(self):
self.db.setAutocommit(True)
self.db.execute("vacuum")
self.db.execute("analyze")
self.db.setAutocommit(False)
self.lock()
# Logging
##########################################################################
def log(self, *args, **kwargs):
if not self._debugLog:
return
def customRepr(x):
if isinstance(x, str):
return x
return pprint.pformat(x)
path, num, fn, y = traceback.extract_stack(
limit=2+kwargs.get("stack", 0))[0]
buf = "[%s] %s:%s(): %s" % (intTime(), os.path.basename(path), fn,
", ".join([customRepr(x) for x in args]))
self._logHnd.write(buf + "\n")
if devMode:
print(buf)
def _openLog(self):
if not self._debugLog:
return
lpath = re.sub(r"\.anki2$", ".log", self.path)
if os.path.exists(lpath) and os.path.getsize(lpath) > 10*1024*1024:
lpath2 = lpath + ".old"
if os.path.exists(lpath2):
os.unlink(lpath2)
os.rename(lpath, lpath2)
self._logHnd = open(lpath, "a", encoding="utf8")
def _closeLog(self):
if not self._debugLog:
return
self._logHnd.close()
self._logHnd = None
# Card Flags
##########################################################################
def setUserFlag(self, flag, cids):
assert 0 <= flag <= 7
self.db.execute("update cards set flags = (flags & ~?) | ?, usn=?, mod=? where id in %s" %
ids2str(cids), 0b111, flag, self._usn, intTime())