PK!ankisync/__init__.pyPK!d1QQankisync/anki.pyfrom typing import Union import warnings import psutil from time import time import tinydb as tdb from tinydb.storages import MemoryStorage from . import db from .dir import get_collection_path from .builder.models import ModelBuilder from .builder.decks import DeckBuilder, DConfBuilder from .builder.notes import NoteBuilder, CardBuilder class Anki: def __init__(self, anki2_path=None, disallow_unsafe: Union[bool, None]=False, **kwargs): if anki2_path is None: anki2_path = get_collection_path(account_name=kwargs.setdefault('account_name', None)) 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: warnings.warn(e) kwargs.pop('account_name') db.database.init(anki2_path, pragmas={ 'foreign_keys': 0 }, **kwargs) self.disallow_unsafe = disallow_unsafe self.tdb = tdb.TinyDB(storage=MemoryStorage) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): pass def __iter__(self): for db_card in db.Cards.select(): db_note = db.Notes.get(id=db_card.nid) record = db_note.flds record += [' '.join(db_note.tags)] db_col = db.Col.get() db_deck = db_col.decks[str(db_card.did)] record += db_deck['name'] db_model = db_col.models[str(db_note.mid)] model_name = db_model['name'] template_names = [f['name'] for f in db_model['tmpls']] try: template = template_names[db_card.ord] except IndexError: template = template_names[0] record += [model_name, template, db_card.ord] header = [f['name'] for f in db_model['flds']] header += ['tags', 'deck', 'model', 'template', 'order'] yield dict(zip(header, record)) def iter_notes(self): for db_note in db.Notes.select(): record = db_note.flds record = [db_note.id, db_note.mid] + record + [' '.join(db_note.tags)] header = self.model_field_names_by_id(db_note.mid) header = ['nid', 'mid'] + header + ['tags'] yield dict(zip(header, record)) def _warning(self): msg = 'Please use _id() methods instead.' if self.disallow_unsafe is True: raise ValueError(msg) elif self.disallow_unsafe is False: warnings.warn(msg) else: pass @classmethod def init(cls, first_model: ModelBuilder, first_deck: DeckBuilder, first_dconf: DConfBuilder=None, first_note_data=None): db.database.create_tables([db.Col, db.Notes, db.Cards, db.Revlog, db.Graves]) db_models = dict() db_models[str(first_model.id)] = first_model db_decks = dict() db_decks[str(first_deck.id)] = first_deck if first_dconf is None: first_dconf = DConfBuilder('Default') db_dconf = dict() db_dconf[str(first_dconf.id)] = first_dconf if not db.Col.get_or_none(): db.Col.create( models=db_models, decks=db_decks, dconf=db_dconf ) if not db.Notes.get_or_none(): if first_note_data is None: first_note_data = dict() first_note = NoteBuilder(model_id=first_model.id, model_field_names=first_model.field_names, data=first_note_data) db_notes = db.Notes.create(**first_note) first_note.id = db_notes.id for template_name in first_model.template_names: first_card = CardBuilder(first_note, first_deck.id, template_name) db.Cards.create(**first_card) @classmethod def add_model(cls, name, fields, templates, **kwargs): db_col = db.Col.get() db_models = db_col.models new_model = ModelBuilder(name, fields, templates, **kwargs) db_models[str(new_model.id)] = new_model db_col.models = db_models db_col.save() return new_model.id @classmethod def iter_model(cls, model_id): header = cls.model_field_names_by_id(model_id) for db_note in db.Notes.select().where(db.Notes.mid == model_id): yield dict( id=db_note.id, **dict(zip(header, db_note.flds)) ) def get_tinydb_table(self): if len(self.tdb) == 0: for note_data in self.iter_notes(): self.tdb.insert(note_data) return self.tdb @classmethod def change_deck_by_id(cls, card_ids, deck_id)->None: db.Cards.update(did=deck_id).where(db.Cards.id.in_(card_ids)).execute() @classmethod def delete_decks_by_id(cls, deck_ids, cards_too=False)->None: db_col = db.Col.get() db_decks = db_col.decks for deck_id in deck_ids: db_decks.pop(str(deck_id)) if cards_too: for db_card in db.Cards.select().where(db.Cards.did == int(deck_id)): db_card.delete_instance() db_col.decks = db_decks db_col.save() @classmethod def model_by_id(cls, model_id): return db.Col.get().models[str(model_id)] @classmethod def model_field_names_by_id(cls, model_id): model = cls.model_by_id(model_id) return [f['name'] for f in model['flds']] @classmethod def model_template_names_by_id(cls, model_id): model = cls.model_by_id(model_id) return [t['name'] for t in model['tmpls']] @classmethod def note_to_cards(cls, note_id): def _get_dict(): db_note = db.Notes.get(id=note_id) template_names = cls.model_template_names_by_id(db_note.mid) for c in db.Cards.select(db.Cards.id, db.Cards.ord, db.Cards.nid).where(db.Cards.nid == note_id): yield template_names[c.ord], c.id return dict(_get_dict()) @classmethod def card_set_next_review(cls, card_id, type_, queue, due): """ :param card_id: :param type_: -- 0=new, 1=learning, 2=due, 3=filtered :param queue: -- -3=sched buried, -2=user buried, -1=suspended, -- 0=new, 1=learning, 2=due (as for type) -- 3=in learning, next rev in at least a day after the previous review :param due: -- Due is used differently for different card types: -- new: note id or random int -- due: integer day, relative to the collection's creation time -- learning: integer timestamp :return: """ db_card = db.Cards.get(id=card_id) db_card.type = type_ db_card.queue = queue db_card.due = due db_card.save() @classmethod def card_set_stat(cls, card_id, reps, lapses, **revlog): """ :param card_id: :param reps: -- number of reviews :param lapses: -- the number of times the card went from a "was answered correctly" -- to "was answered incorrectly" state :param revlog: usn integer not null, -- update sequence number: for finding diffs when syncing. -- See the description in the cards table for more info ease integer not null, -- which button you pushed to score your recall. -- review: 1(wrong), 2(hard), 3(ok), 4(easy) -- learn/relearn: 1(wrong), 2(ok), 3(easy) ivl integer not null, -- interval lastIvl integer not null, -- last interval factor integer not null, -- factor time integer not null, -- how many milliseconds your review took, up to 60000 (60s) type integer not null -- 0=learn, 1=review, 2=relearn, 3=cram :return: """ with db.database.atomic(): db_card = db.Cards.get(id=card_id) db_card.reps = reps db_card.lapses = lapses db_card.save() db.Revlog.create( cid=db_card.id, **revlog ) @classmethod def get_deck_config_by_deck_name(cls, deck_name): deck_id = cls.deck_names_and_ids()[deck_name] conf_id = db.Col.get().decks[str(deck_id)]['conf'] db_dconf = db.Col.get().dconf return db_dconf[str(conf_id)] @classmethod def deck_config_names_and_ids(cls): def _gen_dict(): for dconf_id, d in db.Col.get().dconf.items(): yield d['name'], int(dconf_id) return dict(_gen_dict()) @classmethod def note_info(cls, note_id): db_note = db.Notes.get(id=note_id) header, row = cls._raw_note_info(db_note) return { 'noteId': db_note.id, 'modelId': db_note.mid, 'tags': db_note.tags, 'fields': dict(zip(header, row)) } @classmethod def _raw_note_info(cls, db_note): db_model = cls.model_by_id(db_note.mid) return db_model['flds'], db_note.flds def _extract_ac_note(self, ac_note): data = ac_note['fields'] model_id = ac_note.get('modelId', None) if model_id is None: self._warning() model_name = ac_note['modelName'] model_id = self.model_names_and_ids()[model_name] return data, model_id def upsert_note(self, ac_note, defaults_key='defaults', _lock=True): """ :param ac_note: ac_note['data'] uses the same format as http://docs.peewee-orm.com/en/latest/peewee/api.html?highlight=get_or_create#Model.get_or_create :param defaults_key: :param _lock: :return: """ def _atomic_action(): for note_id in matching_ids: self.update_note_fields(note_id=note_id, fields=data) data, model_id = self._extract_ac_note(ac_note) tdb_table = self.get_tinydb_table() original_data = data.copy() data.update(data.pop(defaults_key)) matching_t_doc_ids = tdb_table.upsert(data, self._build_tdb_query(original_data, model_id=model_id, _skip=defaults_key)) if matching_t_doc_ids: matching_ids = [self.tdb.get(doc_id=doc_id)['nid'] for doc_id in matching_t_doc_ids] if _lock: with db.database.atomic(): _atomic_action() else: _atomic_action() return matching_ids else: return [self._add_note(data, model_id, ac_note)] def upsert_notes(self, ac_notes, defaults_key='defaults'): note_ids_2d = [] with db.database.atomic(): for ac_note in ac_notes: note_ids_2d.append(self.upsert_note(ac_note, defaults_key=defaults_key, _lock=False)) return note_ids_2d def search_notes(self, conditions): return self.get_tinydb_table().search(self._build_tdb_query(conditions)) @staticmethod def _build_tdb_query(data, model_id=None, _skip=None): query = None for k, v in data.items(): if k != _skip: if query is None: query = (tdb.Query()[k] == v) else: query &= (tdb.Query()[k] == v) if model_id: query &= (tdb.Query()['mid'] == model_id) return query def _add_note(self, data, model_id, ac_note): deck_id = ac_note.get('deckId', None) if deck_id is None: self._warning() deck_name = ac_note['deckName'] deck_id = self.deck_names_and_ids().get(deck_name, None) if deck_id is None: deck_id = self.create_deck(deck_name, conf=ac_note.get('dconf', 1)) tags = ac_note.get('tags', []) model_field_names = self.model_field_names_by_id(model_id) first_note = NoteBuilder(model_id=model_id, model_field_names=model_field_names, data=data, tags=tags) db_notes = db.Notes.create(**first_note) first_note.id = db_notes.id for i, template_name in enumerate(self.model_template_names_by_id(model_id)): first_card = CardBuilder(first_note, deck_id, template=i) db.Cards.create(**first_card) tdb_table = self.get_tinydb_table() tdb_table.insert({ 'nid': db_notes.id, 'mid': model_id, **data }) return db_notes.id ################################ # Original AnkiConnect Methods # ################################ @classmethod def deck_names(cls): return [d['name'] for d in db.Col.get().decks.values()] @classmethod def deck_names_and_ids(cls): def _gen_dict(): for did, d in db.Col.get().decks.items(): yield d['name'], int(did) return dict(_gen_dict()) @classmethod def get_decks(cls, card_ids): def _gen_dict(): for did, d in db.Col.get().decks.items(): db_cards = db.Cards.select(db.Cards.id, db.Cards.did)\ .where((db.Cards.did == int(did)) & (db.Cards.id.in_(card_ids))) if len(db_cards) > 0: yield d['name'], [c.id for c in db_cards] return dict(_gen_dict()) @classmethod def create_deck(cls, deck_name, desc='', dconf=1, **kwargs): db_col = db.Col.get() db_decks = db_col.decks existing_decks = cls.deck_names() deck_name_parts = deck_name.split('::') sub_deck_parts = [] for i, part in enumerate(deck_name_parts): sub_deck_parts.append(part) sub_deck = '::'.join(sub_deck_parts) if sub_deck not in existing_decks: new_deck = DeckBuilder(name=sub_deck, desc=desc, dconf=dconf, id_=int(time() * 1000) + i, **kwargs) db_decks[str(new_deck.id)] = new_deck db_col.decks = db_decks db_col.save() return cls.deck_names_and_ids()[deck_name] def change_deck(self, card_ids, deck_name, dconf=1): self._warning() deck_id = self.deck_names_and_ids().get(deck_name, None) if deck_id is None: deck_id = self.create_deck(deck_name, dconf=dconf) self.change_deck_by_id(card_ids, deck_id) def delete_decks(self, deck_names, cards_too=False): self._warning() deck_mapping = self.deck_names_and_ids() deck_ids = [deck_mapping[deck_name] for deck_name in deck_names] self.delete_decks_by_id(deck_ids, cards_too) def get_deck_config(self, deck_name): self._warning() return self.get_deck_config_by_deck_name(deck_name) @classmethod def save_deck_config(cls, config: dict): db_col = db.Col.get() db_dconf = db_col.dconf dconf = DConfBuilder(config.pop('name'), **config) db_dconf[str(dconf.id)] = dconf db_col.dconf = db_dconf db_col.save() return dconf.id @classmethod def set_deck_config_id(cls, deck_names, config_id): is_edited = False db_col = db.Col.get() db_decks = db_col.decks for k, v in cls.deck_names_and_ids().items(): if k in deck_names: db_decks[str(v)]['conf'] = config_id is_edited = True if is_edited: db_col.decks = db_decks db_col.save() return is_edited @classmethod def clone_deck_config_id(cls, dconf_name, clone_from: int): db_col = db.Col.get() db_dconf = db_col.dconf new_dconf = DConfBuilder(dconf_name) new_dconf.update(db_dconf[str(clone_from)]) db_dconf[new_dconf.id] = new_dconf db_col.dconf = db_dconf db_col.save() return new_dconf.id @classmethod def remove_deck_config_id(cls, config_id): db_col = db.Col.get() db_dconf = db_col.dconf db_dconf.pop(config_id) db_col.dconf = db_dconf db_col.save() return True @classmethod def model_names(cls): return [m['name'] for m in db.Col.get().models.values()] @classmethod def model_names_and_ids(cls): def _gen_dict(): for mid, m in db.Col.get().models.items(): yield m['name'], int(mid) return dict(_gen_dict()) @classmethod def model_field_names(cls, model_name): model_id = cls.model_names_and_ids()[model_name] return cls.model_field_names_by_id(model_id) @classmethod def model_template_names(cls, model_name): model_id = cls.model_names_and_ids()[model_name] return cls.model_template_names_by_id(model_id) # @classmethod # def model_fields_on_templates(cls, model_name): # raise NotImplementedError def add_note(self, ac_note): data, model_id = self._extract_ac_note(ac_note) return self._add_note(data, model_id, ac_note) def add_notes(self, ac_notes): return [self.add_note(ac_note) for ac_note in ac_notes] # @classmethod # def can_add_notes(cls, ac_notes): # raise NotImplementedError @classmethod def update_note_fields(cls, note_id, fields: dict): db_note = db.Notes.get(id=note_id) field_names = cls.model_field_names_by_id(db_note.mid) prev_note_fields = db_note.flds note_fields = [] for i, name in enumerate(field_names): note_field = fields.get(name, None) if note_field is not None: note_fields.append(note_field) else: note_fields.append(prev_note_fields[i]) db_note.flds = note_fields db_note.save() @classmethod def add_tags(cls, note_ids, tags: Union[str, list]): if isinstance(tags, str): tags = [tags] db.Notes.update( tags=sorted(set(db.Notes.tags) | set(tags)) ).where(db.Notes.id.in_(note_ids)) @classmethod def remove_tags(cls, note_ids, tags: Union[str, list]): if isinstance(tags, str): tags = [tags] db.Notes.update( tags=sorted(set(db.Notes.tags) - set(tags)) ).where(db.Notes.id.in_(note_ids)) @classmethod def get_tags(cls): all_tags = set() for db_note in db.Notes.select(db.Notes.tags): all_tags.update(db_note.tags) return sorted(all_tags) # @classmethod # def find_notes(cls, query: str): # raise NotImplementedError @classmethod def notes_info(cls, note_ids): return [cls.note_info(note_id) for note_id in note_ids] @classmethod def suspend(cls, card_ids): if db.Cards.update(queue=-1).where(db.Cards.id.in_(card_ids)).execute() > 0: return True return False @classmethod def unsuspend(cls, card_ids): if db.Cards.update(queue=db.Cards.type).where(db.Cards.id.in_(card_ids)).execute() > 0: return True return False @classmethod def are_suspended(cls, card_ids): def _gen_list(): for card_id in card_ids: db_card = db.Cards.get(id=card_id) yield (db_card.queue == -1) return list(_gen_list()) @classmethod def are_due(cls, card_ids): def _gen_list(): for card_id in card_ids: db_card = db.Cards.get(id=card_id) yield (db_card.type == 2) return list(_gen_list()) # @classmethod # def get_intervals(cls, card_ids, complete=False): # raise NotImplementedError # # @classmethod # def find_cards(cls, query): # raise NotImplementedError @classmethod def cards_to_notes(cls, card_ids): note_ids = set() for db_card in db.Cards.select(db.Cards.id, db.Cards.nid).where(db.Cards.id.in_(card_ids)): note_ids.update(db_card.nid) return sorted(note_ids) @classmethod def cards_info(cls, card_ids): all_info = list() for card_id in card_ids: db_card = db.Cards.get(id=card_id) all_info += cls.notes_info([db_card.nid]) return all_info PK!D ankisync/anki_util.pyfrom hashlib import sha1 import re from html.entities import name2codepoint def checksum(data): if isinstance(data, str): data = data.encode("utf-8") return sha1(data).hexdigest() def field_checksum(data): # 32 bit unsigned number from first 8 digits of sha1 hash return int(checksum(data)[:8], 16) reComment = re.compile("(?s)") reStyle = re.compile("(?si).*?") reScript = re.compile("(?si).*?") reTag = re.compile("(?s)<.*?>") reEnts = re.compile("&#?\w+;") reMedia = re.compile("(?i)]+src=[\"']?([^\"'>]+)[\"']?[^>]*>") def stripHTML(s): s = reComment.sub("", s) s = reStyle.sub("", s) s = reScript.sub("", s) s = reTag.sub("", s) s = entsToTxt(s) return s def stripHTMLMedia(s): "Strip HTML but keep media filenames" s = reMedia.sub(" \\1 ", s) return stripHTML(s) def minimizeHTML(s): "Correct Qt's verbose bold/underline/etc." s = re.sub('(.*?)', '\\1', s) s = re.sub('(.*?)', '\\1', s) s = re.sub('(.*?)', '\\1', s) return s def htmlToTextLine(s): s = s.replace("
", " ") s = s.replace("
", " ") s = s.replace("
", " ") s = s.replace("\n", " ") s = re.sub("\[sound:[^]]+\]", "", s) s = re.sub("\[\[type:[^]]+\]\]", "", s) s = stripHTMLMedia(s) s = s.strip() return s def entsToTxt(html): # entitydefs defines nbsp as \xa0 instead of a standard space, so we # replace it first html = html.replace(" ", " ") def fixup(m): text = m.group(0) if text[:2] == "&#": # character reference try: if text[:3] == "&#x": return chr(int(text[3:-1], 16)) else: return chr(int(text[2:-1])) except ValueError: pass else: # named entity try: text = chr(name2codepoint[text[1:-1]]) except KeyError: pass return text # leave as is return reEnts.sub(fixup, html) def bodyClass(col, card): bodyclass = "card card%d" % (card.ord+1) if col.conf.get("nightMode"): bodyclass += " nightMode" return bodyclass PK!gankisync/ankiconnect.pyimport requests from typing import Union class AnkiConnect: URL = 'http://localhost:8765' VERSION = 6 def __init__(self, version=6): self.VERSION = version def post(self, action, params: dict=None): j = { 'action': action, 'version': self.VERSION } if params: j['params'] = params r = requests.post(self.URL, json=j) resp = r.json() if resp['error']: raise ValueError(resp['error']) return resp['result'] def version(self): return self.post('version') def upgrade(self): return self.post('upgrade') def sync(self): return self.post('sync') def multi(self, params): return self.post('multi', params=params) def deck_names(self): return self.post('deckNames') def deck_names_and_ids(self): return self.post('deckNamesAndIds') def get_decks(self, card_ids): return self.post('getDecks', params={ 'cards': card_ids }) def create_deck(self, deck_name): return self.post('createDeck', params={ 'deck': deck_name }) def change_deck(self, card_ids, deck_name): return self.post('changeDeck', params={ 'cards': card_ids, 'deck': deck_name }) def delete_decks(self, deck_names, cards_too: bool=False): return self.post('deleteDecks', params={ 'decks': deck_names, 'cardsToo': cards_too }) def get_deck_config(self, deck_name): return self.post('getDeckConfig', params={ 'deck': deck_name }) def save_deck_config(self, config: dict): return self.post('saveDeckConfig', params={ 'config': config }) def set_deck_config_id(self, deck_names, config_id): return self.post('setDeckConfigId', params={ 'decks': deck_names, 'configId': config_id }) def clone_deck_config_id(self, deck_name, clone_from: int): return self.post('cloneDeckConfigId', params={ 'deck': deck_name, 'cloneFrom': clone_from }) def remove_deck_config_id(self, config_id): return self.post('removeDeckConfigId', params={ 'configId': config_id }) def model_names(self): return self.post('modelNames') def model_names_and_ids(self): return self.post('modelNamesAndIds') def model_field_names(self, model_name): return self.post('modelFieldNames', params={ 'modelName': model_name }) def model_fields_on_templates(self, model_name): return self.post('modelFieldsOnTemplates', params={ 'modelName': model_name }) def add_note(self, ac_note): return self.post('addNote', params={ 'note': ac_note }) def add_notes(self, ac_notes): return self.post('addNotes', params={ 'notes': ac_notes }) def can_add_notes(self, ac_notes): return self.post('canAddNotes', params={ 'notes': ac_notes }) def update_note_fields(self, note_id, fields: dict): return self.post('updateNoteFields', params={ 'note': { 'id': note_id, 'fields': fields } }) def add_tags(self, note_ids, tags: Union[str, list]): return self.post('addTags', params={ 'notes': note_ids, 'tags': tags }) def remove_tags(self, note_ids, tags: Union[str, list]): return self.post('removeTags', params={ 'notes': note_ids, 'tags': tags }) def get_tags(self): return self.post('getTags') def find_notes(self, query: str): return self.post('findNotes', params={ 'query': query }) def notes_info(self, note_ids): return self.post('notesInfo', params={ 'notes': note_ids }) def suspend(self, card_ids): return self.post('suspend', params={ 'cards': card_ids }) def unsuspend(self, card_ids): return self.post('unsuspend', params={ 'cards': card_ids }) def are_suspended(self, card_ids): return self.post('areSuspended', params={ 'cards': card_ids }) def are_due(self, card_ids): return self.post('areDue', params={ 'cards': card_ids }) def get_intervals(self, card_ids, complete=False): return self.post('getIntervals', params={ 'cards': card_ids, 'complete': complete }) def find_cards(self, query): return self.post('findCards', params={ 'query': query }) def cards_to_notes(self, card_ids): return self.post('cardsToNotes', params={ 'cards': card_ids }) def cards_info(self, card_ids): return self.post('cardsInfo', params={ 'cards': card_ids }) def store_media_file(self, filename, data_b64): return self.post('storeMediaFile', params={ 'filename': filename, 'data': data_b64 }) def retrieve_media_file(self, filename): return self.post('retrieveMediaFile', params={ 'filename': filename }) def delete_media_file(self, filename): return self.post('deleteMediaFile', params={ 'filename': filename }) def gui_browse(self, query): return self.post('guiBrowse', params={ 'query': query }) def gui_add_cards(self): return self.post('guiAddCards') def gui_current_card(self): return self.post('guiCurrentCard') def gui_start_timer(self): return self.post('guiStartTimer') def gui_show_question(self): return self.post('guiShowQuestion') def gui_show_answer(self): return self.post('guiShowAnswer') def gui_answer_card(self, ease: int): return self.post('guiAnswerCard', params={ 'ease': ease }) def gui_deck_overview(self, deck_name): return self.post('guiDeckOverview', params={ 'name': deck_name }) def gui_deck_browser(self): return self.post('guiDeckBrowser') def gui_deck_review(self, deck_name): return self.post('guiDeckReview', params={ 'name': deck_name }) def gui_exit_anki(self): return self.post('guiExitAnki') PK!܂nnankisync/apkg.pyfrom zipfile import ZipFile from tempfile import mkdtemp import atexit import shutil from pathlib import Path import json from .anki import Anki class Apkg(Anki): def __init__(self, filename): self.filename = str(filename) self.temp_dir = mkdtemp() with ZipFile(self.filename) as zf: zf.extractall(path=self.temp_dir) atexit.register(shutil.rmtree, self.temp_dir, ignore_errors=True) self.media = json.loads(Path(self.temp_dir).joinpath('media').read_text()) super(Apkg, self).__init__(str(Path(self.temp_dir).joinpath('collection.anki2'))) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def close(self): self.save() shutil.rmtree(self.temp_dir) def save(self): with ZipFile(self.filename, 'w') as zf: zf.write(str(Path(self.temp_dir).joinpath('collection.anki2')), arcname='collection.anki2') for file_path in Path(self.temp_dir).glob('*'): if str(file_path).isdigit(): zf.write(Path(self.temp_dir).joinpath(file_path).resolve(), arcname=file_path.name) zf.writestr('media', json.dumps(self.media)) def store_media_file(self, filename, data_binary): media_id = max(int(k) for k in self.media.keys()) + 1 with Path(self.temp_dir).joinpath(str(media_id)).open('rb') as f: f.write(data_binary) self.media[str(media_id)] = filename def retrieve_media_file(self, filename): for k, v in self.media.items(): if v == filename: return Path(self.temp_dir).joinpath(k).read_bytes() def delete_media_file(self, filename): for k, v in self.media.items(): if v == filename: self.media.pop(k) return True return False PK!t>qankisync/builder/__init__.pyfrom .decks import DeckBuilder, DConfBuilder from .models import ModelBuilder, TemplateBuilder, FieldBuilder from .notes import NoteBuilder, CardBuilder PK!i)ankisync/builder/decks.pyfrom time import time from .default import default class DeckBuilder(dict): """ { name: "name of deck", extendRev: "extended review card limit (for custom study)", usn: "usn: Update sequence number: used in same way as other usn vales in db", collapsed: "true when deck is collapsed", browserCollapsed: "true when deck collapsed in browser", newToday: "two number. First one currently not used. Second is the negation (-) of the number of new cards added today by custom study", timeToday: "two number array used somehow for custom study. Currently unused in the code", dyn: "1 if dynamic (AKA filtered) deck", extendNew: "extended new card limit (for custom study)", conf: "id of option group from dconf in `col` table", revToday: "two number. First one currently not used. Second is the negation (-) of the number of review cards added today by custom study", lrnToday: "two number array used somehow for custom study. Currently unused in the code", id: "deck ID (automatically generated long)", mod: "last modification time", desc: "deck description" } """ def __init__(self, name, desc='', dconf=1, id_=None, **kwargs): if id_ is None: self.id = int(time() * 1000) else: self.id = id_ self.name = name d = next(iter(default['col']['decks'].values())).copy() d.update({ "name": self.name, "id": self.id, "mod": int(time()), "desc": desc, "conf": dconf, **kwargs }) super(DeckBuilder, self).__init__(d) class DConfBuilder(dict): """ { "model id (epoch time in milliseconds)" : { autoplay : "whether the audio associated to a question should be played when the question is shown" dyn : "Whether this deck is dynamic. Not present by default in decks.py" id : "deck ID (automatically generated long). Not present by default in decks.py" lapse : { "The configuration for lapse cards." delays : "The list of successive delay between the learning steps of the new cards, as explained in the manual." leechAction : "What to do to leech cards. 0 for suspend, 1 for mark. Numbers according to the order in which the choices appear in aqt/dconf.ui" leechFails : "the number of lapses authorized before doing leechAction." minInt: "a lower limit to the new interval after a leech" mult : "percent by which to multiply the current interval when a card goes has lapsed" } maxTaken : "The number of seconds after which to stop the timer" mod : "Last modification time" name : "The name of the configuration" new : { "The configuration for new cards." bury : "Whether to bury cards related to new cards answered" delays : "The list of successive delay between the learning steps of the new cards, as explained in the manual." initialFactor : "The initial ease factor" ints : "The list of delays according to the button pressed while leaving the learning mode. Good, easy and unused. In the GUI, the first two elements corresponds to Graduating Interval and Easy interval" order : "In which order new cards must be shown. NEW_CARDS_RANDOM = 0 and NEW_CARDS_DUE = 1." perDay : "Maximal number of new cards shown per day." separate : "Seems to be unused in the code." } replayq : "whether the audio associated to a question should be played when the answer is shown" rev : { "The configuration for review cards." bury : "Whether to bury cards related to new cards answered" ease4 : "the number to add to the easyness when the easy button is pressed" fuzz : "The new interval is multiplied by a random number between -fuzz and fuzz" ivlFct : "multiplication factor applied to the intervals Anki generates" maxIvl : "the maximal interval for review" minSpace : "not currently used according to decks.py code's comment" perDay : "Numbers of cards to review per day" } timer : "whether timer should be shown (1) or not (0)" usn : "See usn in cards table for details." } } """ def __init__(self, name, **kwargs): self.name = name self.id = kwargs.setdefault('id', None) if self.id is None: self.id = int(time() * 1000) kwargs.pop('id') d = next(iter(default['col']['dconf'].values())).copy() d.update({ "name": self.name, "id": self.id, **kwargs }) super(DConfBuilder, self).__init__(d) PK!ankisync/builder/default.pyfrom ankisync.presets import default def create_conf(**kwargs): d = default.copy()['conf'] d.update(kwargs) return d def create_tags(): return default.copy()['tags'] PK!xߕ00ankisync/builder/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!vМ>ankisync/builder/models.pyfrom time import time from .default import default class ModelBuilder(dict): """ { "model id (epoch time in milliseconds)" : { css : "CSS, shared for all templates", did : "Long specifying the id of the deck that cards are added to by default", flds : [ "JSONArray containing object for each field in the model as follows:", { font : "display font", media : "array of media. appears to be unused", name : "field name", ord : "ordinal of the field - goes from 0 to num fields -1", rtl : "boolean, right-to-left script", size : "font size", sticky : "sticky fields retain the value that was last added when adding new notes" } ], id : "model ID, matches notes.mid", latexPost : "String added to end of LaTeX expressions (usually \\end{document})", latexPre : "preamble for LaTeX expressions", mod : "modification time in milliseconds", name : "model name", req : [ "Array of arrays describing which fields are required for each card to be generated, looks like: [[0, "any", [0, 3, 6]]], this is required to display a template", [ "the 'ord' value of the template object from the 'tmpls' array you are setting the required fields of", '? string, "all" or "any"', ["? another array of 'ord' values from field object you want to require from the 'flds' array"] ] ], sortf : "Integer specifying which field is used for sorting in the browser", tags : "Anki saves the tags of the last added note to the current model, use an empty array []", tmpls : [ "JSONArray containing object of CardTemplate for each card in model", { afmt : "answer template string", bafmt : "browser answer format: used for displaying answer in browser", bqfmt : "browser question format: used for displaying question in browser", did : "deck override (null by default)", name : "template name", ord : "template number, see flds", qfmt : "question format string" } ], type : "Integer specifying what type of model. 0 for standard, 1 for cloze", usn : "usn: Update sequence number: used in same way as other usn vales in db", vers : "Legacy version number (unused), use an empty array []" } } """ def __init__(self, name, fields, templates, type_=0, **kwargs): self.id = int(time() * 1000) self.name = name self.fields = fields self.templates = templates req = [[i, "all", [i]] for i in range(len(self.templates))] d = next(iter(default['col']['models'].values())).copy() d.update( id=self.id, name=self.name, flds=self.fields, tmpls=self.templates, mod=int(time()), type=type_, req=req ) d.update(kwargs) super(ModelBuilder, self).__init__(d) @property def field_names(self): return [f.name for f in self.fields] @property def template_names(self): return [t.name for t in self.templates] class FieldBuilder(dict): """ "JSONArray containing object for each field in the model as follows:", { font : "display font", media : "array of media. appears to be unused", name : "field name", ord : "ordinal of the field - goes from 0 to num fields -1", rtl : "boolean, right-to-left script", size : "font size", sticky : "sticky fields retain the value that was last added when adding new notes" } """ def __init__(self, name, order, **kwargs): self.name = name self.order = order d = next(iter(default['col']['models'].values()))['flds'][0].copy() d.update( name=self.name, ord=self.order, **kwargs ) super(FieldBuilder, self).__init__(d) class TemplateBuilder(dict): """ "JSONArray containing object of CardTemplate for each card in model", { afmt : "answer template string", bafmt : "browser answer format: used for displaying answer in browser", bqfmt : "browser question format: used for displaying question in browser", did : "deck override (null by default)", name : "template name", ord : "template number, see flds", qfmt : "question format string" } """ def __init__(self, name, question, answer, order, **kwargs): self.name = name self.question = question self.answer = answer self.order = order d = next(iter(default['col']['models'].values()))['tmpls'][0].copy() d.update( name=self.name, qfmt=self.question, afmt=self.answer, ord=self.order, **kwargs ) super(TemplateBuilder, self).__init__(d) PK!XjMooankisync/builder/notes.pyfrom typing import Union from .models import ModelBuilder class NoteBuilder(dict): def __init__(self, model_id, model_field_names, data: dict, tags=None, note_id=None, **kwargs): self.model_id = model_id self.model_field_names = model_field_names self.data = data self.id = note_id if tags: self.tags = tags else: self.tags = list() super(NoteBuilder, self).__init__( mid=self.model_id, tags=self.tags, **kwargs ) flds = [] for field_name in self.model_field_names: flds.append(str(self.data.get(field_name, ''))) self['flds'] = flds class CardBuilder(dict): def __init__(self, note: Union[NoteBuilder, int], deck_id: int, template: Union[int, str], model: ModelBuilder=None, **kwargs): self.note = note self.deck_id = deck_id self.template = template self.model = model super(CardBuilder, self).__init__( nid=getattr(self.note, 'id', self.note), did=self.deck_id, **kwargs ) if isinstance(self.template, int): self['ord'] = self.template else: self['ord'] = self.model.template_names.index(self.template) PK!>7070ankisync/db.pyimport peewee as pv from playhouse import signals import json from time import time from ankisync.builder.guid import guid64 from ankisync.builder.default import create_conf, create_tags from ankisync.anki_util import field_checksum, stripHTMLMedia database = pv.SqliteDatabase(None) class BaseModel(signals.Model): class Meta: database = database class TagField(pv.TextField): def db_value(self, value): return ' '.join(value) def python_value(self, value): return value.strip().split(' ') class JSONField(pv.TextField): def db_value(self, value): all_names = set() for v2 in value.values(): if isinstance(v2, dict): if 'name' in v2.keys(): if v2['name'] in all_names: raise ValueError('Duplicate name: {}'.format(v2['name'])) all_names.add(v2['name']) return json.dumps(value) def python_value(self, value): return json.loads(value) class ListField(pv.TextField): def db_value(self, value): if value: return '\u001f'.join(value) def python_value(self, value): return value.split('\u001f') class Col(BaseModel): """ -- col contains a single row that holds various information about the collection CREATE TABLE col ( id integer primary key, -- arbitrary number since there is only one row crt integer not null, -- created timestamp mod integer not null, -- last modified in milliseconds scm integer not null, -- schema mod time: time when "schema" was modified. -- If server scm is different from the client scm a full-sync is required ver integer not null, -- version dty integer not null, -- dirty: unused, set to 0 usn integer not null, -- update sequence number: used for finding diffs when syncing. -- See usn in cards table for more details. ls integer not null, -- "last sync time" conf text not null, -- json object containing configuration options that are synced models text not null, -- json array of json objects containing the models (aka Note types) decks text not null, -- json array of json objects containing the deck dconf text not null, -- json array of json objects containing the deck options tags text not null -- a cache of tags used in the collection (This list is displayed in the browser. Potentially at other place) ); """ id = pv.IntegerField(primary_key=True, default=1) crt = pv.IntegerField(default=lambda: int(time())) mod = pv.IntegerField() # autogenerated scm = pv.IntegerField(default=lambda: int(time() * 1000)) ver = pv.IntegerField(default=11) dty = pv.IntegerField(default=0) usn = pv.IntegerField(default=0) ls = pv.IntegerField(default=0) conf = JSONField(default=create_conf) models = JSONField() decks = JSONField() dconf = JSONField() tags = JSONField(default=create_tags) @signals.pre_save(sender=Col) def col_pre_save(model_class, instance, created): instance.mod = int(time()) class Notes(BaseModel): """ -- Notes contain the raw information that is formatted into a number of cards -- according to the models CREATE TABLE notes ( id integer primary key, -- epoch seconds of when the note was created guid text not null, -- globally unique id, almost certainly used for syncing mid integer not null, -- model id mod integer not null, -- modification timestamp, epoch seconds usn integer not null, -- update sequence number: for finding diffs when syncing. -- See the description in the cards table for more info tags text not null, -- space-separated string of tags. -- includes space at the beginning and end, for LIKE "% tag %" queries flds text not null, -- the values of the fields in this note. separated by 0x1f (31) character. sfld text not null, -- sort field: used for quick sorting and duplicate check csum integer not null, -- field checksum used for duplicate check. -- integer representation of first 8 digits of sha1 hash of the first field flags integer not null, -- unused data text not null -- unused ); """ id = pv.IntegerField(primary_key=True, default=lambda: int(time() * 1000)) guid = pv.TextField(unique=True, default=guid64) # autogenerated mid = pv.IntegerField() mod = pv.IntegerField() # autogenerated usn = pv.IntegerField(default=-1) tags = TagField(default=list) flds = ListField() sfld = pv.TextField() # autogenerated csum = pv.IntegerField() # autogenerated flags = pv.IntegerField(default=0) data = pv.TextField(default='') class Meta: indexes = [ pv.SQL('CREATE INDEX ix_notes_usn on notes (usn)'), pv.SQL('CREATE INDEX ix_notes_csum on notes (csum)') ] @signals.pre_save(sender=Notes) def notes_pre_save(model_class, instance, created): while model_class.get_or_none(id=instance.id) is not None: instance.id = model_class.select(pv.fn.Max(model_class.id)).scalar() + 1 while model_class.get_or_none(guid=instance.guid) is not None: instance.guid = guid64() instance.mod = int(time() * 1000) instance.sfld = stripHTMLMedia(instance.flds[0]) instance.csum = field_checksum(instance.sfld) class Cards(BaseModel): """ -- Cards are what you review. -- There can be multiple cards for each note, as determined by the Template. CREATE TABLE cards ( id integer primary key, -- the epoch milliseconds of when the card was created nid integer not null,-- -- notes.id did integer not null, -- deck id (available in col table) ord integer not null, -- ordinal : identifies which of the card templates it corresponds to -- valid values are from 0 to num templates - 1 mod integer not null, -- modificaton time as epoch seconds usn integer not null, -- update sequence number : used to figure out diffs when syncing. -- value of -1 indicates changes that need to be pushed to server. -- usn < server usn indicates changes that need to be pulled from server. type integer not null, -- 0=new, 1=learning, 2=due, 3=filtered queue integer not null, -- -3=sched buried, -2=user buried, -1=suspended, -- 0=new, 1=learning, 2=due (as for type) -- 3=in learning, next rev in at least a day after the previous review due integer not null, -- Due is used differently for different card types: -- new: note id or random int -- due: integer day, relative to the collection's creation time -- learning: integer timestamp ivl integer not null, -- interval (used in SRS algorithm). Negative = seconds, positive = days factor integer not null, -- factor (used in SRS algorithm) reps integer not null, -- number of reviews lapses integer not null, -- the number of times the card went from a "was answered correctly" -- to "was answered incorrectly" state left integer not null, -- of the form a*1000+b, with: -- b the number of reps left till graduation -- a the number of reps left today odue integer not null, -- original due: only used when the card is currently in filtered deck odid integer not null, -- original did: only used when the card is currently in filtered deck flags integer not null, -- currently unused data text not null -- currently unused ); """ id = pv.IntegerField(primary_key=True, default=lambda: int(time() * 1000)) nid = pv.IntegerField() did = pv.IntegerField() ord = pv.IntegerField() mod = pv.IntegerField() # autogenerated usn = pv.IntegerField(default=-1) type = pv.IntegerField(default=0) queue = pv.IntegerField(default=0) due = pv.IntegerField() # autogenerated ivl = pv.IntegerField(default=0) factor = pv.IntegerField(default=0) reps = pv.IntegerField(default=0) lapses = pv.IntegerField(default=0) left = pv.IntegerField(default=0) odue = pv.IntegerField(default=0) odid = pv.IntegerField(default=0) flags = pv.IntegerField(default=0) data = pv.TextField(default='') class Meta: indexes = [ pv.SQL('CREATE INDEX ix_cards_usn on cards (usn)'), pv.SQL('CREATE INDEX ix_cards_nid on cards (nid)'), pv.SQL('CREATE INDEX ix_cards_sched on cards (did, queue, due)') ] @signals.pre_save(sender=Cards) def cards_pre_save(model_class, instance, created): while model_class.get_or_none(id=instance.id) is not None: instance.id = model_class.select(pv.fn.Max(model_class.id)).scalar() + 1 instance.mod = int(time()) if instance.due is None: instance.due = instance.nid class Revlog(BaseModel): """ -- revlog is a review history; it has a row for every review you've ever done! CREATE TABLE revlog ( id integer primary key, -- epoch-milliseconds timestamp of when you did the review cid integer not null, -- cards.id usn integer not null, -- update sequence number: for finding diffs when syncing. -- See the description in the cards table for more info ease integer not null, -- which button you pushed to score your recall. -- review: 1(wrong), 2(hard), 3(ok), 4(easy) -- learn/relearn: 1(wrong), 2(ok), 3(easy) ivl integer not null, -- interval lastIvl integer not null, -- last interval factor integer not null, -- factor time integer not null, -- how many milliseconds your review took, up to 60000 (60s) type integer not null -- 0=learn, 1=review, 2=relearn, 3=cram ); """ id = pv.IntegerField(primary_key=True, default=lambda: int(time() * 1000)) cid = pv.IntegerField() usn = pv.IntegerField(default=-1) ease = pv.IntegerField() ivl = pv.IntegerField() lastIvl = pv.IntegerField() factor = pv.IntegerField() time = pv.IntegerField() type = pv.IntegerField() class Meta: indexes = [ pv.SQL('CREATE INDEX ix_revlog_usn on revlog (usn)'), pv.SQL('CREATE INDEX ix_revlog_cid on revlog (cid)') ] @signals.pre_save(sender=Revlog) def cards_pre_save(model_class, instance, created): while model_class.get_or_none(id=instance.id) is not None: instance.id = model_class.select(pv.fn.Max(model_class.id)).scalar() + 1 class Graves(BaseModel): """ -- Contains deleted cards, notes, and decks that need to be synced. -- usn should be set to -1, -- oid is the original id. -- type: 0 for a card, 1 for a note and 2 for a deck CREATE TABLE graves ( usn integer not null, oid integer not null, type integer not null ); """ usn = pv.IntegerField(default=-1) oid = pv.IntegerField() type = pv.IntegerField() class Meta: primary_key = False PK!  ankisync/dir.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!ڶcankisync/presets/__init__.pyimport json try: from importlib.resources import read_text except ImportError: from importlib_resources import read_text from ankisync.util import deep_merge_dicts default = json.loads(read_text('ankisync.presets', 'default.json')) wanki_min = json.loads(read_text('ankisync.presets', 'wanki_min.json')) deep_merge_dicts(original=default, incoming=wanki_min) def get_wanki_min_dconf(): d = next(iter(wanki_min['col']['dconf'].values())).copy() return d PK! ankisync/presets/default.json{ "col": { "id": 1, "crt": 1540846800, "mod": 1540929280995, "scm": 1540929274845, "ver": 11, "dty": 0, "usn": 0, "ls": 0, "conf": { "nextPos": 1, "estTimes": true, "activeDecks": [ 1 ], "sortType": "noteFld", "timeLim": 0, "sortBackwards": false, "addToCur": true, "curDeck": 1, "newBury": true, "newSpread": 0, "activeCols": [ "noteFld", "template", "cardDue", "deck" ], "savedFilters": {}, "dueCounts": true, "curModel": "1540929274846", "collapseTime": 1200 }, "models": { "1540929274841": { "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": "1540929274841", "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": 1540929274 }, "1540929274842": { "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": "1540929274842", "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n", "mod": 1540929274 }, "1540929274845": { "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": "1540929274845", "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n", "mod": 1540929274 }, "1540929274846": { "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": "1540929274846", "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n", "mod": 1540929274 } }, "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": 1540929274, "desc": "" } }, "dconf": { "1": { "name": "Default", "replayq": true, "lapse": { "leechFails": 8, "minInt": 1, "delays": [ 10 ], "leechAction": 0, "mult": 0 }, "rev": { "perDay": 200, "ivlFct": 1, "maxIvl": 36500, "minSpace": 1, "ease4": 1.3, "bury": false, "fuzz": 0.05 }, "timer": 0, "maxTaken": 60, "usn": 0, "new": { "separate": true, "delays": [ 1, 10 ], "perDay": 20, "ints": [ 1, 4, 7 ], "initialFactor": 2500, "bury": false, "order": 1 }, "autoplay": true, "id": 1, "mod": 0 } }, "tags": {} }, "notes": null, "cards": null, "revlog": null, "graves": null, "sqlite_stat1": { "tbl": "col", "idx": null, "stat": "1" } }PK!wooankisync/presets/wanki_min.json{ "col": { "dconf": { "1540931682693": { "name": "wanki.min", "replayq": true, "lapse": { "leechFails": 8, "minInt": 1, "delays": [ 10 ], "leechAction": 0, "mult": 0 }, "rev": { "perDay": 200, "fuzz": 0.05, "ivlFct": 1, "maxIvl": 36500, "ease4": 1.3, "bury": false, "minSpace": 1 }, "timer": 0, "dyn": false, "maxTaken": 60, "usn": -1, "new": { "perDay": 10, "delays": [ 1, 10, 240, 480 ], "separate": true, "ints": [ 1, 4, 7 ], "initialFactor": 2500, "bury": false, "order": 0 }, "mod": 1540931774, "id": 1540931682693, "autoplay": true } } } }PK!aLankisync/util.pydef update_config(additional_config): from ankisync.builder.default import default deep_merge_dicts(original=default, incoming=additional_config) def deep_merge_lists(original, incoming): """ Deep merge two lists. Modifies original. Reursively call deep merge on each correlated element of list. If item type in both elements are a. dict: call deep_merge_dicts on both values. b. list: Calls deep_merge_lists on both values. # c. any other type: Value is overridden. # d. conflicting types: Value is overridden. If length of incoming list is more that of original then extra values are appended. """ common_length = min(len(original), len(incoming)) for idx in range(common_length): if isinstance(original[idx], dict) and isinstance(incoming[idx], dict): deep_merge_dicts(original[idx], incoming[idx]) elif isinstance(original[idx], list) and isinstance(incoming[idx], list): deep_merge_lists(original[idx], incoming[idx]) else: raise ValueError('Cannot merge at {}'.format(idx)) for idx in range(common_length, len(incoming)): original.append(incoming[idx]) def deep_merge_dicts(original, incoming): """ Deep merge two dictionaries. Modfies original. For key conflicts if both values are: a. dict: Recursivley call deep_merge_dicts on both values. b. list: Calls deep_merge_lists on both values. # c. any other type: Value is overridden. # d. conflicting types: Value is overridden. """ for key in incoming: if key in original: if isinstance(original[key], dict) and isinstance(incoming[key], dict): deep_merge_dicts(original[key], incoming[key]) elif isinstance(original[key], list) and isinstance(incoming[key], list): deep_merge_lists(original[key], incoming[key]) else: raise ValueError('Cannot merge at {}'.format(key)) else: original[key] = incoming[key] PK! :: ankisync-0.1.4.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 WXankisync-0.1.4.dist-info/WHEEL A н#Z."jm)Afb~ڠO68oF04UhoAf f4=4h0k::wXPK!H"%!ankisync-0.1.4.dist-info/METADATAWmo6_qm>d9v1mYҮFkCĔDۄ%R!8;J~E0#0s/<>snY,kɵJv 09|veeL=8SBN@H8A$̧̺S%%-LJe!QV&htjmnzDi9,W鼽tI\ğ+*8ef3x'*݃! FìdBiV~{hψh |Ik,^o heOLN <'%zjW<^;kq}ܴ΄xyms6Fd6Tc^9': ޾~ b +R' V2*+Nm挛Xb ,uUcmfUuy7/npU &&궹(M•h?,%4a*= l_:L+6?%^=-:HTsfIA9~ Y>bB JxB[KnkG ?>_RcѨ"&YVXPcQ`-PHe9P1/4_?g4`" [V:q٦(_xh4nk-C]/.֭Q!҄=1JY^ 96Zg3z,I4>I1V,ecGͺ&>@N E5:o4kP?FǸq߮|:dt*e|`Cv_pCYoʙ4slS b)[9 >`9PoWzk揯./:\\^ҹ©Uupw.C 0M߯d>ΑkDl;Kp Di"FwQy'?ceaP8pΉ_gôF..b:9s ^4f(RZV~Ѝd9_(ޝ ?~1,}+ɴ>tP'#D`5-Xz" Sĸ再 J<5&-ƂU_%e:PK!Hi!mankisync-0.1.4.dist-info/RECORDurH{? ٗC*$0؄BEA=}Yg0a>KD?f[pǝ}ECG=Vt2Si]k/8w{\u}>6DI38 lNah7TU&S`ϱpjÙ~O>z:I3rS=t9Q0!E0ǍǻCG@zǡ.^Tq/[FBa8xmmiܵ 'GW20N/]DA'/^ylSX4UE@nX Uk_ݻ~*ɯfY VaL=bѦ w:U|'Gp/mrc7O.LuN̦VyA絕{m첹ƒzdAeBW}V{'&F`zvK( 0jQU] u?W*t{hӘf@<Ֆzݰ;0LYra+b(6љ+|^I/~WCzIaDsxk~tÜ[Ќ.l*v4^n@ͩ埣\7戟aq[z,KZVJJNkboBḟQ 8^pL=nGtʧ1` _rB8Ih 0ga?PK!ankisync/__init__.pyPK!d1QQ2ankisync/anki.pyPK!D [Rankisync/anki_util.pyPK!g"\ankisync/ankiconnect.pyPK!܂nnvankisync/apkg.pyPK!t>q~ankisync/builder/__init__.pyPK!i)nankisync/builder/decks.pyPK!ankisync/builder/default.pyPK!xߕ00ankisync/builder/guid.pyPK!vМ>ankisync/builder/models.pyPK!XjMooankisync/builder/notes.pyPK!>7070Dankisync/db.pyPK!  ankisync/dir.pyPK!ڶcankisync/presets/__init__.pyPK! ankisync/presets/default.jsonPK!woo ankisync/presets/wanki_min.jsonPK!aL ankisync/util.pyPK! :: ankisync-0.1.4.dist-info/LICENSEPK!H WXrankisync-0.1.4.dist-info/WHEELPK!H"%!ankisync-0.1.4.dist-info/METADATAPK!Hi!m1 ankisync-0.1.4.dist-info/RECORDPK-$