This commit is contained in:
Charles Henry 2018-04-23 16:02:41 +01:00
commit 01ba3bec53
15 changed files with 1669 additions and 421 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.pyc *.pyc
AnkiConnect.zip

19
.travis.yml Normal file
View File

@ -0,0 +1,19 @@
sudo: required
language: python
addons:
hosts:
- docker
services:
- docker
python:
- "2.7"
install:
- docker build -f tests/docker/$ANKI_VERSION/Dockerfile -t txgio/anki-connect:$ANKI_VERSION .
script:
- docker run -ti -d --rm -p 8888:8765 -e ANKICONNECT_BIND_ADDRESS=0.0.0.0 txgio/anki-connect:$ANKI_VERSION
- ./tests/scripts/wait-up.sh http://docker:8888
- python -m unittest discover -s tests -v
env:
- ANKI_VERSION=2.0.x
- ANKI_VERSION=2.1.x

View File

@ -17,25 +17,30 @@
import anki import anki
import aqt import aqt
import base64
import hashlib import hashlib
import inspect import inspect
import json import json
import os
import os.path import os.path
import re
import select import select
import socket import socket
import sys import sys
from time import time from time import time
from unicodedata import normalize
from operator import itemgetter
# #
# Constants # Constants
# #
API_VERSION = 4 API_VERSION = 5
TICK_INTERVAL = 25 TICK_INTERVAL = 25
URL_TIMEOUT = 10 URL_TIMEOUT = 10
URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py' URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py'
NET_ADDRESS = '127.0.0.1' NET_ADDRESS = os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1')
NET_BACKLOG = 5 NET_BACKLOG = 5
NET_PORT = 8765 NET_PORT = 8765
@ -64,9 +69,13 @@ else:
# Helpers # Helpers
# #
def webApi(func): def webApi(*versions):
func.webApi = True def decorator(func):
return func method = lambda *args, **kwargs: func(*args, **kwargs)
setattr(method, 'versions', versions)
setattr(method, 'api', True)
return method
return decorator
def makeBytes(data): def makeBytes(data):
@ -80,11 +89,11 @@ def makeStr(data):
def download(url): def download(url):
try: try:
resp = web.urlopen(url, timeout=URL_TIMEOUT) resp = web.urlopen(url, timeout=URL_TIMEOUT)
except web.URLError: except web.URLError as e:
return None raise Exception('A urlError has occurred for url ' + url + '. Error messages was: ' + e.message)
if resp.code != 200: if resp.code != 200:
return None raise Exception('Return code for url request' + url + 'was not 200. Error code: ' + resp.code)
return resp.read() return resp.read()
@ -108,7 +117,6 @@ def verifyStringList(strings):
return True return True
# #
# AjaxRequest # AjaxRequest
# #
@ -177,10 +185,10 @@ class AjaxClient:
headers = {} headers = {}
for line in parts[0].split(makeBytes('\r\n')): for line in parts[0].split(makeBytes('\r\n')):
pair = line.split(makeBytes(': ')) pair = line.split(makeBytes(': '))
headers[pair[0]] = pair[1] if len(pair) > 1 else None headers[pair[0].lower()] = pair[1] if len(pair) > 1 else None
headerLength = len(parts[0]) + 4 headerLength = len(parts[0]) + 4
bodyLength = int(headers.get(makeBytes('Content-Length'), 0)) bodyLength = int(headers.get(makeBytes('content-length'), 0))
totalLength = headerLength + bodyLength totalLength = headerLength + bodyLength
if totalLength > len(data): if totalLength > len(data):
@ -199,6 +207,27 @@ class AjaxServer:
self.handler = handler self.handler = handler
self.clients = [] self.clients = []
self.sock = None self.sock = None
self.resetHeaders()
def setHeader(self, name, value):
self.extraHeaders[name] = value
def resetHeaders(self):
self.headers = [
['HTTP/1.1 200 OK', None],
['Content-Type', 'text/json'],
['Access-Control-Allow-Origin', '*']
]
self.extraHeaders = {}
def getHeaders(self):
headers = self.headers[:]
for name in self.extraHeaders:
headers.append([name, self.extraHeaders[name]])
return headers
def advance(self): def advance(self):
@ -240,14 +269,12 @@ class AjaxServer:
params = json.loads(makeStr(req.body)) params = json.loads(makeStr(req.body))
body = makeBytes(json.dumps(self.handler(params))) body = makeBytes(json.dumps(self.handler(params)))
except ValueError: except ValueError:
body = json.dumps(None); body = makeBytes(json.dumps(None))
resp = bytes() resp = bytes()
headers = [
['HTTP/1.1 200 OK', None], self.setHeader('Content-Length', str(len(body)))
['Content-Type', 'text/json'], headers = self.getHeaders()
['Content-Length', str(len(body))]
]
for key, value in headers: for key, value in headers:
if value is None: if value is None:
@ -311,19 +338,45 @@ class AnkiNoteParams:
) )
def __str__(self):
return 'DeckName: ' + self.deckName + '. ModelName: ' + self.modelName + '. Fields: ' + str(self.fields) + '. Tags: ' + str(self.tags) + '.'
# #
# AnkiBridge # AnkiBridge
# #
class AnkiBridge: class AnkiBridge:
def storeMediaFile(self, filename, data):
self.deleteMediaFile(filename)
self.media().writeData(filename, base64.b64decode(data))
def retrieveMediaFile(self, filename):
# based on writeData from anki/media.py
filename = os.path.basename(filename)
filename = normalize('NFC', filename)
filename = self.media().stripIllegal(filename)
path = os.path.join(self.media().dir(), filename)
if os.path.exists(path):
with open(path, 'rb') as file:
return base64.b64encode(file.read()).decode('ascii')
return False
def deleteMediaFile(self, filename):
self.media().syncDelete(filename)
def addNote(self, params): def addNote(self, params):
collection = self.collection() collection = self.collection()
if collection is None: if collection is None:
return raise Exception('Collection was not found.')
note = self.createNote(params) note = self.createNote(params)
if note is None: if note is None:
return raise Exception('Failed to create note from params: ' + str(params))
if params.audio is not None and len(params.audio.fields) > 0: if params.audio is not None and len(params.audio.fields) > 0:
data = download(params.audio.url) data = download(params.audio.url)
@ -348,21 +401,24 @@ class AnkiBridge:
def canAddNote(self, note): def canAddNote(self, note):
return bool(self.createNote(note)) try:
return bool(self.createNote(note))
except:
return False
def createNote(self, params): def createNote(self, params):
collection = self.collection() collection = self.collection()
if collection is None: if collection is None:
return raise Exception('Collection was not found.')
model = collection.models.byName(params.modelName) model = collection.models.byName(params.modelName)
if model is None: if model is None:
return raise Exception('Model was not found for model: ' + params.modelName)
deck = collection.decks.byName(params.deckName) deck = collection.decks.byName(params.deckName)
if deck is None: if deck is None:
return raise Exception('Deck was not found for deck: ' + params.deckName)
note = anki.notes.Note(collection, model) note = anki.notes.Note(collection, model)
note.model()['did'] = deck['id'] note.model()['did'] = deck['id']
@ -372,16 +428,40 @@ class AnkiBridge:
if name in note: if name in note:
note[name] = value note[name] = value
if not note.dupeOrEmpty(): # Returns 1 if empty. 2 if duplicate. Otherwise returns False
duplicateOrEmpty = note.dupeOrEmpty()
if duplicateOrEmpty == 1:
raise Exception('Note was empty. Param were: ' + str(params))
elif duplicateOrEmpty == 2:
raise Exception('Note is duplicate of existing note. Params were: ' + str(params))
elif duplicateOrEmpty == False:
return note return note
def updateNoteFields(self, params):
collection = self.collection()
if collection is None:
raise Exception('Collection was not found.')
note = collection.getNote(params['id'])
if note is None:
raise Exception('Failed to get note:{}'.format(params['id']))
for name, value in params['fields'].items():
if name in note:
note[name] = value
note.flush()
def addTags(self, notes, tags, add=True): def addTags(self, notes, tags, add=True):
self.startEditing() self.startEditing()
self.collection().tags.bulkAdd(notes, tags, add) self.collection().tags.bulkAdd(notes, tags, add)
self.stopEditing() self.stopEditing()
def getTags(self):
return self.collection().tags.all()
def suspend(self, cards, suspend=True): def suspend(self, cards, suspend=True):
for card in cards: for card in cards:
isSuspended = self.isSuspended(card) isSuspended = self.isSuspended(card)
@ -402,14 +482,18 @@ class AnkiBridge:
return False return False
def isSuspended(self, card):
card = self.collection().getCard(card)
if card.queue == -1:
return True
else:
return False
def areSuspended(self, cards): def areSuspended(self, cards):
suspended = [] suspended = []
for card in cards: for card in cards:
card = self.collection().getCard(card) suspended.append(self.isSuspended(card))
if card.queue == -1:
suspended.append(True)
else:
suspended.append(False)
return suspended return suspended
@ -474,6 +558,13 @@ class AnkiBridge:
return self.collection().sched return self.collection().sched
def multi(self, actions):
response = []
for item in actions:
response.append(AnkiConnect.handler(ac, item))
return response
def media(self): def media(self):
collection = self.collection() collection = self.collection()
if collection is not None: if collection is not None:
@ -486,6 +577,18 @@ class AnkiBridge:
return collection.models.allNames() return collection.models.allNames()
def modelNamesAndIds(self):
models = {}
modelNames = self.modelNames()
for model in modelNames:
mid = self.collection().models.byName(model)['id']
mid = int(mid) # sometimes Anki stores the ID as a string
models[model] = mid
return models
def modelNameFromId(self, modelId): def modelNameFromId(self, modelId):
collection = self.collection() collection = self.collection()
if collection is not None: if collection is not None:
@ -502,11 +605,90 @@ class AnkiBridge:
return [field['name'] for field in model['flds']] return [field['name'] for field in model['flds']]
def multi(self, actions): def modelFieldsOnTemplates(self, modelName):
response = [] model = self.collection().models.byName(modelName)
for item in actions:
response.append(AnkiConnect.handler(ac, item)) if model is not None:
return response templates = {}
for template in model['tmpls']:
fields = []
for side in ['qfmt', 'afmt']:
fieldsForSide = []
# based on _fieldsOnTemplate from aqt/clayout.py
matches = re.findall('{{[^#/}]+?}}', template[side])
for match in matches:
# remove braces and modifiers
match = re.sub(r'[{}]', '', match)
match = match.split(':')[-1]
# for the answer side, ignore fields present on the question side + the FrontSide field
if match == 'FrontSide' or side == 'afmt' and match in fields[0]:
continue
fieldsForSide.append(match)
fields.append(fieldsForSide)
templates[template['name']] = fields
return templates
def getDeckConfig(self, deck):
if not deck in self.deckNames():
return False
did = self.collection().decks.id(deck)
return self.collection().decks.confForDid(did)
def saveDeckConfig(self, config):
configId = str(config['id'])
if not configId in self.collection().decks.dconf:
return False
mod = anki.utils.intTime()
usn = self.collection().usn()
config['mod'] = mod
config['usn'] = usn
self.collection().decks.dconf[configId] = config
self.collection().decks.changed = True
return True
def setDeckConfigId(self, decks, configId):
for deck in decks:
if not deck in self.deckNames():
return False
if not str(configId) in self.collection().decks.dconf:
return False
for deck in decks:
did = str(self.collection().decks.id(deck))
aqt.mw.col.decks.decks[did]['conf'] = configId
return True
def cloneDeckConfigId(self, name, cloneFrom=1):
if not str(cloneFrom) in self.collection().decks.dconf:
return False
cloneFrom = self.collection().decks.getConf(cloneFrom)
return self.collection().decks.confId(name, cloneFrom)
def removeDeckConfigId(self, configId):
if configId == 1 or not str(configId) in self.collection().decks.dconf:
return False
self.collection().decks.remConf(configId)
return True
def deckNames(self): def deckNames(self):
@ -515,6 +697,17 @@ class AnkiBridge:
return collection.decks.allNames() return collection.decks.allNames()
def deckNamesAndIds(self):
decks = {}
deckNames = self.deckNames()
for deck in deckNames:
did = self.collection().decks.id(deck)
decks[deck] = did
return decks
def deckNameFromId(self, deckId): def deckNameFromId(self, deckId):
collection = self.collection() collection = self.collection()
if collection is not None: if collection is not None:
@ -537,6 +730,74 @@ class AnkiBridge:
return [] return []
def cardsInfo(self,cards):
result = []
for cid in cards:
try:
card = self.collection().getCard(cid)
model = card.model()
note = card.note()
fields = {}
for info in model['flds']:
order = info['ord']
name = info['name']
fields[name] = {'value': note.fields[order], 'order': order}
result.append({
'cardId': card.id,
'fields': fields,
'fieldOrder': card.ord,
'question': card._getQA()['q'],
'answer': card._getQA()['a'],
'modelName': model['name'],
'deckName': self.deckNameFromId(card.did),
'css': model['css'],
'factor': card.factor,
#This factor is 10 times the ease percentage,
# so an ease of 310% would be reported as 3100
'interval': card.ivl,
'note': card.nid
})
except TypeError as e:
# Anki will give a TypeError if the card ID does not exist.
# Best behavior is probably to add an 'empty card' to the
# returned result, so that the items of the input and return
# lists correspond.
result.append({})
return result
def notesInfo(self,notes):
result = []
for nid in notes:
try:
note = self.collection().getNote(nid)
model = note.model()
fields = {}
for info in model['flds']:
order = info['ord']
name = info['name']
fields[name] = {'value': note.fields[order], 'order': order}
result.append({
'noteId': note.id,
'tags' : note.tags,
'fields': fields,
'modelName': model['name'],
'cards': self.collection().db.list(
'select id from cards where nid = ? order by ord', note.id)
})
except TypeError as e:
# Anki will give a TypeError if the note ID does not exist.
# Best behavior is probably to add an 'empty card' to the
# returned result, so that the items of the input and return
# lists correspond.
result.append({})
return result
def getDecks(self, cards): def getDecks(self, cards):
decks = {} decks = {}
for card in cards: for card in cards:
@ -551,6 +812,14 @@ class AnkiBridge:
return decks return decks
def createDeck(self, deck):
self.startEditing()
deckId = self.collection().decks.id(deck)
self.stopEditing()
return deckId
def changeDeck(self, cards, deck): def changeDeck(self, cards, deck):
self.startEditing() self.startEditing()
@ -571,8 +840,8 @@ class AnkiBridge:
def deleteDecks(self, decks, cardsToo=False): def deleteDecks(self, decks, cardsToo=False):
self.startEditing() self.startEditing()
for deck in decks: for deck in decks:
id = self.collection().decks.id(deck) did = self.collection().decks.id(deck)
self.collection().decks.rem(id, cardsToo) self.collection().decks.rem(did, cardsToo)
self.stopEditing() self.stopEditing()
@ -605,7 +874,7 @@ class AnkiBridge:
def guiCurrentCard(self): def guiCurrentCard(self):
if not self.guiReviewActive(): if not self.guiReviewActive():
return raise Exception('Gui review is not currently active.')
reviewer = self.reviewer() reviewer = self.reviewer()
card = reviewer.card card = reviewer.card
@ -625,9 +894,10 @@ class AnkiBridge:
'fieldOrder': card.ord, 'fieldOrder': card.ord,
'question': card._getQA()['q'], 'question': card._getQA()['q'],
'answer': card._getQA()['a'], 'answer': card._getQA()['a'],
'buttons': map(lambda b: b[0], reviewer._answerButtonList()), 'buttons': [b[0] for b in reviewer._answerButtonList()],
'modelName': model['name'], 'modelName': model['name'],
'deckName': self.deckNameFromId(card.did) 'deckName': self.deckNameFromId(card.did),
'css': model['css']
} }
@ -643,6 +913,7 @@ class AnkiBridge:
else: else:
return False return False
def guiShowQuestion(self): def guiShowQuestion(self):
if self.guiReviewActive(): if self.guiReviewActive():
self.reviewer()._showQuestion() self.reviewer()._showQuestion()
@ -697,10 +968,24 @@ class AnkiBridge:
return False return False
def guiExitAnki(self):
timer = QTimer()
def exitAnki():
timer.stop()
self.window().close()
timer.timeout.connect(exitAnki)
timer.start(1000) # 1s should be enough to allow the response to be sent.
def sync(self):
self.window().onSync()
# #
# AnkiConnect # AnkiConnect
# #
class AnkiConnect: class AnkiConnect:
def __init__(self): def __init__(self):
self.anki = AnkiBridge() self.anki = AnkiBridge()
@ -725,70 +1010,148 @@ class AnkiConnect:
def handler(self, request): def handler(self, request):
action = request.get('action', '') name = request.get('action', '')
if hasattr(self, action): version = request.get('version', 4)
handler = getattr(self, action) params = request.get('params', {})
if callable(handler) and hasattr(handler, 'webApi') and getattr(handler, 'webApi'): reply = {'result': None, 'error': None}
spec = inspect.getargspec(handler)
argsAll = spec.args[1:]
argsReq = argsAll
argsDef = spec.defaults try:
if argsDef is not None: method = None
argsReq = argsAll[:-len(argsDef)]
params = request.get('params', {}) for methodName, methodInst in inspect.getmembers(self, predicate=inspect.ismethod):
for argReq in argsReq: apiVersionLast = 0
if argReq not in params: apiNameLast = None
return
for param in params:
if param not in argsAll:
return
return handler(**params) if getattr(methodInst, 'api', False):
for apiVersion, apiName in getattr(methodInst, 'versions', []):
if apiVersionLast < apiVersion <= version:
apiVersionLast = apiVersion
apiNameLast = apiName
if apiNameLast is None and apiVersionLast == 0:
apiNameLast = methodName
if apiNameLast is not None and apiNameLast == name:
method = methodInst
break
if method is None:
raise Exception('unsupported action')
else:
reply['result'] = methodInst(**params)
except Exception as e:
reply['error'] = str(e)
if version > 4:
return reply
else:
return reply['result']
@webApi @webApi()
def deckNames(self):
return self.anki.deckNames()
@webApi
def modelNames(self):
return self.anki.modelNames()
@webApi
def modelFieldNames(self, modelName):
return self.anki.modelFieldNames(modelName)
@webApi
def multi(self, actions): def multi(self, actions):
return self.anki.multi(actions) return self.anki.multi(actions)
@webApi @webApi()
def storeMediaFile(self, filename, data):
return self.anki.storeMediaFile(filename, data)
@webApi()
def retrieveMediaFile(self, filename):
return self.anki.retrieveMediaFile(filename)
@webApi()
def deleteMediaFile(self, filename):
return self.anki.deleteMediaFile(filename)
@webApi()
def deckNames(self):
return self.anki.deckNames()
@webApi()
def deckNamesAndIds(self):
return self.anki.deckNamesAndIds()
@webApi()
def modelNames(self):
return self.anki.modelNames()
@webApi()
def modelNamesAndIds(self):
return self.anki.modelNamesAndIds()
@webApi()
def modelFieldNames(self, modelName):
return self.anki.modelFieldNames(modelName)
@webApi()
def modelFieldsOnTemplates(self, modelName):
return self.anki.modelFieldsOnTemplates(modelName)
@webApi()
def getDeckConfig(self, deck):
return self.anki.getDeckConfig(deck)
@webApi()
def saveDeckConfig(self, config):
return self.anki.saveDeckConfig(config)
@webApi()
def setDeckConfigId(self, decks, configId):
return self.anki.setDeckConfigId(decks, configId)
@webApi()
def cloneDeckConfigId(self, name, cloneFrom=1):
return self.anki.cloneDeckConfigId(name, cloneFrom)
@webApi()
def removeDeckConfigId(self, configId):
return self.anki.removeDeckConfigId(configId)
@webApi()
def addNote(self, note): def addNote(self, note):
params = AnkiNoteParams(note) params = AnkiNoteParams(note)
if params.validate(): if params.validate():
return self.anki.addNote(params) return self.anki.addNote(params)
@webApi @webApi()
def addNotes(self, notes): def addNotes(self, notes):
results = [] results = []
for note in notes: for note in notes:
params = AnkiNoteParams(note) try:
if params.validate(): params = AnkiNoteParams(note)
results.append(self.anki.addNote(params)) if params.validate():
else: results.append(self.anki.addNote(params))
else:
results.append(None)
except Exception:
results.append(None) results.append(None)
return results return results
@webApi @webApi()
def updateNoteFields(self, note):
return self.anki.updateNoteFields(note)
@webApi()
def canAddNotes(self, notes): def canAddNotes(self, notes):
results = [] results = []
for note in notes: for note in notes:
@ -798,42 +1161,47 @@ class AnkiConnect:
return results return results
@webApi @webApi()
def addTags(self, notes, tags, add=True): def addTags(self, notes, tags, add=True):
return self.anki.addTags(notes, tags, add) return self.anki.addTags(notes, tags, add)
@webApi @webApi()
def removeTags(self, notes, tags): def removeTags(self, notes, tags):
return self.anki.addTags(notes, tags, False) return self.anki.addTags(notes, tags, False)
@webApi @webApi()
def getTags(self):
return self.anki.getTags()
@webApi()
def suspend(self, cards, suspend=True): def suspend(self, cards, suspend=True):
return self.anki.suspend(cards, suspend) return self.anki.suspend(cards, suspend)
@webApi @webApi()
def unsuspend(self, cards): def unsuspend(self, cards):
return self.anki.suspend(cards, False) return self.anki.suspend(cards, False)
@webApi @webApi()
def areSuspended(self, cards): def areSuspended(self, cards):
return self.anki.areSuspended(cards) return self.anki.areSuspended(cards)
@webApi @webApi()
def areDue(self, cards): def areDue(self, cards):
return self.anki.areDue(cards) return self.anki.areDue(cards)
@webApi @webApi()
def getIntervals(self, cards, complete=False): def getIntervals(self, cards, complete=False):
return self.anki.getIntervals(cards, complete) return self.anki.getIntervals(cards, complete)
@webApi @webApi()
def upgrade(self): def upgrade(self):
response = QMessageBox.question( response = QMessageBox.question(
self.anki.window(), self.anki.window(),
@ -856,91 +1224,116 @@ class AnkiConnect:
return False return False
@webApi @webApi()
def version(self): def version(self):
return API_VERSION return API_VERSION
@webApi @webApi()
def findNotes(self, query=None): def findNotes(self, query=None):
return self.anki.findNotes(query) return self.anki.findNotes(query)
@webApi @webApi()
def findCards(self, query=None): def findCards(self, query=None):
return self.anki.findCards(query) return self.anki.findCards(query)
@webApi @webApi()
def getDecks(self, cards): def getDecks(self, cards):
return self.anki.getDecks(cards) return self.anki.getDecks(cards)
@webApi @webApi()
def createDeck(self, deck):
return self.anki.createDeck(deck)
@webApi()
def changeDeck(self, cards, deck): def changeDeck(self, cards, deck):
return self.anki.changeDeck(cards, deck) return self.anki.changeDeck(cards, deck)
@webApi @webApi()
def deleteDecks(self, decks, cardsToo=False): def deleteDecks(self, decks, cardsToo=False):
return self.anki.deleteDecks(decks, cardsToo) return self.anki.deleteDecks(decks, cardsToo)
@webApi @webApi()
def cardsToNotes(self, cards): def cardsToNotes(self, cards):
return self.anki.cardsToNotes(cards) return self.anki.cardsToNotes(cards)
@webApi @webApi()
def guiBrowse(self, query=None): def guiBrowse(self, query=None):
return self.anki.guiBrowse(query) return self.anki.guiBrowse(query)
@webApi @webApi()
def guiAddCards(self): def guiAddCards(self):
return self.anki.guiAddCards() return self.anki.guiAddCards()
@webApi @webApi()
def guiCurrentCard(self): def guiCurrentCard(self):
return self.anki.guiCurrentCard() return self.anki.guiCurrentCard()
@webApi @webApi()
def guiStartCardTimer(self): def guiStartCardTimer(self):
return self.anki.guiStartCardTimer() return self.anki.guiStartCardTimer()
@webApi @webApi()
def guiAnswerCard(self, ease): def guiAnswerCard(self, ease):
return self.anki.guiAnswerCard(ease) return self.anki.guiAnswerCard(ease)
@webApi @webApi()
def guiShowQuestion(self): def guiShowQuestion(self):
return self.anki.guiShowQuestion() return self.anki.guiShowQuestion()
@webApi @webApi()
def guiShowAnswer(self): def guiShowAnswer(self):
return self.anki.guiShowAnswer() return self.anki.guiShowAnswer()
@webApi @webApi()
def guiDeckOverview(self, name): def guiDeckOverview(self, name):
return self.anki.guiDeckOverview(name) return self.anki.guiDeckOverview(name)
@webApi @webApi()
def guiDeckBrowser(self): def guiDeckBrowser(self):
return self.anki.guiDeckBrowser() return self.anki.guiDeckBrowser()
@webApi @webApi()
def guiDeckReview(self, name): def guiDeckReview(self, name):
return self.anki.guiDeckReview(name) return self.anki.guiDeckReview(name)
@webApi()
def guiExitAnki(self):
return self.anki.guiExitAnki()
@webApi()
def cardsInfo(self, cards):
return self.anki.cardsInfo(cards)
@webApi()
def notesInfo(self, notes):
return self.anki.notesInfo(notes)
@webApi()
def sync(self):
return self.anki.sync()
# #
# Entry # Entry
# #

1353
README.md

File diff suppressed because it is too large Load Diff

5
build_zip.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/bash
rm AnkiConnect.zip
cp AnkiConnect.py __init__.py
7za a AnkiConnect.zip __init__.py
rm __init__.py

View File

@ -0,0 +1,14 @@
FROM txgio/anki:2.0.45
RUN apt-get update && \
apt-get install -y xvfb
COPY AnkiConnect.py /data/addons/AnkiConnect.py
COPY tests/docker/2.0.x/prefs.db /data/prefs.db
ADD tests/docker/2.0.x/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["anki", "-b", "/data"]

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
# Start Xvfb
Xvfb -ac -screen scrn 1280x2000x24 :99.0 &
export DISPLAY=:99.0
exec "$@"

BIN
tests/docker/2.0.x/prefs.db Normal file

Binary file not shown.

View File

@ -0,0 +1,14 @@
FROM txgio/anki:2.1.0beta14
RUN apt-get update && \
apt-get install -y xvfb
COPY AnkiConnect.py /data/addons21/AnkiConnect/__init__.py
COPY tests/docker/2.1.x/prefs21.db /data/prefs21.db
ADD tests/docker/2.1.x/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["anki", "-b", "/data"]

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
# Start Xvfb
Xvfb -ac -screen scrn 1280x2000x24 :99.0 &
export DISPLAY=:99.0
exec "$@"

Binary file not shown.

28
tests/scripts/wait-up.sh Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -e
if [ $# -lt 1 ]; then
printf "First parameter URL required.\n"
exit 1
fi
COUNTER=0
STEP_SIZE=1
MAX_SECONDS=${2:-10} # Wait 10 seconds if parameter not provided
MAX_RETRIES=$(( $MAX_SECONDS / $STEP_SIZE))
URL=$1
printf "Waiting URL: "$URL"\n"
until $(curl --insecure --output /dev/null --silent --fail $URL) || [ $COUNTER -eq $MAX_RETRIES ]; do
printf '.'
sleep $STEP_SIZE
COUNTER=$(($COUNTER + 1))
done
if [ $COUNTER -eq $MAX_RETRIES ]; then
printf "\nTimeout after "$(( $COUNTER * $STEP_SIZE))" second(s).\n"
exit 2
else
printf "\nUp successfully after "$(( $COUNTER * $STEP_SIZE))" second(s).\n"
fi

16
tests/test_decks.py Normal file
View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import unittest
from unittest import TestCase
from util import callAnkiConnectEndpoint
class TestDeckNames(TestCase):
def test_deckNames(self):
response = callAnkiConnectEndpoint({'action': 'deckNames'})
self.assertEqual(['Default'], response)
class TestGetDeckConfig(TestCase):
def test_getDeckConfig(self):
response = callAnkiConnectEndpoint({'action': 'getDeckConfig', 'params': {'deck': 'Default'}})
self.assertDictContainsSubset({'name': 'Default', 'replayq': True}, response)

10
tests/test_misc.py Normal file
View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
import unittest
from unittest import TestCase
from util import callAnkiConnectEndpoint
class TestVersion(TestCase):
def test_version(self):
response = callAnkiConnectEndpoint({'action': 'version'})
self.assertEqual(5, response)

11
tests/util.py Normal file
View File

@ -0,0 +1,11 @@
import json
import urllib
import urllib2
def callAnkiConnectEndpoint(data):
url = 'http://docker:8888'
dumpedData = json.dumps(data)
req = urllib2.Request(url, dumpedData)
response = urllib2.urlopen(req).read()
responseData = json.loads(response)
return responseData