Skip to content

Commit

Permalink
Initial working version
Browse files Browse the repository at this point in the history
  • Loading branch information
davidshepherd7 committed May 6, 2017
1 parent 6ca17db commit 600ce96
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 22 deletions.
40 changes: 40 additions & 0 deletions README.md
@@ -1,3 +1,43 @@
# Anki-cli

A command line interface for managing Anki decks.

This is a proof-of-concept version which is currently only capable of adding new
cards to your decks from a simple text file format.

## Installation

Requires python3 and pip already installed (e.g. `sudo apt install python3 python3-pip`)

1. Install [anki-connect](https://github.com/FooSoft/anki-connect) (anki addon 2055492159) and restart Anki
2. Run `pip3 install -r` in this directory


## Usage

Example:

./bin/anki-cli --card_file docs/example-card.txt

run with `-h` for more details


## File format

The `@` character at the beginning of a line is used to mark the name of a
field. Everything else on the line is used as the field name (with leading and
trailing whitespace removed), everything from the next line until the start of
the next field is used as the contents of the field.

For example a standard "Basic" card type would be written as:

```
@Front
What is a foo?
@Back
A particularly vicious variety of dandelion.
```

No other parsing or modification is done, so html, cloze deletions, LaTeX, etc.
should all work as normal.
56 changes: 56 additions & 0 deletions ankicli/ankicli.py
@@ -0,0 +1,56 @@
import requests
import json

import re


def pretty_print_request(r):
"""
At this point it is completely built and ready
to be fired; it is "prepared".
However pay attention at the formatting used in
this function because it is programmed to be pretty
printed and may differ from the actual request.
"""
print('{}\n{}\n{}\n\n{}\n{}'.format(
'-----------START-----------',
r.request.method + ' ' + r.request.url,
'\n'.join('{}: {}'.format(k, v) for k, v in r.request.headers.items()),
r.request.body,
'-----------END-----------',
))
print('\n{}\n{}\n{}\n{}'.format(
'------START-RESPONSE-----',
'Status Code: ' + str(r.status_code),
r.text,
'-------END-RESPONSE------',
))


def anki_connect(method, params={}, debug=False):
r = requests.post('http://localhost:8765', data=json.dumps({'action': method, 'params': params}))

if debug:
pretty_print_request(r)

r.raise_for_status()

if r.json() is None:
raise Exception('Anki connect responded with null, this is normally an error')

return r.json()


def parse(string):
sections = re.split('^\s*@', string, flags=re.MULTILINE)
nonempty_sections = (sec for sec in sections if sec.strip() != '')

out = {}
for sec in nonempty_sections:
lines = sec.split('\n')
key = lines[0].strip().lstrip('@')
value = '\n'.join(lines[1:])
out[key] = value

return out
77 changes: 77 additions & 0 deletions bin/anki-cli
@@ -0,0 +1,77 @@
#! /usr/bin/env python3

import sys
import argparse
import re

import ankicli.ankicli as ankicli

def parse_arguments(argv):
parser = argparse.ArgumentParser()
parser.add_argument('--deck', default='Default', help = 'The Anki deck to use')
parser.add_argument('--model', default='Basic', help='The model (card type) to use')
parser.add_argument('--card_file', default='-', help='The file to read card data from, - means use stdin')

args = parser.parse_args(argv)

return args


def error(*args, **kwargs):
print(*args, **kwargs, file=sys.stderr)
sys.exit(1)


def stdin_or_file(filename):
if filename == '-':
return sys.stdin
else:
return open(filename, 'r')


def main(argv):
"""
"""

# Parse cli arguments
args = parse_arguments(argv)

version = ankicli.anki_connect('version')
if version < 2:
error('anki-cli requires anki connect version 2 or greated')

decks = ankicli.anki_connect('deckNames')
if args.deck not in decks:
error('Bad deck name, expected one of', decks)

models = ankicli.anki_connect('modelNames')
if args.model not in models:
error('Bad model name, expected one of', models)

fields = ankicli.anki_connect('modelFieldNames', {'modelName': args.model})

with stdin_or_file(args.card_file) as card_file:
card = ankicli.parse(card_file.read())

# For some reason I can't get addNote working, but this works fine.
response = ankicli.anki_connect('addNotes', {
'notes': [{
'deckName': args.deck,
'modelName': args.model,
'fields': card,
'tags': [],
}]
})
new_card_id = response[0]

if new_card_id is None:
error('Failed to create card, does it have unique fields?')

print('Created new card with id', response[0])

# Return success
return 0

# If this script is run from a shell then run main() and return the result.
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
10 changes: 10 additions & 0 deletions docs/example-card.html
@@ -0,0 +1,10 @@
@Front
What is a <b>foo</b>?
@Back
A fluffy fluffy

<ul>
<li>doge</li>
<li>cat</li>
<li>or fish</li>
</ul>
4 changes: 4 additions & 0 deletions docs/example-card.txt
@@ -0,0 +1,4 @@
@Front
What is a foo?
@Back
A particularly vicious variety of dandelion.
1 change: 1 addition & 0 deletions requirements.txt
@@ -0,0 +1 @@
requests
19 changes: 0 additions & 19 deletions setup.py

This file was deleted.

22 changes: 19 additions & 3 deletions tests/anki-cli-tests.py
@@ -1,5 +1,21 @@
from nose.tools import *
import ankicli
import ankicli.ankicli as ankicli

def test_basic():
print("I RAN!")

def test_parse_single_field():
assert ankicli.parse("") == {}
assert ankicli.parse("@First") == {'First': ''}
assert ankicli.parse("@First\nFoo") == {'First': 'Foo'}
assert ankicli.parse("@First\nFoo\n") == {'First': 'Foo\n'}
assert ankicli.parse("@First\nFoo@@\n") == {'First': 'Foo@@\n'}
assert ankicli.parse("@First\nFoo\nBar\nBaz") == {'First': 'Foo\nBar\nBaz'}


def test_parse_whitespace():
assert ankicli.parse("@First \nFoo\n") == {'First': 'Foo\n'}
assert ankicli.parse("@ First \nFoo\n") == {'First': 'Foo\n'}
assert ankicli.parse(" @First") == {'First': ''}


def test_parse_multi_field():
assert ankicli.parse("@First\nFoo\n@Second\nbar\n") == {'First': 'Foo\n', 'Second': 'bar\n'}

0 comments on commit 600ce96

Please sign in to comment.