PK![rJ77ankix/__init__.pyfrom .main import Ankix from .convert import from_apkg PK!T|ankix/config.pyfrom datetime import timedelta config = { 'database': 'default.ankix', 'markdown': True, 'srs': { 0: timedelta(minutes=10), 1: timedelta(hours=4), 2: timedelta(hours=8), 3: timedelta(days=1), 4: timedelta(days=3), 5: timedelta(weeks=1), 6: timedelta(weeks=2), 7: timedelta(weeks=4), 8: timedelta(weeks=16) } } PK!Q e ankix/convert.pyimport sqlite3 from zipfile import ZipFile from tempfile import TemporaryDirectory import json from tqdm import tqdm import os import re import logging from .config import config def from_apkg(src_apkg, dst_ankix=None): if dst_ankix is None: dst_ankix = os.path.splitext(src_apkg)[0] + '.ankix' with TemporaryDirectory() as temp_dir: config['database'] = dst_ankix info = dict() from . import db # Tag Media Model Template Deck Note Card db.create_all_tables() with db.database.atomic(): with ZipFile(src_apkg) as zf: zf.extractall(temp_dir) conn = sqlite3.connect(os.path.join(temp_dir, 'collection.anki2')) conn.row_factory = sqlite3.Row try: d = dict(conn.execute('''SELECT * FROM col LIMIT 1''').fetchone()) for model in tqdm(tuple(json.loads(d['models']).values()), desc='models'): db.Model.create( id=model['id'], name=model['name'], css=model['css'] ) info.setdefault('model', dict())[model['id']] = model for template in model['tmpls']: db_template = db.Template.get_or_create( model_id=model['id'], name=template['name'], question=template['qfmt'], answer=template['afmt'], )[0] info.setdefault('template', dict())[db_template.id] = template for deck in tqdm(tuple(json.loads(d['decks']).values()), desc='decks'): db.Deck.create( id=deck['id'], name=deck['name'] ) c = conn.execute('''SELECT * FROM notes''') for note in tqdm(c.fetchall(), desc='notes'): info_model = info['model'][note['mid']] header = [field['name'] for field in info_model['flds']] db_note = db.Note.create( id=note['id'], data=dict(zip(header, note['flds'].split('\u001f'))) ) for tag in set(t for t in note['tags'].split(' ') if t): db_tag = db.Tag.get_or_create( name=tag )[0] db_tag.notes.add(db_note) info.setdefault('note', dict())[note['id']] = dict(note) for media_name in re.findall(r'src=[\'\"]((?!//)[^\'\"]+)[\'\"]', note['flds']): info.setdefault('media', dict()).setdefault(media_name, []).append(note['id']) c = conn.execute('''SELECT * FROM cards''') for card in tqdm(c.fetchall(), desc='cards'): info_note = info['note'][card['nid']] info_model = info['model'][info_note['mid']] db_model = db.Model.get(id=info_model['id']) db_template = db_model.templates[0] if '{{cloze:' not in db_template.question: db_template = db_model.templates[card['ord']] db.Card.create( id=card['id'], note_id=card['nid'], deck_id=card['did'], template_id=db_template.id ) else: for db_template_n in db_model.templates: db.Card.create( id=card['id'], note_id=card['nid'], deck_id=card['did'], cloze_order=card['ord']+1, template_id=db_template_n.id ) for db_deck in db.Deck.select(): if not db_deck.cards: db_deck.delete_instance() finally: conn.close() with open(os.path.join(temp_dir, 'media')) as f: info_media = info['media'] for media_id, media_name in tqdm(json.load(f).items(), desc='medias'): with open(os.path.join(temp_dir, media_id), 'rb') as image_f: db_media = db.Media.create( id=int(media_id), name=media_name, data=image_f.read() ) for note_id in info_media.get(media_name, []): db_media.notes.add(db.Note.get(id=note_id)) if not db_media.notes: logging.error('%s not connected to Notes. Deleting...', media_name) db_media.delete_instance() return dst_ankix PK!Wexx ankix/db.pyimport peewee as pv from playhouse import sqlite_ext from playhouse.shortcuts import model_to_dict from datetime import datetime, timedelta import random import sys import datauri import re from .config import config from .jupyter import HTML database = sqlite_ext.SqliteDatabase(config['database']) class BaseModel(pv.Model): viewer_config = dict() @classmethod def get_viewer(cls, records): from htmlviewer import PagedViewer return PagedViewer([r.to_viewer() for r in records], **cls.viewer_config) class Meta: database = database class Tag(BaseModel): name = pv.TextField(unique=True, collation='NOCASE') # notes def __repr__(self): return f'' def to_viewer(self): d = model_to_dict(self) d['notes'] = [repr(n) for n in self.notes] return d class Media(BaseModel): name = pv.TextField(unique=True) data = pv.BlobField() # notes def __repr__(self): return f'' @property def src(self): return datauri.build(bytes(self.data)) @property def html(self): return f'' def to_viewer(self): d = model_to_dict(self) d['data'] = self.html d['notes'] = [repr(n) for n in self.notes] return d viewer_config = { 'renderer': { 'data': 'html' }, 'colWidth': { 'data': 600 } } class Model(BaseModel): name = pv.TextField(unique=True) css = pv.TextField() # templates def __repr__(self): return f'' def to_viewer(self): d = model_to_dict(self) d['templates'] = [t.name for t in self.templates] return d class Template(BaseModel): model = pv.ForeignKeyField(Model, backref='templates') name = pv.TextField() question = pv.TextField() answer = pv.TextField() class Meta: indexes = ( (('model_id', 'name'), True), ) def __repr__(self): return f'' def to_viewer(self): d = model_to_dict(self) d['model'] = self.model.name d['cards'] = [repr(c) for c in self.cards] return d viewer_config = { 'renderer': { 'question': 'html', 'answer': 'html' }, 'colWidth': { 'question': 400, 'answer': 400 } } class Deck(BaseModel): name = pv.TextField(unique=True) def __repr__(self): return f'' def to_viewer(self): d = model_to_dict(self) d['cards'] = [repr(c) for c in self.cards] return d class Note(BaseModel): data = sqlite_ext.JSONField() medias = pv.ManyToManyField(Media, backref='notes', on_delete='cascade') tags = pv.ManyToManyField(Tag, backref='notes', on_delete='cascade') def mark(self, tag): Tag.get_or_create(name=tag)[0].notes.add(self) def unmark(self, tag): Tag.get_or_create(name=tag)[0].notes.remove(self) def to_viewer(self): d = model_to_dict(self) d['cards'] = '
'.join(c.html for c in self.cards) d['tags'] = [t.name for t in self.tags] return d viewer_config = { 'renderer': { 'cards': 'html' }, 'colWidth': { 'cards': 400 } } NoteTag = Note.tags.get_through_model() NoteMedia = Note.medias.get_through_model() class Card(BaseModel): note = pv.ForeignKeyField(Note, backref='cards') deck = pv.ForeignKeyField(Deck, backref='cards') template = pv.ForeignKeyField(Template, backref='cards') cloze_order = pv.IntegerField(null=True) srs_level = pv.IntegerField(null=True) next_review = pv.DateTimeField(null=True) def to_viewer(self): d = model_to_dict(self) d.update({ 'question': str(self.question), 'answer': str(self.answer), 'tags': [t.name for t in self.note.tags] }) return d viewer_config = { 'renderer': { 'question': 'html', 'answer': 'html' }, 'colWidth': { 'question': 400, 'answer': 400 } } def _pre_render(self, html, is_question): for k, v in self.note.data.items(): html = html.replace('{{%s}}' % k, v) html = html.replace('{{cloze:%s}}' % k, v) if self.cloze_order is not None: if is_question: html = re.sub(r'{{c%d::([^}]+)}}' % self.cloze_order, '[...]', html) html = re.sub(r'{{c\d+::([^}]+)}}', '\g<1>', html) return html @property def question(self): html = self._pre_render(self.template.question, is_question=True) return HTML( html, medias=self.note.medias, css=self.css ) @property def answer(self): html = self._pre_render(self.template.answer, is_question=False) html = html.replace('{{FrontSide}}', self.question.raw) return HTML( html, medias=self.note.medias, css=self.css ) @property def css(self): return self.template.model.css @property def html(self): return f'''

