PK ! AnkiTools/defaults.json{
"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": 1531228931,
"desc": ""
}
},
"models": {
"1531228931967": {
"vers": [],
"name": "Basic (optional reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1,
2
]
]
],
"flds": [
{
"size": 20,
"name": "Front",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Back",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Add Reverse",
"media": [],
"rtl": false,
"ord": 2,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
},
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"name": "Card 2",
"qfmt": "{{#Add Reverse}}{{Back}}{{/Add Reverse}}",
"did": null,
"ord": 1,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 0,
"id": "1531228931967",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"mod": 1531228931
},
"1531228931966": {
"vers": [],
"name": "Cloze",
"tags": [],
"did": 1,
"usn": -1,
"flds": [
{
"size": 20,
"name": "Text",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Extra",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{cloze:Text}}
\n{{Extra}}",
"name": "Cloze",
"qfmt": "{{cloze:Text}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 1,
"id": "1531228931966",
"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}",
"mod": 1531228931
},
"1531228931972": {
"vers": [],
"name": "Basic",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
]
],
"flds": [
{
"size": 20,
"name": "Front",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Back",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 0,
"id": "1531228931972",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"mod": 1531228931
},
"1531228931970": {
"vers": [],
"name": "Basic (and reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1
]
]
],
"flds": [
{
"size": 20,
"name": "Front",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Back",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
},
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"name": "Card 2",
"qfmt": "{{Back}}",
"did": null,
"ord": 1,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 0,
"id": "1531228931970",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"mod": 1531228931
}
}
}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 ! 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 ! 6S79 9 ankitools/api/ankidirect.pyimport sqlite3
from time import time
import psutil
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
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)
self.conn = sqlite3.connect(anki_database)
self._id_to_record = self.data
self._name_to_id = self.name_to_id
self.creator = AnkiContentCreator(self._id_to_record)
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 ! ankitools/defaults.json{
"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": 1531228931,
"desc": ""
}
},
"models": {
"1531228931967": {
"vers": [],
"name": "Basic (optional reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1,
2
]
]
],
"flds": [
{
"size": 20,
"name": "Front",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Back",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Add Reverse",
"media": [],
"rtl": false,
"ord": 2,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
},
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"name": "Card 2",
"qfmt": "{{#Add Reverse}}{{Back}}{{/Add Reverse}}",
"did": null,
"ord": 1,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 0,
"id": "1531228931967",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"mod": 1531228931
},
"1531228931966": {
"vers": [],
"name": "Cloze",
"tags": [],
"did": 1,
"usn": -1,
"flds": [
{
"size": 20,
"name": "Text",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Extra",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{cloze:Text}}
\n{{Extra}}",
"name": "Cloze",
"qfmt": "{{cloze:Text}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 1,
"id": "1531228931966",
"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}",
"mod": 1531228931
},
"1531228931972": {
"vers": [],
"name": "Basic",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
]
],
"flds": [
{
"size": 20,
"name": "Front",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Back",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 0,
"id": "1531228931972",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"mod": 1531228931
},
"1531228931970": {
"vers": [],
"name": "Basic (and reversed card)",
"tags": [],
"did": 1,
"usn": -1,
"req": [
[
0,
"all",
[
0
]
],
[
1,
"all",
[
1
]
]
],
"flds": [
{
"size": 20,
"name": "Front",
"media": [],
"rtl": false,
"ord": 0,
"font": "Arial",
"sticky": false
},
{
"size": 20,
"name": "Back",
"media": [],
"rtl": false,
"ord": 1,
"font": "Arial",
"sticky": false
}
],
"sortf": 0,
"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",
"tmpls": [
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Back}}",
"name": "Card 1",
"qfmt": "{{Front}}",
"did": null,
"ord": 0,
"bafmt": "",
"bqfmt": ""
},
{
"afmt": "{{FrontSide}}\n\n
\n\n{{Front}}",
"name": "Card 2",
"qfmt": "{{Back}}",
"did": null,
"ord": 1,
"bafmt": "",
"bqfmt": ""
}
],
"latexPost": "\\end{document}",
"type": 0,
"id": "1531228931970",
"css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n",
"mod": 1531228931
}
}
}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 ! 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 ! -O O ankitools/editor.pyimport shutil
import atexit
import os
from zipfile import ZipFile
from .excel import AnkiExcelSync
class AnkiFormatEditor:
def __init__(self, tmp_path='tmp/'):
self.tmp_path = tmp_path
atexit.register(self.close)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def close(self):
shutil.rmtree(self.tmp_path)
def convert(self, in_file, out_file):
in_file_type = os.path.splitext(in_file)[1]
out_file_type = os.path.splitext(out_file)[1]
assert in_file_type != out_file_type, 'File types must be different'
convert_process = {
('.apkg', '.anki2'): self.unzip(in_file, out_file=out_file, out_path=''),
('.apkg', '.xlsx'): self.export_anki_sqlite(self.unzip(in_file, out_path=self.tmp_path), out_file),
('.anki2', '.apkg'): self.zip(in_file, out_file),
('.anki2', '.xlsx'): self.export_anki_sqlite(in_file, out_file),
('.xlsx', '.anki2'): self.import_anki_sqlite(in_file, out_file, out_path=''),
('.xlsx', '.apkg'): self.zip(self.import_anki_sqlite(in_file, out_path=self.tmp_path), out_file)
}.get((in_file_type, out_file_type), False)
assert convert_process is False, "Unsupported conversion."
@staticmethod
def unzip(in_file, out_file='collection.anki2', out_path=''):
with ZipFile(in_file) as zf:
zf.extract('collection.anki2', path=out_path)
os.rename(os.path.join(out_path, 'collection.anki2'),
os.path.join(out_path, out_file))
return os.path.join(out_path, out_file)
@staticmethod
def zip(in_file, out_file):
with ZipFile(out_file, 'w') as zf:
zf.write(in_file, arcname='collection.anki2')
@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()
@staticmethod
def import_anki_sqlite(in_file, out_file='collection.anki2', out_path=''):
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):
with AnkiFormatEditor() as afe:
afe.convert(in_file, out_file)
PK ! eS S ankitools/excel.pyimport openpyxl as px
from collections import OrderedDict, namedtuple
from datetime import datetime
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 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 = [cell.value for cell in next(row_iter)]
for row in row_iter:
record = OrderedDict(zip(header, [cell.value for cell in row]))
formatted_record = {
'data': record,
'decks': {
'Card 1': sheet_name
}
}
payload['data'][sheet_name].append(formatted_record)
payload['definitions'][sheet_name] = DEFAULT_API_MODEL_DEFINITION
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']))
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
PK ! ankitools/tools/__init__.pyPK ! 欇N N ankitools/tools/create.pyfrom time import time
from collections import OrderedDict
from bs4 import BeautifulSoup
from hashlib import sha1
from .defaults import DEFAULT_TEMPLATE, DEFAULT_MODEL, DEFAULT_API_MODEL_DEFINITION
from .guid import guid64
class AnkiContentCreator:
def __init__(self, ids=None):
"""
:param dict ids:
"""
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())
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['css'])
if css is None:
css = DEFAULT_TEMPLATE['css']
model_id = self._unique_id('models')
model = {
"vers": kwargs.get('vers', []),
"name": model_name,
"tags": kwargs.get('tags', []),
"did": kwargs.get('did', None),
"usn": kwargs.get('usn', -1),
"req": kwargs.get('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": kwargs.get('sortf', 0),
"latexPre": kwargs.get('latexPre', DEFAULT_MODEL['latexPre']),
"tmpls": tmpls,
"latexPost": kwargs.get('latexPost', DEFAULT_MODEL['latexPost']),
"type": kwargs.get('type', 0),
"id": model_id,
"css": css,
"mod": modified
}
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 = {
'name': field_name,
'rtl': kwargs.get('rtl', False),
'sticky': kwargs.get('sticky', False),
'media': kwargs.get('media', []),
'ord': ordering,
'font': kwargs.get('font', 'Arial'),
'size': kwargs.get('size', 12)
}
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 = {
'name': template_name,
'qfmt': kwargs.get('qfmt', DEFAULT_TEMPLATE['qfmt']),
'did': kwargs.get('did', None),
'bafmt': kwargs.get('bafmt', DEFAULT_TEMPLATE['bafmt']),
'afmt': kwargs.get('afmt', DEFAULT_TEMPLATE['afmt']),
'ord': ordering,
'bqfmt': kwargs.get('bqfmt', DEFAULT_TEMPLATE['bqfmt'])
}
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', kwargs.get('usn', -1)),
('tags', ' '.join(tags_list)),
('flds', '\x1f'.join(flds_list)),
('sfld', sfld),
('csum', sha1(sfld.encode('utf8')).hexdigest()),
('flags', kwargs.get('flags', 0)),
('data', kwargs.get('data', ''))
])
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', kwargs.get('usn', -1)),
('type', kwargs.get('type', 0)),
('queue', kwargs.get('queue', 0)),
('due', kwargs.get('due', note_id)),
('ivl', kwargs.get('ivl', 0)),
('factor', kwargs.get('factor', 0)),
('reps', kwargs.get('reps', 0)),
('lapses', kwargs.get('lapses', 0)),
('left', kwargs.get('left', 0)),
('odue', kwargs.get('odue', 0)),
('odid', kwargs.get('odid', 0)),
('flags', kwargs.get('flags', 0)),
('data', kwargs.get('data', ''))
])
assert len(card) == 18, 'Invalid Anki Card format.'
return card
def new_deck(self, deck_name, **kwargs):
deck = {
'desc': kwargs.get('desc', ''),
'name': deck_name,
'extendRev': kwargs.get('extendRev', 50),
'usn': kwargs.get('usn', 0),
'collapsed': kwargs.get('collapsed', False),
'newToday': kwargs.get('newToday', [0, 0]),
'timeToday': kwargs.get('timeToday', [0, 0]),
'dyn': kwargs.get('dyn', 0),
'extendNew': kwargs.get('extendNew', 10),
'conf': kwargs.get('conf', 1),
'revToday': kwargs.get('revToday', [0, 0]),
'lrnToday': kwargs.get('lrnToday', [0, 0]),
'id': self._unique_id('decks'),
'mod': int(time())
}
return deck
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 ! j-=7 7 ankitools/tools/defaults.pyimport json
from collections import OrderedDict
from AnkiTools.dir import module_path
# 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_MODEL = tuple(defaults['models'].values())[0]
DEFAULT_TEMPLATE = 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 = defaults['model_definition']
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 ! 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))
return json.loads(cursor.fetchone()[0])
PK ! 4㈗s s 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()
PK ! : : ! ankitools-0.3.2.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.2.dist-info/WHEEL
A
н#Z;/"d&F[xzw@Zpy3Fv]n0H*J>mlcA PK !H
{!.& " ankitools-0.3.2.dist-info/METADATAWQs6~ׯ܇;KNyq7'n\5ևmHx9l9[.| > ȼ̱/nP L΅SF&*Kf!DŽ_sTQ!:!rnDǚM19mTY5N2Uv51Lb%ߗ+qie/60WĨѰr3epβY5[2溒kDK&>4~DoJn%b1itR0kDp_~\PknxNpgrnr
VX tųHXE:AsK1c~r90Js{GVNxSl+??FfFhDIǥ/{[j!4}]pu\"\e?;v#lD&Mױa2
Jf71oS܄v&xDƏ(I#ݛ:~1C\o&dqurˡǴVrWX[&!88B.aagEPaP*8`O\Q0~[T5q]
cD0r! }J ~a1,F2F3&ʔ́<1J$9@ۉQe`9֗ wb.gE'_tx4DKsU%\K)Y<Ż3gU8P.8#0\eXYfju\pݣG
+@(Fw%P#-4.l
yS\&)7>P:j<526k&Qb;ns0f`/(|bX ND->o:B|cy>x8&5)}Ebi*04cY~ATȉJ(|j? Pi`%bޤjCٕΔ&;5kqm#E-W
-frz^ B|дE>Z}nZMJ0DU&Z6s,Z١:&ոѬ:[i=tŷ.,^c؍ m~jH`"]Bp0i?nUSf!.;(J?MJG&wwRV6Sy/hf@&킛k1ڵ;s[8~6:X+>$CpG^pukJQп FFnx
Y
1Ż6C8O2?.,AB#jjT%V.f8'(U
PK !H ankitools-0.3.2.dist-info/RECORDMcH}E/$ x8;Oz<33Ӌ9qwnUyz`(#
̷ĥ(w罩źs$n)'2)qLcX|889{' ,8zTԌʬu#Dҡ$Bp!9 8X%O`bca\,C?wDĈN? 2h*~p ^^?oA/86tn=M̠4gu#]^+WwT1N!*KzP`YD7CyhmQZQ*#345+5A,"ChovG+Y`YW^IaƽZ,/6
Í0rmG-;*6 5%bkM\L&MI1%+)([q1ON|P#JOk mjOJ
jCDuoUu0f+#6%UvW#Y!ttڼ\F%ArHp1Hz7=>kfsNϝO*B3J˶`\ }&!O)D!!FZafPK ! AnkiTools/defaults.jsonPK ! Y<`! AnkiTools/defaults_api.jsonPK ! qm m ankitools/__init__.pyPK ! u ankitools/ankiconnect.pyPK ! ankitools/api/__init__.pyPK ! 6S79 9 ankitools/api/ankidirect.pyPK ! p/P
X6 ankitools/api/verify.pyPK ! RD ankitools/defaults.jsonPK ! Y<`! _ ankitools/defaults_api.jsonPK ! 7 C` ankitools/dir.pyPK ! -O O a ankitools/editor.pyPK ! eS S k ankitools/excel.pyPK ! } ankitools/tools/__init__.pyPK ! 欇N N } ankitools/tools/create.pyPK ! j-=7 7 P ankitools/tools/defaults.pyPK ! xߕ0 0 ankitools/tools/guid.pyPK !
% ankitools/tools/path.pyPK ! d ankitools/tools/read.pyPK ! 4㈗s s ) ankitools/tools/write.pyPK ! : : ! ҧ ankitools-0.3.2.dist-info/LICENSEPK !H_zT T K ankitools-0.3.2.dist-info/WHEELPK !H
{!.& " ܬ ankitools-0.3.2.dist-info/METADATAPK !H B ankitools-0.3.2.dist-info/RECORDPK a 3