PK ! r; ; AnkiTools/defaults.json{
"col": {
"id": 1,
"crt": 1531256400,
"mod": 1531317401308,
"scm": 1531313706730,
"ver": 11,
"dty": 0,
"usn": 0,
"ls": 0,
"conf": "{\"nextPos\": 1, \"estTimes\": true, \"sortBackwards\": false, \"sortType\": \"noteFld\", \"timeLim\": 0, \"activeDecks\": [1], \"addToCur\": true, \"curDeck\": 1, \"curModel\": \"1531313706731\", \"collapseTime\": 1200, \"dueCounts\": true, \"newBury\": true, \"newSpread\": 0}",
"models": "{\"1531313706724\": {\"vers\": [], \"name\": \"Basic (optional reversed card)\", \"tags\": [], \"did\": 1, \"usn\": -1, \"req\": [[0, \"all\", [0]], [1, \"all\", [1, 2]]], \"flds\": [{\"name\": \"Front\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Back\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Add Reverse\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 2, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Card 1\", \"qfmt\": \"{{Front}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Back}}\", \"ord\": 0, \"bqfmt\": \"\"}, {\"name\": \"Card 2\", \"qfmt\": \"{{#Add Reverse}}{{Back}}{{/Add Reverse}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Front}}\", \"ord\": 1, \"bqfmt\": \"\"}], \"mod\": 1531313706, \"latexPost\": \"\\\\end{document}\", \"type\": 0, \"id\": \"1531313706724\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}, \"1531313706731\": {\"vers\": [], \"name\": \"Basic\", \"tags\": [], \"did\": 1, \"usn\": -1, \"req\": [[0, \"all\", [0]]], \"flds\": [{\"name\": \"Front\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Back\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Card 1\", \"qfmt\": \"{{Front}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Back}}\", \"ord\": 0, \"bqfmt\": \"\"}], \"mod\": 1531317394, \"latexPost\": \"\\\\end{document}\", \"type\": 0, \"id\": \"1531313706731\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}, \"1531313706723\": {\"vers\": [], \"name\": \"Cloze\", \"tags\": [], \"did\": 1, \"usn\": -1, \"flds\": [{\"name\": \"Text\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Extra\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Cloze\", \"qfmt\": \"{{cloze:Text}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{cloze:Text}}
\\n{{Extra}}\", \"ord\": 0, \"bqfmt\": \"\"}], \"mod\": 1531313706, \"latexPost\": \"\\\\end{document}\", \"type\": 1, \"id\": \"1531313706723\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\\n.cloze {\\n font-weight: bold;\\n color: blue;\\n}\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}, \"1531313706730\": {\"vers\": [], \"name\": \"Basic (and reversed card)\", \"tags\": [], \"did\": 1, \"usn\": -1, \"req\": [[0, \"all\", [0]], [1, \"all\", [1]]], \"flds\": [{\"name\": \"Front\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Back\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Card 1\", \"qfmt\": \"{{Front}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Back}}\", \"ord\": 0, \"bqfmt\": \"\"}, {\"name\": \"Card 2\", \"qfmt\": \"{{Back}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Front}}\", \"ord\": 1, \"bqfmt\": \"\"}], \"mod\": 1531313706, \"latexPost\": \"\\\\end{document}\", \"type\": 0, \"id\": \"1531313706730\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}}",
"decks": "{\"1\": {\"name\": \"Default\", \"extendRev\": 50, \"usn\": 0, \"collapsed\": false, \"newToday\": [0, 0], \"timeToday\": [0, 0], \"dyn\": 0, \"extendNew\": 10, \"conf\": 1, \"revToday\": [0, 0], \"lrnToday\": [0, 0], \"id\": 1, \"mod\": 1531313706, \"desc\": \"\"}}",
"dconf": "{\"1\": {\"name\": \"Default\", \"replayq\": true, \"lapse\": {\"leechFails\": 8, \"minInt\": 1, \"delays\": [10], \"leechAction\": 0, \"mult\": 0}, \"rev\": {\"perDay\": 200, \"ivlFct\": 1, \"maxIvl\": 36500, \"minSpace\": 1, \"ease4\": 1.3, \"bury\": false, \"fuzz\": 0.05}, \"timer\": 0, \"maxTaken\": 60, \"usn\": 0, \"new\": {\"separate\": true, \"delays\": [1, 10], \"perDay\": 20, \"ints\": [1, 4, 7], \"initialFactor\": 2500, \"bury\": false, \"order\": 1}, \"autoplay\": true, \"id\": 1, \"mod\": 0}}",
"tags": "{}"
},
"notes": {
"id": 1531317373402,
"guid": "biW-6UCffE",
"mid": 1531313706731,
"mod": 1531317394,
"usn": -1,
"tags": "",
"flds": "Test front\u001ftest back",
"sfld": "Test front",
"csum": 2694937848,
"flags": 0,
"data": "",
"formatted_flds": [
"Test front",
"test back"
],
"formatted_tags": [
""
]
},
"cards": {
"id": 1531317394829,
"nid": 1531317373402,
"did": 1,
"ord": 0,
"mod": 1531317394,
"usn": -1,
"type": 0,
"queue": 0,
"due": 0,
"ivl": 0,
"factor": 0,
"reps": 0,
"lapses": 0,
"left": 0,
"odue": 0,
"odid": 0,
"flags": 0,
"data": ""
},
"revlog": null,
"graves": {
"usn": -1,
"oid": 1531317394960,
"type": 1
},
"sqlite_stat1": {
"tbl": "col",
"idx": null,
"stat": "1"
}
}PK ! Y<`! AnkiTools/defaults_api.json{
"model_definition": {
"templates": [
{
"name": "Card 1",
"data": {
"qfmt": "{{%s}}",
"afmt": "{{FrontSide}}\r\n\r\n
\r\n\r\n{{%s}}"
}
}
],
"css": null
}
}PK ! \nt) t) ! AnkiTools/defaults_formatted.json{
"col": {
"id": 1,
"crt": 1531256400,
"mod": 1531317401308,
"scm": 1531313706730,
"ver": 11,
"dty": 0,
"usn": 0,
"ls": 0,
"conf": {
"is_json": true,
"data": {
"nextPos": 1,
"estTimes": true,
"sortBackwards": false,
"sortType": "noteFld",
"timeLim": 0,
"activeDecks": [
1
],
"addToCur": true,
"curDeck": 1,
"curModel": "1531313706731",
"collapseTime": 1200,
"dueCounts": true,
"newBury": true,
"newSpread": 0
}
},
"models": {
"is_json": true,
"data": {
"1531313706724": {
"vers": [],
"name": "Basic (optional reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1,
2
]
]
],
"flds": [
{
"name": "Front",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Back",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
},
{
"name": "Add Reverse",
"media": [],
"sticky": false,
"rtl": false,
"ord": 2,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"ord": 0,
"bqfmt": ""
},
{
"name": "Card 2",
"qfmt": "{{#Add Reverse}}{{Back}}{{/Add Reverse}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"ord": 1,
"bqfmt": ""
}
],
"mod": 1531313706,
"latexPost": "\\end{document}",
"type": 0,
"id": "1531313706724",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
},
"1531313706731": {
"vers": [],
"name": "Basic",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
]
],
"flds": [
{
"name": "Front",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Back",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"ord": 0,
"bqfmt": ""
}
],
"mod": 1531317394,
"latexPost": "\\end{document}",
"type": 0,
"id": "1531313706731",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
},
"1531313706723": {
"vers": [],
"name": "Cloze",
"tags": [],
"did": 1,
"usn": -1,
"flds": [
{
"name": "Text",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Extra",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Cloze",
"qfmt": "{{cloze:Text}}",
"did": null,
"bafmt": "",
"afmt": "{{cloze:Text}}
\n{{Extra}}",
"ord": 0,
"bqfmt": ""
}
],
"mod": 1531313706,
"latexPost": "\\end{document}",
"type": 1,
"id": "1531313706723",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.cloze {\n font-weight: bold;\n color: blue;\n}",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
},
"1531313706730": {
"vers": [],
"name": "Basic (and reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1
]
]
],
"flds": [
{
"name": "Front",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Back",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"ord": 0,
"bqfmt": ""
},
{
"name": "Card 2",
"qfmt": "{{Back}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"ord": 1,
"bqfmt": ""
}
],
"mod": 1531313706,
"latexPost": "\\end{document}",
"type": 0,
"id": "1531313706730",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
}
}
},
"decks": {
"is_json": true,
"data": {
"1": {
"name": "Default",
"extendRev": 50,
"usn": 0,
"collapsed": false,
"newToday": [
0,
0
],
"timeToday": [
0,
0
],
"dyn": 0,
"extendNew": 10,
"conf": 1,
"revToday": [
0,
0
],
"lrnToday": [
0,
0
],
"id": 1,
"mod": 1531313706,
"desc": ""
}
}
},
"dconf": {
"is_json": true,
"data": {
"1": {
"name": "Default",
"replayq": true,
"lapse": {
"leechFails": 8,
"minInt": 1,
"delays": [
10
],
"leechAction": 0,
"mult": 0
},
"rev": {
"perDay": 200,
"ivlFct": 1,
"maxIvl": 36500,
"minSpace": 1,
"ease4": 1.3,
"bury": false,
"fuzz": 0.05
},
"timer": 0,
"maxTaken": 60,
"usn": 0,
"new": {
"separate": true,
"delays": [
1,
10
],
"perDay": 20,
"ints": [
1,
4,
7
],
"initialFactor": 2500,
"bury": false,
"order": 1
},
"autoplay": true,
"id": 1,
"mod": 0
}
}
},
"tags": {
"is_json": true,
"data": {}
}
},
"notes": {
"id": 1531317373402,
"guid": "biW-6UCffE",
"mid": 1531313706731,
"mod": 1531317394,
"usn": -1,
"tags": "",
"flds": "Test front\u001ftest back",
"sfld": "Test front",
"csum": 2694937848,
"flags": 0,
"data": "",
"formatted_flds": [
"Test front",
"test back"
],
"formatted_tags": [
""
]
},
"cards": {
"id": 1531317394829,
"nid": 1531317373402,
"did": 1,
"ord": 0,
"mod": 1531317394,
"usn": -1,
"type": 0,
"queue": 0,
"due": 0,
"ivl": 0,
"factor": 0,
"reps": 0,
"lapses": 0,
"left": 0,
"odue": 0,
"odid": 0,
"flags": 0,
"data": ""
},
"revlog": null,
"graves": {
"usn": -1,
"oid": 1531317394960,
"type": 1
},
"sqlite_stat1": {
"tbl": "col",
"idx": null,
"stat": {
"is_json": true,
"data": 1
}
}
}PK ! qm m ankitools/__init__.pyfrom .ankiconnect import AnkiConnect
from .api.ankidirect import AnkiDirect
from .editor import anki_convert
PK ! u ankitools/ankiconnect.pyimport requests
import requests.exceptions
class AnkiConnect:
URL = 'http://127.0.0.1:8765'
def __init__(self):
# assert self.is_online(), \
# 'AnkiConnect is not installed, or Anki app is not open.' # Does not work with @staticmethod
pass
@staticmethod
def post(action, params=None, version=6):
"""
For the documentation, see https://foosoft.net/projects/anki-connect/
:param str action:
:param dict params:
:param int version:
:return:
"""
if params is None:
params = dict()
to_send = {
'action': action,
'version': version,
'params': params
}
r = requests.post(AnkiConnect.URL, json=to_send)
return r.json()
@staticmethod
def is_online():
try:
requests.head(AnkiConnect.URL)
except requests.exceptions.ConnectionError:
return False
return True
PK ! ankitools/api/__init__.pyPK ! P ankitools/api/ankidirect.pyimport sqlite3
from time import time
import psutil
import os
from AnkiTools.tools.path import get_collection_path
from AnkiTools.tools.create import AnkiContentCreator
from AnkiTools.tools.write import write_anki_json, write_anki_table, write_anki_schema
from AnkiTools.tools.read import read_anki_json, read_anki_table
from .verify import AnkiContentVerify
class AnkiDirect:
def __init__(self, anki_database: str=None):
if anki_database is None:
anki_database = get_collection_path()
try:
assert 'Anki' not in (p.name() for p in psutil.process_iter()), \
"Please close Anki first before accessing Application Data collection.anki2 directly."
except psutil.ZombieProcess as e:
print(e)
do_init = False
if not os.path.exists(anki_database):
do_init = True\
self.conn = sqlite3.connect(anki_database)
if do_init:
self.creator = AnkiContentCreator()
write_anki_schema(self.conn)
anki_collection = self.creator.new_collection()
write_anki_table(self.conn, 'col', [anki_collection], do_commit=True)
self._id_to_record = self.data
else:
self._id_to_record = self.data
self.creator = AnkiContentCreator(self._id_to_record)
self._name_to_id = self.name_to_id
self.verify = AnkiContentVerify(self._id_to_record)
@property
def data(self):
data = {
'decks': self.decks_dict,
'models': self.models_dict,
'notes': dict(),
'cards': dict()
}
for record in self.notes:
data['notes'][str(record['id'])] = record
for record in self.cards:
data['cards'][str(record['id'])] = record
return data
@property
def name_to_id(self):
name_to_id = {
'models': dict(),
'decks': dict()
}
for k, v in self.models_dict.items():
name_to_id['models'][v['name']] = k
for k, v in self.decks_dict.items():
name_to_id['decks'][v['name']] = k
return name_to_id
def _get_model_id(self, model_name, model_header, model_definition, **kwargs):
try:
model_id = self._name_to_id['models'][model_name]
except KeyError:
anki_model = self.creator.new_model(model_name, model_header, model_definition,
modified=kwargs.get('modified', None))
self._id_to_record['models'][str(anki_model['id'])] = anki_model
write_anki_json(self.conn, 'models', [anki_model], do_commit=True)
model_id = anki_model['id']
self._name_to_id['models'][model_name] = model_id
return model_id
def _get_card_ordering(self, model_id, note_side):
note_sides = [template['name'] for template in self._id_to_record['models'][str(model_id)]['tmpls']]
return note_sides.index(note_side)
@property
def models_dict(self):
return read_anki_json(self.conn, 'models')
@property
def decks_dict(self):
return read_anki_json(self.conn, 'decks')
@property
def notes(self):
yield from read_anki_table(self.conn, 'notes')
@property
def cards(self):
yield from read_anki_table(self.conn, 'cards')
def add(self, data):
if not self.verify.verify_add_info(data):
return False
modified = int(time())
for model_name, notes in data['data'].items():
model_id = self._get_model_id(model_name,
notes[0]['data'].keys(),
data.get('definitions', dict()).get(model_name, dict()))
anki_notes = []
anki_cards = []
anki_decks = []
for note in notes:
anki_note = self.creator.new_note(flds_list=list(note['data'].values()),
model_id=model_id,
modified=modified)
self._id_to_record['notes'][str(anki_note['id'])] = anki_note
anki_notes.append(anki_note)
for note_side, deck_name in note['decks'].items():
try:
deck_id = self._name_to_id['decks'][deck_name]
except KeyError:
anki_deck = self.creator.new_deck(deck_name)
self._id_to_record['decks'][str(anki_deck['id'])] = anki_deck
anki_decks.append(anki_deck)
deck_id = anki_deck['id']
self._name_to_id['decks'][deck_name] = deck_id
anki_card = self.creator.new_card(anki_note['id'],
deck_id,
self._get_card_ordering(model_id, note_side),
modified=modified)
self._id_to_record['cards'][str(anki_card['id'])] = anki_card
anki_cards.append(anki_card)
missing_deck_names = self.verify.missing_decks()
for deck_name in missing_deck_names:
anki_deck = self.creator.new_deck(deck_name)
self._id_to_record['decks'][str(anki_deck['id'])] = anki_deck
anki_decks.append(anki_deck)
write_anki_table(self.conn, 'notes', anki_notes, do_commit=False)
write_anki_table(self.conn, 'cards', anki_cards, do_commit=False)
write_anki_json(self.conn, 'decks', anki_decks, do_commit=False)
self.conn.commit()
return True
PK ! p/P
ankitools/api/verify.pyclass AnkiContentVerify:
def __init__(self, anki_content):
self.anki_content = anki_content
def missing_decks(self):
deck_dirs = set()
for deck in self.anki_content['decks'].values():
deck_dirs.add(tuple(deck['name'].split('::')))
new_deck_names = set()
for deck_dir in deck_dirs:
for i in range(1, len(deck_dir)):
super_deck_dir = tuple(deck_dir[:i])
if super_deck_dir not in deck_dirs:
new_deck_names.add('::'.join(super_deck_dir))
return new_deck_names
def get_model_id(self, model_name):
for model_id, model in self.anki_content['models'].items():
if model['name'] == model_name:
return model_id
return None
def check_header(self, header, model_id):
for header_item in header:
if header_item not in (fld['name'] for fld in self.anki_content['models'][model_id]['flds']):
return False
return True
def check_card_sides(self, card_sides, model_id):
for card_side in card_sides:
if card_side not in (tmpl['name'] for tmpl in self.anki_content['models'][model_id]['tmpls']):
return False
return True
@staticmethod
def check_qfmt_afmt(card_side_format, header):
def has_field(qfmt_afmt):
for header_item in header:
if ("{{%s}}" % header_item) in qfmt_afmt:
return True
return False
if not has_field(card_side_format['qfmt']):
return False
if not has_field(card_side_format['afmt']):
return False
return True
def verify_add_info(self, add_info):
missing_models_requirement = dict()
for model_name, notes in add_info['data'].items():
model_id = self.get_model_id(model_name)
if model_id is None:
try:
if model_name not in add_info['definitions'].keys():
return False
except KeyError as e:
print(e)
return False
missing_models_requirement[model_name] = {
'header': set(),
'card_sides': set()
}
if model_name not in add_info['definitions'].keys():
return False
for note in notes:
if model_id is not None:
if not self.check_header(note['data'].keys(), model_id):
return False
if not self.check_card_sides(note['decks'].keys(), model_id):
return False
else:
missing_models_requirement[model_name]['header'].update(note['data'].keys())
missing_models_requirement[model_name]['card_sides'].update(note['decks'].keys())
if len(missing_models_requirement) > 0:
for model_name, model_template in add_info['definitions'].items():
for card_template in model_template['templates']:
if not self.check_qfmt_afmt(card_template['data'],
missing_models_requirement[model_name]['header']):
return False
if card_template['name'] not in missing_models_requirement[model_name]['card_sides']:
return False
return True
PK ! r; ; ankitools/defaults.json{
"col": {
"id": 1,
"crt": 1531256400,
"mod": 1531317401308,
"scm": 1531313706730,
"ver": 11,
"dty": 0,
"usn": 0,
"ls": 0,
"conf": "{\"nextPos\": 1, \"estTimes\": true, \"sortBackwards\": false, \"sortType\": \"noteFld\", \"timeLim\": 0, \"activeDecks\": [1], \"addToCur\": true, \"curDeck\": 1, \"curModel\": \"1531313706731\", \"collapseTime\": 1200, \"dueCounts\": true, \"newBury\": true, \"newSpread\": 0}",
"models": "{\"1531313706724\": {\"vers\": [], \"name\": \"Basic (optional reversed card)\", \"tags\": [], \"did\": 1, \"usn\": -1, \"req\": [[0, \"all\", [0]], [1, \"all\", [1, 2]]], \"flds\": [{\"name\": \"Front\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Back\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Add Reverse\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 2, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Card 1\", \"qfmt\": \"{{Front}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Back}}\", \"ord\": 0, \"bqfmt\": \"\"}, {\"name\": \"Card 2\", \"qfmt\": \"{{#Add Reverse}}{{Back}}{{/Add Reverse}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Front}}\", \"ord\": 1, \"bqfmt\": \"\"}], \"mod\": 1531313706, \"latexPost\": \"\\\\end{document}\", \"type\": 0, \"id\": \"1531313706724\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}, \"1531313706731\": {\"vers\": [], \"name\": \"Basic\", \"tags\": [], \"did\": 1, \"usn\": -1, \"req\": [[0, \"all\", [0]]], \"flds\": [{\"name\": \"Front\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Back\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Card 1\", \"qfmt\": \"{{Front}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Back}}\", \"ord\": 0, \"bqfmt\": \"\"}], \"mod\": 1531317394, \"latexPost\": \"\\\\end{document}\", \"type\": 0, \"id\": \"1531313706731\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}, \"1531313706723\": {\"vers\": [], \"name\": \"Cloze\", \"tags\": [], \"did\": 1, \"usn\": -1, \"flds\": [{\"name\": \"Text\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Extra\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Cloze\", \"qfmt\": \"{{cloze:Text}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{cloze:Text}}
\\n{{Extra}}\", \"ord\": 0, \"bqfmt\": \"\"}], \"mod\": 1531313706, \"latexPost\": \"\\\\end{document}\", \"type\": 1, \"id\": \"1531313706723\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\\n.cloze {\\n font-weight: bold;\\n color: blue;\\n}\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}, \"1531313706730\": {\"vers\": [], \"name\": \"Basic (and reversed card)\", \"tags\": [], \"did\": 1, \"usn\": -1, \"req\": [[0, \"all\", [0]], [1, \"all\", [1]]], \"flds\": [{\"name\": \"Front\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Back\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"tmpls\": [{\"name\": \"Card 1\", \"qfmt\": \"{{Front}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Back}}\", \"ord\": 0, \"bqfmt\": \"\"}, {\"name\": \"Card 2\", \"qfmt\": \"{{Back}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{FrontSide}}\\n\\n
\\n\\n{{Front}}\", \"ord\": 1, \"bqfmt\": \"\"}], \"mod\": 1531313706, \"latexPost\": \"\\\\end{document}\", \"type\": 0, \"id\": \"1531313706730\", \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\", \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\"}}",
"decks": "{\"1\": {\"name\": \"Default\", \"extendRev\": 50, \"usn\": 0, \"collapsed\": false, \"newToday\": [0, 0], \"timeToday\": [0, 0], \"dyn\": 0, \"extendNew\": 10, \"conf\": 1, \"revToday\": [0, 0], \"lrnToday\": [0, 0], \"id\": 1, \"mod\": 1531313706, \"desc\": \"\"}}",
"dconf": "{\"1\": {\"name\": \"Default\", \"replayq\": true, \"lapse\": {\"leechFails\": 8, \"minInt\": 1, \"delays\": [10], \"leechAction\": 0, \"mult\": 0}, \"rev\": {\"perDay\": 200, \"ivlFct\": 1, \"maxIvl\": 36500, \"minSpace\": 1, \"ease4\": 1.3, \"bury\": false, \"fuzz\": 0.05}, \"timer\": 0, \"maxTaken\": 60, \"usn\": 0, \"new\": {\"separate\": true, \"delays\": [1, 10], \"perDay\": 20, \"ints\": [1, 4, 7], \"initialFactor\": 2500, \"bury\": false, \"order\": 1}, \"autoplay\": true, \"id\": 1, \"mod\": 0}}",
"tags": "{}"
},
"notes": {
"id": 1531317373402,
"guid": "biW-6UCffE",
"mid": 1531313706731,
"mod": 1531317394,
"usn": -1,
"tags": "",
"flds": "Test front\u001ftest back",
"sfld": "Test front",
"csum": 2694937848,
"flags": 0,
"data": "",
"formatted_flds": [
"Test front",
"test back"
],
"formatted_tags": [
""
]
},
"cards": {
"id": 1531317394829,
"nid": 1531317373402,
"did": 1,
"ord": 0,
"mod": 1531317394,
"usn": -1,
"type": 0,
"queue": 0,
"due": 0,
"ivl": 0,
"factor": 0,
"reps": 0,
"lapses": 0,
"left": 0,
"odue": 0,
"odid": 0,
"flags": 0,
"data": ""
},
"revlog": null,
"graves": {
"usn": -1,
"oid": 1531317394960,
"type": 1
},
"sqlite_stat1": {
"tbl": "col",
"idx": null,
"stat": "1"
}
}PK ! Y<`! ankitools/defaults_api.json{
"model_definition": {
"templates": [
{
"name": "Card 1",
"data": {
"qfmt": "{{%s}}",
"afmt": "{{FrontSide}}\r\n\r\n
\r\n\r\n{{%s}}"
}
}
],
"css": null
}
}PK ! \nt) t) ! ankitools/defaults_formatted.json{
"col": {
"id": 1,
"crt": 1531256400,
"mod": 1531317401308,
"scm": 1531313706730,
"ver": 11,
"dty": 0,
"usn": 0,
"ls": 0,
"conf": {
"is_json": true,
"data": {
"nextPos": 1,
"estTimes": true,
"sortBackwards": false,
"sortType": "noteFld",
"timeLim": 0,
"activeDecks": [
1
],
"addToCur": true,
"curDeck": 1,
"curModel": "1531313706731",
"collapseTime": 1200,
"dueCounts": true,
"newBury": true,
"newSpread": 0
}
},
"models": {
"is_json": true,
"data": {
"1531313706724": {
"vers": [],
"name": "Basic (optional reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1,
2
]
]
],
"flds": [
{
"name": "Front",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Back",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
},
{
"name": "Add Reverse",
"media": [],
"sticky": false,
"rtl": false,
"ord": 2,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"ord": 0,
"bqfmt": ""
},
{
"name": "Card 2",
"qfmt": "{{#Add Reverse}}{{Back}}{{/Add Reverse}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"ord": 1,
"bqfmt": ""
}
],
"mod": 1531313706,
"latexPost": "\\end{document}",
"type": 0,
"id": "1531313706724",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
},
"1531313706731": {
"vers": [],
"name": "Basic",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
]
],
"flds": [
{
"name": "Front",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Back",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"ord": 0,
"bqfmt": ""
}
],
"mod": 1531317394,
"latexPost": "\\end{document}",
"type": 0,
"id": "1531313706731",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
},
"1531313706723": {
"vers": [],
"name": "Cloze",
"tags": [],
"did": 1,
"usn": -1,
"flds": [
{
"name": "Text",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Extra",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Cloze",
"qfmt": "{{cloze:Text}}",
"did": null,
"bafmt": "",
"afmt": "{{cloze:Text}}
\n{{Extra}}",
"ord": 0,
"bqfmt": ""
}
],
"mod": 1531313706,
"latexPost": "\\end{document}",
"type": 1,
"id": "1531313706723",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.cloze {\n font-weight: bold;\n color: blue;\n}",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
},
"1531313706730": {
"vers": [],
"name": "Basic (and reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1
]
]
],
"flds": [
{
"name": "Front",
"media": [],
"sticky": false,
"rtl": false,
"ord": 0,
"font": "Arial",
"size": 20
},
{
"name": "Back",
"media": [],
"sticky": false,
"rtl": false,
"ord": 1,
"font": "Arial",
"size": 20
}
],
"sortf": 0,
"tmpls": [
{
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"ord": 0,
"bqfmt": ""
},
{
"name": "Card 2",
"qfmt": "{{Back}}",
"did": null,
"bafmt": "",
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"ord": 1,
"bqfmt": ""
}
],
"mod": 1531313706,
"latexPost": "\\end{document}",
"type": 0,
"id": "1531313706730",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage[utf8]{inputenc}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n"
}
}
},
"decks": {
"is_json": true,
"data": {
"1": {
"name": "Default",
"extendRev": 50,
"usn": 0,
"collapsed": false,
"newToday": [
0,
0
],
"timeToday": [
0,
0
],
"dyn": 0,
"extendNew": 10,
"conf": 1,
"revToday": [
0,
0
],
"lrnToday": [
0,
0
],
"id": 1,
"mod": 1531313706,
"desc": ""
}
}
},
"dconf": {
"is_json": true,
"data": {
"1": {
"name": "Default",
"replayq": true,
"lapse": {
"leechFails": 8,
"minInt": 1,
"delays": [
10
],
"leechAction": 0,
"mult": 0
},
"rev": {
"perDay": 200,
"ivlFct": 1,
"maxIvl": 36500,
"minSpace": 1,
"ease4": 1.3,
"bury": false,
"fuzz": 0.05
},
"timer": 0,
"maxTaken": 60,
"usn": 0,
"new": {
"separate": true,
"delays": [
1,
10
],
"perDay": 20,
"ints": [
1,
4,
7
],
"initialFactor": 2500,
"bury": false,
"order": 1
},
"autoplay": true,
"id": 1,
"mod": 0
}
}
},
"tags": {
"is_json": true,
"data": {}
}
},
"notes": {
"id": 1531317373402,
"guid": "biW-6UCffE",
"mid": 1531313706731,
"mod": 1531317394,
"usn": -1,
"tags": "",
"flds": "Test front\u001ftest back",
"sfld": "Test front",
"csum": 2694937848,
"flags": 0,
"data": "",
"formatted_flds": [
"Test front",
"test back"
],
"formatted_tags": [
""
]
},
"cards": {
"id": 1531317394829,
"nid": 1531317373402,
"did": 1,
"ord": 0,
"mod": 1531317394,
"usn": -1,
"type": 0,
"queue": 0,
"due": 0,
"ivl": 0,
"factor": 0,
"reps": 0,
"lapses": 0,
"left": 0,
"odue": 0,
"odid": 0,
"flags": 0,
"data": ""
},
"revlog": null,
"graves": {
"usn": -1,
"oid": 1531317394960,
"type": 1
},
"sqlite_stat1": {
"tbl": "col",
"idx": null,
"stat": {
"is_json": true,
"data": 1
}
}
}PK ! 7 ankitools/dir.py"""
Defines ROOT as project_name/project_name/. Useful when installing using pip/setup.py.
"""
import os
import inspect
ROOT = os.path.abspath(os.path.dirname(inspect.getframeinfo(inspect.currentframe()).filename))
def module_path(filename):
return os.path.join(ROOT, filename)
PK ! ankitools/editor.pyimport shutil
from tempfile import mkdtemp
from tempfile import mktemp as temp_filename
import os
from zipfile import ZipFile
from .excel import AnkiExcelSync
class AnkiFormatEditor:
def __init__(self):
self.tempdir = mkdtemp()
def convert(self, in_file, out_file=None, out_format=None):
in_file_type = os.path.splitext(in_file)[1]
if out_format is None:
assert out_file is not None, "Either out_file or out_format must be specified."
out_file_type = os.path.splitext(out_file)[1]
else:
if out_format[0] == '.':
out_file_type = out_format
else:
out_file_type = '.' + out_format
if out_file is not None:
out_file_header = os.path.splitext(out_file)[0]
else:
out_file_header = os.path.splitext(in_file)[0]
out_file = '{}{}'.format(out_file_header, out_file_type)
assert in_file_type != out_file_type, 'File types must be different'
conversion = (in_file_type, out_file_type)
if conversion == ('.apkg', '.anki2'):
self.unzip(in_file, out_file=out_file)
elif conversion == ('.apkg', '.xlsx'):
self.export_anki_sqlite(self.unzip(in_file,
os.path.join(self.tempdir,
temp_filename())),
out_file)
elif conversion == ('.anki2', '.apkg'):
self.zip(in_file, out_file)
elif conversion == ('.anki2', '.xlsx'):
self.export_anki_sqlite(in_file, out_file)
elif conversion == ('.xlsx', '.anki2'):
self.import_anki_sqlite(in_file, out_file, out_path='')
elif conversion == ('.xlsx', '.apkg'):
self.zip(self.import_anki_sqlite(in_file), out_file)
else:
raise Exception("Unsupported conversion.")
def unzip(self, in_file, out_file):
with ZipFile(in_file) as zf:
zf.extract('collection.anki2', path=self.tempdir)
shutil.move(os.path.join(self.tempdir, 'collection.anki2'),
out_file)
return out_file
@staticmethod
def zip(in_file, out_file):
with ZipFile(out_file, 'w') as zf:
zf.write(in_file, arcname='collection.anki2')
zf.writestr('media', '{}')
@staticmethod
def export_anki_sqlite(in_file, out_file):
with AnkiExcelSync(anki_database=in_file, excel=out_file) as sync_portal:
sync_portal.to_excel()
def import_anki_sqlite(self, in_file, out_file=None, out_path=''):
if out_file is None:
out_file = os.path.join(self.tempdir, 'collection.anki2')
with AnkiExcelSync(anki_database=out_file, excel=in_file) as sync_portal:
sync_portal.to_sqlite()
return os.path.join(out_path, out_file)
def anki_convert(in_file, out_file=None, out_format=None, out_path=None):
AnkiFormatEditor().convert(in_file, out_file, out_format)
PK ! ZV V ankitools/excel.pyimport openpyxl as px
from collections import OrderedDict, namedtuple
from datetime import datetime
import json
from .api.ankidirect import AnkiDirect
from .tools.defaults import DEFAULT_API_MODEL_DEFINITION
DeckTuple = namedtuple('DeckTuple', ['deck_id', 'deck_name'])
CardTuple = namedtuple('CardTuple', ['card_id', 'note_id', 'deck_name', 'template_order'])
class AnkiExcelSync:
SHEET_SETTINGS = '.settings'
SHEET_DECKS = '.decks'
def __init__(self, excel: str, anki_database: str):
self.anki_direct = AnkiDirect(anki_database=anki_database)
self.excel_filename = excel
self.settings = {
'models': dict(),
'decks': dict()
}
try:
self.wb = px.load_workbook(self.excel_filename)
except FileNotFoundError:
self.wb = self.create()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.save()
self.close()
def close(self):
self.wb.close()
def save(self):
self.wb.save(self.excel_filename)
def to_excel(self):
self.wb.save(self.excel_filename)
def to_sqlite(self):
self.anki_direct.add(self.to_json())
def to_json(self):
payload = {
'data': dict(),
'definitions': dict()
}
sheet_names = self.wb.sheetnames
try:
sheet_names.remove(self.SHEET_SETTINGS)
sheet_names.remove(self.SHEET_DECKS)
except ValueError:
pass
for sheet_name in sheet_names:
payload['data'][sheet_name] = list()
row_iter = self.wb[sheet_name].iter_rows()
header = list(self.get_cell_value_iter(next(row_iter)))
for row in row_iter:
record = OrderedDict(zip(header, self.get_cell_value_iter(row)))
formatted_record = {
'data': record,
'decks': {
'Card 1': sheet_name
}
}
payload['data'][sheet_name].append(formatted_record)
# This will further be "string-formatted", so it needs to be deep-copied.
# Currently implemented using a ReadOnlyJsonObject object.
payload['definitions'][sheet_name] = DEFAULT_API_MODEL_DEFINITION.to_json_object()
payload['definitions'][sheet_name]['templates'][0]['data']['qfmt'] = \
payload['definitions'][sheet_name]['templates'][0]['data']['qfmt'] % header[0]
payload['definitions'][sheet_name]['templates'][0]['data']['afmt'] = \
payload['definitions'][sheet_name]['templates'][0]['data']['afmt'] % header[1]
return payload
def create(self):
wb = px.Workbook()
ws = wb.active
ws.title = self.SHEET_SETTINGS
ws.append(['Created', datetime.fromtimestamp(datetime.now().timestamp()).isoformat()])
ws.append(['Modified', datetime.fromtimestamp(datetime.now().timestamp()).isoformat()])
# Getting sheet names
models = self.anki_direct.models_dict
model_id_to_name = dict()
for model_id, model_dict in models.items():
sheet_name = model_dict['name']
print('Creating sheet {}'.format(sheet_name))
# Writing header
if sheet_name not in wb.sheetnames:
header = ['id']
field_pairs = [(fld['ord'], fld['name']) for fld in model_dict['flds']]
header.extend([x[1] for x in sorted(field_pairs)])
header.append('Tags')
wb.create_sheet(sheet_name)
wb[sheet_name].append(header)
model_id_to_name[model_id] = sheet_name
self.settings['models'][sheet_name] = {
'id': model_id,
'templates': model_dict['tmpls']
}
# Getting sheet contents
notes_iter = self.anki_direct.notes
for note in notes_iter:
try:
sheet_name = model_id_to_name[str(note['mid'])]
except KeyError:
continue
# Writing record
print('Creating note {} - {}'.format(note['id'],
json.dumps(note['formatted_flds'], ensure_ascii=False)))
record = [note['id']]
record.extend(note['formatted_flds'])
record.append(note['tags'])
wb[sheet_name].append(record)
# Getting deck id and names
decks_dict = self.anki_direct.decks_dict
for deck_info in decks_dict.values():
self.settings['decks'][deck_info['name']] = deck_info
# Getting card distribution
wb.create_sheet(self.SHEET_DECKS, 1)
wb[self.SHEET_DECKS].append(CardTuple._fields)
cards_iter = self.anki_direct.cards
for card in cards_iter:
record = CardTuple(
card_id=card['id'],
note_id=card['nid'],
deck_name=decks_dict[str(card['did'])]['name'],
template_order=card['ord']
)
wb[self.SHEET_DECKS].append(record)
return wb
@staticmethod
def get_cell_value_iter(cell_iter):
for cell in cell_iter:
value = cell.value
if not value:
yield ''
else:
yield value
PK ! ankitools/tools/__init__.pyPK ! #+ + ankitools/tools/create.pyfrom time import time
from collections import OrderedDict
from bs4 import BeautifulSoup
from hashlib import sha1
import json
from .defaults import (DEFAULT_COLLECTION,
DEFAULT_TEMPLATE,
DEFAULT_MODEL,
DEFAULT_API_MODEL_DEFINITION,
IS_JSON)
from .guid import guid64
class AnkiContentCreator:
def __init__(self, ids=None, formatted_defaults=True):
"""
:param dict ids:
:param bool formatted_defaults:
"""
if not ids:
ids = {
'models': dict(),
'decks': dict(),
'cards': dict(),
'notes': dict()
}
self.ids = dict()
for k, v in ids.items():
self.ids[k] = set(ids[k].keys())
self.formatted_defaults = formatted_defaults
def new_model(self, model_name, model_header, model_definition=None, modified=None, **kwargs):
"""
:param str model_name:
:param list model_header:
:param OrderedDict model_definition:
:param int modified:
:param kwargs:
:return:
"""
if not model_definition:
model_definition = DEFAULT_API_MODEL_DEFINITION
if not modified:
modified = int(time())
tmpls = kwargs.get('tmpls', [self.new_template(template['name'], i, formatting=template['data'])
for i, template in enumerate(model_definition['templates'])])
css = kwargs.get('css', model_definition.get('css', None))
if css is None:
css = DEFAULT_MODEL['css']
model_id = self._unique_id('models')
model = dict([
("vers", []),
("name", model_name),
("tags", []),
("did", None),
("usn", -1),
("req", [[0, "all", [0]]]),
("flds", [self.new_field(field_name, i, **kwargs.get('flds_kwargs', dict()))
for i, field_name in enumerate(model_header)]),
("sortf", 0),
("latexPre", DEFAULT_MODEL['latexPre']),
("tmpls", tmpls),
("latexPost", DEFAULT_MODEL['latexPost']),
("type", 0),
("id", model_id),
("css", css),
("mod", modified)
])
for k, v in model.items():
if k in kwargs.keys():
model[k] = kwargs[k]
return model
@staticmethod
def new_field(field_name: str, ordering: int, **kwargs):
"""
Fields have no unique ID.
:param field_name:
:param ordering:
:param kwargs:
:return:
"""
field = dict([
('name', field_name),
('rtl', False),
('sticky', False),
('media', []),
('ord', ordering),
('font', 'Arial'),
('size', 12)
])
for k, v in field.items():
if k in kwargs.keys():
field[k] = kwargs[k]
return field
@staticmethod
def new_template(template_name: str, ordering: int, formatting: dict=None, **kwargs):
"""
Templates have no unique ID.
:param template_name:
:param ordering:
:param formatting:
:param kwargs:
:return:
"""
if formatting is not None:
kwargs.update(formatting)
template = dict([
('name', template_name),
('qfmt', DEFAULT_TEMPLATE['qfmt']),
('did', None),
('bafmt', DEFAULT_TEMPLATE['bafmt']),
('afmt', DEFAULT_TEMPLATE['afmt']),
('ord', ordering),
('bqfmt', DEFAULT_TEMPLATE['bqfmt'])
])
for k, v in template.items():
if k in kwargs.keys():
template[k] = kwargs[k]
return template
def new_note(self, flds_list: iter, model_id: int, modified: int=None, tags_list: iter=None, **kwargs):
if tags_list is None:
tags_list = []
if modified is None:
modified = int(time())
sfld = BeautifulSoup(flds_list[0], 'html.parser').text
note = OrderedDict([
('id', self._unique_id('notes')),
('guid', guid64()),
('mid', model_id),
('mod', modified),
('usn', -1),
('tags', ' '.join(tags_list)),
('flds', '\x1f'.join(flds_list)),
('sfld', sfld),
('csum', sha1(sfld.encode('utf8')).hexdigest()),
('flags', 0),
('data', '')
])
for k, v in note.items():
if k in kwargs.keys():
note[k] = kwargs[k]
assert len(note) == 11, 'Invalid Anki Note format.'
return note
def new_card(self, note_id: int, deck_id: int, ordering: int, modified: int, **kwargs):
card = OrderedDict([
('id', self._unique_id('cards')),
('nid', note_id),
('did', deck_id),
('ord', ordering),
('mod', modified),
('usn', -1),
('type', 0),
('queue', 0),
('due', note_id), # Due is used differently for different card types:
# new: note id or random int
# due: integer day, relative to the collection's creation time
# learning: integer timestamp
('ivl', 0),
('factor', 0),
('reps', 0),
('lapses', 0),
('left', 0),
('odue', 0),
('odid', 0),
('flags', 0),
('data', '')
])
for k, v in card.items():
if k in kwargs.keys():
card[k] = kwargs[k]
assert len(card) == 18, 'Invalid Anki Card format.'
return card
def new_deck(self, deck_name, **kwargs):
deck = dict([
('desc', ''),
('name', deck_name),
('extendRev', 50),
('usn', 0),
('collapsed', False),
('newToday', [0, 0]),
('timeToday', [0, 0]),
('dyn', 0),
('extendNew', 10),
('conf', 1),
('revToday', [0, 0]),
('lrnToday', [0, 0]),
('id', self._unique_id('decks')),
('mod', int(time()))
])
for k, v in deck.items():
if k in kwargs.keys():
deck[k] = kwargs[k]
return deck
def new_collection(self, modified: int=None, models=None, decks=None, **kwargs):
"""
:param int modified:
:param OrderedDict models:
:param OrderedDict decks:
:param kwargs:
:return:
"""
if modified is None:
modified = int(time() * 1000)
if models is None:
models = DEFAULT_COLLECTION['models']
if decks is None:
decks = DEFAULT_COLLECTION['decks']
collection = OrderedDict([
('id', 1),
('crt', int(time())),
('mod', modified),
('scm', int(time() * 1000)),
('ver', DEFAULT_COLLECTION['ver']),
('dty', 0),
('usn', 0),
('ls', 0),
('conf', DEFAULT_COLLECTION['conf']),
('models', models),
('decks', decks),
('dconf', DEFAULT_COLLECTION['dconf']),
('tags', DEFAULT_COLLECTION['tags'])
])
for k, v in kwargs.items():
if k in collection.keys():
collection[k] = v
return self.stringify_for_sqlite('col', collection)
@staticmethod
def stringify_for_sqlite(item_type, item):
for header_item, is_json in IS_JSON[item_type].items():
if is_json:
item[header_item] = json.dumps(item[header_item], default=lambda obj: obj.__dict__)
return item
def _unique_id(self, item_type: str):
item_id = int(time() * 1000)
while item_id in self.ids[item_type]:
item_id += 1
self.ids[item_type].add(item_id)
return item_id
PK ! PU6 6 ankitools/tools/defaults.pyimport json
from json.decoder import JSONDecodeError
from collections import OrderedDict
from collections.abc import Mapping
from AnkiTools.dir import module_path
def _default(self, obj):
return getattr(obj.__class__, "to_json_object", _default.default)(obj)
_default.default = json.JSONEncoder().default
json.JSONEncoder.default = _default
class ReadOnlyJsonObject(Mapping):
def __init__(self, data, dumps_kw: dict=None, loads_kw: dict=None):
if dumps_kw is None:
dumps_kw = dict()
if loads_kw is None:
self._loads_kw = dict(object_pairs_hook=OrderedDict)
else:
self._loads_kw = loads_kw
if isinstance(data, str):
self._json_string = data
else:
self._json_string = json.dumps(data, **dumps_kw)
@property
def _data(self):
return json.loads(self._json_string, **self._loads_kw)
def to_json_object(self):
return self._data
def __getitem__(self, key):
try:
return json.loads(self._data[key], **self._loads_kw)
except (TypeError, JSONDecodeError):
return self._data[key]
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def __str__(self):
return json.dumps(self._data, indent=2)
def __repr__(self):
return self._json_string
# Load auto-generated default values from Anki (collection.anki2)
with open(module_path('defaults.json')) as f:
defaults = json.load(f, object_pairs_hook=OrderedDict)
DEFAULT_COLLECTION = ReadOnlyJsonObject(defaults['col'])
DEFAULT_MODEL = ReadOnlyJsonObject(tuple(DEFAULT_COLLECTION['models'].values())[0])
DEFAULT_TEMPLATE = ReadOnlyJsonObject(DEFAULT_MODEL['tmpls'][0])
# Load author-defined default values
with open(module_path('defaults_api.json')) as f:
defaults = json.load(f, object_pairs_hook=OrderedDict)
DEFAULT_API_MODEL_DEFINITION = ReadOnlyJsonObject(defaults['model_definition'])
# Load is_json settings
IS_JSON = OrderedDict()
with open(module_path('defaults_formatted.json')) as f:
defaults = json.load(f, object_pairs_hook=OrderedDict)
for table_name, table_dict in defaults.items():
IS_JSON[table_name] = OrderedDict()
if table_dict is None:
continue
for header_item, v in table_dict.items():
try:
if v['is_json'] is True:
IS_JSON[table_name][header_item] = True
else:
IS_JSON[table_name][header_item] = False
except TypeError:
IS_JSON[table_name][header_item] = False
IS_JSON = ReadOnlyJsonObject(IS_JSON)
def get_constants():
constants = OrderedDict()
for k, v in globals().items():
if k.isupper():
constants[k] = v
return constants
if __name__ == '__main__':
print(IS_JSON['col'])
PK ! xߕ0 0 ankitools/tools/guid.py# -*- coding: utf-8 -*-
# Copyright: Damien Elmes
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import random
import string
_base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~"
def base62(num, extra=""):
s = string; table = s.ascii_letters + s.digits + extra
buf = ""
while num:
num, i = divmod(num, len(table))
buf = table[i] + buf
return buf
def base91(num):
# all printable characters minus quotes, backslash and separators
return base62(num, _base91_extra_chars)
def guid64():
"Return a base91-encoded 64bit random number."
return base91(random.randint(0, 2**64-1))
# increment a guid by one, for note type conflicts
def incGuid(guid):
return _incGuid(guid[::-1])[::-1]
def _incGuid(guid):
s = string; table = s.ascii_letters + s.digits + _base91_extra_chars
idx = table.index(guid[0])
if idx + 1 == len(table):
# overflow
guid = table[0] + _incGuid(guid[1:])
else:
guid = table[idx+1] + guid[1:]
return guid
PK !
ankitools/tools/path.pyimport os
import appdirs
def get_collection_path(account_name: str=None):
if account_name is None:
account_name = 'User 1'
collection_path = os.path.join(appdirs.user_data_dir('Anki2'), account_name, 'collection.anki2')
return collection_path
PK ! "i9 ankitools/tools/read.pyimport json
from collections import OrderedDict
def read_anki_table(conn, table_name):
"""
:param sqlite3.Connection conn:
:param str table_name:
:return generator of OrderedDict:
"""
cursor = conn.execute('SELECT * FROM {}'.format(table_name))
header = [description[0] for description in cursor.description]
for record in cursor:
formatted_record = OrderedDict(zip(header, record))
if table_name == 'notes':
formatted_record['formatted_flds'] = formatted_record['flds'].split('\x1f')
formatted_record['formatted_tags'] = formatted_record['tags'].split(' ')
yield formatted_record
def read_anki_json(conn, json_name):
"""
:param sqlite3.Connection conn:
:param str json_name:
:return dict:
"""
cursor = conn.execute('SELECT {} FROM col'.format(json_name))
record = cursor.fetchone()
if record is not None:
return json.loads(record[0])
else:
return dict()
PK ! b-u ankitools/tools/write.pyimport json
def write_anki_table(conn, table_name, new_records, do_commit=True):
"""
:param sqlite3.Connection conn:
:param 'notes'|'cards' table_name:
:param iter of OrderedDict new_records:
:param bool do_commit:
:return:
"""
for new_record in new_records:
conn.execute('INSERT INTO {} ({}) VALUES ({})'
.format(table_name,
','.join(new_record.keys()), ','.join(['?' for _ in range(len(new_record))])),
tuple(new_record.values()))
if do_commit:
conn.commit()
def write_anki_json(conn, json_name, new_dicts, do_commit=True):
"""
:param sqlite3.Connection conn:
:param 'models'|'decks' json_name:
:param iter of dict new_dicts:
:param bool do_commit:
:return:
"""
cursor = conn.execute('SELECT {} FROM col'.format(json_name))
json_item = json.loads(cursor.fetchone()[0])
for new_dict in new_dicts:
json_item[new_dict['id']] = new_dict
conn.execute('UPDATE col SET {}=?'.format(json_name), (json.dumps(json_item),))
if do_commit:
conn.commit()
def write_anki_schema(conn):
"""
:param sqlite3.Connection conn:
:return:
"""
conn.executescript("""
-- Cards are what you review.
-- There can be multiple cards for each note, as determined by the Template.
CREATE TABLE cards (
id integer primary key,
-- the epoch milliseconds of when the card was created
nid integer not null,--
-- notes.id
did integer not null,
-- deck id (available in col table)
ord integer not null,
-- ordinal : identifies which of the card templates it corresponds to
-- valid values are from 0 to num templates - 1
mod integer not null,
-- modificaton time as epoch seconds
usn integer not null,
-- update sequence number : used to figure out diffs when syncing.
-- value of -1 indicates changes that need to be pushed to server.
-- usn < server usn indicates changes that need to be pulled from server.
type integer not null,
-- 0=new, 1=learning, 2=due, 3=filtered
queue integer not null,
-- -3=sched buried, -2=user buried, -1=suspended,
-- 0=new, 1=learning, 2=due (as for type)
-- 3=in learning, next rev in at least a day after the previous review
due integer not null,
-- Due is used differently for different card types:
-- new: note id or random int
-- due: integer day, relative to the collection's creation time
-- learning: integer timestamp
ivl integer not null,
-- interval (used in SRS algorithm). Negative = seconds, positive = days
factor integer not null,
-- factor (used in SRS algorithm)
reps integer not null,
-- number of reviews
lapses integer not null,
-- the number of times the card went from a "was answered correctly"
-- to "was answered incorrectly" state
left integer not null,
-- reps left till graduation
odue integer not null,
-- original due: only used when the card is currently in filtered deck
odid integer not null,
-- original did: only used when the card is currently in filtered deck
flags integer not null,
-- currently unused
data text not null
-- currently unused
);
-- col contains a single row that holds various information about the collection
CREATE TABLE col (
id integer primary key,
-- arbitrary number since there is only one row
crt integer not null,
-- created timestamp
mod integer not null,
-- last modified in milliseconds
scm integer not null,
-- schema mod time: time when "schema" was modified.
-- If server scm is different from the client scm a full-sync is required
ver integer not null,
-- version
dty integer not null,
-- dirty: unused, set to 0
usn integer not null,
-- update sequence number: used for finding diffs when syncing.
-- See usn in cards table for more details.
ls integer not null,
-- "last sync time"
conf text not null,
-- json object containing configuration options that are synced
models text not null,
-- json array of json objects containing the models (aka Note types)
decks text not null,
-- json array of json objects containing the deck
dconf text not null,
-- json array of json objects containing the deck options
tags text not null
-- a cache of tags used in the collection (This list is displayed in the browser. Potentially at other place)
);
-- Contains deleted cards, notes, and decks that need to be synced.
-- usn should be set to -1,
-- oid is the original id.
-- type: 0 for a card, 1 for a note and 2 for a deck
CREATE TABLE graves (
usn integer not null,
oid integer not null,
type integer not null
);
-- Notes contain the raw information that is formatted into a number of cards
-- according to the models
CREATE TABLE notes (
id integer primary key,
-- epoch seconds of when the note was created
guid text not null,
-- globally unique id, almost certainly used for syncing
mid integer not null,
-- model id
mod integer not null,
-- modification timestamp, epoch seconds
usn integer not null,
-- update sequence number: for finding diffs when syncing.
-- See the description in the cards table for more info
tags text not null,
-- space-separated string of tags.
-- includes space at the beginning and end, for LIKE "% tag %" queries
flds text not null,
-- the values of the fields in this note. separated by 0x1f (31) character.
sfld text not null,
-- sort field: used for quick sorting and duplicate check
csum integer not null,
-- field checksum used for duplicate check.
-- integer representation of first 8 digits of sha1 hash of the first field
flags integer not null,
-- unused
data text not null
-- unused
);
-- revlog is a review history; it has a row for every review you've ever done!
CREATE TABLE revlog (
id integer primary key,
-- epoch-milliseconds timestamp of when you did the review
cid integer not null,
-- cards.id
usn integer not null,
-- update sequence number: for finding diffs when syncing.
-- See the description in the cards table for more info
ease integer not null,
-- which button you pushed to score your recall.
-- review: 1(wrong), 2(hard), 3(ok), 4(easy)
-- learn/relearn: 1(wrong), 2(ok), 3(easy)
ivl integer not null,
-- interval
lastIvl integer not null,
-- last interval
factor integer not null,
-- factor
time integer not null,
-- how many milliseconds your review took, up to 60000 (60s)
type integer not null
-- 0=learn, 1=review, 2=relearn, 3=cram
);
CREATE INDEX ix_cards_nid on cards (nid);
CREATE INDEX ix_cards_sched on cards (did, queue, due);
CREATE INDEX ix_cards_usn on cards (usn);
CREATE INDEX ix_notes_csum on notes (csum);
CREATE INDEX ix_notes_usn on notes (usn);
CREATE INDEX ix_revlog_cid on revlog (cid);
CREATE INDEX ix_revlog_usn on revlog (usn);
""")
conn.commit()
PK ! : : ! ankitools-0.3.3.dist-info/LICENSEMIT License
Copyright (c) 2018 Pacharapol Withayasakpunt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
PK !H_zT T ankitools-0.3.3.dist-info/WHEEL
A
н#Z;/"d&F[xzw@Zpy3Fv]n0H*J>mlcA PK !H73M " ankitools-0.3.3.dist-info/METADATAWms6_|ԑ(4T'竧MOS8$! h.@Jt\=c\>
-,e
~%Gp?ɕJe&.Ei0-8`LEĊ?SHT"wD'9Kvk(U,-h8\,(Q@k4+T3piPm=J[اDIZqiJ`Gw*fت(e19v
Y+f0>?;^3FGV