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!ف?eeankitools/AnkiConnect.pyimport requests import requests.exceptions class AnkiConnect: URL = 'http://127.0.0.1:8765' @staticmethod def post(action, version=6, params=None): """ For the documentation, see https://foosoft.net/projects/anki-connect/ :param action: :param version: :param params: :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 if __name__ == '__main__': print(AnkiConnect.is_online()) PK!VR??ankitools/AnkiDirect.pyimport sqlite3 from time import time 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 AnkiTools.tools.verify import AnkiContentVerify class AnkiDirect: def __init__(self, anki_database: str=None): if anki_database is None: anki_database = get_collection_path() self.conn = sqlite3.connect(anki_database) self.ids = { 'notes': dict(), 'cards': dict(), 'models': dict(), 'decks': dict() } self.name_to_id = { 'models': dict(), 'decks': dict() } self.anki_creator = AnkiContentCreator(self.ids) self.anki_verify = AnkiContentVerify(self.ids) def add(self, data): modified = int(time()) for model_name, notes in data['data'].items(): model_id = self.get_model_id(model_name, notes[0]['data'].keys(), data['definitions'].get(model_name, dict())) anki_notes = [] anki_cards = [] anki_decks = [] for note in notes: anki_note = self.anki_creator.new_note(flds_list=list(note['data'].values()), model_id=model_id, modified=modified) self.ids['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.anki_creator.new_deck(deck_name) self.ids['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.anki_creator.new_card(anki_note['id'], deck_id, self.get_card_ordering(model_id, note_side), modified=modified) self.ids['cards'][str(anki_card['id'])] = anki_card anki_cards.append(anki_card) missing_deck_names = self.anki_verify.missing_decks() for deck_name in missing_deck_names: anki_deck = self.anki_creator.new_deck(deck_name) self.ids['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() 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.anki_creator.new_model(model_name, model_header, model_definition, modified=kwargs.get('modified', None)) self.ids['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.ids['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') PK!aaankitools/AnkiExcelSync.pyimport openpyxl as px from collections import OrderedDict, namedtuple from datetime import datetime from AnkiTools.AnkiDirect import AnkiDirect from AnkiTools.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!m:` ` ankitools/AnkiFormatEditor.pyimport shutil import atexit import os from zipfile import ZipFile from AnkiTools.AnkiExcelSync 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!?ssankitools/__init__.pyfrom .AnkiConnect import AnkiConnect from .AnkiDirect import AnkiDirect from .AnkiFormatEditor import anki_convert 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!ankitools/tools/__init__.pyPK!LMsllankitools/tools/create.pyfrom time import time from collections import OrderedDict from bs4 import BeautifulSoup from hashlib import sha1 from AnkiTools.tools.defaults import DEFAULT_TEMPLATE, DEFAULT_MODEL, DEFAULT_API_MODEL_DEFINITION from AnkiTools.tools.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!0HHankitools/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 if __name__ == '__main__': print(get_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!?ankitools/tools/verify.pyclass AnkiContentVerify: def __init__(self, ids=None): if ids is None: ids = { 'decks': dict() } self.ids = ids def missing_decks(self): deck_dirs = set() for deck in self.ids['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 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.1.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.1.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]n0H*J>mlcAPK!H)H "ankitools-0.3.1.dist-info/METADATAVms7_%l: SƤšn=IOi>8H:c8LճϳZx-Dݿ>C(e`LfeU-C\_3HMQ4 8)20cRvb<&'xefRϕW*#{)s2_sпhذ q}i.w*b!J]Y U6% OMOrw/E{QDI><ʌޑz˅3S'R)zZtIkH#L9ic)~9a. gJ| :eI:e/Rbyzؗ5cObk^e?\V2P N*M7nkqv R ]k۔bqq!+YdMUNycمUۇ2q߀{Mʢ/)?]/jpߟ?]j̷bop (:T7UC@i=qMY^ԣ7EjLwf̥8(挝)1s ?Rtb+ tKx41)T0t0p}'Δ5Cd:bl\ ~f挔#$"oz%M/Hl Nēcb3*۩tgu~GxepU*'5-8X"em q!vO`h+p%j(*[[A D[(}TD.J 5.ŚnN}DrLNxE:.i' >Ȱ> Ls=8ly޼5hu iJSH*Ksu'n {9%kS'5>TDb0"۟t?b ܘ``V[g6se٢+h9ƃ `Dڢ>0-&/%z%~#!|e1B-X:qklEs8qs2r«F)NIv`7&)V4DisXsk\x|ߪ~\ץ\.!Fl0g<\L.{M' s鞯pn+r>԰)ⷈ8E ?+baz!Wx=^-@Kg?PK!H?xצ ankitools-0.3.1.dist-info/RECORDɒJ}= TC2/z! Z!@Lf7Ze^6#<`Qx4e#IX{75ѣpOtd0{ۤ'a6GHŋF-bvtv'gUJ=0jڙKT^ nau%KEr+M]Ὕ?b2a%hR8i*ǐy雧+s?nq^bhO ?PK!AnkiTools/defaults.jsonPK!Y<`!AnkiTools/defaults_api.jsonPK!ف?eeankitools/AnkiConnect.pyPK!VR??ankitools/AnkiDirect.pyPK!aa1ankitools/AnkiExcelSync.pyPK!m:` ` Cankitools/AnkiFormatEditor.pyPK!?ss4Mankitools/__init__.pyPK!Mankitools/defaults.jsonPK!Y<`!hankitools/defaults_api.jsonPK!7iankitools/dir.pyPK!kankitools/tools/__init__.pyPK!LMsllPkankitools/tools/create.pyPK!j-=77ankitools/tools/defaults.pyPK!xߕ00cankitools/tools/guid.pyPK!0HHȋankitools/tools/path.pyPK!Eankitools/tools/read.pyPK!? ankitools/tools/verify.pyPK!4㈗ss˓ankitools/tools/write.pyPK! ::!tankitools-0.3.1.dist-info/LICENSEPK!H_zTTankitools-0.3.1.dist-info/WHEELPK!H)H "~ankitools-0.3.1.dist-info/METADATAPK!H?xצ ankitools-0.3.1.dist-info/RECORDPK*