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!qmmankitools/__init__.pyfrom .ankiconnect import AnkiConnect from .api.ankidirect import AnkiDirect from .editor import anki_convert PK!uankitools/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!6S799ankitools/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!7ankitools/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!eSSankitools/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!欇NNankitools/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-=77ankitools/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ߕ00ankitools/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㈗ssankitools/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_zTTankitools-0.3.2.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]n0H*J>mlcAPK!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_~\PknxNpgrn԰r VX‡ tųHXE:AsK1c~r9޿0Js{GVNxSl+??׎FfFhDIǥ/{[j!4}]pu\" \e?;v#lD&Mױa2 Jf71oS܄v&xDƏ(I#ݛ:~1C\o&dqurˡǴVrWX[&!88B.aagEPaP*8`O\Q0~[T 5q] cD0r! }J~a1,F2F3&ʔ́<1J $9@ۉQe`9֗ wb.gE'_tx4DKsU%\K)Y<Ż3 gU8P.8#0\eXYfju\pݣG +@(Fw%P#-4.l yS\&)7>P:j<526 k&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&Z6 s,Z١:&ոѬ:[i=tŷ.,^c؍ m~jH`"]Bp0i?nUSf!.;(J?MJG&wwRV6S y/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`YD7Cyhm QZQ*#345+5A,"ChovG+Y`YW^Ia ƽZ,/6 Í0rmG-;*6 5%bk򽺱M\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!qmmankitools/__init__.pyPK!uankitools/ankiconnect.pyPK! ankitools/api/__init__.pyPK!6S799 ankitools/api/ankidirect.pyPK!p/P X6ankitools/api/verify.pyPK!RDankitools/defaults.jsonPK!Y<`!_ankitools/defaults_api.jsonPK!7C`ankitools/dir.pyPK!-O O aankitools/editor.pyPK!eSSkankitools/excel.pyPK!}ankitools/tools/__init__.pyPK!欇NN}ankitools/tools/create.pyPK!j-=77Pankitools/tools/defaults.pyPK!xߕ00ankitools/tools/guid.pyPK!  %ankitools/tools/path.pyPK!dankitools/tools/read.pyPK!4㈗ss)ankitools/tools/write.pyPK! ::!ҧankitools-0.3.2.dist-info/LICENSEPK!H_zTTKankitools-0.3.2.dist-info/WHEELPK!H {!.& "ܬankitools-0.3.2.dist-info/METADATAPK!H Bankitools-0.3.2.dist-info/RECORDPKa3