From 2116a24a17e7e9ed48777533702118a697f268a8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 2 Jan 2016 22:08:02 +1600 Subject: [PATCH] [PATCH 2/5] notmuch-report: Rename from nmbug-status --- 7a/9a58a895a7643ff592a48b67084d3f7c51b4e9 | 935 ++++++++++++++++++++++ 1 file changed, 935 insertions(+) create mode 100644 7a/9a58a895a7643ff592a48b67084d3f7c51b4e9 diff --git a/7a/9a58a895a7643ff592a48b67084d3f7c51b4e9 b/7a/9a58a895a7643ff592a48b67084d3f7c51b4e9 new file mode 100644 index 000000000..c6e787bcc --- /dev/null +++ b/7a/9a58a895a7643ff592a48b67084d3f7c51b4e9 @@ -0,0 +1,935 @@ +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 B483C6DE1B58 + for ; Fri, 1 Jan 2016 22:10:42 -0800 (PST) +X-Virus-Scanned: Debian amavisd-new at cworth.org +X-Amavis-Alert: BAD HEADER SECTION, Duplicate header field: "References" +X-Spam-Flag: NO +X-Spam-Score: 0.04 +X-Spam-Level: +X-Spam-Status: No, score=0.04 tagged_above=-999 required=5 tests=[AWL=0.041, + DKIM_SIGNED=0.1, DKIM_VALID=-0.1, RCVD_IN_DNSWL_NONE=-0.0001, + SPF_PASS=-0.001] 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 neE8vqetwSAG for ; + Fri, 1 Jan 2016 22:10:39 -0800 (PST) +Received: from resqmta-po-04v.sys.comcast.net (resqmta-po-04v.sys.comcast.net + [96.114.154.163]) + by arlo.cworth.org (Postfix) with ESMTPS id 4765F6DE1B50 + for ; Fri, 1 Jan 2016 22:10:02 -0800 (PST) +Received: from resomta-po-19v.sys.comcast.net ([96.114.154.243]) + by resqmta-po-04v.sys.comcast.net with comcast + id 0uA11s0015FMDhs01uA1rC; Sat, 02 Jan 2016 06:10:01 +0000 +Received: from mail.tremily.us ([73.221.72.168]) + by resomta-po-19v.sys.comcast.net with comcast + id 0u7z1s00L3dr3C901u80VP; Sat, 02 Jan 2016 06:08:01 +0000 +Received: from ullr.tremily.us (unknown [192.168.10.7]) + by mail.tremily.us (Postfix) with ESMTPS id 6ED401B2F57F; + Fri, 1 Jan 2016 22:07:59 -0800 (PST) +Received: (nullmailer pid 15181 invoked by uid 1000); + Sat, 02 Jan 2016 06:08:07 -0000 +From: "W. Trevor King" +To: notmuch@notmuchmail.org +Cc: David Bremner , + Tomi Ollila , Jani Nikula , + Carl Worth , "W. Trevor King" +Subject: [PATCH 2/5] notmuch-report: Rename from nmbug-status +Date: Fri, 1 Jan 2016 22:08:02 -0800 +Message-Id: + <7df3f39aa67ed3862b937c7bf047a59d913e9c44.1451714099.git.wking@tremily.us> +X-Mailer: git-send-email 2.1.0.60.g85f0837 +In-Reply-To: +References: +In-Reply-To: +References: +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=comcast.net; + s=q20140121; t=1451715001; + bh=b+eqQb79fXv+mPmYMNt7f5OjvZ4+EPolsU093hd980c=; + h=Received:Received:Received:Received:From:To:Subject:Date: + Message-Id; + b=pA4yTDI161iiniYaUP+XOFA4BsADTNzuzbQ2ktl3DzWNOqDzNT3lHfl3241hAVZaL + dQQSgQXW4iIakjEE2IpFYS5bAgG66uukBP5ndleopd3FtQuhbhDv0S02BqtqU6IM3Y + zuRjkzqPsVpbSwqudOusiOQtkCn2hoE25e4rDpoOZUF2to1RVUd9u6N5bBGA8RlcV1 + Iz0T8+ev5ImEGfXuEZqqRMjJ87TFfos1UzfoK8uaBu6rhKD3D2MN1c3H3hFb3CXOBX + nCZtUyHqSnkP7XcZezzbSfiGoMWgvE5VGvF2pHReSxOvmJgMwZap65ErCB09X4jptK + ywBmS/HtBZkqg== +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: Sat, 02 Jan 2016 06:10:42 -0000 + +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 + +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' +- ' \n' +- ' \n' +- ' \n' +- ' \n' +- ' \n' +- ' \n' +- ' \n' +- ).format(**message_display_data)) +- stream.write(' \n') +- if thread != threads[-1]: +- stream.write( +- ' \n') +- stream.write('
{date}{message-id-term}
{from}{subject}

\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' ++ ' \n' ++ ' \n' ++ ' \n' ++ ' \n' ++ ' \n' ++ ' \n' ++ ' \n' ++ ).format(**message_display_data)) ++ stream.write(' \n') ++ if thread != threads[-1]: ++ stream.write( ++ ' \n') ++ stream.write('
{date}{message-id-term}
{from}{subject}

\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