Python API

A Simple Example

You can access account information from Avendesora using Python using a simple relatively high-level interface as shown in this example:

from avendesora import PasswordGenerator, PasswordError
from inform import display, fatal, os_error
from shlib import Run
from pathlib import Path

try:
    pw = PasswordGenerator()
    account = pw.get_account('mybank')
    name = account.get_value('name')
    username = account.get_username()
    passcode = account.get_passcode()
    url = account.get_value('ofxurl')
except PasswordError as e:
    e.terminate()

try:
    curl = Run(
        f'curl -K - {url!s}',
        stdin = f'user="{username!s}:{passcode!s}"',
        modes='sOEW0'
    )
    Path(f'{name!s}.ofx').write_text(curl.stdout)
except OSError as e:
    fatal(os_error(e))

Basically, the approach is to open the password generator, open an account, and then access values of that account. The various components of the Avendesora programming interface are described next.

Components

This section documents the programming interface for Avendesora. You can view the Avendesora source code, particularly avendesora.command, for further examples on how to use this interface.

PasswordGenerator Class

This is the entry class to Avendesora. It is the only class you need instantiate directly. By instantiating it you cause Avendesora to read the user’s account files.

class avendesora.PasswordGenerator(init=False, gpg_ids=None, warnings=True)

Initializes the password generator. You should pass no arguments unless you are creating the user’s Avendesora data directory.

Calling this class causes Avendesora to open all the various account files and returns an object that allows you access to the accounts. Specifically you can use the get_account() or all_accounts() methods to access an account or all the accounts.

Parameters:
  • init (bool) – Create user’s directory.
  • gpg_ids (list of strings) – List of GPG identities to use when creating user’s directory.
Raises:

avendesora.PasswordError – Indicates an issue opening the user’s accounts.

all_accounts()

Iterate through all accounts.

challenge_response(name, challenge)

Generate a response to a challenge.

Given the name of a master seed (actually the basename of the file that contains the master seed), returns an identifying response to a challenge. If no challenge is provided, one is generated based on the time and date. Returns both the challenge and the expected response as a tuple.

Parameters:
  • name (str) – The name of the master seed.
  • challenge (str) – The challenge (may be empty).
discover_account(url=None, title=None, verbose=False)

Discover the account from the environment.

Examine the environment and return the script that matches (the script is initialized, and so contains a pointer to the right account). If more than one account/secret matches, user is queried to resolve the ambiguity.

