changeset 3:e0a6a02b3213

Can now get labels from command-line. Config file is optional.
author Brian Neal <bgneal@gmail.com>
date Fri, 18 May 2012 20:32:21 -0500
parents 3c5c1269e037
children ce84c480ffec
files weighmail/config.py weighmail/main.py weighmail/utils.py
diffstat 3 files changed, 113 insertions(+), 69 deletions(-) [+]
line wrap: on
line diff
--- a/weighmail/config.py	Thu May 17 20:34:38 2012 -0500
+++ b/weighmail/config.py	Fri May 18 20:32:21 2012 -0500
@@ -1,10 +1,6 @@
-import collections
-import re
 from ConfigParser import SafeConfigParser
 
-
-class ConfigError(Exception):
-    pass
+from utils import make_label
 
 
 DEFAULTS = dict(
@@ -13,23 +9,9 @@
     host='imap.gmail.com',
     ssl='True',
     port='993',
+    labels=[],
 )
 
-LIMIT_RE = re.compile(r'^(\d+)(GB|MB|KB)?$', re.IGNORECASE)
-
-KB = 1024
-MB = KB * KB
-GB = MB * MB
-
-SUFFIX_SIZES = {
-    None: 1,
-    'KB': KB,
-    'MB': MB,
-    'GB': GB
-}
-
-Label = collections.namedtuple('Label', 'name min max')
-
 
 def parse_config_file(path):
     """Parse INI file containing configuration details.
@@ -38,28 +20,16 @@
     # Parse options file
     parser = SafeConfigParser(defaults=DEFAULTS)
 
-    try:
-        with open(path, 'r') as fp:
-            parser.readfp(fp)
-    except IOError, ex:
-        raise ConfigError(ex)
+    with open(path, 'r') as fp:
+        parser.readfp(fp)
 
     # Build a list of label named tuples
 
-    label_set = set(parser.sections()) - {'auth', 'connection'}
-    if not label_set:
-        raise ConfigError("please specify at least 1 label section")
+    sections = [s for s in parser.sections() if s not in ('auth', 'connection')]
 
-    labels = []
-    for label in label_set:
-        min_val = get_limit(parser.get(label, 'min'))
-        max_val = get_limit(parser.get(label, 'max'))
-
-        if (min_val is not None and max_val is not None and 
-                min_val > max_val):
-            raise ConfigError("min is > max for label %s" % label)
-
-        labels.append(Label(name=label, min=min_val, max=max_val))
+    labels = [make_label(sec,
+                         parser.get(sec, 'min'),
+                         parser.get(sec, 'max')) for sec in sections]
 
     # Build an options object and return it
 
@@ -72,18 +42,3 @@
         labels=labels,
     )
     return opts
-
-
-def get_limit(val):
-    """Turns a string limit (e.g. 3MB) into an integer"""
-
-    # An empty string is OK, it means no limit, which we translate to None
-    if val == '':
-        return None
-
-    match = LIMIT_RE.match(val)
-    if match is None:
-        raise ConfigError("invalid min/max value %s" % val)
-
-    return int(match.group(1)) * SUFFIX_SIZES[match.group(2).upper()]
-
--- a/weighmail/main.py	Thu May 17 20:34:38 2012 -0500
+++ b/weighmail/main.py	Fri May 18 20:32:21 2012 -0500
@@ -1,35 +1,53 @@
-from argparse import ArgumentParser
+import argparse
 import getpass
-import os.path
 import sys
 
-from config import parse_config_file, ConfigError
+import config
+from utils import make_label, AppError
 
 
 PROG_DESC = "Adds labels to your Gmail according to message size"
 
 
+class LabelAction(argparse.Action):
+
+    def __call__(self, parser, namespace, values, option_string=None):
+
+        labels = getattr(namespace, self.dest)
+        for value in values:
+            try:
+                label = self.parse_label(value)
+            except ValueError, ex:
+                parser.error(ex)
+            else:
+                labels.append(label)
+
+    def parse_label(self, value):
+        name, size_range = value.split(':')
+        min_size, max_size = size_range.split('-')
+        return make_label(name, min_size, max_size)
+                
+
 def parse_args():
-    parser = ArgumentParser(description=PROG_DESC,
+    parser = argparse.ArgumentParser(description=PROG_DESC,
         epilog="Command-line arguments override config file settings.")
 
-    default_config_file = os.path.expanduser(os.path.join('~',
-        '.weighmail.ini'))
-    parser.add_argument('-c', '--config', default=default_config_file,
-            help="path to configuration file [default=%(default)s]")
+    parser.add_argument('-c', '--config', help="path to configuration file")
     parser.add_argument('-u', '--user', help="user name")
     parser.add_argument('-p', '--password', help="password")
     parser.add_argument('-H', '--host', help="server name")
     parser.add_argument('-P', '--port', type=int, help="server port")
     parser.add_argument('-n', '--nossl', default=None, action='store_true',
             help="do not use SSL")
+    parser.add_argument('-l', '--labels', default=[], nargs='+',
+            action=LabelAction, help="label specification: name:min-max")
 
     args = parser.parse_args()
 
-    # Remove items with a value of None, which indicates the user didn't specify
+    # Remove items that eval to False which indicates the user didn't specify
     # the option; this makes updating options from the config file easier:
 
-    args = { k : v for k, v in vars(args).items() if v is not None }
+    args = { k : v for k, v in vars(args).items() if v }
     return args
 
 
@@ -37,11 +55,14 @@
     # Parse command-line arguments
     args = parse_args()
 
-    config_file = args.pop('config')
+    config_file = args.pop('config', None)
     no_ssl = args.pop('nossl', False)
 
-    # Read config file:
-    opts = parse_config_file(config_file)
+    # Read config file if the option was specified
+    if config_file is not None:
+        opts = config.parse_config_file(config_file)
+    else:
+        opts = config.DEFAULTS
 
     # Command-line arguments override config file settings
     opts.update(args)
@@ -51,14 +72,17 @@
 
     # If the user or password is not specified, prompt for them now
     for opt in ('user', 'password'):
-        if opts[opt] is None:
+        if opt not in opts or opts[opt] is None:
             opts[opt] = getpass.getpass(opt + ': ')
 
     print opts
 
+    if 'labels' not in opts or not opts['labels']:
+        raise AppError("Please specify some label definitions")
+
 
 if __name__ == '__main__':
     try:
         main()
-    except ConfigError, ex:
-        sys.stderr.write("Configuration error: %s\n" % ex)
+    except (IOError, AppError), ex:
+        sys.stderr.write("%s\n" % ex)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weighmail/utils.py	Fri May 18 20:32:21 2012 -0500
@@ -0,0 +1,65 @@
+"""Common utilities for weighmail.
+
+"""
+import collections
+import re
+
+
+class AppError(Exception):
+    pass
+
+
+LIMIT_RE = re.compile(r'^(\d+)(GB|MB|KB)?$', re.IGNORECASE)
+
+KB = 1024
+MB = KB * KB
+GB = MB * MB
+
+SUFFIX_SIZES = {
+    None: 1,
+    'KB': KB,
+    'MB': MB,
+    'GB': GB
+}
+
+Label = collections.namedtuple('Label', 'name min max')
+
+
+def make_label(name, min_str, max_str):
+    """Make a Label object from 3 string parameters:
+
+    name - label name
+    min_str - minimum value, e.g. '5MB'
+    max_str - maximum value, e.g. '10GB'
+
+    Either min_str or max_str can be empty or None, in which
+    case None will be used in the Label object.
+
+    It is not valid to have None for both min and max.
+
+    """
+    min_val = get_limit(min_str)
+    max_val = get_limit(max_str)
+
+    if (min_val is not None and max_val is not None and
+            min_val > max_val) or (min_val is None and max_val is None):
+        raise ValueError("invalid label range: %s" % name)
+
+    return Label(name, min_val, max_val)
+
+
+def get_limit(val):
+    """Turns a string limit (e.g. 3MB) into an integer
+
+    An empty string or None will be translated to None.
+    
+    """
+    if val is None or val == '':
+        return None
+
+    match = LIMIT_RE.match(val)
+    if match is None:
+        raise ValueError("invalid min/max value %s" % val)
+
+    suffix = match.group(2).upper() if match.group(2) else None
+    return int(match.group(1)) * SUFFIX_SIZES[suffix]