{self.question.raw}
''' def _repr_html_(self): return self.html def hide(self): return self.question def show(self): return self.answer def mark(self, name='marked'): self.note.mark(name) def unmark(self, name='marked'): self.note.unmark(name) def right(self): if not self.srs_level: self.srs_level = 1 else: self.srs_level = self.srs_level + 1 self.next_review = (datetime.now() + config['srs'].get(int(self.srs_level), timedelta(weeks=4))) self.save() correct = next_srs = right def wrong(self, duration=timedelta(minutes=1)): if self.srs_level and self.srs_level > 1: self.srs_level = self.srs_level - 1 self.bury(duration) incorrect = previous_srs = wrong def bury(self, duration=timedelta(hours=4)): self.next_review = datetime.now() + duration self.save() @classmethod def iter_quiz(cls, deck=None, tags=None): db_cards = list(cls.search(deck=deck, tags=tags)) random.shuffle(db_cards) return iter(db_cards) iter_due = iter_quiz @classmethod def search(cls, deck=None, tags=None): query = cls.select() if deck: query = query.join(Deck).where(Deck.name == deck) if tags: query_params = (Tag.name == tags[0]) for tag in tags[1:]: query_params = (query_params | (Tag.name == tag)) query = query.join(Note).join(NoteTag).join(Tag).where(query_params) return query.order_by(cls.next_review.desc()) def create_all_tables(): for cls in sys.modules[__name__].__dict__.values(): if hasattr(cls, '__bases__') and issubclass(cls, pv.Model): if cls not in (BaseModel, pv.Model): cls.create_table() PK!S@[eeankix/jupyter.pyimport mistune from .util import is_html from .config import config markdown = mistune.Markdown() class HTML: def __init__(self, html, medias=None, css=''): if medias is None: medias = [] self.medias = medias if config['markdown'] or not is_html(html): self._raw = markdown(html) else: self._raw = html self.css = css def _repr_html_(self): return self.html def __repr__(self): return self.html def __str__(self): return self.html @property def html(self): return f'''
{self.raw} ''' @property def raw(self): result = self._raw for image in self.medias: result = result.replace(image.name, image.src) return result PK!š ankix/main.pyfrom .config import config from .convert import from_apkg class Ankix: def __init__(self, database, **kwargs): config.update({ 'database': database, **kwargs }) from . import db # Tag Media Model Template Deck Note Card db.create_all_tables() self.tables = { 'tag': db.Tag, 'media': db.Media, 'model': db.Model, 'template': db.Template, 'deck': db.Deck, 'note': db.Note, 'card': db.Card } @classmethod def from_apkg(cls, src_apkg, dst_ankix=None, **kwargs): return cls(from_apkg(src_apkg=src_apkg, dst_ankix=dst_ankix), **kwargs) def __getitem__(self, item): return self.tables[item] def __getattr__(self, item): return getattr(self.tables['card'], item) def __iter__(self): return iter(self.tables['card'].select()) def __len__(self): return self.tables['card'].select().count() PK!Q ankix/util.pyimport re RE_IS_HTML = re.compile(r"(?:)|(?:<[^<]+/>)") def is_html(s): return RE_IS_HTML.search(s) is not None PK!H WXankix-0.1.0.dist-info/WHEEL A н#Z."jm)Afb~ڠO68oF04UhoAf f4=4h0k::wXPK!Hankix-0.1.0.dist-info/METADATAVO#7~_16H'G^<W%mU!zqkބP ч}cϏo& ))38ؕ0=}٤*a7\D[ >`HV~Y+EL=ڕ(*aFɯ#f%h*֢. T6vrԎȣ)7kckN]-r]5z)ty†_X.IJn2X UfE8Y)Sst62\&ŠW.ck +J..7zYoG==ߗϏx{{|`K,r>ƪm4Bgp?ïyu+KEVp̏>0VAe3C:׾;$LGӲf;"ܴavܔh{-iBafa +Q ϲ 7*\3c?5o: [S $c09NˢxtI C$T^|Re 3ܢ(y|mNՅBUwG>vZ8Vgb EDj]DrffOӐT2v&-GO$B;Zp K.|ttd'R,JHļ`F*xؠ4LUwbG$UmlA^kktԈP[PH$Q&!`065QZQaT\ƟJ,ڻfSZTI7bPN($H~*A(.JG"x4{qr;cwA)c?m7-|S_FN}|_ C:-HnMޛRi* |VКtC%%Gk.֗ME(FԪE'qIf:.D&'`;PGCrPK!Hϓankix-0.1.0.dist-info/RECORDuM0@}! ¢(JD%O׌ָ |ށ& MQ=pM~@WaJ]nX6m4c$ԕw$3}lrq=˛2U_y@X=)V[!iVJuC4#1n,־9 fU=PEEHYױȘaJxpϏP%u[ΝmB][|'Y=@R >t{e^T͚jsv'h| /e@(9I|MULߐ!fȣ "3V[!Y4R *Un8"t-.p:ͲTGhFtm ôgQG#~G?r 0=дՋBK] 'qQùMIQ1.5,dGON~mWTW^֎hz4UVc|r V^տ!_XWo\>PK![rJ77ankix/__init__.pyPK!T|fankix/config.pyPK!Q e "ankix/convert.pyPK!Wexx Iankix/db.pyPK!S@[ee5ankix/jupyter.pyPK!š }9ankix/main.pyPK!Q =ankix/util.pyPK!H WXO>ankix-0.1.0.dist-info/WHEELPK!H>ankix-0.1.0.dist-info/METADATAPK!HϓCankix-0.1.0.dist-info/RECORDPK E