Parameters:
  • url (str) – Specifying the URL short-circuits the processing of the title that is used to find the URL.
  • title (str) – Override the window title. This is used for debugging.
  • verbose (bool) – Run the discovery process in verbose mode (adds more information to log file that can help debug account discovery.
Raises:

avendesora.PasswordError – There is no account that matches the given environment.

find_accounts(target)

Find accounts with names or aliases that contain a substring.

Parameters:target (str) – The desired substring.
Returns:Iterates through matching accounts.
Return type:avendesora.Account
get_account(name, request_seed=False, stealth_name=None)

Return a specific account.

Parameters:
  • name (str) – Looks up an account by name and returns it. This name must match an account name or an account alias. The matching algorithm ignores case and treats dash and underscore as equivalent.
  • request_seed (str or bool) – If specified an additional seed is provided to the account (see: misdirection). It may be specified as a string, in which case it is used as the seed. Otherwise if true, the seed it requested directly from the user.
  • stealth_name (str) – The name used as the account name if the account is a stealth account.
Returns:

An account. The class itself is returned, and not an instance of the class.

Return type:

avendesora.Account

Raises:

avendesora.PasswordError – There is no account that matches the given name.

search_accounts(target)

Find accounts with values that contain a substring.

Parameters:target (str) – The desired substring.
Returns:Iterates through matching accounts.
Return type:avendesora.Account

Account Class

class avendesora.Account

Class that holds all the information specific to an account.

Add desired account information as attributes of the class.

classmethod get_composite(name)

Get field value given a field name.

A lower level interface than get_value() that given a name returns the value of the associated field, which may be a scalar (string or integer) or a composite (array of dictionary). Unlike get_value(), the actual value is returned, not a object that contains multiple facets of the value.

Parameters:name (str) – The name of the field.
Returns:The requested value.
classmethod get_fields(all=False)

Iterate through fields.

Iterates through the field names.

Example:

for name, keys in account.get_fields():
    if keys:
        display(name + ':')
        for key, value in account.get_values(name):
            display(indent(
                value.render(('{k}) {d}: {v}', '{k}: {v}'))
            ))
    else:
        value = account.get_value(name)
        display(value.render('{n}: {v}'))
Parameters:all (bool) – If False, ignore the tool fields.
Returns:A pair (2-tuple) that contains both field name and the key names. None is returned for the key names if the field holds a scalar value.
classmethod get_name()

Get account name.

Returns:Returns the primary account name. This is generally the class name converted to lower case unless it was overridden with the NAME attribute.
classmethod get_passcode()

Get the passcode.

Like get_value(), but tries the credential_secrets in order and returns the first found. credential_secrets is an Avendesora configuration setting that by default is password, passphrase, and passcode.

Returns:The passcode.
classmethod get_scalar(name, key=None, default=False)

Get field Value given a field name and key.

A lower level interface than get_value() that given a name and perhaps a key returns a scalar value. Also takes an optional default value that is returned if the value is not found. Unlike get_value(), the actual value is returned, not a object that contains multiple facets of the value.

The name is the field name, and the key would identity which value is desired if the field is a composite. If default is False, an error is raised if the value is not present, otherwise the default value itself is returned.

Parameters:
  • name (str) – The name of the field.
  • key (str or int) – The key for the desired value (should be None for scalar values).
  • default – The value to return if the requested value is not available.
Returns:

The requested value.

classmethod get_username()

Get the username.

Like get_value(), but tries the credential_ids in order and returns the first found. credential_ids is an Avendesora configuration setting that by default is username and email.

Returns:The username or email address.
classmethod get_value(field=None)

Get account value.

Return value from the account given a user friendly identifier or script. User friendly identifiers include:

None: value of default field
name: scalar value
name.key or name[key]:
member of a dictionary or array
key is string for dictionary, integer for array

Scripts are simply strings with embedded attributes. Ex: ‘username: {username}, password: {passcode}’

Parameters:field (str) – Field identifier or script.
Returns:the desired value.
Return type:avendesora.AccountValue
classmethod get_values(name)

Iterate through the values for a field.

Parameters:name (str) – The name of the field.
Returns:Returns a pair (2-tuple) that contains the key and the value given as an avendesora.AccountValue for each of the values. If the value is a scalar, the key is None.

AccountValue Class

class avendesora.AccountValue(value, is_secret, name=None, key=None, desc=None)

An account value.

This is the object returned by avendesora.Account.get_value() and avendesora.Account.get_values(). It contains information about a single account value. Specifically, it provides the following attributes: value, is_secret, name, key, field, and desc.

render(fmts=('{f} ({d}): {v}', '{f}: {v}'))

Return value formatted as a string.

Parameters:fmts (collection of strings) –

fmts contains a sequence of format strings that are tried in sequence. The first one for which all keys are known is used. The possible keys are:

n – name (identifier for the first level of a field)
k – key (identifier for the second level of a field)
f – field (name.key)
d – description
v – value

If none work, the value alone is returned.

Returns:The value rendered as a string.

PasswordError Exception

exception avendesora.PasswordError(*args, **kwargs)

Password error.

This exception subclasses Inform.Error.

This exception subclasses inform.Error.

get_culprit()

Get exception culprit.

If the culprit keyword argument was specified as a string, it is returned. If it was specified as a collection, the members are converted to strings and joined with culprit_sep. The resulting string is returned.

get_message(template=None)

Get exception message.

Parameters:template (str) –

This argument is treated as a format string and is passed both the unnamed and named arguments. The resulting string is treated as the message and returned.

If not specified, the template keyword argument passed to the exception is used. If there was no template argument, then the positional arguments of the exception are joined using sep and that is returned.

Returned:
The formatted message without the culprits.
render(template=None)

Convert exception to a string for use in an error message.

Parameters:template (str) –

This argument is treated as a format string and is passed both the unnamed and named arguments. The resulting string is treated as the message and returned.

If not specified, the template keyword argument passed to the exception is used. If there was no template argument, then the positional arguments of the exception are joined using sep and that is returned.

report(template=None)

Report exception.

The inform.error() function is called with the exception arguments.

Parameters:template (str) –

This argument is treated as a format string and is passed both the unnamed and named arguments. The resulting string is treated as the message and returned.

If not specified, the template keyword argument passed to the exception is used. If there was no template argument, then the positional arguments of the exception are joined using sep and that is returned.

terminate(template=None)

Report exception and terminate.

The inform.fatal() function is called with the exception arguments.

Parameters:template (str) –

This argument is treated as a format string and is passed both the unnamed and named arguments. The resulting string is treated as the message and returned.

If not specified, the template keyword argument passed to the exception is used. If there was no template argument, then the positional arguments of the exception are joined using sep and that is returned.

with_traceback()

Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.

Example: Displaying Account Values

The following example prints out all account values for account whose name are found in a list.

from avendesora import PasswordGenerator
from inform import display, indent, Error

accounts = ['bank', 'credit-union', 'brokerage']

try:
    pw = PasswordGenerator()

    for account_name in accounts:
        account = pw.get_account(account_name)
        description = account.get_scalar('desc', None, account_name)
        display(description, len(description)*'=', sep='\n')

        for name, keys in account.get_fields():
            if keys:
                display(name + ':')
                for key, value in account.get_values(name):
                    display(indent(
                        value.render(('{k}) {d}: {v}', '{k}: {v}'))
                    ))
            else:
                value = account.get_value(name)
                display(value.render('{n}: {v}'))
        display()
except Error as e:
    e.terminate()

Example: Add SSH Keys

This example adds SSH keys to your SSH agent. It uses pexpect to manage the interaction between this script and ssh-add.

#!/usr/bin/env python3
"""
Add SSH keys

Add SSH keys to SSH agent.
The following keys are added: {keys}.

Usage:
    addsshkeys [options]

Options:
    -v, --verbose    list the keys as they are being added
"""
# This assumes that the Avendesora account that contains the ssh key's
# passphrase has a name or alias of the form <name>-ssh-key. It also assumes
# that the account contains a field named 'keyfile' or 'keyfiles' that contains
# an absolute path or paths to the ssh key files in a string.

from avendesora import PasswordGenerator, PasswordError
from inform import Inform, codicil, error, fatal, narrate
from docopt import docopt
from pathlib import Path
import pexpect

SSHkeys = ['primary', 'github', 'backups']
SSHadd = 'ssh-add'

cmdline = docopt(__doc__.format(keys = ', '.join(SSHkeys)))
Inform(narrate=cmdline['--verbose'])

try:
    pw = PasswordGenerator()
except PasswordError as e:
    e.terminate()

for key in SSHkeys:
    name = key + '-ssh-key'
    try:
        account = pw.get_account(name)
        passphrase = account.get_passcode().value
        if account.has_field('keyfiles'):
            keyfiles = account.get_value('keyfiles').value
        else:
            keyfiles = account.get_value('keyfile').value
        for keyfile in keyfiles.split():
            path = Path(keyfile).expanduser()
            narrate('adding.', culprit=keyfile)
            try:
                sshadd = pexpect.spawn(SSHadd, [str(path)])
                sshadd.expect('Enter passphrase for %s: ' % (path), timeout=4)
                sshadd.sendline(passphrase)
                sshadd.expect(pexpect.EOF)
                sshadd.close()
                response = sshadd.before.decode('utf-8')
                if 'identity added' in response.lower():
                    continue
            except (pexpect.EOF, pexpect.TIMEOUT):
                pass
            error('failed.', culprit=path)
            codicil('response:', sshadd.before.decode('utf8'), culprit=SSHadd)
            codicil('exit status:', sshadd.exitstatus , culprit=SSHadd)
    except PasswordError as e:
        fatal(e, culprit=path)

Example: Postmortem Letter

This is a program that generates messages for a person’s children and partners. It is assumed that these messages would be placed into a safe place to be found and read upon the person’s death.

It examines all accounts looking for a special field, postmortem_recipients. If the field exists, then that account is included in the file of accounts sent to that recipient. The script also looks for another special field, estimated_value. It includes this value in the message and prints the values to the standard output when generating the messages. This gives you a chance to review the values and update them if they are stale. The generated files are encrypted so that only the intended recipients can read them.

#!/usr/bin/env python3

from avendesora import PasswordGenerator, PasswordError
from inform import Error, cull, display, indent, os_error, terminate, warn
import gnupg

me = 'morgase@andor.gov'
recipients = dict(
    kids='galad@trakand.name gawyn@trakand.name elayne@trakand.name',
    partners='taringail.damodred@andor.gov',
)

try:
    pw = PasswordGenerator()
    accounts = {}

    # scan accounts and gather information for recipients
    for account in pw.all_accounts():
        account_name = account.get_name()
        class_name = account.__name__
        description = account.get_scalar('desc', None, None)

        # summarize account values
        value = account.get_scalar('estimated_value', default=None)
        postmortem_recipients = account.get_scalar('postmortem_recipients', default=None)
        if value and not postmortem_recipients:
            warn(f'{account.get_name()}: no recipients.')
            continue
        if not postmortem_recipients:
            continue
        postmortem_recipients = postmortem_recipients.split()

        # gather information for recipients
        for recipient in recipients:
            if recipient in postmortem_recipients:
                # output title
                title = ' - '.join(cull([class_name, description]))
                lines = [title, len(title)*'=']

                # output avendesora names
                aliases = account.get_composite('aliases')
                names = [account_name] + (aliases if aliases else [])
                lines.append('avendesora names: ' + ', '.join(names))

                # output user fields
                for name, keys in account.get_fields():
                    if name in ['postmortem_recipients', 'desc', 'NAME']:
                        continue
                    if keys:
                        lines.append(name + ':')
                        for key, value in account.get_values(name):
                            lines += indent(
                                value.render(('{k}) {d}: {v}', '{k}: {v}'))
                            ).split('\n')
                    else:
                        value = account.get_value(name)
                        lines += value.render('{n}: {v}').split('\n')
                if recipient not in accounts:
                    accounts[recipient] = []
                accounts[recipient].append('\n'.join(lines))


    # generate encrypted files than contain about accounts for each recipient
    gpg = gnupg.GPG(gpgbinary='gpg2')
    for recipient, idents in recipients.items():
        if recipient in accounts:
            content = accounts[recipient]
            num_accounts = len(content)
            encrypted = gpg.encrypt(
                '\n\n\n'.join(content),
                idents.split() + me.split()
            )
            if not encrypted.ok:
                raise Error(
                    'unable to encrypt:', encrypted.stderr, culprit=recipient
                )
            try:
                filename = recipient + '.gpg'
                with open(filename, 'w') as file:
                    file.write(str(encrypted))
                display(f'contains {num_accounts} accounts.', culprit=filename)
            except OSError as e:
                raise Error(os_error(e))
        else:
            warn('no accounts found.', culprit=recipient)

except KeyboardInterrupt:
    terminate('Killed by user.')
except (PasswordError, Error) as e:
    e.terminate()

Example: Net Worth

If you have added estimated_value to all of your accounts that hold significant value as proposed in the previous example, then the following script will summarize the values and estimate your net worth:

#!/usr/bin/env python3
"""Networth

Usage:
    networth [options]

Options:
    -v, --verbose  output actual estimated value text rather than just the value
"""

from avendesora import PasswordGenerator, PasswordError
from inform import display, terminate
from quantiphy import Quantity
from docopt import docopt
Quantity.set_prefs(prec=2)


cmdline = docopt(__doc__)
verbose = cmdline['--verbose']

try:
    pw = PasswordGenerator()

    # scan accounts and gather values for appropriate accounts
    amounts = {}
    width = 0
    total = 0
    for account in pw.all_accounts():
        text = account.get_scalar('estimated_value', default=None)
        if text:
            components = text.split()
            value = Quantity(components[0])
            if verbose:
                to_output = text
            else:
                to_output = value
            if value.units in ['$', 'USD']:
                total += value
            else:
                to_output = f'{to_output} (not in total)'
            account_name = account.get_name()
            width = max(width, len(account_name))
            amounts[account_name] = to_output
    total = Quantity(total, "$")

    # display the account values
    for key, val in amounts.items():
        display(f'{key:>{width+2}s}: {val}')
    display(f'{"TOTAL":>{width+2}s}: {total}')

except KeyboardInterrupt:
    terminate('Killed by user.')
except PasswordError as e:
    e.terminate()

This script assumes that the account value with units are the first thing in the estimated_value string. It is common to combine the estimated value with the date it was last updated. Something like this:

estimated_value = '$11k in January 2018'

The text before the first whitespace is considered the value, and if it has units of dollars it will be added to the reported total. Here is a typical output:

      mint: $19k
betterment: $22k
     chase: $12k
 southwest: 78kmiles (not in total)
     TOTAL: $53k