From 3974f2464655dc1871b95c4fe9945965d79bba78 Mon Sep 17 00:00:00 2001
From: Jani Nikula
Date: Sun, 3 Jan 2016 18:19:50 +0200
Subject: [PATCH] Re: [PATCH 2/5] notmuch-report: Rename from nmbug-status
---
c2/d757867984e3344bc44f03da0c5b2f4868ec6c | 952 ++++++++++++++++++++++
1 file changed, 952 insertions(+)
create mode 100644 c2/d757867984e3344bc44f03da0c5b2f4868ec6c
diff --git a/c2/d757867984e3344bc44f03da0c5b2f4868ec6c b/c2/d757867984e3344bc44f03da0c5b2f4868ec6c
new file mode 100644
index 000000000..772b6709b
--- /dev/null
+++ b/c2/d757867984e3344bc44f03da0c5b2f4868ec6c
@@ -0,0 +1,952 @@
+Return-Path:
+X-Original-To: notmuch@notmuchmail.org
+Delivered-To: notmuch@notmuchmail.org
+Received: from localhost (localhost [127.0.0.1])
+ by arlo.cworth.org (Postfix) with ESMTP id BA0416DE17F3
+ for ; Sun, 3 Jan 2016 08:20:46 -0800 (PST)
+X-Virus-Scanned: Debian amavisd-new at cworth.org
+X-Spam-Flag: NO
+X-Spam-Score: -0.548
+X-Spam-Level:
+X-Spam-Status: No, score=-0.548 tagged_above=-999 required=5 tests=[AWL=0.172,
+ DKIM_SIGNED=0.1, DKIM_VALID=-0.1, RCVD_IN_DNSWL_LOW=-0.7,
+ RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01] autolearn=disabled
+Received: from arlo.cworth.org ([127.0.0.1])
+ by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 0tUlmOA8t7mZ for ;
+ Sun, 3 Jan 2016 08:20:44 -0800 (PST)
+Received: from mail-wm0-f45.google.com (mail-wm0-f45.google.com
+ [74.125.82.45]) by arlo.cworth.org (Postfix) with ESMTPS id 8CAA56DE18EA for
+ ; Sun, 3 Jan 2016 08:20:43 -0800 (PST)
+Received: by mail-wm0-f45.google.com with SMTP id f206so117347195wmf.0
+ for ; Sun, 03 Jan 2016 08:20:43 -0800 (PST)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=nikula-org.20150623.gappssmtp.com; s=20150623;
+ h=from:to:cc:subject:in-reply-to:references:user-agent:date
+ :message-id:mime-version:content-type;
+ bh=ujIh41SRvnfSEpPuxJQUZTOcgAI8EnnqCiJpRqtprOE=;
+ b=Kl0nlqzxjphgJAMuqHuJWo96ks78nq8BMKjl4yDCZqzyntsDe7dBgTh/vhqNn2+UiA
+ 8tYzM1nIjGroUB2c+u8xCEmP6ULcC1WSySF0Ceu4Vv7D0q/EVAMF4Xm2piqCSSo85kfS
+ jFOPdEPwoCtnC3Qvc+xtjsql6A7GGH208zMJbljke8olAfka8Jurkh12+sixFkxoNMHI
+ tjPdLZjMNsXNc51/z+OFxkF50TR0V5uaH8TQGqxQTdVwtX7L8yx0oAvaz6X9nXvf/QBl
+ MsF3KFl1Tg5CE7oyn8QhLaeoA+K+a9Mkjxzw8El78U0TJSx1+QrlBL0IDfVuig6gxYC/
+ DIbw==
+X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=1e100.net; s=20130820;
+ h=x-gm-message-state:from:to:cc:subject:in-reply-to:references
+ :user-agent:date:message-id:mime-version:content-type;
+ bh=ujIh41SRvnfSEpPuxJQUZTOcgAI8EnnqCiJpRqtprOE=;
+ b=A1vk7GHLi5ZWlrBO6/s7JJMaAHjPdah5OKa9R0CpPEfYk6O+J5hX4nbY3oTx3N7xAU
+ FBxisP89jwsYRHUNobIKqhL6uTlVONSkzcuTe8R6CIJsOMWb3fcjxgK8lB14wkA9DMNd
+ 9ZyzPqpcGjLM+qG1mpkUkj3kfQfBCytIcHKHvQzGBHX8wY0UqbkVGSNVy2SRRyH/zGi1
+ ipxCAj0OQbWVOZ4wXFyVnJj6qQ8Kf1NljGXsVVDl0BsoEgw7swgMVlOCU81FXnS4jHXS
+ EiA3IyW95ABsKRqv1bZ4Y0s3U4H7xSuWLZc8lCVJVUCi5xpnIyEtozV86ptvdegdorsK
+ BAUA==
+X-Gm-Message-State: ALoCoQl4DSuAqv9FxjTO+U5OM65or6e19XvqjzkPNQys1C5grkiw35vgkiF5lHA/a646d3D4np1py9yRd3WvqmozO1KFjvRRlA==
+X-Received: by 10.28.102.5 with SMTP id a5mr44951081wmc.85.1451838041907;
+ Sun, 03 Jan 2016 08:20:41 -0800 (PST)
+Received: from localhost (mobile-access-bceec9-49.dhcp.inet.fi.
+ [188.238.201.49])
+ by smtp.gmail.com with ESMTPSA id z127sm26444878wme.2.2016.01.03.08.20.40
+ (version=TLSv1/SSLv3 cipher=OTHER);
+ Sun, 03 Jan 2016 08:20:41 -0800 (PST)
+From: Jani Nikula
+To: "W. Trevor King" , notmuch@notmuchmail.org
+Cc: David Bremner , Tomi Ollila ,
+ Carl Worth , "W. Trevor King"
+Subject: Re: [PATCH 2/5] notmuch-report: Rename from nmbug-status
+In-Reply-To:
+ <7df3f39aa67ed3862b937c7bf047a59d913e9c44.1451714099.git.wking@tremily.us>
+References:
+ <7df3f39aa67ed3862b937c7bf047a59d913e9c44.1451714099.git.wking@tremily.us>
+User-Agent: Notmuch/0.21+34~ge1fb729 (http://notmuchmail.org) Emacs/24.4.1
+ (x86_64-pc-linux-gnu)
+Date: Sun, 03 Jan 2016 18:19:50 +0200
+Message-ID: <874meuws7t.fsf@nikula.org>
+MIME-Version: 1.0
+Content-Type: text/plain
+X-BeenThere: notmuch@notmuchmail.org
+X-Mailman-Version: 2.1.20
+Precedence: list
+List-Id: "Use and development of the notmuch mail system."
+
+List-Unsubscribe: ,
+
+List-Archive:
+List-Post:
+List-Help:
+List-Subscribe: ,
+
+X-List-Received-Date: Sun, 03 Jan 2016 16:20:46 -0000
+
+On Sat, 02 Jan 2016, "W. Trevor King" wrote:
+> This script generates reports based on notmuch queries, and doesn't
+> really have anything to do with nmbug, except for sharing the NMBGIT
+> environment variable.
+> ---
+> devel/nmbug/nmbug-status | 419 ---------------------------------------------
+> devel/nmbug/notmuch-report | 419 +++++++++++++++++++++++++++++++++++++++++++++
+> 2 files changed, 419 insertions(+), 419 deletions(-)
+> delete mode 100755 devel/nmbug/nmbug-status
+> create mode 100755 devel/nmbug/notmuch-report
+
+FYI, at least with git format-patch you can add -M option to detect
+renames, and the resulting patch will be trivial to review.
+
+BR,
+Jani.
+
+
+>
+> diff --git a/devel/nmbug/nmbug-status b/devel/nmbug/nmbug-status
+> deleted file mode 100755
+> index 22e3b5b..0000000
+> --- a/devel/nmbug/nmbug-status
+> +++ /dev/null
+> @@ -1,419 +0,0 @@
+> -#!/usr/bin/python
+> -#
+> -# Copyright (c) 2011-2012 David Bremner
+> -#
+> -# dependencies
+> -# - python 2.6 for json
+> -# - argparse; either python 2.7, or install separately
+> -#
+> -# This program is free software: you can redistribute it and/or modify
+> -# it under the terms of the GNU General Public License as published by
+> -# the Free Software Foundation, either version 3 of the License, or
+> -# (at your option) any later version.
+> -#
+> -# This program is distributed in the hope that it will be useful,
+> -# but WITHOUT ANY WARRANTY; without even the implied warranty of
+> -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+> -# GNU General Public License for more details.
+> -#
+> -# You should have received a copy of the GNU General Public License
+> -# along with this program. If not, see http://www.gnu.org/licenses/ .
+> -
+> -"""Generate HTML for one or more notmuch searches.
+> -
+> -Messages matching each search are grouped by thread. Each message
+> -that contains both a subject and message-id will have the displayed
+> -subject link to the Gmane view of the message.
+> -"""
+> -
+> -from __future__ import print_function
+> -from __future__ import unicode_literals
+> -
+> -import codecs
+> -import collections
+> -import datetime
+> -import email.utils
+> -try: # Python 3
+> - from urllib.parse import quote
+> -except ImportError: # Python 2
+> - from urllib import quote
+> -import json
+> -import argparse
+> -import os
+> -import re
+> -import sys
+> -import subprocess
+> -import xml.sax.saxutils
+> -
+> -
+> -_ENCODING = 'UTF-8'
+> -_PAGES = {}
+> -
+> -
+> -if not hasattr(collections, 'OrderedDict'): # Python 2.6 or earlier
+> - class _OrderedDict (dict):
+> - "Just enough of a stub to get through Page._get_threads"
+> - def __init__(self, *args, **kwargs):
+> - super(_OrderedDict, self).__init__(*args, **kwargs)
+> - self._keys = [] # record key order
+> -
+> - def __setitem__(self, key, value):
+> - super(_OrderedDict, self).__setitem__(key, value)
+> - self._keys.append(key)
+> -
+> - def values(self):
+> - for key in self._keys:
+> - yield self[key]
+> -
+> -
+> - collections.OrderedDict = _OrderedDict
+> -
+> -
+> -class ConfigError (Exception):
+> - """Errors with config file usage
+> - """
+> - pass
+> -
+> -
+> -def read_config(path=None, encoding=None):
+> - "Read config from json file"
+> - if not encoding:
+> - encoding = _ENCODING
+> - if path:
+> - try:
+> - with open(path, 'rb') as f:
+> - config_bytes = f.read()
+> - except IOError as e:
+> - raise ConfigError('Could not read config from {}'.format(path))
+> - else:
+> - nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
+> - branch = 'config'
+> - filename = 'status-config.json'
+> -
+> - # read only the first line from the pipe
+> - sha1_bytes = subprocess.Popen(
+> - ['git', '--git-dir', nmbhome, 'show-ref', '-s', '--heads', branch],
+> - stdout=subprocess.PIPE).stdout.readline()
+> - sha1 = sha1_bytes.decode(encoding).rstrip()
+> - if not sha1:
+> - raise ConfigError(
+> - ("No local branch '{branch}' in {nmbgit}. "
+> - 'Checkout a local {branch} branch or explicitly set --config.'
+> - ).format(branch=branch, nmbgit=nmbhome))
+> -
+> - p = subprocess.Popen(
+> - ['git', '--git-dir', nmbhome, 'cat-file', 'blob',
+> - '{}:{}'.format(sha1, filename)],
+> - stdout=subprocess.PIPE)
+> - config_bytes, err = p.communicate()
+> - status = p.wait()
+> - if status != 0:
+> - raise ConfigError(
+> - ("Missing {filename} in branch '{branch}' of {nmbgit}. "
+> - 'Add the file or explicitly set --config.'
+> - ).format(filename=filename, branch=branch, nmbgit=nmbhome))
+> -
+> - config_json = config_bytes.decode(encoding)
+> - try:
+> - return json.loads(config_json)
+> - except ValueError as e:
+> - if not path:
+> - path = "{} in branch '{}' of {}".format(
+> - filename, branch, nmbhome)
+> - raise ConfigError(
+> - 'Could not parse JSON from the config file {}:\n{}'.format(
+> - path, e))
+> -
+> -
+> -class Thread (list):
+> - def __init__(self):
+> - self.running_data = {}
+> -
+> -
+> -class Page (object):
+> - def __init__(self, header=None, footer=None):
+> - self.header = header
+> - self.footer = footer
+> -
+> - def write(self, database, views, stream=None):
+> - if not stream:
+> - try: # Python 3
+> - byte_stream = sys.stdout.buffer
+> - except AttributeError: # Python 2
+> - byte_stream = sys.stdout
+> - stream = codecs.getwriter(encoding=_ENCODING)(stream=byte_stream)
+> - self._write_header(views=views, stream=stream)
+> - for view in views:
+> - self._write_view(database=database, view=view, stream=stream)
+> - self._write_footer(views=views, stream=stream)
+> -
+> - def _write_header(self, views, stream):
+> - if self.header:
+> - stream.write(self.header)
+> -
+> - def _write_footer(self, views, stream):
+> - if self.footer:
+> - stream.write(self.footer)
+> -
+> - def _write_view(self, database, view, stream):
+> - # sort order, default to oldest-first
+> - sort_key = view.get('sort', 'oldest-first')
+> - # dynamically accept all values in Query.SORT
+> - sort_attribute = sort_key.upper().replace('-', '_')
+> - try:
+> - sort = getattr(notmuch.Query.SORT, sort_attribute)
+> - except AttributeError:
+> - raise ConfigError('Invalid sort setting for {}: {!r}'.format(
+> - view['title'], sort_key))
+> - if 'query-string' not in view:
+> - query = view['query']
+> - view['query-string'] = ' and '.join(query)
+> - q = notmuch.Query(database, view['query-string'])
+> - q.set_sort(sort)
+> - threads = self._get_threads(messages=q.search_messages())
+> - self._write_view_header(view=view, stream=stream)
+> - self._write_threads(threads=threads, stream=stream)
+> -
+> - def _get_threads(self, messages):
+> - threads = collections.OrderedDict()
+> - for message in messages:
+> - thread_id = message.get_thread_id()
+> - if thread_id in threads:
+> - thread = threads[thread_id]
+> - else:
+> - thread = Thread()
+> - threads[thread_id] = thread
+> - thread.running_data, display_data = self._message_display_data(
+> - running_data=thread.running_data, message=message)
+> - thread.append(display_data)
+> - return list(threads.values())
+> -
+> - def _write_view_header(self, view, stream):
+> - pass
+> -
+> - def _write_threads(self, threads, stream):
+> - for thread in threads:
+> - for message_display_data in thread:
+> - stream.write(
+> - ('{date:10.10s} {from:20.20s} {subject:40.40s}\n'
+> - '{message-id-term:>72}\n'
+> - ).format(**message_display_data))
+> - if thread != threads[-1]:
+> - stream.write('\n')
+> -
+> - def _message_display_data(self, running_data, message):
+> - headers = ('thread-id', 'message-id', 'date', 'from', 'subject')
+> - data = {}
+> - for header in headers:
+> - if header == 'thread-id':
+> - value = message.get_thread_id()
+> - elif header == 'message-id':
+> - value = message.get_message_id()
+> - data['message-id-term'] = 'id:"{0}"'.format(value)
+> - elif header == 'date':
+> - value = str(datetime.datetime.utcfromtimestamp(
+> - message.get_date()).date())
+> - else:
+> - value = message.get_header(header)
+> - if header == 'from':
+> - (value, addr) = email.utils.parseaddr(value)
+> - if not value:
+> - value = addr.split('@')[0]
+> - data[header] = value
+> - next_running_data = data.copy()
+> - for header, value in data.items():
+> - if header in ['message-id', 'subject']:
+> - continue
+> - if value == running_data.get(header, None):
+> - data[header] = ''
+> - return (next_running_data, data)
+> -
+> -
+> -class HtmlPage (Page):
+> - _slug_regexp = re.compile('\W+')
+> -
+> - def _write_header(self, views, stream):
+> - super(HtmlPage, self)._write_header(views=views, stream=stream)
+> - stream.write('\n')
+> - for view in views:
+> - if 'id' not in view:
+> - view['id'] = self._slug(view['title'])
+> - stream.write(
+> - '- {title}
\n'.format(**view))
+> - stream.write('
\n')
+> -
+> - def _write_view_header(self, view, stream):
+> - stream.write('{title}
\n'.format(**view))
+> - stream.write('\n')
+> - if 'comment' in view:
+> - stream.write(view['comment'])
+> - stream.write('\n')
+> - for line in [
+> - 'The view is generated from the following query:',
+> - '
',
+> - '',
+> - ' ',
+> - view['query-string'],
+> - '
',
+> - '
',
+> - ]:
+> - stream.write(line)
+> - stream.write('\n')
+> -
+> - def _write_threads(self, threads, stream):
+> - if not threads:
+> - return
+> - stream.write('\n')
+> - for thread in threads:
+> - stream.write(' \n')
+> - for message_display_data in thread:
+> - stream.write((
+> - ' \n'
+> - ' {date} | \n'
+> - ' {message-id-term} | \n'
+> - '
\n'
+> - ' \n'
+> - ' {from} | \n'
+> - ' {subject} | \n'
+> - '
\n'
+> - ).format(**message_display_data))
+> - stream.write(' \n')
+> - if thread != threads[-1]:
+> - stream.write(
+> - '
|
\n')
+> - stream.write('
\n')
+> -
+> - def _message_display_data(self, *args, **kwargs):
+> - running_data, display_data = super(
+> - HtmlPage, self)._message_display_data(
+> - *args, **kwargs)
+> - if 'subject' in display_data and 'message-id' in display_data:
+> - d = {
+> - 'message-id': quote(display_data['message-id']),
+> - 'subject': xml.sax.saxutils.escape(display_data['subject']),
+> - }
+> - display_data['subject'] = (
+> - '{subject}'
+> - ).format(**d)
+> - for key in ['message-id', 'from']:
+> - if key in display_data:
+> - display_data[key] = xml.sax.saxutils.escape(display_data[key])
+> - return (running_data, display_data)
+> -
+> - def _slug(self, string):
+> - return self._slug_regexp.sub('-', string)
+> -
+> -parser = argparse.ArgumentParser(description=__doc__)
+> -parser.add_argument('--text', help='output plain text format',
+> - action='store_true')
+> -parser.add_argument('--config', help='load config from given file',
+> - metavar='PATH')
+> -parser.add_argument('--list-views', help='list views',
+> - action='store_true')
+> -parser.add_argument('--get-query', help='get query for view',
+> - metavar='VIEW')
+> -
+> -args = parser.parse_args()
+> -
+> -try:
+> - config = read_config(path=args.config)
+> -except ConfigError as e:
+> - print(e, file=sys.stderr)
+> - sys.exit(1)
+> -
+> -header_template = config['meta'].get('header', '''
+> -
+> -
+> -
+> - {title}
+> -
+> -
+> -
+> -{title}
+> -{blurb}
+> -
+> -Views
+> -''')
+> -
+> -footer_template = config['meta'].get('footer', '''
+> -
+> -Generated: {datetime}
+> -
+> -
+> -''')
+> -
+> -now = datetime.datetime.utcnow()
+> -context = {
+> - 'date': now,
+> - 'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
+> - 'title': config['meta']['title'],
+> - 'blurb': config['meta']['blurb'],
+> - 'encoding': _ENCODING,
+> - 'inter_message_padding': '0.25em',
+> - 'border_radius': '0.5em',
+> - }
+> -
+> -_PAGES['text'] = Page()
+> -_PAGES['html'] = HtmlPage(
+> - header=header_template.format(**context),
+> - footer=footer_template.format(**context),
+> - )
+> -
+> -if args.list_views:
+> - for view in config['views']:
+> - print(view['title'])
+> - sys.exit(0)
+> -elif args.get_query != None:
+> - for view in config['views']:
+> - if args.get_query == view['title']:
+> - print(' and '.join(view['query']))
+> - sys.exit(0)
+> -else:
+> - # only import notmuch if needed
+> - import notmuch
+> -
+> -if args.text:
+> - page = _PAGES['text']
+> -else:
+> - page = _PAGES['html']
+> -
+> -db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+> -page.write(database=db, views=config['views'])
+> diff --git a/devel/nmbug/notmuch-report b/devel/nmbug/notmuch-report
+> new file mode 100755
+> index 0000000..22e3b5b
+> --- /dev/null
+> +++ b/devel/nmbug/notmuch-report
+> @@ -0,0 +1,419 @@
+> +#!/usr/bin/python
+> +#
+> +# Copyright (c) 2011-2012 David Bremner
+> +#
+> +# dependencies
+> +# - python 2.6 for json
+> +# - argparse; either python 2.7, or install separately
+> +#
+> +# This program is free software: you can redistribute it and/or modify
+> +# it under the terms of the GNU General Public License as published by
+> +# the Free Software Foundation, either version 3 of the License, or
+> +# (at your option) any later version.
+> +#
+> +# This program is distributed in the hope that it will be useful,
+> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
+> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+> +# GNU General Public License for more details.
+> +#
+> +# You should have received a copy of the GNU General Public License
+> +# along with this program. If not, see http://www.gnu.org/licenses/ .
+> +
+> +"""Generate HTML for one or more notmuch searches.
+> +
+> +Messages matching each search are grouped by thread. Each message
+> +that contains both a subject and message-id will have the displayed
+> +subject link to the Gmane view of the message.
+> +"""
+> +
+> +from __future__ import print_function
+> +from __future__ import unicode_literals
+> +
+> +import codecs
+> +import collections
+> +import datetime
+> +import email.utils
+> +try: # Python 3
+> + from urllib.parse import quote
+> +except ImportError: # Python 2
+> + from urllib import quote
+> +import json
+> +import argparse
+> +import os
+> +import re
+> +import sys
+> +import subprocess
+> +import xml.sax.saxutils
+> +
+> +
+> +_ENCODING = 'UTF-8'
+> +_PAGES = {}
+> +
+> +
+> +if not hasattr(collections, 'OrderedDict'): # Python 2.6 or earlier
+> + class _OrderedDict (dict):
+> + "Just enough of a stub to get through Page._get_threads"
+> + def __init__(self, *args, **kwargs):
+> + super(_OrderedDict, self).__init__(*args, **kwargs)
+> + self._keys = [] # record key order
+> +
+> + def __setitem__(self, key, value):
+> + super(_OrderedDict, self).__setitem__(key, value)
+> + self._keys.append(key)
+> +
+> + def values(self):
+> + for key in self._keys:
+> + yield self[key]
+> +
+> +
+> + collections.OrderedDict = _OrderedDict
+> +
+> +
+> +class ConfigError (Exception):
+> + """Errors with config file usage
+> + """
+> + pass
+> +
+> +
+> +def read_config(path=None, encoding=None):
+> + "Read config from json file"
+> + if not encoding:
+> + encoding = _ENCODING
+> + if path:
+> + try:
+> + with open(path, 'rb') as f:
+> + config_bytes = f.read()
+> + except IOError as e:
+> + raise ConfigError('Could not read config from {}'.format(path))
+> + else:
+> + nmbhome = os.getenv('NMBGIT', os.path.expanduser('~/.nmbug'))
+> + branch = 'config'
+> + filename = 'status-config.json'
+> +
+> + # read only the first line from the pipe
+> + sha1_bytes = subprocess.Popen(
+> + ['git', '--git-dir', nmbhome, 'show-ref', '-s', '--heads', branch],
+> + stdout=subprocess.PIPE).stdout.readline()
+> + sha1 = sha1_bytes.decode(encoding).rstrip()
+> + if not sha1:
+> + raise ConfigError(
+> + ("No local branch '{branch}' in {nmbgit}. "
+> + 'Checkout a local {branch} branch or explicitly set --config.'
+> + ).format(branch=branch, nmbgit=nmbhome))
+> +
+> + p = subprocess.Popen(
+> + ['git', '--git-dir', nmbhome, 'cat-file', 'blob',
+> + '{}:{}'.format(sha1, filename)],
+> + stdout=subprocess.PIPE)
+> + config_bytes, err = p.communicate()
+> + status = p.wait()
+> + if status != 0:
+> + raise ConfigError(
+> + ("Missing {filename} in branch '{branch}' of {nmbgit}. "
+> + 'Add the file or explicitly set --config.'
+> + ).format(filename=filename, branch=branch, nmbgit=nmbhome))
+> +
+> + config_json = config_bytes.decode(encoding)
+> + try:
+> + return json.loads(config_json)
+> + except ValueError as e:
+> + if not path:
+> + path = "{} in branch '{}' of {}".format(
+> + filename, branch, nmbhome)
+> + raise ConfigError(
+> + 'Could not parse JSON from the config file {}:\n{}'.format(
+> + path, e))
+> +
+> +
+> +class Thread (list):
+> + def __init__(self):
+> + self.running_data = {}
+> +
+> +
+> +class Page (object):
+> + def __init__(self, header=None, footer=None):
+> + self.header = header
+> + self.footer = footer
+> +
+> + def write(self, database, views, stream=None):
+> + if not stream:
+> + try: # Python 3
+> + byte_stream = sys.stdout.buffer
+> + except AttributeError: # Python 2
+> + byte_stream = sys.stdout
+> + stream = codecs.getwriter(encoding=_ENCODING)(stream=byte_stream)
+> + self._write_header(views=views, stream=stream)
+> + for view in views:
+> + self._write_view(database=database, view=view, stream=stream)
+> + self._write_footer(views=views, stream=stream)
+> +
+> + def _write_header(self, views, stream):
+> + if self.header:
+> + stream.write(self.header)
+> +
+> + def _write_footer(self, views, stream):
+> + if self.footer:
+> + stream.write(self.footer)
+> +
+> + def _write_view(self, database, view, stream):
+> + # sort order, default to oldest-first
+> + sort_key = view.get('sort', 'oldest-first')
+> + # dynamically accept all values in Query.SORT
+> + sort_attribute = sort_key.upper().replace('-', '_')
+> + try:
+> + sort = getattr(notmuch.Query.SORT, sort_attribute)
+> + except AttributeError:
+> + raise ConfigError('Invalid sort setting for {}: {!r}'.format(
+> + view['title'], sort_key))
+> + if 'query-string' not in view:
+> + query = view['query']
+> + view['query-string'] = ' and '.join(query)
+> + q = notmuch.Query(database, view['query-string'])
+> + q.set_sort(sort)
+> + threads = self._get_threads(messages=q.search_messages())
+> + self._write_view_header(view=view, stream=stream)
+> + self._write_threads(threads=threads, stream=stream)
+> +
+> + def _get_threads(self, messages):
+> + threads = collections.OrderedDict()
+> + for message in messages:
+> + thread_id = message.get_thread_id()
+> + if thread_id in threads:
+> + thread = threads[thread_id]
+> + else:
+> + thread = Thread()
+> + threads[thread_id] = thread
+> + thread.running_data, display_data = self._message_display_data(
+> + running_data=thread.running_data, message=message)
+> + thread.append(display_data)
+> + return list(threads.values())
+> +
+> + def _write_view_header(self, view, stream):
+> + pass
+> +
+> + def _write_threads(self, threads, stream):
+> + for thread in threads:
+> + for message_display_data in thread:
+> + stream.write(
+> + ('{date:10.10s} {from:20.20s} {subject:40.40s}\n'
+> + '{message-id-term:>72}\n'
+> + ).format(**message_display_data))
+> + if thread != threads[-1]:
+> + stream.write('\n')
+> +
+> + def _message_display_data(self, running_data, message):
+> + headers = ('thread-id', 'message-id', 'date', 'from', 'subject')
+> + data = {}
+> + for header in headers:
+> + if header == 'thread-id':
+> + value = message.get_thread_id()
+> + elif header == 'message-id':
+> + value = message.get_message_id()
+> + data['message-id-term'] = 'id:"{0}"'.format(value)
+> + elif header == 'date':
+> + value = str(datetime.datetime.utcfromtimestamp(
+> + message.get_date()).date())
+> + else:
+> + value = message.get_header(header)
+> + if header == 'from':
+> + (value, addr) = email.utils.parseaddr(value)
+> + if not value:
+> + value = addr.split('@')[0]
+> + data[header] = value
+> + next_running_data = data.copy()
+> + for header, value in data.items():
+> + if header in ['message-id', 'subject']:
+> + continue
+> + if value == running_data.get(header, None):
+> + data[header] = ''
+> + return (next_running_data, data)
+> +
+> +
+> +class HtmlPage (Page):
+> + _slug_regexp = re.compile('\W+')
+> +
+> + def _write_header(self, views, stream):
+> + super(HtmlPage, self)._write_header(views=views, stream=stream)
+> + stream.write('\n')
+> + for view in views:
+> + if 'id' not in view:
+> + view['id'] = self._slug(view['title'])
+> + stream.write(
+> + '- {title}
\n'.format(**view))
+> + stream.write('
\n')
+> +
+> + def _write_view_header(self, view, stream):
+> + stream.write('{title}
\n'.format(**view))
+> + stream.write('\n')
+> + if 'comment' in view:
+> + stream.write(view['comment'])
+> + stream.write('\n')
+> + for line in [
+> + 'The view is generated from the following query:',
+> + '
',
+> + '',
+> + ' ',
+> + view['query-string'],
+> + '
',
+> + '
',
+> + ]:
+> + stream.write(line)
+> + stream.write('\n')
+> +
+> + def _write_threads(self, threads, stream):
+> + if not threads:
+> + return
+> + stream.write('\n')
+> + for thread in threads:
+> + stream.write(' \n')
+> + for message_display_data in thread:
+> + stream.write((
+> + ' \n'
+> + ' {date} | \n'
+> + ' {message-id-term} | \n'
+> + '
\n'
+> + ' \n'
+> + ' {from} | \n'
+> + ' {subject} | \n'
+> + '
\n'
+> + ).format(**message_display_data))
+> + stream.write(' \n')
+> + if thread != threads[-1]:
+> + stream.write(
+> + '
|
\n')
+> + stream.write('
\n')
+> +
+> + def _message_display_data(self, *args, **kwargs):
+> + running_data, display_data = super(
+> + HtmlPage, self)._message_display_data(
+> + *args, **kwargs)
+> + if 'subject' in display_data and 'message-id' in display_data:
+> + d = {
+> + 'message-id': quote(display_data['message-id']),
+> + 'subject': xml.sax.saxutils.escape(display_data['subject']),
+> + }
+> + display_data['subject'] = (
+> + '{subject}'
+> + ).format(**d)
+> + for key in ['message-id', 'from']:
+> + if key in display_data:
+> + display_data[key] = xml.sax.saxutils.escape(display_data[key])
+> + return (running_data, display_data)
+> +
+> + def _slug(self, string):
+> + return self._slug_regexp.sub('-', string)
+> +
+> +parser = argparse.ArgumentParser(description=__doc__)
+> +parser.add_argument('--text', help='output plain text format',
+> + action='store_true')
+> +parser.add_argument('--config', help='load config from given file',
+> + metavar='PATH')
+> +parser.add_argument('--list-views', help='list views',
+> + action='store_true')
+> +parser.add_argument('--get-query', help='get query for view',
+> + metavar='VIEW')
+> +
+> +args = parser.parse_args()
+> +
+> +try:
+> + config = read_config(path=args.config)
+> +except ConfigError as e:
+> + print(e, file=sys.stderr)
+> + sys.exit(1)
+> +
+> +header_template = config['meta'].get('header', '''
+> +
+> +
+> +
+> + {title}
+> +
+> +
+> +
+> +{title}
+> +{blurb}
+> +
+> +Views
+> +''')
+> +
+> +footer_template = config['meta'].get('footer', '''
+> +
+> +Generated: {datetime}
+> +
+> +
+> +''')
+> +
+> +now = datetime.datetime.utcnow()
+> +context = {
+> + 'date': now,
+> + 'datetime': now.strftime('%Y-%m-%d %H:%M:%SZ'),
+> + 'title': config['meta']['title'],
+> + 'blurb': config['meta']['blurb'],
+> + 'encoding': _ENCODING,
+> + 'inter_message_padding': '0.25em',
+> + 'border_radius': '0.5em',
+> + }
+> +
+> +_PAGES['text'] = Page()
+> +_PAGES['html'] = HtmlPage(
+> + header=header_template.format(**context),
+> + footer=footer_template.format(**context),
+> + )
+> +
+> +if args.list_views:
+> + for view in config['views']:
+> + print(view['title'])
+> + sys.exit(0)
+> +elif args.get_query != None:
+> + for view in config['views']:
+> + if args.get_query == view['title']:
+> + print(' and '.join(view['query']))
+> + sys.exit(0)
+> +else:
+> + # only import notmuch if needed
+> + import notmuch
+> +
+> +if args.text:
+> + page = _PAGES['text']
+> +else:
+> + page = _PAGES['html']
+> +
+> +db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY)
+> +page.write(database=db, views=config['views'])
+> --
+> 2.1.0.60.g85f0837
--
2.26.2