├── CaseReview ├── static │ ├── card.css │ ├── favicon.ico │ ├── vendor │ │ └── sprintf │ │ │ ├── .gitattributes │ │ │ ├── sprintf.min.js │ │ │ └── sprintf.min.js.map │ ├── card.js │ ├── default.js │ ├── converter.js │ ├── renderer.js │ ├── index.css │ └── index.js ├── config.py ├── srs.py ├── __main__.py ├── templates │ ├── card.html │ └── index.html ├── __init__.py ├── flashcards.py ├── views.py ├── util.py ├── tags.py ├── databases.py └── api.py ├── run.py ├── .idea ├── vcs.xml ├── modules.xml ├── misc.xml └── CaseReview.iml ├── pyproject.toml ├── .gitignore └── pyproject.lock /CaseReview/static/card.css: -------------------------------------------------------------------------------- 1 | .imageViewer { 2 | height: 500px; 3 | } 4 | -------------------------------------------------------------------------------- /CaseReview/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patarapolw/CaseReview/master/CaseReview/static/favicon.ico -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from CaseReview import load_db 2 | 3 | 4 | if __name__ == '__main__': 5 | load_db('user/case.db', debug=True) 6 | -------------------------------------------------------------------------------- /CaseReview/static/vendor/sprintf/.gitattributes: -------------------------------------------------------------------------------- 1 | #ignore all generated files from diff 2 | #also skip line ending check 3 | *.js -diff -text 4 | *.map -diff -text 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CaseReview/static/card.js: -------------------------------------------------------------------------------- 1 | if(!show){ 2 | document.getElementById('show-area').innerHTML = markdown2html(card.front, card); 3 | } else { 4 | document.getElementById('show-area').innerHTML = markdown2html(card.back, card); 5 | } 6 | -------------------------------------------------------------------------------- /CaseReview/static/default.js: -------------------------------------------------------------------------------- 1 | let defaultConfig = { 2 | rowHeaders: false, 3 | manualRowResize: true, 4 | manualColumnResize: true, 5 | maxColWidth: 500, 6 | contextMenu: true, 7 | undo: true, 8 | copyPaste: true, 9 | autoWrapCol: false 10 | }; 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CaseReview/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config(object): 5 | DATABASE_URI = os.path.abspath(os.getenv('DATABASE_URI', 'new.db')) 6 | IMAGE_DATABASE_FOLDER = os.path.splitext(DATABASE_URI)[0] 7 | 8 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE_URI 9 | SQLALCHEMY_TRACK_MODIFICATIONS = False 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /CaseReview/srs.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | SRS = { 4 | 1: timedelta(minutes=10), 5 | 2: timedelta(hours=4), 6 | 3: timedelta(hours=8), 7 | 4: timedelta(days=1), 8 | 5: timedelta(days=3), 9 | 6: timedelta(days=7), 10 | 7: timedelta(weeks=2), 11 | 8: timedelta(weeks=4), 12 | 9: timedelta(weeks=16) 13 | } 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "CaseReview" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["patarapolw "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "*" 9 | flask = "^1.0" 10 | jupyter = "^1.0" 11 | notebook = "^5.6" 12 | flask_sqlalchemy = "^2.3" 13 | python-dateutil = "^2.7" 14 | 15 | [tool.poetry.dev-dependencies] 16 | -------------------------------------------------------------------------------- /CaseReview/__main__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from . import load_db 4 | 5 | 6 | @click.command() 7 | @click.argument('filename') 8 | @click.option('--host', default='localhost') 9 | @click.option('--port', default=8000) 10 | @click.option('--debug', is_flag=True) 11 | def cli(filename, host, port, debug): 12 | load_db(filename, host, port, debug) 13 | 14 | 15 | if __name__ == '__main__': 16 | cli() 17 | -------------------------------------------------------------------------------- /.idea/CaseReview.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /CaseReview/templates/card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CaseReview/static/converter.js: -------------------------------------------------------------------------------- 1 | const markdownConverter = new showdown.Converter; 2 | const img_regex = /(?:(?=^)|(?=\s).|^)([^\s<>"\']+\.(?:png|jpg|jpeg|gif))/gi; 3 | 4 | function markdown2html(text, card){ 5 | text = text.replace(/\n+/g, "\n\n"); 6 | text = text.replace(img_regex, ""); 7 | 8 | if(card.data){ 9 | let itemData = JSON.parse(card.data); 10 | // console.log(itemData); 11 | text = sprintf(text, itemData); 12 | } 13 | 14 | return markdownConverter.makeHtml(text); 15 | } 16 | 17 | function imageUrl2html(text){ 18 | return text.replace(img_regex, ""); 19 | } 20 | -------------------------------------------------------------------------------- /CaseReview/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | import os 5 | 6 | from .util import open_browser_tab 7 | from .config import Config 8 | 9 | app = Flask(__name__) 10 | app.config.from_object(Config) 11 | 12 | db = SQLAlchemy(app) 13 | 14 | from .views import * 15 | from .api import * 16 | 17 | 18 | def load_db(filename, host='localhost', port=8000, debug=False): 19 | os.environ['DATABASE_URI'] = filename 20 | if not os.path.exists(filename): 21 | db.create_all() 22 | 23 | os.environ['HOST'] = host 24 | os.environ['PORT'] = str(port) 25 | 26 | open_browser_tab('http://{}:{}'.format(host, port)) 27 | 28 | app.run( 29 | host=host, 30 | port=port, 31 | debug=debug 32 | ) 33 | -------------------------------------------------------------------------------- /CaseReview/static/renderer.js: -------------------------------------------------------------------------------- 1 | (function(Handsontable){ 2 | function customRenderer(hotInstance, td, row, column, prop, value, cellProperties) { 3 | let text = Handsontable.helper.stringify(value); 4 | td.innerHTML = markdown2html(text, data[row]); 5 | 6 | return td; 7 | } 8 | 9 | // Register an alias 10 | Handsontable.renderers.registerRenderer('markdownRenderer', customRenderer); 11 | 12 | })(Handsontable); 13 | 14 | (function(Handsontable){ 15 | function customRenderer(hotInstance, td, row, column, prop, value, cellProperties) { 16 | let text = Handsontable.helper.stringify(value); 17 | td.innerHTML = imageUrl2html(text); 18 | 19 | return td; 20 | } 21 | 22 | // Register an alias 23 | Handsontable.renderers.registerRenderer('imageRenderer', customRenderer); 24 | 25 | })(Handsontable); 26 | -------------------------------------------------------------------------------- /CaseReview/flashcards.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime 3 | 4 | from .databases import CaseRecord 5 | from .tags import tag_reader 6 | 7 | 8 | def iter_quiz(is_due=True, tag=None): 9 | def _filter_is_due(srs_record): 10 | if is_due is None: 11 | return True 12 | elif is_due is True: 13 | if not srs_record.next_review or srs_record.next_review < datetime.now(): 14 | return True 15 | else: 16 | if srs_record.next_review is None: 17 | return True 18 | return False 19 | 20 | def _filter_tag(srs_record): 21 | if not tag: 22 | return True 23 | elif tag in tag_reader(srs_record.tags): 24 | return True 25 | else: 26 | return False 27 | 28 | def _filter(): 29 | for srs_record in CaseRecord.query.order_by(CaseRecord.modified.desc()): 30 | if all((_filter_is_due(srs_record), 31 | _filter_tag(srs_record))): 32 | yield srs_record 33 | 34 | all_records = list(_filter()) 35 | random.shuffle(all_records) 36 | 37 | return iter(all_records) 38 | -------------------------------------------------------------------------------- /CaseReview/views.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, send_from_directory 2 | 3 | import os 4 | 5 | from . import app, Config 6 | from .databases import CaseRecord, CaseTuple 7 | 8 | 9 | @app.route('/') 10 | def index(): 11 | config = { 12 | 'colHeaders': CaseTuple.__slots__, 13 | 'renderers': { 14 | 'front': 'markdownRenderer', 15 | 'back': 'markdownRenderer' 16 | }, 17 | 'colWidths': [92, 88, 243, 529, 148, 125, 206] 18 | } 19 | 20 | return render_template('index.html', title=os.getenv('DATABASE_URI', ''), config=config) 21 | 22 | 23 | @app.route('/card/') 24 | def card(card_id): 25 | record = CaseRecord.query.filter_by(id=card_id).first() 26 | return render_template('card.html', card=dict(CaseTuple().from_db(record)), show=False) 27 | 28 | 29 | @app.route('/card//show') 30 | def card_show(card_id): 31 | record = CaseRecord.query.filter_by(id=card_id).first() 32 | return render_template('card.html', card=dict(CaseTuple().from_db(record)), show=True) 33 | 34 | 35 | @app.route('/images/') 36 | def get_image(filename): 37 | return send_from_directory(Config.IMAGE_DATABASE_FOLDER, filename) 38 | -------------------------------------------------------------------------------- /CaseReview/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | # from markdown import markdown 3 | import webbrowser 4 | from threading import Thread 5 | from time import sleep 6 | 7 | 8 | def get_url_images_in_text(text): 9 | return re.findall(r'((?:(?<=^)|(?<=\s))[^\s<>\"\']+\.(?:png|jpg|jpeg|gif))', text, flags=re.IGNORECASE) 10 | 11 | 12 | def compare_list_match_regex(subset, superset): 13 | def _sub_compare(): 14 | for super_item in superset: 15 | if re.search(sub_item, super_item, flags=re.IGNORECASE): 16 | return True 17 | 18 | return False 19 | 20 | result = [] 21 | for sub_item in subset: 22 | result.append(_sub_compare()) 23 | 24 | return all(result) 25 | 26 | 27 | # def parse_markdown(text, image_width=500): 28 | # text = re.sub(r'\n+', '\n\n', text) 29 | # for url in get_url_images_in_text(text): 30 | # text = text.replace(url, ''.format(url, image_width)) 31 | # 32 | # return markdown(text) 33 | 34 | 35 | def open_browser_tab(url): 36 | def _open_tab(): 37 | sleep(1) 38 | webbrowser.open_new_tab(url) 39 | 40 | thread = Thread(target=_open_tab) 41 | thread.daemon = True 42 | thread.start() 43 | -------------------------------------------------------------------------------- /CaseReview/static/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0px; 3 | } 4 | 5 | body { 6 | width: 100%; 7 | min-width: 100%; 8 | } 9 | 10 | #nav-area { 11 | border: 1px solid #ccc; 12 | background-color: #f1f1f1; 13 | display: inline-table; 14 | width: 100%; 15 | } 16 | 17 | /* Style the buttons that are used to open the tab content */ 18 | #new-record, .page-button { 19 | background-color: inherit; 20 | border: none; 21 | outline: none; 22 | padding: 14px 16px; 23 | margin: 0px; 24 | transition: 0.3s; 25 | font-size: 17px; 26 | } 27 | 28 | .page-button:disabled { 29 | color: white; 30 | } 31 | 32 | #nav-page-button, #nav-page-button > *, #page-label > * { 33 | display: inherit; 34 | } 35 | 36 | #page-label > * { 37 | margin-left: 0.5em; 38 | margin-right: 0.5em; 39 | } 40 | 41 | .nav-right { 42 | float: right; 43 | z-index: 100; 44 | } 45 | 46 | #search-bar { 47 | font-size: 15px; 48 | margin-right: 7px; 49 | } 50 | 51 | #new-record { 52 | background-color: #27ae60; 53 | color: white; 54 | } 55 | 56 | #handsontable-container { 57 | overflow: hidden; 58 | } 59 | 60 | ::-webkit-scrollbar { 61 | width: 0px; /* remove scrollbar space */ 62 | background: transparent; /* optional: just make scrollbar invisible */ 63 | } 64 | 65 | td p { 66 | margin-top: 0; 67 | margin-bottom: 0; 68 | } 69 | 70 | td ul { 71 | margin-top: -1em; 72 | margin-bottom: -1em; 73 | } 74 | 75 | td li { 76 | margin: -0.5em; 77 | } 78 | 79 | .imageViewer { 80 | width: 200px; 81 | } 82 | -------------------------------------------------------------------------------- /CaseReview/tags.py: -------------------------------------------------------------------------------- 1 | def tag_reader(raw_tags: str): 2 | """ 3 | :param str raw_tags: 4 | :return list: 5 | >>> tag_reader('presenilin-1 presenilin-2') 6 | {'presenilin-1', 'presenilin-2'} 7 | >>> tag_reader('“Bouchard microaneurysms”') 8 | {'Bouchard microaneurysms'} 9 | >>> tag_reader('astrocytoma “Rosenthal fibers”') 10 | {'astrocytoma', 'Rosenthal fibers'} 11 | >>> tag_reader('“Frontotemporal dementia” TDP-43') 12 | {'Frontotemporal dementia', 'TDP-43'} 13 | """ 14 | output = set() 15 | tag = '' 16 | do_purge = True 17 | for char in raw_tags: 18 | if char == '“': 19 | do_purge = False 20 | elif char == '”': 21 | do_purge = True 22 | elif char == '\"': 23 | do_purge = not do_purge 24 | elif char == ' ' and do_purge: 25 | output.add(tag) 26 | tag = '' 27 | else: 28 | tag += char 29 | 30 | if tag != '': 31 | output.add(tag) 32 | 33 | return output 34 | 35 | 36 | def to_raw_tags(tags: iter): 37 | """ 38 | :param iter tags: 39 | :return str: 40 | >>> to_raw_tags(['presenilin-1', 'presenilin-2']) 41 | 'presenilin-1 presenilin-2' 42 | >>> to_raw_tags(['Bouchard microaneurysms']) 43 | '“Bouchard microaneurysms”' 44 | >>> to_raw_tags(['astrocytoma', 'Rosenthal fibers']) 45 | 'astrocytoma “Rosenthal fibers”' 46 | >>> to_raw_tags(['Frontotemporal dementia', 'TDP-43']) 47 | '“Frontotemporal dementia” TDP-43' 48 | """ 49 | if tags is None: 50 | tags = list() 51 | 52 | if isinstance(tags, str): 53 | tags = tags.split(' ') 54 | 55 | formatted_tags = set() 56 | for entry in set(tags): 57 | if ' ' in entry or '\"' in entry: 58 | entry = '“{}”'.format(entry.replace('\"', '\\\"')) 59 | formatted_tags.add(entry) 60 | 61 | return ' '.join(formatted_tags) 62 | -------------------------------------------------------------------------------- /CaseReview/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ title }} 14 | 15 | 16 | 33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /CaseReview/static/vendor/sprintf/sprintf.min.js: -------------------------------------------------------------------------------- 1 | /*! sprintf-js v1.1.1 | Copyright (c) 2007-present, Alexandru Mărășteanu | BSD-3-Clause */ 2 | !function(){"use strict";function e(e){return r(n(e),arguments)}function t(t,r){return e.apply(null,[t].concat(r||[]))}function r(t,r){var n,i,a,o,p,c,u,f,l,d=1,g=t.length,b="";for(i=0;i=0),o[8]){case"b":n=parseInt(n,10).toString(2);break;case"c":n=String.fromCharCode(parseInt(n,10));break;case"d":case"i":n=parseInt(n,10);break;case"j":n=JSON.stringify(n,null,o[6]?parseInt(o[6]):0);break;case"e":n=o[7]?parseFloat(n).toExponential(o[7]):parseFloat(n).toExponential();break;case"f":n=o[7]?parseFloat(n).toFixed(o[7]):parseFloat(n);break;case"g":n=o[7]?String(Number(n.toPrecision(o[7]))):parseFloat(n);break;case"o":n=(parseInt(n,10)>>>0).toString(8);break;case"s":n=String(n),n=o[7]?n.substring(0,o[7]):n;break;case"t":n=String(!!n),n=o[7]?n.substring(0,o[7]):n;break;case"T":n=Object.prototype.toString.call(n).slice(8,-1).toLowerCase(),n=o[7]?n.substring(0,o[7]):n;break;case"u":n=parseInt(n,10)>>>0;break;case"v":n=n.valueOf(),n=o[7]?n.substring(0,o[7]):n;break;case"x":n=(parseInt(n,10)>>>0).toString(16);break;case"X":n=(parseInt(n,10)>>>0).toString(16).toUpperCase()}s.json.test(o[8])?b+=n:(!s.number.test(o[8])||f&&!o[3]?l="":(l=f?"+":"-",n=n.toString().replace(s.sign,"")),c=o[4]?"0"===o[4]?"0":o[4].charAt(1):" ",u=o[6]-(l+n).length,p=o[6]&&u>0?c.repeat(u):"",b+=o[5]?l+n+p:"0"===c?l+p+n:p+l+n)}return b}function n(e){if(i[e])return i[e];for(var t,r=e,n=[],a=0;r;){if(null!==(t=s.text.exec(r)))n.push(t[0]);else if(null!==(t=s.modulo.exec(r)))n.push("%");else{if(null===(t=s.placeholder.exec(r)))throw new SyntaxError("[sprintf] unexpected placeholder");if(t[2]){a|=1;var o=[],p=t[2],c=[];if(null===(c=s.key.exec(p)))throw new SyntaxError("[sprintf] failed to parse named argument key");for(o.push(c[1]);""!==(p=p.substring(c[0].length));)if(null!==(c=s.key_access.exec(p)))o.push(c[1]);else{if(null===(c=s.index_access.exec(p)))throw new SyntaxError("[sprintf] failed to parse named argument key");o.push(c[1])}t[2]=o}else a|=2;if(3===a)throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");n.push(t)}r=r.substring(t[0].length)}return i[e]=n}var s={not_string:/[^s]/,not_bool:/[^t]/,not_type:/[^T]/,not_primitive:/[^v]/,number:/[diefg]/,numeric_arg:/[bcdiefguxX]/,json:/[j]/,not_json:/[^j]/,text:/^[^\x25]+/,modulo:/^\x25{2}/,placeholder:/^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,key:/^([a-z_][a-z_\d]*)/i,key_access:/^\.([a-z_][a-z_\d]*)/i,index_access:/^\[(\d+)\]/,sign:/^[\+\-]/},i=Object.create(null);"undefined"!=typeof exports&&(exports.sprintf=e,exports.vsprintf=t),"undefined"!=typeof window&&(window.sprintf=e,window.vsprintf=t,"function"==typeof define&&define.amd&&define(function(){return{sprintf:e,vsprintf:t}}))}(); 3 | //# sourceMappingURL=sprintf.min.js.map 4 | -------------------------------------------------------------------------------- /CaseReview/databases.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import dateutil.parser 3 | from IPython.display import IFrame 4 | import os 5 | 6 | from . import db 7 | from .srs import SRS 8 | from .tags import tag_reader, to_raw_tags 9 | from .util import get_url_images_in_text 10 | 11 | 12 | class CaseRecord(db.Model): 13 | __tablename__ = 'case' 14 | 15 | id = db.Column(db.Integer, primary_key=True, nullable=False, unique=True, autoincrement=True) 16 | 17 | patho_id = db.Column(db.String, nullable=False, unique=True) 18 | patient_id = db.Column(db.String) 19 | 20 | front = db.Column(db.String) 21 | back = db.Column(db.String) 22 | 23 | data = db.Column(db.String) 24 | 25 | keywords = db.Column(db.String) 26 | tags = db.Column(db.String) 27 | 28 | created = db.Column(db.DateTime, default=datetime.now) 29 | modified = db.Column(db.DateTime, default=datetime.now) 30 | 31 | srs_level = db.Column(db.Integer) 32 | next_review = db.Column(db.DateTime) 33 | 34 | def hide(self): 35 | if len(get_url_images_in_text(self.front)) > 0: 36 | height = 500 37 | else: 38 | height = 100 39 | 40 | return IFrame('http://{}:{}/card/{}'.format(os.getenv('HOST', 'localhost'), 41 | os.getenv('PORT', 8000), 42 | self.id), 43 | width=800, height=height) 44 | 45 | def show(self): 46 | if len(get_url_images_in_text(self.back)) > 0: 47 | height = 500 48 | else: 49 | height = 200 50 | 51 | return IFrame('http://{}:{}/card/{}/show'.format(os.getenv('HOST', 'localhost'), 52 | os.getenv('PORT', 8000), 53 | self.id), 54 | width=800, height=height) 55 | 56 | def next_srs(self): 57 | if not self.srs_level: 58 | self.srs_level = 1 59 | else: 60 | self.srs_level = self.srs_level + 1 61 | 62 | self.next_review = (datetime.now() 63 | + SRS.get(int(self.srs_level), timedelta(weeks=4))) 64 | self.modified = datetime.now() 65 | 66 | correct = right = next_srs 67 | 68 | def previous_srs(self, duration=timedelta(minutes=1)): 69 | if self.srs_level and self.srs_level > 1: 70 | self.srs_level = self.srs_level - 1 71 | 72 | self.bury(duration) 73 | 74 | incorrect = wrong = previous_srs 75 | 76 | def bury(self, duration=timedelta(minutes=1)): 77 | self.next_review = datetime.now() + duration 78 | self.modified = datetime.now() 79 | 80 | def mark(self, tag_name: str='marked'): 81 | if self.tags is None: 82 | self.tags = '' 83 | 84 | all_tags = tag_reader(self.tags) 85 | all_tags.add(tag_name) 86 | self.tags = to_raw_tags(all_tags) 87 | 88 | def unmark(self, tag_name: str='marked'): 89 | if self.tags is None: 90 | self.tags = '' 91 | 92 | all_tags = tag_reader(self.tags) 93 | if tag_name in all_tags: 94 | all_tags.remove(tag_name) 95 | self.tags = to_raw_tags(all_tags) 96 | 97 | 98 | class CaseTuple: 99 | __slots__ = ( 100 | 'patho_id', 'patient_id', 101 | 'front', 'back', 'keywords', 'tags', 102 | 'created', 103 | # 'srs_level', 'next_review' 104 | ) 105 | 106 | def __init__(self, *args): 107 | for i, arg in enumerate(args): 108 | setattr(self, self.__slots__[i], arg) 109 | 110 | def to_db(self): 111 | entry = dict() 112 | for key in self.__slots__: 113 | entry[key] = getattr(self, key, None) 114 | if entry[key] is not None: 115 | self.parse(key, entry[key]) 116 | 117 | return entry 118 | 119 | @staticmethod 120 | def parse(key, value): 121 | if key in ('created', 'modified', 'next_review'): 122 | return dateutil.parser.parse(value) 123 | else: 124 | return value 125 | 126 | def from_db(self, srs_record): 127 | yield 'id', getattr(srs_record, 'id', None) 128 | 129 | for field in self.__slots__: 130 | value = getattr(srs_record, field, None) 131 | if isinstance(value, datetime): 132 | value = value.isoformat() 133 | 134 | setattr(self, field, value) 135 | 136 | yield field, value 137 | 138 | yield 'data', getattr(srs_record, 'data', None) 139 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,python,pycharm 3 | 4 | ### OSX ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### PyCharm ### 33 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 34 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 35 | 36 | # User-specific stuff 37 | .idea/**/workspace.xml 38 | .idea/**/tasks.xml 39 | .idea/**/usage.statistics.xml 40 | .idea/**/dictionaries 41 | .idea/**/shelf 42 | 43 | # Sensitive or high-churn files 44 | .idea/**/dataSources/ 45 | .idea/**/dataSources.ids 46 | .idea/**/dataSources.local.xml 47 | .idea/**/sqlDataSources.xml 48 | .idea/**/dynamic.xml 49 | .idea/**/uiDesigner.xml 50 | .idea/**/dbnavigator.xml 51 | 52 | # Gradle 53 | .idea/**/gradle.xml 54 | .idea/**/libraries 55 | 56 | # Gradle and Maven with auto-import 57 | # When using Gradle or Maven with auto-import, you should exclude module files, 58 | # since they will be recreated, and may cause churn. Uncomment if using 59 | # auto-import. 60 | # .idea/modules.xml 61 | # .idea/*.iml 62 | # .idea/modules 63 | 64 | # CMake 65 | cmake-build-*/ 66 | 67 | # Mongo Explorer plugin 68 | .idea/**/mongoSettings.xml 69 | 70 | # File-based project format 71 | *.iws 72 | 73 | # IntelliJ 74 | out/ 75 | 76 | # mpeltonen/sbt-idea plugin 77 | .idea_modules/ 78 | 79 | # JIRA plugin 80 | atlassian-ide-plugin.xml 81 | 82 | # Cursive Clojure plugin 83 | .idea/replstate.xml 84 | 85 | # Crashlytics plugin (for Android Studio and IntelliJ) 86 | com_crashlytics_export_strings.xml 87 | crashlytics.properties 88 | crashlytics-build.properties 89 | fabric.properties 90 | 91 | # Editor-based Rest Client 92 | .idea/httpRequests 93 | 94 | ### PyCharm Patch ### 95 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 96 | 97 | # *.iml 98 | # modules.xml 99 | # .idea/misc.xml 100 | # *.ipr 101 | 102 | # Sonarlint plugin 103 | .idea/sonarlint 104 | 105 | ### Python ### 106 | # Byte-compiled / optimized / DLL files 107 | __pycache__/ 108 | *.py[cod] 109 | *$py.class 110 | 111 | # C extensions 112 | *.so 113 | 114 | # Distribution / packaging 115 | .Python 116 | build/ 117 | develop-eggs/ 118 | dist/ 119 | downloads/ 120 | eggs/ 121 | .eggs/ 122 | lib/ 123 | lib64/ 124 | parts/ 125 | sdist/ 126 | var/ 127 | wheels/ 128 | *.egg-info/ 129 | .installed.cfg 130 | *.egg 131 | MANIFEST 132 | 133 | # PyInstaller 134 | # Usually these files are written by a python script from a template 135 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 136 | *.manifest 137 | *.spec 138 | 139 | # Installer logs 140 | pip-log.txt 141 | pip-delete-this-directory.txt 142 | 143 | # Unit test / coverage reports 144 | htmlcov/ 145 | .tox/ 146 | .coverage 147 | .coverage.* 148 | .cache 149 | nosetests.xml 150 | coverage.xml 151 | *.cover 152 | .hypothesis/ 153 | .pytest_cache/ 154 | 155 | # Translations 156 | *.mo 157 | *.pot 158 | 159 | # Django stuff: 160 | *.log 161 | local_settings.py 162 | db.sqlite3 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### Python Patch ### 212 | .venv/ 213 | 214 | ### Python.VirtualEnv Stack ### 215 | # Virtualenv 216 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 217 | [Bb]in 218 | [Ii]nclude 219 | [Ll]ib 220 | [Ll]ib64 221 | [Ll]ocal 222 | [Ss]cripts 223 | pyvenv.cfg 224 | pip-selfcheck.json 225 | 226 | 227 | # End of https://www.gitignore.io/api/osx,python,pycharm 228 | 229 | user/* 230 | -------------------------------------------------------------------------------- /CaseReview/api.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify, Response 2 | from werkzeug.utils import secure_filename 3 | 4 | import math 5 | from datetime import datetime 6 | import os 7 | import re 8 | import sqlalchemy.exc 9 | 10 | from . import app, db, Config 11 | from .databases import CaseRecord, CaseTuple 12 | from .util import get_url_images_in_text 13 | 14 | sort_by = { 15 | 'column': 'modified', 16 | 'desc': True 17 | } 18 | 19 | 20 | def get_ordering(): 21 | result = getattr(CaseRecord, sort_by['column']) 22 | 23 | if sort_by['desc']: 24 | return result.desc() 25 | else: 26 | return result 27 | 28 | 29 | @app.route('/api/all/') 30 | def all_records(page_number, page_size=10): 31 | def _filter(): 32 | for srs_record in CaseRecord.query.order_by(get_ordering()): 33 | yield srs_record 34 | 35 | page_number = int(page_number) 36 | 37 | query = list(_filter()) 38 | total = len(query) 39 | 40 | if page_number < 0: 41 | page_number = math.ceil(total / page_size) + page_number + 1 42 | 43 | offset = (page_number - 1) * page_size 44 | 45 | records = query[offset:offset + page_size] 46 | 47 | data = [dict(CaseTuple().from_db(record)) for record in records] 48 | 49 | if len(data) == 0: 50 | data = [dict(CaseTuple().from_db(None))] 51 | 52 | return jsonify({ 53 | 'data': data, 54 | 'pages': { 55 | 'from': offset + 1 if total > 0 else 0, 56 | 'to': total if offset + page_size > total else offset + page_size, 57 | 'number': page_number, 58 | 'total': total 59 | } 60 | }) 61 | 62 | 63 | @app.route('/api/search/', methods=['POST']) 64 | def search(page_number, page_size=10): 65 | def _search(): 66 | query_string = request.get_json()['q'].lower() 67 | 68 | for srs_record in CaseRecord.query.order_by(get_ordering()): 69 | front = srs_record.front 70 | if front: 71 | for url in get_url_images_in_text(front): 72 | front = front.replace(url, ' ') 73 | 74 | back = srs_record.back 75 | if back: 76 | for url in get_url_images_in_text(back): 77 | back = back.replace(url, ' ') 78 | 79 | if any([query_string in cell.lower() 80 | for cell in (front, back, srs_record.tags, srs_record.keywords) if cell]): 81 | yield srs_record 82 | 83 | page_number = int(page_number) 84 | 85 | query = list(_search()) 86 | total = len(query) 87 | 88 | if page_number < 0: 89 | page_number = math.ceil(total / page_size) + page_number + 1 90 | 91 | offset = (page_number - 1) * page_size 92 | 93 | records = query[offset:offset + page_size] 94 | 95 | data = [dict(CaseTuple().from_db(record)) for record in records] 96 | 97 | if len(data) == 0: 98 | data = [dict(CaseTuple().from_db(None))] 99 | 100 | return jsonify({ 101 | 'data': data, 102 | 'pages': { 103 | 'from': offset + 1 if total > 0 else 0, 104 | 'to': total if offset + page_size > total else offset + page_size, 105 | 'number': page_number, 106 | 'total': total 107 | } 108 | }) 109 | 110 | 111 | @app.route('/api/edit', methods=['POST']) 112 | def edit_record(): 113 | record = request.get_json() 114 | old_record = CaseRecord.query.filter_by(id=record['id']).first() 115 | if old_record is None: 116 | srs_tuple = CaseTuple() 117 | setattr(srs_tuple, record['fieldName'], record['data']) 118 | 119 | new_record = CaseRecord(**srs_tuple.to_db()) 120 | 121 | try: 122 | db.session.add(new_record) 123 | db.session.commit() 124 | record_id = new_record.id 125 | except sqlalchemy.exc.IntegrityError: 126 | return Response(status=400) 127 | else: 128 | setattr(old_record, 129 | record['fieldName'], 130 | CaseTuple.parse(record['fieldName'], record['data'])) 131 | old_record.modified = datetime.now() 132 | 133 | db.session.commit() 134 | record_id = old_record.id 135 | 136 | return jsonify({ 137 | 'id': record_id 138 | }), 201 139 | 140 | 141 | @app.route('/api/delete/', methods=['DELETE']) 142 | def delete_record(record_id): 143 | srs_record = CaseRecord.query.filter_by(id=record_id).first() 144 | front = srs_record.front 145 | 146 | db.session.delete(srs_record) 147 | db.session.commit() 148 | 149 | return jsonify({ 150 | 'id': record_id, 151 | 'front': front 152 | }), 303 153 | 154 | 155 | @app.route('/api/images/create', methods=['POST']) 156 | def create_image(): 157 | if not os.path.exists(Config.IMAGE_DATABASE_FOLDER): 158 | os.mkdir(Config.IMAGE_DATABASE_FOLDER) 159 | 160 | if 'file' in request.files: 161 | file = request.files['file'] 162 | filename = secure_filename(file.filename) 163 | while os.path.exists(os.path.join(Config.IMAGE_DATABASE_FOLDER, filename)): 164 | filename_stem, filename_ext = os.path.splitext(filename) 165 | today_date = datetime.now().isoformat()[:10] 166 | 167 | match_obj = re.search(f'(.*{today_date}-)(\d+)$', filename_stem) 168 | if match_obj is not None: 169 | filename_stem = match_obj.group(1) + str(int(match_obj.group(2)) + 1) 170 | else: 171 | match_obj = re.search(f'.*{today_date}$', filename_stem) 172 | if match_obj is not None: 173 | filename_stem = filename_stem + '-0' 174 | else: 175 | filename_stem = filename_stem + today_date 176 | 177 | filename = filename_stem + filename_ext 178 | 179 | file.save(os.path.join(Config.IMAGE_DATABASE_FOLDER, filename)) 180 | 181 | return jsonify({ 182 | 'filename': filename 183 | }), 201 184 | 185 | return Response(status=304) 186 | 187 | 188 | @app.route('/api/sort_by/', methods=['POST']) 189 | def new_sort(column): 190 | if column == sort_by['column']: 191 | sort_by['desc'] = not sort_by['desc'] 192 | else: 193 | sort_by['column'] = column 194 | 195 | return Response(status=201) 196 | -------------------------------------------------------------------------------- /CaseReview/static/index.js: -------------------------------------------------------------------------------- 1 | let hot; 2 | let pageNumber = 1; 3 | let data; 4 | 5 | readSearchBarValue(document.getElementById('search-bar').value); 6 | 7 | document.body.addEventListener('keydown', (e)=>{ 8 | e = e || window.event; 9 | const key = e.which || e.keyCode; 10 | const keyF = 102; 11 | const keyf = 70; 12 | 13 | if((key === keyf || key === keyF) && (e.ctrlKey || e.metaKey)){ 14 | e.preventDefault(); 15 | document.getElementById('search-bar').focus(); 16 | } 17 | }); 18 | 19 | document.getElementById('search-bar').addEventListener('keyup', (e)=>{ 20 | pageNumber = 1; 21 | readSearchBarValue(e.target.value); 22 | }); 23 | 24 | document.getElementById('previous-all').onclick = ()=>{ 25 | pageNumber = 1; 26 | readSearchBarValue(document.getElementById('search-bar').value); 27 | } 28 | 29 | document.getElementById('previous').onclick = ()=>{ 30 | pageNumber--; 31 | readSearchBarValue(document.getElementById('search-bar').value); 32 | } 33 | 34 | document.getElementById('next-all').onclick = ()=>{ 35 | pageNumber = -1; 36 | readSearchBarValue(document.getElementById('search-bar').value); 37 | } 38 | 39 | document.getElementById('next').onclick = ()=>{ 40 | pageNumber++; 41 | readSearchBarValue(document.getElementById('search-bar').value); 42 | } 43 | 44 | document.getElementById('new-record').onclick = ()=>{ 45 | insertEmptyRow(); 46 | }; 47 | 48 | window.addEventListener('resize', ()=>{ 49 | const container = document.getElementById('handsontable-container'); 50 | const dimension = getTrueWindowDimension(); 51 | 52 | Object.assign(container.style, dimension); 53 | Object.assign(document.getElementsByClassName('wtHolder')[0].style, dimension); 54 | }); 55 | 56 | function fetchAll(){ 57 | fetch('/api/all/' + pageNumber).then((response)=>{ 58 | response.json().then((responseJson)=>{ 59 | data = responseJson.data; 60 | loadData(); 61 | setPageNav(responseJson.pages); 62 | }); 63 | }); 64 | } 65 | 66 | function readSearchBarValue(queryString){ 67 | if(!queryString){ 68 | fetchAll() 69 | } else { 70 | fetch('/api/search/' + pageNumber, { 71 | method: 'POST', 72 | headers: { 73 | "Content-Type": "application/json; charset=utf-8" 74 | }, 75 | body: JSON.stringify({ 76 | q: queryString 77 | }) 78 | }).then((response)=>{ 79 | response.json().then((responseJson)=>{ 80 | data = responseJson.data; 81 | loadData(); 82 | setPageNav(responseJson.pages); 83 | }); 84 | }); 85 | } 86 | } 87 | 88 | function setPageNav(pages){ 89 | pageNumber = pages.number; 90 | 91 | document.getElementById('page-label-current').innerHTML = pages.from + '-' + pages.to; 92 | document.getElementById('page-label-total').innerHTML = pages.total; 93 | 94 | if(pages.from > 1){ 95 | document.getElementById('previous-all').disabled = false; 96 | document.getElementById('previous').disabled = false; 97 | } else { 98 | document.getElementById('previous-all').disabled = true; 99 | document.getElementById('previous').disabled = true; 100 | } 101 | 102 | if(pages.to < pages.total){ 103 | document.getElementById('next-all').disabled = false; 104 | document.getElementById('next').disabled = false; 105 | } else { 106 | document.getElementById('next-all').disabled = true; 107 | document.getElementById('next').disabled = true; 108 | } 109 | } 110 | 111 | function getTrueWindowDimension(){ 112 | return { 113 | height: (window.innerHeight 114 | - document.getElementById('nav-area').offsetHeight 115 | - 10) + 'px', 116 | width: window.innerWidth + 'px' 117 | }; 118 | } 119 | 120 | function loadData() { 121 | if(hot) hot.destroy(); 122 | 123 | const container = document.getElementById('handsontable-container'); 124 | const dimension = getTrueWindowDimension(); 125 | 126 | Object.assign(container.style, dimension); 127 | 128 | let actualConfig = { 129 | columns: [], 130 | data: data, 131 | afterChange: (changes, source)=>{ 132 | sendChanges(changes, source); 133 | }, 134 | beforeRemoveRow: (index, amount, physicalRows)=>{ 135 | if(data[index].id){ 136 | if(confirm('Are you sure you want to remove ' + data[index].id + ': ' + data[index].front + '?')){ 137 | fetch('/api/delete/' + data[index].id, { 138 | method: 'DELETE' 139 | }).then(response=>{ 140 | if(response.status === 303){ 141 | response.json().then(responseJson=>{ 142 | alert('Removed ' + responseJson.id +': ' + responseJson.front); 143 | }); 144 | } else { 145 | alert('Not removed from database (refresh to reload).'); 146 | } 147 | }).catch(error => console.error(`Fetch Error =\n`, error)); 148 | } else { 149 | return false; 150 | } 151 | } 152 | }, 153 | afterBeginEditing: (row, column)=>{ 154 | if(data[row].data){ 155 | let itemData = JSON.parse(data[row].data); 156 | console.log(itemData); 157 | } 158 | }, 159 | afterOnCellMouseDown: (event, coords, td)=>{ 160 | if(coords.row === -1){ 161 | fetch('/api/sort_by/' + td.textContent.trim(), { 162 | method: 'POST', 163 | }).then(response=>{ 164 | if(response.status === 201){ 165 | readSearchBarValue(document.getElementById('search-bar').value); 166 | } 167 | }) 168 | } 169 | }, 170 | beforePaste: (data, coords)=>{ 171 | const items = (event.clipboardData || event.originalEvent.clipboardData).items; 172 | // console.log(items); // will give you the mime types 173 | for (index in items) { 174 | const item = items[index]; 175 | if (item.kind === 'file') { 176 | const file = item.getAsFile(); 177 | console.log(file); 178 | let reader = new FileReader(); 179 | reader.onload = function(event) { 180 | const extension = file.type.match(/\/([a-z0-9]+)/i)[1].toLowerCase(); 181 | 182 | let formData = new FormData(); 183 | formData.append('file', file, file.name); 184 | formData.append('extension', extension); 185 | formData.append('mimetype', file.type); 186 | formData.append('submission-type', 'paste'); 187 | fetch('/api/images/create', { 188 | method: 'POST', 189 | body: formData 190 | }).then(response=>response.json()) 191 | .then(responseJson=>{ 192 | hot.setDataAtCell(coords[0].startRow, coords[0].startCol, 193 | '/images/' + responseJson.filename); 194 | }); 195 | }; 196 | reader.readAsBinaryString(file); 197 | 198 | return false; 199 | } 200 | } 201 | } 202 | }; 203 | 204 | Object.keys(defaultConfig).forEach((key)=>{ 205 | if(config[key] === undefined){ 206 | config[key] = defaultConfig[key]; 207 | } 208 | }) 209 | Object.assign(actualConfig, config); 210 | 211 | if(actualConfig.columns.length === 0){ 212 | const renderers = actualConfig.renderers; 213 | 214 | if(typeof renderers === 'string'){ 215 | config.colHeaders.forEach((item, index)=>{ 216 | actualConfig.columns.push({ 217 | data: item, 218 | renderer: renderers 219 | }); 220 | }); 221 | } else if(renderers !== null && typeof renderers === 'object') { 222 | config.colHeaders.forEach((item, index)=>{ 223 | actualConfig.columns.push({ 224 | data: item, 225 | renderer: renderers[item] 226 | }); 227 | }); 228 | } else { 229 | config.colHeaders.forEach((item, index)=>{ 230 | actualConfig.columns.push({ 231 | data: key 232 | }); 233 | }); 234 | } 235 | } 236 | 237 | hot = new Handsontable(document.getElementById('handsontable-area'), actualConfig); 238 | 239 | if(config.maxColWidth && !config.colWidths){ 240 | let colWidths = []; 241 | [...Array(hot.countCols()).keys()].map(i => { 242 | const thisColWidth = hot.getColWidth(i); 243 | if(thisColWidth > config.maxColWidth){ 244 | colWidths.push(config.maxColWidth); 245 | } else { 246 | if(config.minColWidths !== undefined){ 247 | if(thisColWidth > (config.minColWidths[i] || 0)){ 248 | colWidths.push(thisColWidth); 249 | } else { 250 | colWidths.push(config.minColWidths[i]) 251 | } 252 | } 253 | } 254 | }); 255 | 256 | hot.updateSettings({ 257 | colWidths: colWidths 258 | }); 259 | 260 | actualConfig.colWidths = config.colWidths = colWidths; 261 | } 262 | } 263 | 264 | function sendChanges(changes, source){ 265 | console.log(source); 266 | if(['edit', 'Autofill.fill', 'CopyPaste.paste', 'CopyPaste.cut'].indexOf(source) !== -1){ 267 | for(let change of changes){ 268 | if(change[2] !== change[3]){ 269 | const rowEdited = change[0]; 270 | const fieldEdited = change[1]; 271 | const newData = change[3]; 272 | const recordId = data[rowEdited].id; 273 | 274 | const payload = { 275 | id: recordId, 276 | fieldName: fieldEdited, 277 | data: newData 278 | }; 279 | 280 | fetch('/api/edit', { 281 | method: 'POST', 282 | headers: { 283 | "Content-Type": "application/json; charset=utf-8" 284 | }, 285 | body: JSON.stringify(payload) 286 | }).then(response=>{ 287 | if(response.status === 201){ 288 | response.json().then(responseJson=>{ 289 | data[rowEdited].id = responseJson.id; 290 | }); 291 | } else { 292 | alert('Not added. (Have you edited the "front"?)'); 293 | } 294 | }).catch(error => console.error(`Fetch Error =\n`, error)); 295 | } 296 | } 297 | } 298 | } 299 | 300 | function insertEmptyRow(){ 301 | // data.splice(0, 0, {}); The data is already inserted. 302 | hot.alter('insert_row', 0); 303 | } 304 | -------------------------------------------------------------------------------- /CaseReview/static/vendor/sprintf/sprintf.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["sprintf.js"],"names":["sprintf","key","sprintf_format","sprintf_parse","arguments","vsprintf","fmt","argv","apply","concat","parse_tree","arg","i","k","match","pad","pad_character","pad_length","is_positive","sign","cursor","tree_length","length","output","Array","isArray","hasOwnProperty","Error","re","not_type","test","not_primitive","Function","numeric_arg","isNaN","TypeError","number","parseInt","toString","String","fromCharCode","JSON","stringify","parseFloat","toExponential","toFixed","Number","toPrecision","substring","Object","prototype","call","slice","toLowerCase","valueOf","toUpperCase","json","replace","charAt","repeat","sprintf_cache","_fmt","arg_names","text","exec","push","modulo","placeholder","SyntaxError","field_list","replacement_field","field_match","key_access","index_access","not_string","not_bool","not_json","create","exports","window","define"],"mappings":"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","file":"sprintf.min.js","sourcesContent":["/* global window, exports, define */\n\n!function() {\n 'use strict'\n\n var re = {\n not_string: /[^s]/,\n not_bool: /[^t]/,\n not_type: /[^T]/,\n not_primitive: /[^v]/,\n number: /[diefg]/,\n numeric_arg: /[bcdiefguxX]/,\n json: /[j]/,\n not_json: /[^j]/,\n text: /^[^\\x25]+/,\n modulo: /^\\x25{2}/,\n placeholder: /^\\x25(?:([1-9]\\d*)\\$|\\(([^\\)]+)\\))?(\\+)?(0|'[^$])?(-)?(\\d+)?(?:\\.(\\d+))?([b-gijostTuvxX])/,\n key: /^([a-z_][a-z_\\d]*)/i,\n key_access: /^\\.([a-z_][a-z_\\d]*)/i,\n index_access: /^\\[(\\d+)\\]/,\n sign: /^[\\+\\-]/\n }\n\n function sprintf(key) {\n // `arguments` is not an array, but should be fine for this call\n return sprintf_format(sprintf_parse(key), arguments)\n }\n\n function vsprintf(fmt, argv) {\n return sprintf.apply(null, [fmt].concat(argv || []))\n }\n\n function sprintf_format(parse_tree, argv) {\n var cursor = 1, tree_length = parse_tree.length, arg, output = '', i, k, match, pad, pad_character, pad_length, is_positive, sign\n for (i = 0; i < tree_length; i++) {\n if (typeof parse_tree[i] === 'string') {\n output += parse_tree[i]\n }\n else if (Array.isArray(parse_tree[i])) {\n match = parse_tree[i] // convenience purposes only\n if (match[2]) { // keyword argument\n arg = argv[cursor]\n for (k = 0; k < match[2].length; k++) {\n if (!arg.hasOwnProperty(match[2][k])) {\n throw new Error(sprintf('[sprintf] property \"%s\" does not exist', match[2][k]))\n }\n arg = arg[match[2][k]]\n }\n }\n else if (match[1]) { // positional argument (explicit)\n arg = argv[match[1]]\n }\n else { // positional argument (implicit)\n arg = argv[cursor++]\n }\n\n if (re.not_type.test(match[8]) && re.not_primitive.test(match[8]) && arg instanceof Function) {\n arg = arg()\n }\n\n if (re.numeric_arg.test(match[8]) && (typeof arg !== 'number' && isNaN(arg))) {\n throw new TypeError(sprintf('[sprintf] expecting number but found %T', arg))\n }\n\n if (re.number.test(match[8])) {\n is_positive = arg >= 0\n }\n\n switch (match[8]) {\n case 'b':\n arg = parseInt(arg, 10).toString(2)\n break\n case 'c':\n arg = String.fromCharCode(parseInt(arg, 10))\n break\n case 'd':\n case 'i':\n arg = parseInt(arg, 10)\n break\n case 'j':\n arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0)\n break\n case 'e':\n arg = match[7] ? parseFloat(arg).toExponential(match[7]) : parseFloat(arg).toExponential()\n break\n case 'f':\n arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg)\n break\n case 'g':\n arg = match[7] ? String(Number(arg.toPrecision(match[7]))) : parseFloat(arg)\n break\n case 'o':\n arg = (parseInt(arg, 10) >>> 0).toString(8)\n break\n case 's':\n arg = String(arg)\n arg = (match[7] ? arg.substring(0, match[7]) : arg)\n break\n case 't':\n arg = String(!!arg)\n arg = (match[7] ? arg.substring(0, match[7]) : arg)\n break\n case 'T':\n arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()\n arg = (match[7] ? arg.substring(0, match[7]) : arg)\n break\n case 'u':\n arg = parseInt(arg, 10) >>> 0\n break\n case 'v':\n arg = arg.valueOf()\n arg = (match[7] ? arg.substring(0, match[7]) : arg)\n break\n case 'x':\n arg = (parseInt(arg, 10) >>> 0).toString(16)\n break\n case 'X':\n arg = (parseInt(arg, 10) >>> 0).toString(16).toUpperCase()\n break\n }\n if (re.json.test(match[8])) {\n output += arg\n }\n else {\n if (re.number.test(match[8]) && (!is_positive || match[3])) {\n sign = is_positive ? '+' : '-'\n arg = arg.toString().replace(re.sign, '')\n }\n else {\n sign = ''\n }\n pad_character = match[4] ? match[4] === '0' ? '0' : match[4].charAt(1) : ' '\n pad_length = match[6] - (sign + arg).length\n pad = match[6] ? (pad_length > 0 ? pad_character.repeat(pad_length) : '') : ''\n output += match[5] ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)\n }\n }\n }\n return output\n }\n\n var sprintf_cache = Object.create(null)\n\n function sprintf_parse(fmt) {\n if (sprintf_cache[fmt]) {\n return sprintf_cache[fmt]\n }\n\n var _fmt = fmt, match, parse_tree = [], arg_names = 0\n while (_fmt) {\n if ((match = re.text.exec(_fmt)) !== null) {\n parse_tree.push(match[0])\n }\n else if ((match = re.modulo.exec(_fmt)) !== null) {\n parse_tree.push('%')\n }\n else if ((match = re.placeholder.exec(_fmt)) !== null) {\n if (match[2]) {\n arg_names |= 1\n var field_list = [], replacement_field = match[2], field_match = []\n if ((field_match = re.key.exec(replacement_field)) !== null) {\n field_list.push(field_match[1])\n while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {\n if ((field_match = re.key_access.exec(replacement_field)) !== null) {\n field_list.push(field_match[1])\n }\n else if ((field_match = re.index_access.exec(replacement_field)) !== null) {\n field_list.push(field_match[1])\n }\n else {\n throw new SyntaxError('[sprintf] failed to parse named argument key')\n }\n }\n }\n else {\n throw new SyntaxError('[sprintf] failed to parse named argument key')\n }\n match[2] = field_list\n }\n else {\n arg_names |= 2\n }\n if (arg_names === 3) {\n throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported')\n }\n parse_tree.push(match)\n }\n else {\n throw new SyntaxError('[sprintf] unexpected placeholder')\n }\n _fmt = _fmt.substring(match[0].length)\n }\n return sprintf_cache[fmt] = parse_tree\n }\n\n /**\n * export to either browser or node.js\n */\n /* eslint-disable quote-props */\n if (typeof exports !== 'undefined') {\n exports['sprintf'] = sprintf\n exports['vsprintf'] = vsprintf\n }\n if (typeof window !== 'undefined') {\n window['sprintf'] = sprintf\n window['vsprintf'] = vsprintf\n\n if (typeof define === 'function' && define['amd']) {\n define(function() {\n return {\n 'sprintf': sprintf,\n 'vsprintf': vsprintf\n }\n })\n }\n }\n /* eslint-enable quote-props */\n}()\n"]} -------------------------------------------------------------------------------- /pyproject.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Disable App Nap on OS X 10.9" 4 | name = "appnope" 5 | optional = false 6 | platform = "UNKNOWN" 7 | python-versions = "*" 8 | version = "0.1.0" 9 | 10 | [package.requirements] 11 | platform = "darwin" 12 | 13 | [[package]] 14 | category = "main" 15 | description = "Specifications for callback functions passed in to an API" 16 | name = "backcall" 17 | optional = false 18 | platform = "UNKNOWN" 19 | python-versions = "*" 20 | version = "0.1.0" 21 | 22 | [[package]] 23 | category = "main" 24 | description = "Backport of shutil.which from Python 3.3" 25 | name = "backports.shutil-which" 26 | optional = false 27 | platform = "UNKNOWN" 28 | python-versions = "*" 29 | version = "3.5.1" 30 | 31 | [package.requirements] 32 | python = "<3.0" 33 | 34 | [[package]] 35 | category = "main" 36 | description = "An easy safelist-based HTML-sanitizing tool." 37 | name = "bleach" 38 | optional = false 39 | platform = "*" 40 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 41 | version = "2.1.4" 42 | 43 | [package.dependencies] 44 | html5lib = ">=0.99999999pre,<1.0b1 || >1.0b1,<1.0b2 || >1.0b2,<1.0b3 || >1.0b3,<1.0b4 || >1.0b4,<1.0b5 || >1.0b5,<1.0b6 || >1.0b6,<1.0b7 || >1.0b7,<1.0b8 || >1.0b8" 45 | six = "*" 46 | 47 | [[package]] 48 | category = "main" 49 | description = "A simple wrapper around optparse for powerful command line utilities." 50 | name = "click" 51 | optional = false 52 | platform = "*" 53 | python-versions = "*" 54 | version = "6.7" 55 | 56 | [[package]] 57 | category = "main" 58 | description = "Cross-platform colored terminal text." 59 | name = "colorama" 60 | optional = false 61 | platform = "UNKNOWN" 62 | python-versions = "*" 63 | version = "0.3.9" 64 | 65 | [package.requirements] 66 | platform = "win32" 67 | 68 | [[package]] 69 | category = "main" 70 | description = "This library brings the updated configparser from Python 3.5 to Python 2.6-3.5." 71 | name = "configparser" 72 | optional = false 73 | platform = "any" 74 | python-versions = "*" 75 | version = "3.5.0" 76 | 77 | [package.requirements] 78 | python = ">=2.7,<2.8" 79 | 80 | [[package]] 81 | category = "main" 82 | description = "Better living through Python with decorators" 83 | name = "decorator" 84 | optional = false 85 | platform = "All" 86 | python-versions = "*" 87 | version = "4.3.0" 88 | 89 | [[package]] 90 | category = "main" 91 | description = "Discover and load entry points from installed packages." 92 | name = "entrypoints" 93 | optional = false 94 | platform = "*" 95 | python-versions = ">=2.7" 96 | version = "0.2.3" 97 | 98 | [package.dependencies] 99 | [package.dependencies.configparser] 100 | python = ">=2.7,<2.8" 101 | version = ">=3.5" 102 | 103 | [[package]] 104 | category = "main" 105 | description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" 106 | name = "enum34" 107 | optional = false 108 | platform = "UNKNOWN" 109 | python-versions = "*" 110 | version = "1.1.6" 111 | 112 | [package.requirements] 113 | python = ">=2.7,<2.8 || >=3.3,<3.4" 114 | 115 | [[package]] 116 | category = "main" 117 | description = "A simple framework for building complex web applications." 118 | name = "flask" 119 | optional = false 120 | platform = "any" 121 | python-versions = "*" 122 | version = "1.0.2" 123 | 124 | [package.dependencies] 125 | Jinja2 = ">=2.10" 126 | Werkzeug = ">=0.14" 127 | click = ">=5.1" 128 | itsdangerous = ">=0.24" 129 | 130 | [[package]] 131 | category = "main" 132 | description = "Adds SQLAlchemy support to your Flask application" 133 | name = "flask-sqlalchemy" 134 | optional = false 135 | platform = "any" 136 | python-versions = "*" 137 | version = "2.3.2" 138 | 139 | [package.dependencies] 140 | Flask = ">=0.10" 141 | SQLAlchemy = ">=0.8.0" 142 | 143 | [[package]] 144 | category = "main" 145 | description = "Backport of the functools module from Python 3.2.3 for use on 2.7 and PyPy." 146 | name = "functools32" 147 | optional = false 148 | platform = "UNKNOWN" 149 | python-versions = "*" 150 | version = "3.2.3-2" 151 | 152 | [package.requirements] 153 | python = ">=2.7,<2.8" 154 | 155 | [[package]] 156 | category = "main" 157 | description = "HTML parser based on the WHATWG HTML specification" 158 | name = "html5lib" 159 | optional = false 160 | platform = "*" 161 | python-versions = "*" 162 | version = "1.0.1" 163 | 164 | [package.dependencies] 165 | six = ">=1.9" 166 | webencodings = "*" 167 | 168 | [[package]] 169 | category = "main" 170 | description = "IPv4/IPv6 manipulation library" 171 | name = "ipaddress" 172 | optional = false 173 | platform = "*" 174 | python-versions = "*" 175 | version = "1.0.22" 176 | 177 | [package.requirements] 178 | python = ">=2.7,<2.8" 179 | 180 | [[package]] 181 | category = "main" 182 | description = "IPython Kernel for Jupyter" 183 | name = "ipykernel" 184 | optional = false 185 | platform = "Linux" 186 | python-versions = "*" 187 | version = "4.8.2" 188 | 189 | [package.dependencies] 190 | ipython = ">=4.0.0" 191 | jupyter-client = "*" 192 | tornado = ">=4.0" 193 | traitlets = ">=4.1.0" 194 | 195 | [[package]] 196 | category = "main" 197 | description = "IPython: Productive Interactive Computing" 198 | name = "ipython" 199 | optional = false 200 | platform = "Linux" 201 | python-versions = ">=3.3" 202 | version = "6.5.0" 203 | 204 | [package.dependencies] 205 | backcall = "*" 206 | decorator = "*" 207 | jedi = ">=0.10" 208 | pickleshare = "*" 209 | prompt-toolkit = ">=1.0.15,<2.0.0" 210 | pygments = "*" 211 | setuptools = ">=18.5" 212 | simplegeneric = ">0.8" 213 | traitlets = ">=4.2" 214 | 215 | [package.dependencies.appnope] 216 | platform = "darwin" 217 | version = "*" 218 | 219 | [package.dependencies.colorama] 220 | platform = "win32" 221 | version = "*" 222 | 223 | [package.dependencies.pathlib2] 224 | python = ">=3.3,<3.4" 225 | version = "*" 226 | 227 | [package.dependencies.pexpect] 228 | platform = "!=win32" 229 | version = "*" 230 | 231 | [package.dependencies.typing] 232 | python = "<=3.4" 233 | version = "*" 234 | 235 | [package.dependencies.win-unicode-console] 236 | platform = "win32" 237 | python = "<3.6" 238 | version = ">=0.5" 239 | 240 | [[package]] 241 | category = "main" 242 | description = "Vestigial utilities from IPython" 243 | name = "ipython-genutils" 244 | optional = false 245 | platform = "Linux" 246 | python-versions = "*" 247 | version = "0.2.0" 248 | 249 | [[package]] 250 | category = "main" 251 | description = "Various helpers to pass trusted data to untrusted environments and back." 252 | name = "itsdangerous" 253 | optional = false 254 | platform = "UNKNOWN" 255 | python-versions = "*" 256 | version = "0.24" 257 | 258 | [[package]] 259 | category = "main" 260 | description = "An autocompletion tool for Python that can be used for text editors." 261 | name = "jedi" 262 | optional = false 263 | platform = "any" 264 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 265 | version = "0.12.1" 266 | 267 | [package.dependencies] 268 | parso = ">=0.3.0" 269 | 270 | [[package]] 271 | category = "main" 272 | description = "A small but fast and easy to use stand-alone template engine written in pure python." 273 | name = "jinja2" 274 | optional = false 275 | platform = "*" 276 | python-versions = "*" 277 | version = "2.10" 278 | 279 | [package.dependencies] 280 | MarkupSafe = ">=0.23" 281 | 282 | [[package]] 283 | category = "main" 284 | description = "An implementation of JSON Schema validation for Python" 285 | name = "jsonschema" 286 | optional = false 287 | platform = "*" 288 | python-versions = "*" 289 | version = "2.6.0" 290 | 291 | [package.dependencies] 292 | [package.dependencies.functools32] 293 | python = ">=2.7,<2.8" 294 | version = "*" 295 | 296 | [[package]] 297 | category = "main" 298 | description = "Jupyter metapackage. Install all the Jupyter components in one go." 299 | name = "jupyter" 300 | optional = false 301 | platform = "UNKNOWN" 302 | python-versions = "*" 303 | version = "1.0.0" 304 | 305 | [[package]] 306 | category = "main" 307 | description = "Jupyter protocol implementation and client libraries" 308 | name = "jupyter-client" 309 | optional = false 310 | platform = "Linux" 311 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 312 | version = "5.2.3" 313 | 314 | [package.dependencies] 315 | jupyter-core = "*" 316 | python-dateutil = ">=2.1" 317 | pyzmq = ">=13" 318 | tornado = ">=4.1" 319 | traitlets = "*" 320 | 321 | [[package]] 322 | category = "main" 323 | description = "Jupyter core package. A base package on which Jupyter projects rely." 324 | name = "jupyter-core" 325 | optional = false 326 | platform = "*" 327 | python-versions = "*" 328 | version = "4.4.0" 329 | 330 | [package.dependencies] 331 | traitlets = "*" 332 | 333 | [[package]] 334 | category = "main" 335 | description = "Implements a XML/HTML/XHTML Markup safe string for Python" 336 | name = "markupsafe" 337 | optional = false 338 | platform = "UNKNOWN" 339 | python-versions = "*" 340 | version = "1.0" 341 | 342 | [[package]] 343 | category = "main" 344 | description = "The fastest markdown parser in pure Python" 345 | name = "mistune" 346 | optional = false 347 | platform = "any" 348 | python-versions = "*" 349 | version = "0.8.3" 350 | 351 | [[package]] 352 | category = "main" 353 | description = "Converting Jupyter Notebooks" 354 | name = "nbconvert" 355 | optional = false 356 | platform = "Linux" 357 | python-versions = "*" 358 | version = "5.3.1" 359 | 360 | [package.dependencies] 361 | bleach = "*" 362 | entrypoints = ">=0.2.2" 363 | jinja2 = "*" 364 | jupyter-core = "*" 365 | mistune = ">=0.7.4" 366 | nbformat = ">=4.4" 367 | pandocfilters = ">=1.4.1" 368 | pygments = "*" 369 | testpath = "*" 370 | traitlets = ">=4.2" 371 | 372 | [[package]] 373 | category = "main" 374 | description = "The Jupyter Notebook format" 375 | name = "nbformat" 376 | optional = false 377 | platform = "Linux" 378 | python-versions = "*" 379 | version = "4.4.0" 380 | 381 | [package.dependencies] 382 | ipython-genutils = "*" 383 | jsonschema = ">=2.4,<2.5.0 || >2.5.0" 384 | jupyter-core = "*" 385 | traitlets = ">=4.1" 386 | 387 | [[package]] 388 | category = "main" 389 | description = "A web-based notebook environment for interactive computing" 390 | name = "notebook" 391 | optional = false 392 | platform = "Linux" 393 | python-versions = "*" 394 | version = "5.6.0" 395 | 396 | [package.dependencies] 397 | Send2Trash = "*" 398 | ipykernel = "*" 399 | ipython-genutils = "*" 400 | jinja2 = "*" 401 | jupyter-client = ">=5.2.0" 402 | jupyter-core = ">=4.4.0" 403 | nbconvert = "*" 404 | nbformat = "*" 405 | prometheus-client = "*" 406 | pyzmq = ">=17" 407 | terminado = ">=0.8.1" 408 | tornado = ">=4" 409 | traitlets = ">=4.2.1" 410 | 411 | [package.dependencies.ipaddress] 412 | python = ">=2.7,<2.8" 413 | version = "*" 414 | 415 | [[package]] 416 | category = "main" 417 | description = "Utilities for writing pandoc filters in python" 418 | name = "pandocfilters" 419 | optional = false 420 | platform = "*" 421 | python-versions = "*" 422 | version = "1.4.2" 423 | 424 | [[package]] 425 | category = "main" 426 | description = "A Python Parser" 427 | name = "parso" 428 | optional = false 429 | platform = "any" 430 | python-versions = "*" 431 | version = "0.3.1" 432 | 433 | [[package]] 434 | category = "main" 435 | description = "Object-oriented filesystem paths" 436 | name = "pathlib2" 437 | optional = false 438 | platform = "*" 439 | python-versions = "*" 440 | version = "2.3.2" 441 | 442 | [package.dependencies] 443 | six = "*" 444 | 445 | [package.dependencies.scandir] 446 | python = "<3.5" 447 | version = "*" 448 | 449 | [package.requirements] 450 | python = ">=2.6.0,<2.8.0 || >=3.2.0,<3.4.0" 451 | 452 | [[package]] 453 | category = "main" 454 | description = "Pexpect allows easy control of interactive console applications." 455 | name = "pexpect" 456 | optional = false 457 | platform = "UNIX" 458 | python-versions = "*" 459 | version = "4.6.0" 460 | 461 | [package.dependencies] 462 | ptyprocess = ">=0.5" 463 | 464 | [package.requirements] 465 | platform = "!=win32" 466 | 467 | [[package]] 468 | category = "main" 469 | description = "Tiny 'shelve'-like database with concurrency support" 470 | name = "pickleshare" 471 | optional = false 472 | platform = "*" 473 | python-versions = "*" 474 | version = "0.7.4" 475 | 476 | [package.dependencies] 477 | [package.dependencies.pathlib2] 478 | python = ">=2.6.0,<2.8.0 || >=3.2.0,<3.4.0" 479 | version = "*" 480 | 481 | [[package]] 482 | category = "main" 483 | description = "Python client for the Prometheus monitoring system." 484 | name = "prometheus-client" 485 | optional = false 486 | platform = "*" 487 | python-versions = "*" 488 | version = "0.3.1" 489 | 490 | [[package]] 491 | category = "main" 492 | description = "Library for building powerful interactive command lines in Python" 493 | name = "prompt-toolkit" 494 | optional = false 495 | platform = "*" 496 | python-versions = "*" 497 | version = "1.0.15" 498 | 499 | [package.dependencies] 500 | six = ">=1.9.0" 501 | wcwidth = "*" 502 | 503 | [[package]] 504 | category = "main" 505 | description = "Run a subprocess in a pseudo terminal" 506 | name = "ptyprocess" 507 | optional = false 508 | platform = "*" 509 | python-versions = "*" 510 | version = "0.6.0" 511 | 512 | [[package]] 513 | category = "main" 514 | description = "Pygments is a syntax highlighting package written in Python." 515 | name = "pygments" 516 | optional = false 517 | platform = "any" 518 | python-versions = "*" 519 | version = "2.2.0" 520 | 521 | [[package]] 522 | category = "main" 523 | description = "Extensions to the standard Python datetime module" 524 | name = "python-dateutil" 525 | optional = false 526 | platform = "*" 527 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 528 | version = "2.7.3" 529 | 530 | [package.dependencies] 531 | six = ">=1.5" 532 | 533 | [[package]] 534 | category = "main" 535 | description = "Python bindings for the winpty library" 536 | name = "pywinpty" 537 | optional = false 538 | platform = "*" 539 | python-versions = "*" 540 | version = "0.5.4" 541 | 542 | [package.dependencies] 543 | [package.dependencies."backports.shutil-which"] 544 | python = "<3.0" 545 | version = "*" 546 | 547 | [[package]] 548 | category = "main" 549 | description = "Python bindings for 0MQ" 550 | name = "pyzmq" 551 | optional = false 552 | platform = "*" 553 | python-versions = ">=2.7,!=3.0*,!=3.1*,!=3.2*" 554 | version = "17.1.2" 555 | 556 | [[package]] 557 | category = "main" 558 | description = "scandir, a better directory iterator and faster os.walk()" 559 | name = "scandir" 560 | optional = false 561 | platform = "*" 562 | python-versions = "*" 563 | version = "1.9.0" 564 | 565 | [package.requirements] 566 | python = ">=2.6.0,<2.8.0 || >=3.2.0,<3.4.0" 567 | 568 | [[package]] 569 | category = "main" 570 | description = "Send file to trash natively under Mac OS X, Windows and Linux." 571 | name = "send2trash" 572 | optional = false 573 | platform = "*" 574 | python-versions = "*" 575 | version = "1.5.0" 576 | 577 | [[package]] 578 | category = "main" 579 | description = "Simple generic functions (similar to Python's own len(), pickle.dump(), etc.)" 580 | name = "simplegeneric" 581 | optional = false 582 | platform = "UNKNOWN" 583 | python-versions = "*" 584 | version = "0.8.1" 585 | 586 | [[package]] 587 | category = "main" 588 | description = "Python 2 and 3 compatibility utilities" 589 | name = "six" 590 | optional = false 591 | platform = "*" 592 | python-versions = "*" 593 | version = "1.11.0" 594 | 595 | [[package]] 596 | category = "main" 597 | description = "Database Abstraction Library" 598 | name = "sqlalchemy" 599 | optional = false 600 | platform = "*" 601 | python-versions = "*" 602 | version = "1.2.10" 603 | 604 | [[package]] 605 | category = "main" 606 | description = "Terminals served to xterm.js using Tornado websockets" 607 | name = "terminado" 608 | optional = false 609 | platform = "*" 610 | python-versions = "*" 611 | version = "0.8.1" 612 | 613 | [package.dependencies] 614 | ptyprocess = "*" 615 | pywinpty = ">=0.5" 616 | tornado = ">=4" 617 | 618 | [[package]] 619 | category = "main" 620 | description = "Test utilities for code working with files and commands" 621 | name = "testpath" 622 | optional = false 623 | platform = "*" 624 | python-versions = "*" 625 | version = "0.3.1" 626 | 627 | [[package]] 628 | category = "main" 629 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 630 | name = "tornado" 631 | optional = false 632 | platform = "*" 633 | python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, != 3.3.*" 634 | version = "5.1" 635 | 636 | [[package]] 637 | category = "main" 638 | description = "Traitlets Python config system" 639 | name = "traitlets" 640 | optional = false 641 | platform = "Linux,Mac OS X,Windows" 642 | python-versions = "*" 643 | version = "4.3.2" 644 | 645 | [package.dependencies] 646 | decorator = "*" 647 | ipython-genutils = "*" 648 | six = "*" 649 | 650 | [package.dependencies.enum34] 651 | python = ">=2.7,<2.8 || >=3.3,<3.4" 652 | version = "*" 653 | 654 | [[package]] 655 | category = "main" 656 | description = "Type Hints for Python" 657 | name = "typing" 658 | optional = false 659 | platform = "*" 660 | python-versions = "*" 661 | version = "3.6.4" 662 | 663 | [package.requirements] 664 | python = "<=3.4" 665 | 666 | [[package]] 667 | category = "main" 668 | description = "Measures number of Terminal column cells of wide-character codes" 669 | name = "wcwidth" 670 | optional = false 671 | platform = "UNKNOWN" 672 | python-versions = "*" 673 | version = "0.1.7" 674 | 675 | [[package]] 676 | category = "main" 677 | description = "Character encoding aliases for legacy web content" 678 | name = "webencodings" 679 | optional = false 680 | platform = "*" 681 | python-versions = "*" 682 | version = "0.5.1" 683 | 684 | [[package]] 685 | category = "main" 686 | description = "The comprehensive WSGI web application library." 687 | name = "werkzeug" 688 | optional = false 689 | platform = "any" 690 | python-versions = "*" 691 | version = "0.14.1" 692 | 693 | [[package]] 694 | category = "main" 695 | description = "Enable Unicode input and display when running Python from Windows console." 696 | name = "win-unicode-console" 697 | optional = false 698 | platform = "UNKNOWN" 699 | python-versions = "*" 700 | version = "0.5" 701 | 702 | [package.requirements] 703 | platform = "win32" 704 | python = "<3.6" 705 | 706 | [metadata] 707 | content-hash = "df622b818ec38a20fd47515787c8a441d6cc46178d60f4d0a338d084e5709771" 708 | platform = "*" 709 | python-versions = "*" 710 | 711 | [metadata.hashes] 712 | appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] 713 | backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"] 714 | "backports.shutil-which" = ["ce71e4040360e239d142e73d28d592f1951cdff4330db6616efcb43d1abe6ad4", "dd439a7b02433e47968c25a45a76704201c4ef2167deb49830281c379b1a4a9b"] 715 | bleach = ["0ee95f6167129859c5dce9b1ca291ebdb5d8cd7e382ca0e237dfd0dad63f63d8", "24754b9a7d530bf30ce7cbc805bc6cce785660b4a10ff3a43633728438c105ab"] 716 | click = ["29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", "f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"] 717 | colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"] 718 | configparser = ["5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a"] 719 | decorator = ["2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", "c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c"] 720 | entrypoints = ["10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b", "d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f"] 721 | enum34 = ["2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", "644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", "6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", "8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"] 722 | flask = ["2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", "a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"] 723 | flask-sqlalchemy = ["3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", "5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"] 724 | functools32 = ["89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0", "f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d"] 725 | html5lib = ["20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", "66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"] 726 | ipaddress = ["64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794", "b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c"] 727 | ipykernel = ["395f020610e33ffa0b0c9c0cd1a1d927d51ab9aa9f30a7ae36bb0c908a33e89c", "935941dba29d856eee34b8b5261d971bd5012547239ed73ddfff099143748c37", "c091449dd0fad7710ddd9c4a06e8b9e15277da306590bc07a3a1afa6b4453c8f"] 728 | ipython = ["007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62", "b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4"] 729 | ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", "eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"] 730 | itsdangerous = ["cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"] 731 | jedi = ["b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", "c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f"] 732 | jinja2 = ["74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", "f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"] 733 | jsonschema = ["000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", "6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"] 734 | jupyter = ["3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7", "5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78", "d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"] 735 | jupyter-client = ["27befcf0446b01e29853014d6a902dd101ad7d7f94e2252b1adca17c3466b761", "59e6d791e22a8002ad0e80b78c6fd6deecab4f9e1b1aa1a22f4213de271b29ea"] 736 | jupyter-core = ["927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970", "ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7"] 737 | markupsafe = ["a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"] 738 | mistune = ["b4c512ce2fc99e5a62eb95a4aba4b73e5f90264115c40b70a21e1f7d4e0eac91", "bc10c33bfdcaa4e749b779f62f60d6e12f8215c46a292d05e486b869ae306619"] 739 | nbconvert = ["12b1a4671d4463ab73af6e4cbcc965b62254e05d182cd54995dda0d0ef9e2db9", "260d390b989a647575b8ecae2cd06a9eaead10d396733d6e50185d5ebd08996e"] 740 | nbformat = ["b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b", "f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402"] 741 | notebook = ["66dd59e76e755584ae9450eb015c39f55d4bb1d8ec68f2c694d2b3cba7bf5c7e", "e2c8e931cc19db4f8c63e6a396efbc13a228b2cb5b2919df011b946f28239a08"] 742 | pandocfilters = ["b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9"] 743 | parso = ["35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", "895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"] 744 | pathlib2 = ["8eb170f8d0d61825e09a95b38be068299ddeda82f35e96c3301a8a5e7604cb83", "d1aa2a11ba7b8f7b21ab852b1fb5afb277e1bb99d5dfc663380b5015c0d80c5a"] 745 | pexpect = ["2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", "3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"] 746 | pickleshare = ["84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", "c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5"] 747 | prometheus-client = ["17bc24c09431644f7c65d7bce9f4237252308070b6395d6d8e87767afe867e24"] 748 | prompt-toolkit = ["1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", "3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", "858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917"] 749 | ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] 750 | pygments = ["78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", "dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"] 751 | python-dateutil = ["1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", "e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"] 752 | pywinpty = ["349eef36414b038426e65d96ecccfa581c437562cc164fb4faffe6f46963bc80", "4617637c38ae9099a99f73d8dbeb9c752743693bd1dca6ea3b1d520a7248ebf3", "4ee8193b19d77ab59097a000a2c52b36e768e92263812e0c0b40306be8927fb4", "4f6c850db79dd19b1d842d81a8c08fd7efad5e160a1effbba10ba738a5a35cb2", "4fd720b20bb69f1b7ca2060e84503ae843972fcb006ae6e8ddd6ab212fe8911c", "79f2b4584111e36826e587d33eb4e7416a12ae1d6c094cb554e873c5c162fa5f", "87ae1a2301fbce7a3005dac7cdf8ce8a4162f05130348234b87caef260771e96"] 753 | pyzmq = ["25a0715c8f69cf72f67cfe5a68a3f3ed391c67c063d2257bec0fe7fc2c7f08f8", "2bab63759632c6b9e0d5bf19cc63c3b01df267d660e0abcf230cf0afaa966349", "30ab49d99b24bf0908ebe1cdfa421720bfab6f93174e4883075b7ff38cc555ba", "32c7ca9fc547a91e3c26fc6080b6982e46e79819e706eb414dd78f635a65d946", "41219ae72b3cc86d97557fe5b1ef5d1adc1057292ec597b50050874a970a39cf", "4b8c48a9a13cea8f1f16622f9bd46127108af14cd26150461e3eab71e0de3e46", "55724997b4a929c0d01b43c95051318e26ddbae23565018e138ae2dc60187e59", "65f0a4afae59d4fc0aad54a917ab599162613a761b760ba167d66cc646ac3786", "6f88591a8b246f5c285ee6ce5c1bf4f6bd8464b7f090b1333a446b6240a68d40", "75022a4c60dcd8765bb9ca32f6de75a0ec83b0d96e0309dc479f4c7b21f26cb7", "76ea493bfab18dcb090d825f3662b5612e2def73dffc196d51a5194b0294a81d", "7b60c045b80709e4e3c085bab9b691e71761b44c2b42dbb047b8b498e7bc16b3", "8e6af2f736734aef8ed6f278f9f552ec7f37b1a6b98e59b887484a840757f67d", "9ac2298e486524331e26390eac14e4627effd3f8e001d4266ed9d8f1d2d31cce", "9ba650f493a9bc1f24feca1d90fce0e5dd41088a252ac9840131dfbdbf3815ca", "a02a4a385e394e46012dc83d2e8fd6523f039bb52997c1c34a2e0dd49ed839c1", "a3ceee84114d9f5711fa0f4db9c652af0e4636c89eabc9b7f03a3882569dd1ed", "a72b82ac1910f2cf61a49139f4974f994984475f771b0faa730839607eeedddf", "ab136ac51027e7c484c53138a0fab4a8a51e80d05162eb7b1585583bcfdbad27", "c095b224300bcac61e6c445e27f9046981b1ac20d891b2f1714da89d34c637c8", "c5cc52d16c06dc2521340d69adda78a8e1031705924e103c0eb8fc8af861d810", "d612e9833a89e8177f8c1dc68d7b4ff98d3186cd331acd616b01bbdab67d3a7b", "e828376a23c66c6fe90dcea24b4b72cd774f555a6ee94081670872918df87a19", "e9767c7ab2eb552796440168d5c6e23a99ecaade08dda16266d43ad461730192", "ebf8b800d42d217e4710d1582b0c8bff20cdcb4faad7c7213e52644034300924"] 754 | scandir = ["04b8adb105f2ed313a7c2ef0f1cf7aff4871aa7a1883fa4d8c44b5551ab052d6", "1444134990356c81d12f30e4b311379acfbbcd03e0bab591de2696a3b126d58e", "1b5c314e39f596875e5a95dd81af03730b338c277c54a454226978d5ba95dbb6", "346619f72eb0ddc4cf355ceffd225fa52506c92a2ff05318cfabd02a144e7c4e", "44975e209c4827fc18a3486f257154d34ec6eaec0f90fef0cca1caa482db7064", "61859fd7e40b8c71e609c202db5b0c1dbec0d5c7f1449dec2245575bdc866792", "a5e232a0bf188362fa00123cc0bb842d363a292de7126126df5527b6a369586a", "c14701409f311e7a9b7ec8e337f0815baf7ac95776cc78b419a1e6d49889a383", "c7708f29d843fc2764310732e41f0ce27feadde453261859ec0fca7865dfc41b", "c9009c527929f6e25604aec39b0a43c3f831d2947d89d6caaab22f057b7055c8", "f5c71e29b4e2af7ccdc03a020c626ede51da471173b4a6ad1e904f2b2e04b4bd"] 755 | send2trash = ["60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2", "f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"] 756 | simplegeneric = ["dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173"] 757 | six = ["70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"] 758 | sqlalchemy = ["72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8"] 759 | terminado = ["55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a", "65011551baff97f5414c67018e908110693143cfbaeb16831b743fe7cad8b927"] 760 | testpath = ["039fa6a6c9fd3488f8336d23aebbfead5fa602c4a47d49d83845f55a595ec1b4", "0d5337839c788da5900df70f8e01015aec141aa3fe7936cb0d0a2953f7ac7609"] 761 | tornado = ["1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448", "4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582", "5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046", "6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9", "a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f", "c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866", "d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c"] 762 | traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] 763 | typing = ["3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf", "b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8", "d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"] 764 | wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] 765 | webencodings = ["a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", "b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"] 766 | werkzeug = ["c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", "d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"] 767 | win-unicode-console = ["d4142d4d56d46f449d6f00536a73625a871cba040f0bc1a2e305a04578f07d1e"] 768 | --------------------------------------------------------------------------------