Merge branch 'master' of https://github.com/FooSoft/anki-connect
This commit is contained in:
commit
01ba3bec53
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
|
AnkiConnect.zip
|
||||||
|
19
.travis.yml
Normal file
19
.travis.yml
Normal 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
|
603
AnkiConnect.py
603
AnkiConnect.py
@ -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
|
||||||
#
|
#
|
||||||
|
5
build_zip.sh
Executable file
5
build_zip.sh
Executable 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
|
14
tests/docker/2.0.x/Dockerfile
Normal file
14
tests/docker/2.0.x/Dockerfile
Normal 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"]
|
8
tests/docker/2.0.x/entrypoint.sh
Executable file
8
tests/docker/2.0.x/entrypoint.sh
Executable 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
BIN
tests/docker/2.0.x/prefs.db
Normal file
Binary file not shown.
14
tests/docker/2.1.x/Dockerfile
Normal file
14
tests/docker/2.1.x/Dockerfile
Normal 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"]
|
8
tests/docker/2.1.x/entrypoint.sh
Executable file
8
tests/docker/2.1.x/entrypoint.sh
Executable 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.1.x/prefs21.db
Normal file
BIN
tests/docker/2.1.x/prefs21.db
Normal file
Binary file not shown.
28
tests/scripts/wait-up.sh
Executable file
28
tests/scripts/wait-up.sh
Executable 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
16
tests/test_decks.py
Normal 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
10
tests/test_misc.py
Normal 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
11
tests/util.py
Normal 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
|
Loading…
Reference in New Issue
Block a user