class Collection(object):
    fmt = '{v}'     # default value format
    sep = " "       # default separator
    splitter = "|"  # default format splitter (goes between fmt and sep in template)

    """Collection

    Takes a list or dictionary and provides both a list like and dictionary like
    API, meaning that it provides the keys, values, and items methods like
    dictionary and you can iterate through the value like a list.  When applying
    keys to a list, the indices are returned as the keys. You can also use an
    index or a key to get a value, you test to see if a value is in the
    collection using the *in* operator, and you can get the length of the
    collection.

    If splitter is a string or None, then you can pass in a string and it will
    be split into a list to form the collection.  You can also pass in a
    splitting function.
    """

    def __init__(self, collection, splitter=None, **kwargs):
        if is_str(collection):
            if callable(splitter):
                self.collection = splitter(collection, **kwargs)
                return
            elif splitter is not False:
                self.collection = collection.split(splitter)
                return
        if is_collection(collection):
            self.collection = collection
            return
        if collection is None:
            self.collection = []
            return
        # is scalar
        self.collection = {None: collection}

    def keys(self):
        try:
            return list(self.collection.keys())
        except AttributeError:
            return list(range(len(self.collection)))

    def values(self):
        try:
            return [self.collection[k] for k in self.collection.keys()]
        except AttributeError:
            return list(self.collection)

    def items(self):
        try:
            return [(k, self.collection[k]) for k in self.collection.keys()]
        except AttributeError:
            return list(enumerate(self.collection))

    def get(self, key, default=Unspecified):
        try:
            return self.collection[key]
        except (KeyError, IndexError):
            if default == Unspecified:
                raise
            return default

    def render(self, fmt=None, sep=None):
        """Convert the collection into a string

        fmt (str):
            fmt is a format string applied to each of the items in the
            collection where {k} is replaced with the key and {v} replaced with
            the value.
        sep (str):
            The string used to join the formatted items.

        Example:

            >>> from collection import Collection

            >>> dogs = Collection({'collie': 3, 'beagle':1, 'shepherd': 2})
            >>> print('dogs: {}.'.format(dogs.render('{k} ({v})', ', ')))
            dogs: collie (3), beagle (1), shepherd (2).

            >>> print('dogs: {}.'.format(dogs.render(sep=', ')))
            dogs: 3, 1, 2.

        """
        if not fmt:
            fmt = self.fmt
        if not sep:
            sep = self.sep

        if callable(fmt):
            return sep.join(fmt(k, v) for k, v in self.items())
        return sep.join(fmt.format(v, k=k, v=v) for k, v in self.items())

    def __format__(self, template):
        """Convert the collection into a string

        The template consists of two components separated by a vertical bar. The
        first component specifies the formatting from each item. The key and
        value are interpolated using {{k}} to represent the key and {{v}} to
        represent the value.  The second component is the separator. Thus:

            >>> dogs = Collection({'collie': 3, 'beagle':1, 'shepherd': 2})
            >>> print('dogs: {:{{k}} ({{v}})|, }.'.format(dogs))
            dogs: collie (3), beagle (1), shepherd (2).

        """
        if template:
            components = template.split(self.splitter)
            if len(components) == 2:
                fmt, sep = components
            else:
                fmt, sep = components[0], " "
        else:
            fmt, sep = None, None
        if not fmt:
            fmt = self.fmt
        if not sep:
            sep = self.sep

        if callable(fmt):
            return sep.join(fmt(k, v) for k, v in self.items())
        return sep.join(fmt.format(v, k=k, v=v) for k, v in self.items())

    def __contains__(self, item):
        return item in self.values()

    def __iter__(self):
        return iter(self.values())

    def __len__(self):
        return len(self.collection)

    def __getitem__(self, key):
        return self.collection[key]

    def __repr__(self):
        return f'{self.__class__.__name__}({self.collection!r})'

    def __str__(self):
        return str(self.collection) def get_setting(name, default=None, expand=True):
    name = name.lower()
    try:
        value = Config[name]
    except KeyError:
        try:
            value = CONFIG_DEFAULTS[name]
        except KeyError:
            try:
                value = NONCONFIG_SETTINGS[name]
            except KeyError:
                return default
    if value is None:
        return default
    if name == 'gpg_ids':
        value = value.split() if is_str(value) else value
    elif name.endswith('_dir'):
        value = Path(value)
    elif expand and name.endswith('_file'):
        value = get_setting('settings_dir') / value
    return value def override_setting(name, value):
    Config[name.lower()] = value class GenericEditor(Editor):
    NAME = 'generic'

    @classmethod
    def run(cls, filepath, account = None):
        args = dict(
            filepath = filepath,
            account = account,
        )
        try:
            editor_setting = 'edit_account' if account else 'edit_template'
            cmd = get_setting(editor_setting)
            cmd = [e.format(**args) for e in cmd]

            try:
                Run(cmd, 'soEW')
            except Error as e:
                e.reraise(culprit=cmd[0])
        except KeyError as e:
            raise PasswordError(
                *e.args,
                template='invalid field: {0}',
                culprit=setting_path(editor_setting)
            ) class AccountFiles:
    def __init__(self, warnings=True):  # {{{2
        # initialize object {{{3
        self.loaded = set()
        self.name_index = {}
        self.name_manifests = {}
        self.url_manifests = {}
        self.title_manifests = {}
        self.shared_secrets = {}
        self.existing_names = {}

        # check permissions, dates on account files {{{3
        most_recently_updated = 0
        mask = get_setting('account_file_mask')
        for filename in get_setting('accounts_files', []):
            path = get_setting('settings_dir') / filename
            resolved_path = path.resolve()

            # check file permissions
            permissions = getmod(resolved_path)
            violation = permissions & mask
            if violation:
                recommended = permissions & ~mask & 0o777
                warn(
                    "file permissions are too loose;",
                    f"tightening to {recommended:03o}.",
                    culprit = path
                )
                chmod(recommended, resolved_path)

            # determine time of most recently updated account file
            updated = resolved_path.stat().st_mtime
            if updated > most_recently_updated:
                most_recently_updated = updated

        # check for missing or stale archive file {{{3
        archive_file = get_setting('archive_file')
        if archive_file and warnings:
            if archive_file.exists():
                resolved_path = archive_file.resolve()

                # check file permissions
                permissions = getmod(resolved_path)
                violation = permissions & mask
                if violation:
                    recommended = permissions & ~mask & 0o777
                    warn(
                        "file permissions are too loose;",
                        f"tightening to {recommended:03o}.",
                        culprit = path
                    )
                    chmod(recommended, resolved_path)

                # warn user if archive file is out of date
                stale = float(get_setting('archive_stale'))
                archive_updated = resolved_path.stat().st_mtime
                if most_recently_updated > archive_updated:
                    log('Avendesora archive is {:.0f} hours old and out of date.'.format(
                        (most_recently_updated - archive_updated)/3600
                    ))
                    # archive_age = time() - archive_updated
                    account_age = time() - most_recently_updated
                    if account_age > 86400 * stale:
                        warn('stale archive.')
                        codicil(dedent("""\
                            Recommend running 'avendesora changed' to determine
                            which account entries have changed, and if all the
                            changes are expected, running 'avendesora archive'
                            to update the archive.
                        """), wrap=True)
                else:
                    log('Avendesora archive is up to date.')
            else:
                warn('archive missing.')
                codicil(
                    "Recommend running 'avendesora archive'",
                    "to create the archive."
                )

    def load_account_file(self, filename):  # {{{2
        if filename in self.loaded:
            return

        # read the file
        settings_dir = get_setting('settings_dir')
        path = settings_dir / filename
        account_file = PythonFile(path)
        contents = account_file.run()
        master_seed = contents.get('master_seed')
        if master_seed:
            self.shared_secrets[path.stem] = master_seed

        # traverse through all accounts and pass in fileinfo and master seed;
        # will be ignored if already set
        for account in Account.all_loaded_accounts():
            account.preprocess(master_seed, account_file, self.existing_names)

        self.loaded.add(filename)

    def load_account(self, name):  # {{{2
        canonical_name = canonicalize(name)
        self.read_manifests()
        if canonical_name in self.name_index:
            filename = self.name_index[canonical_name]
            self.load_account_file(filename)
            if canonical_name in Account._accounts:
                self.write_manifests()
                return
        # not in name_index, just read files until it is found
        for filename in get_setting('accounts_files', []):
            self.load_account_file(filename)
            if canonical_name in Account._accounts:
                break
        self.write_manifests()

    def load_account_files(self):  # {{{2
        for filename in get_setting('accounts_files', []):
            self.load_account_file(filename)
        self.write_manifests()

    def load_matching(self, title_components):  # {{{2
        self.read_manifests()
        found = False

        # load filenames that contain matching urls
        if 'host' in title_components:
            for filename, urls in self.url_manifests.items():
                for host, path, exact_path in urls:
                    if host != title_components['host']:
                        continue
                    if exact_path:
                        if path != title_components['path']:
                            continue
                    else:
                        if not title_components['path'].startswith(path):
                            continue
                    self.load_account_file(filename)
                    found = True

        # load filenames that contain matching titles
        title = title_components.get('rawtitle')
        if title:
            for filename, titles in self.title_manifests.items():
                for candidate in titles:
                    if fnmatch(title, candidate):
                        self.load_account_file(filename)
                        found = True

        # url/title not in cache (perhaps cache was missing), load everything
        if not found:
            self.load_account_files()

    def read_manifests(self):  # {{{2
        if self.name_index:
            return
        cache_dir = get_setting('cache_dir')
        manifests_path = cache_dir / MANIFESTS_FILENAME
        try:
            encrypted = manifests_path.read_bytes()

            user_key = get_setting('user_key')
            if not user_key:
                raise Error('no user key.')
            key = base64.urlsafe_b64encode(sha256(user_key.encode('ascii')).digest())
            fernet = Fernet(key)
            contents = fernet.decrypt(encrypted)

            try:
                cache = pickle.loads(contents, **PICKLE_ARGS)
                self.name_manifests = cache['names']
                self.url_manifests = cache['urls']
                self.title_manifests = cache['titles']

                # build the name_index by inverting the name_manifests
                self.name_index = {
                    h: n for n, l in self.name_manifests.items() for h in l
                }
            except (ValueError, pickle.UnpicklingError) as e:
                warn('garbled manifest.', culprit=manifests_path, codicil=str(e))
                manifests_path.unlink()
            assert isinstance(self.name_index, dict)
        except OSError as e:
            comment(os_error(e))
        except Exception as e:
            comment(e)

    def write_manifests(self):  # {{{2
        # do not modify existing name_index if no account files were loaded
        if not self.loaded:
            return
        log('Updating manifests.')
        name_manifests = self.name_manifests
        url_manifests = self.url_manifests
        title_manifests = self.title_manifests

        # remove entries for loaded files from name_index
        for filename in self.loaded:
            name_manifests[filename] = []
            url_manifests[filename] = []
            title_manifests[filename] = []

        # build name manifests
        for name, account in Account._accounts.items():
            filename = account._file_info_.path.name
            name_manifests[filename].append(name)

        # build url and title manifests
        for account in Account.all_loaded_accounts():
            filename = account._file_info_.path.name
            if filename not in self.loaded:
                # This should not occur, but if PasswordGenerator is
                # instantiated more than once, which should only occur in the
                # tests, then there could be accounts hanging around from
                # previous instantiations. Ignore them.
                continue
            discovery = getattr(account, 'discovery', ())

            # urls
            urls = []
            for each in Collection(discovery):
                for u in each.all_urls(components=True).values():
                    urls += u
            # urls is a list of triples, each triple consists of the host and
            # path components of the url, and a boolean indicating whether the
            # entire path must match.
            url_manifests[filename].extend(urls)

            # titles
            titles = []
            for each in Collection(discovery):
                for t in each.all_titles().values():
                    titles += t
            title_manifests[filename].extend(titles)

        # trip out the duplicates
        for filename in url_manifests:
            if filename in self.loaded:
                url_manifests[filename] = set(url_manifests[filename])
                title_manifests[filename] = set(title_manifests[filename])

        # write the manifests
        cache_dir = get_setting('cache_dir')
        manifests_path = cache_dir / MANIFESTS_FILENAME
        cache = dict(
            names=name_manifests, urls=url_manifests, titles=title_manifests
        )
        contents = pickle.dumps(cache)

        user_key = get_setting('user_key')
        if not user_key:
            return
        key = base64.urlsafe_b64encode(sha256(user_key.encode('ascii')).digest())
        fernet = Fernet(key)
        encrypted = fernet.encrypt(contents)

        try:
            mkdir(cache_dir)
            manifests_path.write_bytes(encrypted)
        except OSError as e:
            warn(os_error(e))

    @staticmethod
    def delete_manifests():  # {{{2
        cache_dir = get_setting('cache_dir')
        manifests_path = cache_dir / MANIFESTS_FILENAME
        rm(manifests_path) class PasswordError(Error):
    """Password error.

    This exception subclasses
    `Inform.Error <https://github.com/KenKundert/inform#exception>`_.
    """
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs class PasswordGenerator(object):
    """Initializes the password generator.
    You should pass no arguments unless you are creating the user's Avendesora
    data directory.

    Once instantiated, you can use get_account() to load a specific account, or
    all_accounts() to load all accounts.

    Args:
        init (bool): Create user's directory.

        gpg_ids (list of strings):
            List of GPG identities to use when creating user's directory.

        check_integrity (bool):
            If true will validate that certain critical components in Avendesora
            have not be tampered with.  Checking the integrity can take up to a
            second, so recommend you pass False on interactive commands that
            benefit from low startup overhead and True on the more expensive
            commands to assure integrity is occasionally checked.

        warnings (bool):
            Suppress warnings from accounts. Useful when processing many
            accounts.  Does not affect warnings from the integrity check.

    Raises:
        :exc:`avendesora.PasswordError`:
            Indicates an issue opening the user's accounts.
    """

    # Constructor {{{2
    def __init__(
            self, init=False, gpg_ids=None, check_integrity=False, warnings=True
    ):
        # initialize avendesora (these should already be done if called from
        # main, but it is safe to call them again)
        read_config()
        GnuPG.initialize()

        # create the avendesora data directory
        if init:
            self._initialize(gpg_ids, init)
            return

        # check the integrity of avendesora
        if check_integrity:
            self._validate_components()

        # prepare to read accounts files
        self.account_files = AccountFiles(warnings)

    # _initialize() (private){{{2
    def _initialize(self, gpg_ids, filename):
        # If filename is True, this is being called as part of the Avendesora
        # 'initialize' command, in which case all missing files should be created.
        # If filename is a string, this is being called as part of the
        # Avendesora 'new' command, in which case a single new account file
        # should be created.
        def split(s, l=72):
            # Break long string into a series of adjacent shorter strings
            if len(s) < l:
                return '"%s"' % s
            chunks = ['    "%s"' % s[i:i+l] for i in range(0, len(s), l)]
            return '\n' + '\n'.join(chunks) + '\n'

        # create settings directory if it does not exist
        settings_dir = to_path(get_setting('settings_dir'))
        if not settings_dir.exists():
            settings_dir.mkdir(mode=0o700, parents=True)
            settings_dir.chmod(0o700)

        # Create dictionary of available substitutions for CONTENTS strings
        fields = {}
        for key in CONFIG_DEFAULTS:
            value = get_setting(key, expand=False)
            value = render(str(value) if isinstance(value, Path) else value)
            fields.update({key: value})
        for key in NONCONFIG_SETTINGS:
            value = get_setting(key, expand=False)
            value = render(str(value) if isinstance(value, Path) else value)
            fields.update({key: value})
        fields['encoding'] = get_setting('encoding')
            # get this one again, this time without the quotes
        gpg_ids = gpg_ids if gpg_ids else get_setting('gpg_ids', [])
        fields.update({
            'section': '{''{''{''1',
            'master_seed': split(Hidden.conceal(generate_random_string(72))),
            'user_key': split(Hidden.conceal(generate_random_string(72))),
            'gpg_ids': repr(' '.join(gpg_ids)),
        })

        # delete the config_doc_file so we always get an updated version.
        if (get_setting('config_doc_file')):
            try:
                rm(get_setting('config_doc_file'))
            except OSError as e:
                warn(os_error(e))

        # create the initial versions of the files in the settings directory
        if filename is True:
            path = to_path(get_setting('account_list_file'))
            if path.suffix in GPG_EXTENSIONS:
                raise PasswordError('encryption is not supported.', culprit=path)

            # Assure that the default initial set of files is present
            for path, contents in [
                (get_setting('config_file'), CONFIG_FILE_INITIAL_CONTENTS),
                (get_setting('config_doc_file'), CONFIG_DOC_FILE_INITIAL_CONTENTS),
                (get_setting('hashes_file'), HASH_FILE_INITIAL_CONTENTS),
                (get_setting('user_key_file'), USER_KEY_FILE_INITIAL_CONTENTS),
                (get_setting('default_accounts_file'), ACCOUNTS_FILE_INITIAL_CONTENTS),
                (get_setting('default_stealth_accounts_file'), STEALTH_ACCOUNTS_FILE_INITIAL_CONTENTS),
                (get_setting('account_list_file'), ACCOUNT_LIST_FILE_CONTENTS),
            ]:
                if path:
                    log('Creating initial version.', culprit=path)
                    f = PythonFile(path)
                    f.create(contents.format(**fields), gpg_ids)
                        # create will not overwrite an existing file, instead it
                        # reads the file.
        else:
            # Create a new accounts file
            fields['accounts_files'] = render(
                get_setting('accounts_files', []) + [filename]
            )
                # do not sort, the first file is treated special
            path = to_path(get_setting('settings_dir'), filename)
            if path.exists():
                raise PasswordError('exists.', culprit=path)
            if path.suffix in GPG_EXTENSIONS and not gpg_ids:
                raise PasswordError('Must specify GPG IDs.')
            log('Creating accounts file.', culprit=path)
            f = PythonFile(path)
            f.create(ACCOUNTS_FILE_INITIAL_CONTENTS.format(**fields), gpg_ids)

            # update the accounts list file
            path = to_path(
                get_setting('settings_dir'), get_setting('account_list_file')
            )
            log('Update accounts list.', culprit=path)
            f = PythonFile(path)
            try:
                mv(path, str(path) + '~')
                f.create(ACCOUNT_LIST_FILE_CONTENTS.format(**fields), gpg_ids)
            except OSError as e:
                raise PasswordError(os_error(e))

    # _validate_components() (private) {{{2
    def _validate_components(self):
        from pkg_resources import resource_filename

        # check permissions on the settings directory
        path = get_setting('settings_dir')
        mask = get_setting('config_dir_mask')
        try:
            permissions = getmod(path)
        except FileNotFoundError:
            raise PasswordError('missing, must run initialize.', culprit=path)
        violation = permissions & mask
        if violation:
            recommended = permissions & ~mask & 0o777
            warn("directory permissions are too loose.", culprit=path)
            codicil("Recommend running: chmod {:o} {}".format(recommended, path))

        # Check that files that are critical to the integrity of the generated
        # secrets have not changed
        for path, kind in [
            (to_path(resource_filename(__name__, 'secrets.py')), 'secrets_hash'),
            (to_path(resource_filename(__name__, 'charsets.py')), 'charsets_hash'),
            ('default', 'dict_hash'),
            ('mnemonic', 'mnemonic_hash'),
        ]:
            try:
                contents = path.read_text()
            except AttributeError:
                contents = '\n'.join(Dictionary(path).get_words())
            except OSError as e:
                raise PasswordError(os_error(e))
            md5 = hashlib.md5(contents.encode('utf-8')).hexdigest()
            # Check that file has not changed.
            if md5 != get_setting(kind):
                warn("file contents have changed.", culprit=path)
                lines = wrap(dedent("""\
                        This could result in passwords that are inconsistent with
                        those created in the past.  Use 'avendesora changed' to
                        assure that nothing has changed. Then, to suppress this
                        message, change {hashes} to contain:
                    """.format(hashes=get_setting('hashes_file'))
                ))
                lines.append("     {kind} = '{md5}'".format(kind=kind, md5=md5))
                codicil(*lines, sep='\n')

    # get_account() {{{2
    def get_account(self, name, request_seed=False, stealth_name=None):
        """Return a specific account.

        Args:
            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:
                :ref:`misdirection <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:
            :class:`avendesora.Account`: An account. The class itself is
            returned, and not an instance of the class.

        Raises:
            :exc:`avendesora.PasswordError`:
                There is no account that matches the given name.
        """
        if not name:
            raise PasswordError('no account specified.')
        self.account_files.load_account(name)
        account = Account.get_account(name)
        account.initialize(request_seed, stealth_name)
        return account

    # get_value() {{{2
    def get_value(self, path, request_seed=False, stealth_name=None):
        """Get account value given path that includes account and field names.

        Args:
            path (str):
                Path includes account name and field name separated by a colon.
                Paths of the following forms are accepted:

                    |  *account*: account's default field
                    |  *account:name*: account and field name of a scalar value
                    |  *account:name.key* or *account:name[key]*:
                    |      member of a dictionary or array
                    |      key is string for dictionary, integer for array

            request_seed (str or bool):
                If specified an additional seed is provided to the account (see:
                :ref:`misdirection <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:
            :class:`avendesora.Account`: An account. The class itself is
            returned, and not an instance of the class.

        Raises:
            :exc:`avendesora.PasswordError`:
                There is no account of field that matches the given path.
        """
        name, _, field = path.partition(':')
        account = self.get_account(name.strip(), request_seed, stealth_name)
        return account.get_value(field.strip())

    # discover_account() {{{2
    def discover_account(self, 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.

        Args:
            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:
            :exc:`avendesora.PasswordError`:
                There is no account that matches the given environment.
        """
        log('Account Discovery ...')

        if not verbose:
            verbose = get_setting('verbose')

        # get and parse the title
        title_components = Title(url=url, override=title).get_data()

        # load candidate account files
        self.account_files.load_matching(title_components)

        # sweep through accounts to see if any recognize this title data
        matches = {}
        seen = set()
        for account in Account.all_loaded_accounts():
            name = account.get_name()
            if verbose:
                log('Trying:', name)
            for key, script in account.recognize(title_components, verbose):
                # identify and skip duplicates
                if (name, script) in seen:
                    continue
                seen.add((name, script))
                ident = '%s (%s)' % ((name, key) if key else (name, script))
                ident = '%s: %s' % (len(matches), ident)  # assure uniqueness
                matches[ident] = account, script
                log('%s matches' % ident)

        if not matches:
            msg = 'cannot find appropriate account.'
            raise PasswordError(msg)
        if len(matches) > 1:
            choice = show_list_dialog('Choose Secret', sorted(matches.keys()))
            if choice is None:
                raise PasswordError('user abort.')
            log('User selects %s' % choice)
            account, script = matches[choice]
        else:
            account, script = matches.popitem()[1]
                # this odd little piece of code gives the value of the one item in
                # the dictionary
        if script is True:
            # this bit of trickery gets the name of the default field
            n, k = account.split_field(None)
            script = "{%s}{return}" % account.combine_field(n, k)
        if is_str(script):
            script = Script(script)
        script.initialize(account)
        return script

    # all_accounts() {{{2
    def all_accounts(self):
        "Iterate through all accounts."
        self.account_files.load_account_files()
        for account in Account.all_loaded_accounts():
            yield account

    # find_acounts() {{{2
    def find_accounts(self, target):
        """Find accounts with names or aliases that contain a substring.

        Args:
            target (str): The desired substring.

        Returns:
            :class:`avendesora.Account`: Iterates through matching accounts.
        """
        for account in self.all_accounts():
            if account.id_contains(target):
                yield account

    # search_acounts() {{{2
    def search_accounts(self, target):
        """Find accounts with values that contain a substring.

        Args:
            target (str): The desired substring.

        Returns:
            :class:`avendesora.Account`: Iterates through matching accounts.
        """
        for account in self.all_accounts():
            if account.account_contains(target):
                yield account

    # challenge_response() {{{2
    def challenge_response(self, 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.

        Args:
            name (str): The name of the master seed.
            challenge (str): The challenge (may be empty).
        """

        try:
            if not challenge:
                from arrow import utcnow
                now = str(utcnow())
                c = Passphrase()
                c.set_seeds([now])
                challenge = str(c)
            r = Passphrase()
            shared_secrets = self.account_files.shared_secrets
            r.set_seeds([shared_secrets[name], challenge])
            response = str(r)
            return challenge, response
        except KeyError:
            choices = conjoin(sorted(shared_secrets.keys()))
            raise PasswordError(
                'Unknown partner. Choose from %s.' % choices,
                culprit=name
            ) class GnuPG(object):
    def __init__(self, path):
        self.path = to_path(path)

    @classmethod
    def initialize(cls, gpg_path=None, gpg_home=None, armor=None):

        cls.gpg_path = to_path(
            gpg_path if gpg_path else get_setting('gpg_executable')
        )
        override_setting('gpg_executable', cls.gpg_path)

        cls.gpg_home = to_path(
            gpg_home if gpg_home else get_setting('gpg_home')
        )
        override_setting('gpg_home', cls.gpg_home)

        armor = armor if armor is not None else get_setting('gpg_armor')
        if armor not in ARMOR_CHOICES:
            warn(
                "'%s' is not valid, choose from %s." % (
                    armor, conjoin(ARMOR_CHOICES)
                ), culprit=setting_path('gpg_armor')
            )
            armor = 'extension'
        cls.armor = armor
        override_setting('gpg_armor', armor)

        gpg_args = {}
        if cls.gpg_path:
            gpg_args.update({'gpgbinary': str(cls.gpg_path)})
        if cls.gpg_home:
            gpg_args.update({'gnupghome': str(cls.gpg_home)})
        try:
            cls.gpg = gnupg.GPG(**gpg_args)
        except ValueError as e:
            fatal(e)

    def save(self, contents, gpg_ids=None):
        path = self.path
        if not gpg_ids:
            gpg_ids = get_setting('gpg_ids', [])
        if is_str(gpg_ids):
            gpg_ids = gpg_ids.split()
        if not gpg_ids:
            # raise PasswordError('must specify GPG ID.')
            log('no gpg id available, using symmetric encryption.')

        use_gpg, use_armor = self._choices()
        if use_gpg:
            try:
                encoded = contents.encode(get_setting('encoding'))
                if gpg_ids:
                    encrypted = self.gpg.encrypt(encoded, gpg_ids, armor=use_armor)
                else:
                    encrypted = self.gpg.encrypt(encoded, None, symmetric='AES256', armor=use_armor)
                if not encrypted.ok:
                    msg = ' '.join(cull([
                        'unable to encrypt.',
                        getattr(encrypted, 'stderr', None)
                    ]))
                    raise PasswordError(msg, culprit=path, sep='\n')
                else:
                    path.write_bytes(encrypted.data)
            except ValueError as e:
                raise PasswordError(full_stop(e), culprit=path)
        else:
            path.write_text(contents, encoding=get_setting('encoding'))
        self.chmod()

    def read(self):
        path = self.path
        # file is only assumed to be encrypted if path has gpg extension
        if path.suffix.lower() in GPG_EXTENSIONS:
            try:
                with path.open('rb') as f:
                    decrypted = self.gpg.decrypt_file(f)
                    if not decrypted.ok:
                        msg = ' '.join(cull([
                            'unable to decrypt.',
                            getattr(decrypted, 'stderr', None)
                        ]))
                        raise PasswordError(msg, culprit=path, sep='\n')
            except ValueError as e:
                raise PasswordError(full_stop(e), culprit=path)
            except OSError as e:
                raise PasswordError(os_error(e))
            return decrypted.data.decode(get_setting('encoding'))
        else:
            return path.read_text(encoding=get_setting('encoding'))

    def _choices(self):
        extension = self.path.suffix.lower()
        if extension in GPG_EXTENSIONS:
            if self.armor == 'never':
                return True, False
            elif self.armor != 'always' and extension == '.gpg':
                return True, False
            return True, True
        else:
            return False, False

    def remove(self):
        self.path.unlink()

    def backup(self, extension):
        """Creates a backup copy of the file.

        The name of the new file has the specified extension prepended to the
        existing suffixes.
        """
        if extension:
            # prepend extension to list of suffixes
            suffixes = self.path.suffixes
            stem = self.path.stem.partition('.')[0]  # remove all suffixes
            new = to_path(self.path.parent, ''.join([stem, extension] + suffixes))
            self.backup_path = new

            cp(self.path, new)
            return new

    def restore(self):
        "Restores the backup copy of the file."
        cp(self.backup_path, self.path)

    def will_encrypt(self):
        return self.path.suffix in GPG_EXTENSIONS

    def exists(self):
        return self.path.exists()

    def chmod(self, mode=0o600):
        self.path.chmod(mode)

    def __str__(self):
        return str(self.path) class PythonFile(GnuPG):
    def run(self):
        global ActivePythonFile
        ActivePythonFile = self.path
        path = self.path
        self.encrypted = path.suffix in ['.gpg', '.asc']
        narrate('reading.', culprit=path)
        try:
            self.code = self.read()
                # need to save the code for the new command
        except OSError as e:
            raise PasswordError(os_error(e))

        try:
            compiled = compile(self.code, str(path), 'exec')
        except SyntaxError as e:
            culprit = (e.filename, e.lineno)
            if e.text is None or e.offset is None:
                raise PasswordError(full_stop(e.msg), culprit=culprit)
            else:
                raise PasswordError(
                    e.msg + ':', e.text.rstrip(), (e.offset-1)*' ' + '^',
                    culprit=culprit, sep='\n'
                )
                # File "/home/ken/.config/avendesora/config", line 18
                #   'c': 'google-chrome %s'
                #      ^

        contents = {}
        try:
            exec(compiled, contents)
        except Exception as e:
            from .utilities import error_source
            raise PasswordError(full_stop(e), culprit=error_source())
        ActivePythonFile = None
        return contents

    def create(self, contents, gpg_ids=None):
        path = self.path
        try:
            # check to see if file already exists
            if path.exists():
                # file creation (init) requested, but file already exists
                # don't overwrite the file, instead read it so the information
                # can be used to create any remaining files.
                display("%s: already exists." % path)
                return

            # create the file
            display('%s: creating.' % path)
            if path.suffix in ['.gpg', '.asc']:
                narrate('encrypting.', culprit=path)
                # encrypt it
                if not gpg_ids:
                    raise PasswordError('gpg_ids missing.')
                self.save(contents, gpg_ids)
            else:
                narrate('not encrypting.', culprit=path)
                # file is not encrypted
                with path.open('wb') as f:
                    f.write(contents.encode(get_setting('encoding')))
        except OSError as e:
            raise PasswordError(os_error(e)) class ObscuredSecret(object):
    """Base class for obscured secrets."""

    # obscurers() {{{2
    @classmethod
    def obscurers(cls):
        for sub in sorted(cls.__subclasses__(), key=lambda s: s.__name__):
            if hasattr(sub, 'conceal') and hasattr(sub, 'reveal'):
                yield sub
                for each in sub.obscurers():
                    if hasattr(each, 'conceal') and hasattr(each, 'reveal'):
                        yield each

    # get_name() {{{2
    @classmethod
    def get_name(cls):
        try:
            return cls.NAME.lower()
        except AttributeError:
            # consider converting lower to upper case transitions in __name__ to
            # dashes.
            return cls.__name__.lower()

    # get_description() {{{2
    @classmethod
    def get_description(cls):
        return None

    # hide() {{{2
    @classmethod
    def hide(cls, text, encoding=None, decorate=False, symmetric=False, gpg_ids=None):
        encoding = encoding.lower() if encoding else 'base64'
        for obscurer in cls.obscurers():
            if encoding == obscurer.get_name():
                return obscurer.conceal(
                    text, decorate, symmetric=symmetric, gpg_ids=gpg_ids
                )
        raise PasswordError('not found.', culprit=encoding)

    # show() {{{2
    @classmethod
    def show(cls, text):
        match = DECORATED_LIST.match(text)
        if match:
            name = match.group(1)
            value = ''.join([s.strip('"' "'") for s in match.group(2).split()])

            for obscurer in cls.obscurers():
                if name == obscurer.__name__:
                    return obscurer.reveal(value)
            raise PasswordError('not found.', culprit=name)

        match = DECORATED_TEXT.match(text)
        if match:
            name = match.group(1)
            value = match.group(2)

            for obscurer in cls.obscurers():
                if name == obscurer.__name__:
                    return obscurer.reveal(value.strip('"' "'"))
            raise PasswordError('not found.', culprit=name)

        return Hidden.reveal(text)

    # encodings() {{{2
    @classmethod
    def encodings(cls):
        for c in cls.obscurers():
            yield c.get_name(), dedent(getattr(c, 'DESC', '')).strip()

    # default encoding() {{{2
    @classmethod
    def default_encoding(cls):
        return Hidden.NAME

    # __repr__() {{{2
    def __repr__(self):
        secret = ObscuredSecret.hide(self.plaintext, 'base64')
        if hasattr(self, 'is_secret') and not self.is_secret:
            return "Hidden('{}', is_secret=False)".format(secret)
        else:
            return "Hidden('{}')".format(secret)

    # __str__() {{{2
    def __str__(self):
        return self.render() def chmod(mode, *paths):
    "Change the mode bits of one or more file or directory"
    for path in to_paths(paths):
        path.chmod(mode) def cp(*paths):
    "Copy files or directories (equivalent to 'cp -rf')"
    dest = to_path(paths[-1])
    srcs = list(to_paths(paths[:-1]))
    if dest.is_dir():
        for src in srcs:
            if src.is_dir():
                fulldest = Path(dest, src.name)
                # required because dest cannot exist with copytree
                shutil.copytree(to_str(src), to_str(fulldest))
            else:
                shutil.copy2(to_str(src), to_str(dest))
        return
    if len(srcs) > 1:
        raise_os_error(errno.ENOTDIR, dest)
    src = srcs[0]
    if src.is_dir() and dest.is_file():
        raise_os_error(errno.EISDIR, src)
    if src.is_dir():
        # src is directory and dest does not exist
        shutil.copytree(to_str(src), to_str(dest))
    else:
        shutil.copy2(to_str(src), to_str(dest)) def rm(*paths):
    "Remove files or directories (equivalent to rm -rf)"
    for path in to_paths(paths):
        try:
            if path.is_dir() and not path.is_symlink():
                shutil.rmtree(to_str(path))
            else:
                path.unlink()
        # suitable for python3 only
        # except FileNotFoundError:
        #     pass
        except (IOError, OSError) as err:
            # don't complain if the file never existed
            if err.errno != errno.ENOENT:
                raise def to_path(*args):
    try:
        return Path(*args).expanduser()
    except AttributeError:
        return Path(*args) def query_user(msg):
    highlight_color = get_setting('_highlight_color')
    msg = highlight_color(msg)

    try:
        return input(msg + ' ').strip()
    except EOFError:
        output() def two_columns(col1, col2, width=16, indent=True):
    indent = get_setting('indent') if indent else ''
    if len(col1) > width:
        return '%s%s\n%s%s%s' % (
            indent, col1, indent, '  '+width*' ', col2
        )
    else:
        return '%s%-*s  %s' % (indent, width, col1, col2) def name_completion(prompt, choices):

    def complete(entered, state):
        for name in choices:
            if name.startswith(entered):
                if not state:
                    return name
                else:
                    state -= 1

    import readline
    readline.parse_and_bind("tab: complete")
    readline.parse_and_bind("set editing-mode vi")
    readline.set_completer(complete)

    try:
        given = query_user(prompt)
    except EOFError:
        output()
    readline.set_completer(None)
    return given.strip() if given else given def get_writer(tty=None, clipboard=False, stdout=False):
    if clipboard:
        return ClipboardWriter()
    if stdout:
        return StdoutWriter()
    if tty is True:
        return TTY_Writer()
    if tty is False:
        return KeyboardWriter()
    if Color.isTTY():
        return TTY_Writer()
    else:
        return KeyboardWriter()
from .collection import Collection
from .config import get_setting, override_setting
from .editors import GenericEditor
from .files import AccountFiles
from .error import PasswordError
from .generator import PasswordGenerator
from .gpg import GnuPG, PythonFile
from .obscure import ObscuredSecret
from .shlib import chmod, cp, rm, to_path
from .utilities import query_user, two_columns, name_completion
from .writer import get_writer
from inform import (
    codicil, columns, conjoin, cull, display, error, Error, indent,
    is_collection, join, narrate, os_error, output, render, title_case, warn,
)
from docopt import docopt
from textwrap import dedent
            from tempfile import mktemp
        from .preferences import ARCHIVE_FILE_CONTENTS
        from .help import HelpMessage
            from inform import get_informer
        from .__init__ import __version__, __released__
import re
import sys
        import arrow
        import arrow
                # add new account to the contents
                accounts = orig_accounts_code + '\n\n\n' + new

                # save the new version
                new_accounts_file = PythonFile(path)
                new_accounts_file.save(accounts, gpg_ids)

                # check the changes to see if there are any issues
                try:
                    new_accounts_file.run()

                    # success, so delete the now out-of-date cache
                    AccountFiles.delete_manifests()
                    break
                except PasswordError as e:
                    error(e)
                    response = query_user('Try again?')
                    if response.lower() not in ['y', 'yes']:
                        raise PasswordError(
                            'Giving up, restoring original file.'
                        )

        except (PasswordError, OSError) as e:
            orig_accounts_file.restore()
            if isinstance(e, OSError):
                e = os_error(e)
            error(e)
            codicil(
                'What you entered can be found in %s.' % tmpfilename,
                'Delete it when done with it.', sep='\n'
            )
            return

        # delete the temp file
        try:
            tmpfile.remove()
        except Exception:
            pass


# Archive {{{1
class Archive(Command):
    NAMES = 'archive',
    DESCRIPTION = 'generates archive of all account information'
    USAGE = dedent("""
        Usage:
            avendesora archive
    """).strip()

    @classmethod
    def help(cls):
        text = dedent("""
            {title}

            {usage}

            This command creates an encrypted archive that contains all the
            information in your accounts files, including the fully generated
            secrets.  You should never need this file, but its presence protects
            you in case you lose access to Avendesora. To access your secrets
            without Avendesora, simply decrypt the archive file with GPG.  The
            actual secrets will be hidden, but it easy to retrieve them even
            without Avendesora. When hidden, the secrets are encoded in base64.
            You can decode it by running 'base64 -d -' and pasting the encoded
            secret into the terminal.

            When you run this command it overwrites the existing archive. If you
            have accidentally deleted an account or changed a secret, then
            replacing the archive could cause the last copy of the original
            information to be lost. To prevent this from occurring it is a good
            practice to run the 'changed' command before regenerating the
            archive.  It describes all of the changes that have occurred since
            the last time the archive was generated. You should only regenerate
            the archive once you have convinced yourself all of the changes are
            as expected.
        """).strip()
        return text.format(
            title=title(cls.DESCRIPTION), usage=cls.USAGE,
        )

    @classmethod
    def run(cls, command, args):
        # read command line
        docopt(cls.USAGE, argv=[command] + args)
        archive_file = get_setting('archive_file')

        # save existing archive if it exists
        try:
            previous_archive = get_setting('previous_archive_file')
            if previous_archive and archive_file.is_file():
                rm(previous_archive)
                cp(archive_file, previous_archive)
        except OSError as e:
            raise PasswordError(os_error(e))

        # delete the manifests, causing them to be recreated clean
        AccountFiles.delete_manifests()

        # run the generator
        generator = PasswordGenerator(check_integrity=True, warnings=False)

        # get dictionary that fully describes the contents of each account
        entries = []
        seen = set()
        for account in generator.all_accounts():
            name = account.get_name()
            if name in seen:
                warn('duplicate account name.', culprit=name)
            seen.add(name)
            entry = account.archive()
            if entry:
                entries.append(indent('%r: %s,' % (name, render(entry)), '    '))

        # build file contents
        contents = ARCHIVE_FILE_CONTENTS.format(
            encoding = get_setting('encoding'),
            date = str(arrow.now()),
            accounts = '\n\n'.join(entries)
        )


        archive = GnuPG(archive_file)