Jump to content

Recommended Posts

I have a few questions regarding workflow structure, storage, and python how-to's.

 

Anki is a flashcard program, it is written in python, and has a sqlite database.

 

 

1. In order to connect to the database I need to supply a path. Since I'm using the Anki module alone (without anki's version of PyQt, aqt), I have to supply the path manually. The way I'm doing this is rather costly, and I would like to "store" the path after a successful connection is made, so further instances of the workflow have access to and use this path. Below, I've covered the most likely paths to Anki, but at a later time I would like to ask the user, through Alfred, to input the path if an error occurs.

 

As a point of style, is it customary to import a module within a def?

def get_col():    
    default_locations = [
                         os.environ['HOME']+'/Anki/User 1/collection.anki2',
                         os.environ['HOME']+'/.anki/User 1/collection.anki2',
                         'collection.anki2'
                        ]
    for location in default_locations:
        if os.path.exists(location):
            return location
        else:
            import glob
            home_path = os.path.expanduser('~')
            pattern = (home_path + '/**' + '/Anki' + '/*' + '/collection.anki2')
            for path in glob.glob(pattern):
                return path

2. When a result is selected with Alfred, and passed to the next script as the query, how can I get the result of the second script to display in Alfred? The wf.add_item doesn't seem to do it. The script 2, at the end of the post, should display the card count.

 

 

3. Besides querying Anki's database, eventually I would like to add cards from Alfred. The flow of the workflow ideally would be to first select a deck, and then options and deck statistics would display below in the results area. One option would be to add a card to the selected deck. After choosing "Add Card" the user would type in the front side, and by some magic of python, the front side text would display as the first result, the input area would clear and allow for the backside to be entered, which would display as the second result before submitting the change. This is why I'm asking for suggestions for structure. Is there a best way to account for these options by structuring the workflow a particular way?

 

Further down the road I could allow for changes to the front or back after the initial input so that mistakes on card entry could be corrected. And much further down, I could allow for images to be attached with Alfred's file action. I've been playing with Anki heavily, I've authored a addon, HTML 2 ANKI (repo), and generally feel confident to start merging what I've learned into an Alfred workflow. 

 

 

4. Within the workflow I've supplied the Anki module, but to import the module I first have to append the path with sys.path.append, else I receive errors, or run the script from the Anki folder. I did make sure to include __init__.py in all subfolders, however, the Anki module's __init__.py is not empty, and this seems to be the source of my frustration.  I won't need the module until I start adding cards with Alfred (Anki says that doing it through the database is not recommended, mentioning that when updating Anki issues may arise), but I wanted to know if appending the path is acceptable here.

 

I'll be sitting for step1 this summer, so I'm just trying to form a plan at this point, but afterwards I'd like to get this going. Anki's popular for people learning new languages, and med students, so I'm hoping it will be of use for a few people. As always suggestions are most appreciated.

 

 

Script 1:

# -*- coding: utf-8 -*-


# [Run from subdirectory]
import os, sys
#abspath = os.path.abspath(__file__)
#dname = os.path.dirname(abspath) + '/main'
#sys.path.append(dname)

# [IMPORT]
import re
import sqlite3
import json
#from anki import storage, stats
#from BeautifulSoup import BeautifulSoup, Comment, NavigableString
from workflow import Workflow


log = None
    
def get_col():
    default_locations = [
                         os.environ['HOME']+'/Anki/User 1/collection.anki2',
                         os.environ['HOME']+'/.anki/User 1/collection.anki2',
                         'collection.anki2'
                        ]
    for location in default_locations:
        if os.path.exists(location):
            return location
        else:
            import glob
            home_path = os.path.expanduser('~')
            pattern = (home_path + '/**' + '/Anki' + '/*' + '/collection.anki2')
            for path in glob.glob(pattern):
                return path
    return None



def db_decks(db):

    results =[]
    deck_db = db.execute("SELECT decks FROM col")

    decks = json.loads(deck_db.fetchone()[0])

    for d in decks.items():
        deck = {'title': None, 'id': None}

        deck['title'] = d[1]['name']
        deck['id'] = d[0]
        
        log.debug(deck)
        
        if deck['id'] is None:
            continue

        results.append(deck)
    
    return results



def key_for_deck(deck):
    return '{} {}'.format(deck['title'], deck['id'])



def main(wf):
        
    query = None

    if len(wf.args):
        query = wf.args[0]
    
    apath = get_col()
    connection = sqlite3.connect(apath)

    decks = db_decks(connection)

    if query:
        decks = wf.filter(query, decks, key_for_deck)

    if not decks:
        wf.add_item('No items', icon=ICON_WARNING)
        
    for deck in decks:
        wf.add_item(
            title    = deck['title'],
            subtitle = deck['id'],
            arg      = deck['id'] + ' ' + deck['title'] + ' ' + apath,
            valid    = True,
            icon     = 'icon.png')

    wf.send_feedback()


if __name__ == '__main__':
    wf = Workflow()
    log = wf.logger
    sys.exit(wf.run(main))

Script 2:

 

1. How can I get it's result to display in Alfred?

