PK!-ankix/__init__.pyfrom .main import Ankix 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! 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): def _sub(x): field_k, content = x.groups() if self.note.data[field_k]: return content else: return '' html = re.sub(r'{{#([^}]+)}}(.*){{/\1}}', _sub, html, flags=re.DOTALL) 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 = query.join(Note).join(NoteTag).join(Tag).where(Tag.name.in_(tags)) 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!dbb ankix/main.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 class Ankix: def __init__(self, database, **kwargs): config.update({ 'database': database, **kwargs }) from . import db db.create_all_tables() self.tables = { 'tag': db.Tag, 'media': db.Media, 'model': db.Model, 'template': db.Template, 'deck': db.Deck, 'note': db.Note, 'note_tag': db.NoteTag, 'note_media': db.NoteMedia, 'card': db.Card } 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() @classmethod def from_apkg(cls, src_apkg, dst_ankix=None, skip_media=False, **kwargs): 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())[int(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() if not skip_media: with open(os.path.join(temp_dir, 'media')) as f: info_media = info.get('media', dict()) 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 cls(dst_ankix, **kwargs) 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! ::ankix-0.1.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 WXankix-0.1.1.4.dist-info/WHEEL A н#Z."jm)Afb~ڠO68oF04UhoAf f4=4h0k::wXPK!H5H7k ankix-0.1.1.4.dist-info/METADATAVao"7_1U*-Hɥ^JPQḆUˮg{!K]+QE xxp9e)&[\\smJi[,+W$TJf0B\#*F{KT"U6vsUxnEqQbmB)*K`~Z6qǶ)f:k&H/aŲ2:X Y$?[|ʹDr WK'!Ֆѹe)U7B5G:Yog#w|w,~쎷GGTKs-Kk6ssIk]y|qyU#K+)+=ᜟg\M:Wt\gH\eb$coh.$R߷^/˜jSQ>gzW!-\b;h98j0mR?S]6˸E 1( ϲs诨AŌqh,(j  j3*qZcw_1cͅY׃e3}q:z,e eފZ(S6ɗ)B)⢀BjP8x8L|pN@3q-[ HXDQ-OZ^5qi9/N+3 4;~# w~2j?p=8& Xir,әxUs`>n^7y:]Wp^3e&' gQ,̌}?~!Jn#A}-b”9>7ēwvF >ZUw/oCyّ™wRIXm-(>JJ/s3oҁyU M=0!Ҙ=e?Ž*[BQ_PK!-ankix/__init__.pyPK!T|Gankix/config.pyPK! ankix/db.pyPK!S@[ee"ankix/jupyter.pyPK!dbb %ankix/main.pyPK!Q 4?ankix/util.pyPK! ::?ankix-0.1.1.4.dist-info/LICENSEPK!H WXUDankix-0.1.1.4.dist-info/WHEELPK!H5H7k Dankix-0.1.1.4.dist-info/METADATAPK!Hӕ!\Iankix-0.1.1.4.dist-info/RECORDPK oK