2. With very large decks (many cards), I get an error saying so, I'm thinking this is the sqlite, ideas? 

# -*- coding: utf-8 -*-

import os, sys, re
import sqlite3, json
from workflow import Workflow

log = None    

def db_cards(db, did):
        
    cards_did = db.execute("SELECT COUNT(*) "
                                    "FROM cards "
                                    "WHERE cards.did = ?", (did,) )
                    
    card_count = cards_did.fetchone()
    result = card_count[0]
    
    return result


def main(wf):
        
    did, d_title, apath = wf.args[0].split()
    connection = sqlite3.connect(apath)
    count = db_cards(connection, did)

    wf.add_item(
        title=d_title,
        subtitle=unicode(count),
        arg=did,
        valid=True,
        icon='icon.png')

    wf.send_feedback()
    
    
    
if __name__ == '__main__':
    wf = Workflow()
    log = wf.logger
    sys.exit(wf.run(main))
Edited by DrLulz
Link to comment

So, as always it seems, I need to dig a little deeper before asking questions. My first question is outlined, in detail, in the Alfred-Workflow docs under Persistent Data. Please disregard question 1.

 

I'm not a programmer, so I'm really looking for a way to think about structuring this project. Though, I'm sure its hard to say given that I don't have more information regarding the desired goal. I'm trying to prevent a complete rework after I'm months into the project, but then again, that may be just what I need to actually learn something. :o

Link to comment

As a point of style, is it customary to import a module within a def?

It's better to put them all at the top of the module/script, but if you only need a library in a certain function, and it takes a long time to import, you should import it within the def. I tend to do it quite a lot in workflows because you want the scripts to run as fast as possible.

 

When a result is selected with Alfred, and passed to the next script as the query, how can I get the result of the second script to display in Alfred?

That isn't how Alfred works. Only Script Filters (and File Filters) can show results directly in Alfred, but you can't pass the results of one Script Filter into another.

If you want to "drill-down" into a set of results, you have a couple of options:

  • Call Alfred again from a Run Script action via AppleScript. You can either use an External Trigger or the normal tell application "Alfred 2" to search "<keyword> <query>".
  • Use a separator in your query (e.g. a slash or arrow or space) and have the same Script Filter handle the different levels. Here's a fairly simple example of a workflow that does it this way.

Method 1 is much simpler from the developer's perspective in terms of concept and implementation. Method 2 has the advantage that users can "back up" through your UI in Alfred. With Method 1, they have to close Alfred and start again.

 

One option would be to add a card to the selected deck. After choosing "Add Card" the user would type in the front side, and by some magic of python, the front side text would display as the first result, the input area would clear and allow for the backside to be entered, which would display as the second result before submitting the change. This is why I'm asking for suggestions for structure. Is there a best way to account for these options by structuring the workflow a particular way?

I've put together a simple demonstration of how you could do the "magic" using Method 2 above.

Have you read the forum sticky on chaining scripts together? That's very relevant, too.

 

I'm not a programmer, so I'm really looking for a way to think about structuring this project. Though, I'm sure its hard to say given that I don't have more information regarding the desired goal. I'm trying to prevent a complete rework after I'm months into the project, but then again, that may be just what I need to actually learn something.

You should expect the first version of anything non-trivial you code to be terrible and need to be rewritten. If you've recognised fundamental problems already, you need to stop, go back and fix them. Otherwise it'll just be even more difficult later.

 

You're not throwing away months of work: that was months of research and learning.

 

If you're new to programming, the most important thing to remember is that your code should look bad. When it stops looking bad to you, that means you've stopped improving…

Link to comment

I appreciate the demo. Thank you for that.

 

I'm going with the leveled approach for the "drill-down". The easier the workflow is to navigate the more I use it, so usability and functionality are my top concerns. 

 

In the Alfred-Workflow readthedocs tutorial you use argparse, but I've noticed in many of you workflows you use docopt. What's the advantage?

 

Also, in one of my tests I chained a script filter to a run script which saves cached_data before calling a trigger. Its working ok, but is this disadvantageous? It won't be used too often, as I'm shooting for leveled navigation, but its nice to have options.

Link to comment

In the Alfred-Workflow readthedocs tutorial you use argparse, but I've noticed in many of you workflows you use docopt. What's the advantage?

I just prefer docopt. It (often) makes argument parsing easier, and I particularly like that it's literally based on the documentation. If the docs aren't right, the script doesn't work right.

I like to write my workflows (and most things in general) as command-line applications. I used argparse in the tutorial because it's built into Python 2.7. I probably should have used getopt, as Alfred-Workflow supports 2.6, but getopt is icky.

 

Also, in one of my tests I chained a script filter to a run script which saves cached_data before calling a trigger. Its working ok, but is this disadvantageous? It won't be used too often, as I'm shooting for leveled navigation, but its nice to have options.

I'm not entirely sure what you think might be a problem, but there's no issue caching data between calls to your workflow. It's a perfectly sensible and legitimate way to work. You just need to be careful to ensure that any script reading its input from a given cache can only be called from a script that has set the same cache.

Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...