incubator-cvs mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From a..@apache.org
Subject svn commit: r1488835 [1/3] - in /incubator/public/trunk/tools: ./ bin/ src/asf/ src/asf/utils/ tests/ tests/data/
Date Mon, 03 Jun 2013 03:47:53 GMT
Author: adc
Date: Mon Jun  3 03:47:50 2013
New Revision: 1488835

URL: http://svn.apache.org/r1488835
Log:
Handy command for mailing list monitors

Added:
    incubator/public/trunk/tools/bin/
    incubator/public/trunk/tools/bin/check-email   (with props)
    incubator/public/trunk/tools/src/asf/cli.py
    incubator/public/trunk/tools/src/asf/utils/auth.py
    incubator/public/trunk/tools/src/asf/utils/committers.py
    incubator/public/trunk/tools/src/asf/utils/emails.py
    incubator/public/trunk/tools/src/asf/utils/test.py
    incubator/public/trunk/tools/tests/data/
    incubator/public/trunk/tools/tests/data/podlings.xml
    incubator/public/trunk/tools/tests/test_committers.py
    incubator/public/trunk/tools/tests/test_emails.py
Modified:
    incubator/public/trunk/tools/setup.py

Added: incubator/public/trunk/tools/bin/check-email
URL: http://svn.apache.org/viewvc/incubator/public/trunk/tools/bin/check-email?rev=1488835&view=auto
==============================================================================
--- incubator/public/trunk/tools/bin/check-email (added)
+++ incubator/public/trunk/tools/bin/check-email Mon Jun  3 03:47:50 2013
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""
+Mailing list moderator tool.
+"""
+from asf.cli import entrypoint
+from asf.utils.committers import get_committer
+from asf.utils.emails import email_from_alias, is_apache_email_address, username_from_apache_email,
get_mail_aliases
+
+
+def cmd_lookup(args):
+    mail_aliases = get_mail_aliases(args.username, args.password)
+    email_alias = args.email_alias
+    asf_email = email_alias if is_apache_email_address(email_alias) else email_from_alias(email_alias,
mail_aliases)
+    if asf_email:
+        committer = get_committer(username_from_apache_email(asf_email), args.username, args.password)
+        if committer:
+            print committer.fullname
+            if committer.member:
+                print '  *ASF member'
+            if committer.projects:
+                print '  Projects:'
+                for project in sorted(committer.projects):
+                    print '   ', project
+            if committer.mentoring:
+                print '  Mentoring:'
+                for podling in sorted(committer.mentoring):
+                    print '   ', podling
+            if committer.committees:
+                print '  PMCs:'
+                for pmc in sorted(committer.committees):
+                    print '   ', pmc
+    else:
+        print email_alias, 'is not a registered email alias'
+
+def cmd_check(args):
+    print 'Email list checking is not supported yet'
+
+
+@entrypoint
+def main(cli):
+    cli.add_username_password(use_store=True)
+
+    parser = cli.argparser
+
+    subparsers = parser.add_subparsers()
+
+    lookup_parser = subparsers.add_parser('lookup', description='Lookup the email alias')
+    lookup_parser.add_argument('email_alias', help='The email alias to lookup')
+    lookup_parser.set_defaults(func=cmd_lookup)
+
+    reject_parser = subparsers.add_parser('check', description='Lists the projects user is
member of')
+    reject_parser.add_argument('email_alias', help='The email alias to lookup')
+    reject_parser.add_argument('mailing_list', help='The mailing list email alias is attempting
to join')
+    reject_parser.set_defaults(func=cmd_check)
+
+    with cli.run():
+        cli.args.func(cli.args)

Propchange: incubator/public/trunk/tools/bin/check-email
------------------------------------------------------------------------------
    svn:executable = *

Modified: incubator/public/trunk/tools/setup.py
URL: http://svn.apache.org/viewvc/incubator/public/trunk/tools/setup.py?rev=1488835&r1=1488834&r2=1488835&view=diff
==============================================================================
--- incubator/public/trunk/tools/setup.py (original)
+++ incubator/public/trunk/tools/setup.py Mon Jun  3 03:47:50 2013
@@ -102,6 +102,8 @@ setup(
     # don't ever depend on refcounting to close files anywhere else
     long_description=open('README.rst', encoding='utf-8').read(),
 
+    scripts=["bin/check-email"],
+
     namespace_packages=['asf'],
     package_dir={'': 'src'},
     packages=find_packages('src'),

Added: incubator/public/trunk/tools/src/asf/cli.py
URL: http://svn.apache.org/viewvc/incubator/public/trunk/tools/src/asf/cli.py?rev=1488835&view=auto
==============================================================================
--- incubator/public/trunk/tools/src/asf/cli.py (added)
+++ incubator/public/trunk/tools/src/asf/cli.py Mon Jun  3 03:47:50 2013
@@ -0,0 +1,386 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+import ConfigParser
+import contextlib
+import inspect
+from logging import getLogger
+import logging
+import os
+import sys
+
+import argparse
+
+from asf.utils.auth import get_username, get_password, AUTH_SECTION, save_password, AUTH_SECTIONS,
remove_password
+from asf.utils.config import load_config
+
+
+def entrypoint(method, depth=1, cls=None):
+    """
+      Run a method as your __main__ via decorator.
+
+      Example::
+
+        @asf.cli.entrypoint
+        def main(cli):
+          ...
+
+      Shorthand for::
+
+        def main():
+
+          cli = asf.cli.CLI()
+
+          ...
+
+        if __name__ == '__main__':
+          method()
+    """
+
+    current_frame = inspect.currentframe(depth).f_locals
+
+    if '__name__' in current_frame and current_frame['__name__'] == '__main__':
+
+        if cls is None:
+            cls = CLI
+
+        method(cls())
+
+    return method
+
+
+class CLI(object):
+    """
+        Initialize a command line helper instance.
+
+        Example::
+
+          import asf.cli
+
+          # Adding a version variable will automatically let you use --version on the command
line.
+          # VERSION is also acceptable.
+          version = "1.0"
+
+          @asf.cli.entrypoint
+          def main(cli):
+
+            cli.add_argument("-p", "--podling", required=True, default="yoko", help="Podling
to operate on.")
+            cli.add_argument("-q", dest="quiet", action="store_true", help="An example flag")
+
+            with cli.run():
+              if not cli.args.quiet:
+                cli.log.info("Operating in Incubator podling: %s", cli.args.podling)
+
+        .. note::
+
+          When you use asf.cli the following are available in __main__: cli, args & log.
+
+        .. note::
+
+          If using --log or --log-file, you can override the default FileHandler by supplying
+          a log_file_handler() function that returns a valid logging.handler.
+
+          Example::
+
+            def log_file_handler(filename):
+              return logging.handlers.TimedRotatingFileHandler(filename, when='midnight',
backupCount=14)
+
+        .. note::
+
+          Example::
+            @asf.cli.entrypoint
+            def main(cli):
+              ...
+              cli.influx_logger.enable_reporting()
+              with cli.run():
+                ...
+
+    """
+
+    exceptions = {}
+
+    def __init__(self, name=None):
+
+        if name is None:
+            name = os.path.basename(sys.argv[0])
+
+        #: The name of this cli instance.
+        self.name = name
+
+        #: :mod:`argparse` replaces optparse in Python 2.7, it is installable as a stand-alone
module.
+        self.argparser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
+
+        self.argparser.add_argument(
+            '--debug', action='store_true', default=False, help='Turn on debug mode / logging.'
+        )
+
+        self.argparser.add_argument(
+            '--trace', action='store_true', default=False, help='Turn on trace logging. Implies
--debug.'
+        )
+
+        self.argparser.add_argument(
+            '--log', help='Log file destination. Defaults to stdout only.'
+        )
+
+        #: Call into a :mod:`logging` instance.
+        self.log = getLogger(self.name)
+
+        # If the user has version defined, display it.
+        for name in ('version', 'VERSION'):
+            if hasattr(sys.modules['__main__'], name):
+                self.argparser.add_argument('--version', action='version', version='%(prog)s
' + getattr(sys.modules['__main__'], name))
+
+        #: :class:`ConfigParser` configuration object if --config was passed on the command
line.
+        #: Default is None.
+        self.config = None
+
+        #: Configuration file to load. Default is None.
+        self.config_file = None
+
+        #: Description for the argument parser.
+        self.description = None
+
+        #: Epilog for the argument parser.
+        self.epilog = None
+
+        #: Parsed arguments.
+        self.args = None
+
+        #: Unrecognized arguments.
+        self.unrecognized_args = None
+
+        # Disable logging by default, programs can enable if it is wanted
+
+        if self.log:
+            # Keep the handler around, so we can change it's level later.
+            self.console = logging.StreamHandler()
+            self.console.setFormatter(logging.Formatter('[%(levelname)s] %(message)s'))
+            self.console.setLevel(logging.INFO)
+
+            self.log.addHandler(self.console)
+
+            # This controls the global level for loggers. Filtering will occur in handlers
for levels.
+            self.log.setLevel(logging.INFO)
+
+        #: Caller can always write DEBUG level logs to a filename.
+        #: Using --log on the command line will override this variable.
+        self.log_file = None
+
+        #: :class:`logging` FileHandler instance if --log was passed, or `log_file` is set.
+        self.log_file_handler = None
+
+        # Defaults to use only when there are no other arguments.
+        self.argument_defaults = None
+
+        # Flag to determine if it should attempt to send stats or not.
+        self.should_send_stats = False
+
+        # Flag to determine if we should parse the --config file.
+        self.should_parse_config = False
+
+        # Should we include standard documentation links
+        self.should_use_default_wiki_location_for_docstring = False
+
+        # Should we process the username and passwords
+        self.use_username_password_store = None
+
+
+    def add_argument(self, *args, **kwargs):
+        """
+          Add a command line argument.
+
+          The current underlying implementation uses :mod:`argparse`.
+        """
+
+        self.argparser.add_argument(*args, **kwargs)
+
+    def add_argument_defaults(self, **kwargs):
+        """
+          Set defaults to be passed to the argument parser ONLY when there are
+          no other arguments on the command line. If you want regular defaults,
+          use the default= setting on :meth:`add_argument`.
+
+          Example::
+
+            cli.add_argument_defaults(start=True, debug=True)
+        """
+
+        self.argument_defaults = kwargs
+
+    def add_config_option(self, default=None):
+        """ Add a --config option to the argument parser. """
+
+        self.argparser.add_argument('--config', default=default, help='Config file to read.
Defaults to: %(default)s')
+        self.should_parse_config = True
+
+    def add_username_password(self, use_store=False):
+        """ Add --username and --password options
+          :param bool use_store: Name of the section (concept, command line options, API
reference)
+        """
+        self.argparser.add_argument('--username', default=None, help='Username')
+        self.argparser.add_argument('--password', default=None, help='Password')
+        self.argparser.add_argument('--clear-store', action='store_true', default=False,
help='Clear password keystore')
+
+        self.use_username_password_store = use_store
+
+    def _add_documentation_link(self, links, section, variable_name, default=None):
+        """
+          :param list links: List of links to append link of the form "Section: <link>",
if link available
+          :param str section: Name of the section (concept, command line options, API reference)
+          :param str variable_name: Variable name in main module that should hold URL to
documentation
+          :param str default: Default URL to documentation
+        """
+
+        url = getattr(sys.modules['__main__'], variable_name, default)
+
+        if url:
+            links.append('%s: %s' % (section, url))
+
+    def __parse_args(self, accept_unrecognized_args=False):
+        """ Invoke the argument parser. """
+
+        # If the user provided a description, use it. Otherwise grab the doc string.
+        if self.description:
+            self.argparser.description = self.description
+        elif getattr(sys.modules['__main__'], '__doc__', None):
+            self.argparser.description = getattr(sys.modules['__main__'], '__doc__')
+        else:
+            self.argparser.description = 'No documentation defined. Please add a doc string
to %s' % sys.modules['__main__'].__file__
+
+        self.argparser.epilog = self.epilog
+
+        # Only if there aren't any other command line arguments.
+        if len(sys.argv) == 1 and self.argument_defaults:
+            self.argparser.set_defaults(**self.argument_defaults)
+
+        if accept_unrecognized_args:
+            self.args, self.unrecognized_args = self.argparser.parse_known_args()
+        else:
+            self.args = self.argparser.parse_args()
+
+    def __parse_config(self):
+        """ Invoke the config file parser. """
+
+        if self.should_parse_config and (self.args.config or self.config_file):
+            self.config = ConfigParser.SafeConfigParser()
+            self.config.read(self.args.config or self.config_file)
+
+    def __process_username_password(self):
+        """ If indicated, process the username and password """
+
+        if self.use_username_password_store is not None:
+            if self.args.clear_store:
+                with load_config(AUTH_SECTIONS) as config:
+                    config.remove_option(AUTH_SECTION, 'username')
+            if not self.args.username:
+                self.args.username = get_username(use_store=self.use_username_password_store)
+
+            if self.args.clear_store:
+                remove_password(AUTH_SECTION, username=self.args.username)
+            if not self.args.password:
+                self.args.password = get_password(AUTH_SECTION, username=self.args.username)
+                if self.use_username_password_store:
+                    save_password(AUTH_SECTION, self.args.password, self.args.username)
+
+    def __finish_initializing(self):
+        """ Handle any initialization after arguments & config has been parsed. """
+
+        if self.args.debug or self.args.trace:
+            # Set the console (StreamHandler) to allow debug statements.
+
+            if self.args.debug:
+                self.console.setLevel(logging.DEBUG)
+
+            self.console.setFormatter(logging.Formatter('[%(levelname)s] %(asctime)s %(name)s
- %(message)s'))
+
+            # Set the global level to debug.
+            if self.args.debug:
+                self.log.setLevel(logging.DEBUG)
+
+        if self.args.log or self.log_file:
+
+            # Allow the user to override the default log file handler.
+            try:
+                self.log_file_handler = sys.modules['__main__'].log_file_handler(self.args.log
or self.log_file)
+            except Exception:
+                self.log_file_handler = logging.FileHandler(self.args.log or self.log_file)
+
+            self.log_file_handler.setFormatter(logging.Formatter('[%(levelname)s] %(asctime)s
%(name)s - %(message)s'))
+            self.log_file_handler.setLevel(logging.DEBUG)
+
+            self.log.addHandler(self.log_file_handler)
+
+        # Allow cli.log, args & self to be accessed from __main__
+        if not hasattr(sys.modules['__main__'], 'log'):
+            sys.modules['__main__'].log = self.log
+
+        if not hasattr(sys.modules['__main__'], 'cli'):
+            sys.modules['__main__'].cli = self
+
+        if not hasattr(sys.modules['__main__'], 'args'):
+            sys.modules['__main__'].args = self.args
+
+    @classmethod
+    def register_exception(cls, exception, func):
+        """
+          Allow callers to register a function to be run when the given
+          exception is raised while inside a cli.run() context manager.
+        """
+
+        cls.exceptions[exception] = func
+
+    @contextlib.contextmanager
+    def run(self, accept_unrecognized_args=False):
+        """
+          Called via the `with` statement to invoke the :func:`contextlib.contextmanager`.
+
+          Control is then yielded back to the caller.
+
+          All exceptions are caught & a stack trace emitted, except in the case of `asf.cli.ExitedCleanly`.
+        """
+
+        self.__parse_args(accept_unrecognized_args)
+        self.__parse_config()
+        self.__process_username_password()
+        self.__finish_initializing()
+
+        exit_status = 0
+
+        try:
+            yield self
+        except ExitedCleanly:
+            pass
+        except (Exception, KeyboardInterrupt) as e:
+            # Run any method a library or caller might have registered.
+            for base in type(e).mro():
+                if base in self.exceptions:
+                    self.exceptions[base](e)
+                    break
+            else:
+                self.log.exception(e)
+
+            exit_status = os.EX_SOFTWARE
+        finally:
+            logging.shutdown()
+
+        sys.exit(exit_status)
+
+
+class ExitedCleanly(StandardError):
+    """ Use instead of sys.exit() to throw an exception but not log an error. """
+    pass

Added: incubator/public/trunk/tools/src/asf/utils/auth.py
URL: http://svn.apache.org/viewvc/incubator/public/trunk/tools/src/asf/utils/auth.py?rev=1488835&view=auto
==============================================================================
--- incubator/public/trunk/tools/src/asf/utils/auth.py (added)
+++ incubator/public/trunk/tools/src/asf/utils/auth.py Mon Jun  3 03:47:50 2013
@@ -0,0 +1,309 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""
+  Deal with password loading & saving.
+
+  To opt-in to the various key chains, please create a file: ~/.asf-tools.ini with the contents:
+
+    [keychain]
+    gnome-keychain.enable = True
+    kde-keychain.enable = True
+    crypted-keychain.enable = True
+
+  Note that the OS X keychain is always available on OS X.
+"""
+import getpass
+from logging import getLogger
+import os
+import subprocess
+import sys
+
+from brownie.caching import memoize
+import keyring
+from keyring.backend import OSXKeychain, GnomeKeyring, KDEKWallet, CryptedFileKeyring
+from keyring.errors import PasswordSetError
+
+from asf.utils.config import load_config
+
+
+AUTH_SECTION = 'org.asf.auth'
+KEYCHAIN_SECTION = 'keychain'
+
+log = getLogger(__name__)
+
+_unlocked = set()
+
+AUTH_CONFIG_DEFAULTS = {'username': '',
+                        'gnome-keychain.enable': False,
+                        'kde-keychain.enable': False,
+                        'crypted-keychain.enable': False}
+AUTH_SECTIONS = [AUTH_SECTION, KEYCHAIN_SECTION]
+
+
+class FixedOSXKeychain(OSXKeychain):
+    """ OSXKeychain does not implement delete_password() yet """
+
+    def delete_password(self, service, username):
+        """Delete the password for the username of the service.
+        """
+        try:
+            # set up the call for security.
+            call = subprocess.Popen([
+                                        'security',
+                                        'delete-generic-password',
+                                        '-a',
+                                        username,
+                                        '-s',
+                                        service
+                                    ],
+                                    stderr=subprocess.PIPE,
+                                    stdout=subprocess.PIPE,
+            )
+            stdoutdata, stderrdata = call.communicate()
+            code = call.returncode
+            # check return code.
+            if code is not 0:
+                raise PasswordSetError('Can\'t delete password in keychain')
+        except:
+            raise PasswordSetError("Can't delete password in keychain")
+
+
+@memoize
+def initialize_keychain():
+    # NB: keyring has a config file, but it only allows a single keyring to be
+    # selected, instead of reusing it's supported() method check against a list
+    # of backend implementations to try.
+
+    keyring_backends = []
+
+    with load_config(AUTH_SECTIONS, AUTH_CONFIG_DEFAULTS) as config:
+
+        if config.get(KEYCHAIN_SECTION, 'crypted-keychain.enable'):
+            keyring_backends.insert(0, CryptedFileKeyring())
+
+        if config.get(KEYCHAIN_SECTION, 'kde-keychain.enable'):
+            keyring_backends.insert(0, KDEKWallet())
+
+        if config.get(KEYCHAIN_SECTION, 'gnome-keychain.enable'):
+            keyring_backends.insert(0, GnomeKeyring())
+
+    keyring_backends.insert(0, FixedOSXKeychain())
+    keyring_backends.sort(key=lambda x: -x.supported())
+
+    keyring.set_keyring(keyring_backends[0])
+
+    # Return True if there are any supported keychains.
+    return not all(i.supported() == -1 for i in keyring_backends)
+
+
+def clear_username_from_store():
+    with load_config(AUTH_SECTIONS, AUTH_CONFIG_DEFAULTS) as config:
+        config.remove(AUTH_SECTION, 'username')
+
+
+def get_username(use_store=False):
+    if use_store:
+        with load_config(AUTH_SECTIONS, AUTH_CONFIG_DEFAULTS) as config:
+            username = config.get(AUTH_SECTION, 'username')
+            if not username:
+                username = raw_input("Username [%s]: " % getpass.getuser())
+                if not username:
+                    username = getpass.getuser()
+                config.set(AUTH_SECTION, 'username', username)
+    else:
+        username = raw_input("Username [%s]: " % getpass.getuser())
+        if not username:
+            username = getpass.getuser()
+
+    return username
+
+
+def unlock_keychain(username):
+    """ If the user is running via SSH, their Keychain must be unlocked first. """
+
+    if 'SSH_TTY' not in os.environ:
+        return
+
+    # Don't unlock if we've already seen this user.
+    if username in _unlocked:
+        return
+
+    _unlocked.add(username)
+
+    if sys.platform == 'darwin':
+        sys.stderr.write("You are running under SSH. Please unlock your local OS X KeyChain:\n")
+        subprocess.call(['security', 'unlock-keychain'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+
+def save_password(entry, password, username=None):
+    """
+      Saves the given password in the user's keychain.
+
+      :param entry: The entry in the keychain. This is a caller specific key.
+      :param password: The password to save in the keychain.
+      :param username: The username to get the password for. Default is the current user.
+    """
+
+    if username is None:
+        username = get_username()
+
+    has_keychain = initialize_keychain()
+
+    if has_keychain:
+        try:
+            keyring.set_password(entry, username, password)
+        except Exception as e:
+            log.warn("Unable to set password in keyring. Continuing..")
+            log.debug(e)
+
+
+def remove_password(entry, username=None):
+    """
+      Removes the password for the specific user in the user's keychain.
+
+      :param entry: The entry in the keychain. This is a caller specific key.
+      :param username: The username whose password is to be removed. Default is the current
user.
+    """
+
+    if username is None:
+        username = get_username()
+
+    has_keychain = initialize_keychain()
+
+    if has_keychain:
+        try:
+            keyring.delete_password(entry, username)
+        except Exception as e:
+            print e
+            log.warn("Unable to delete password in keyring. Continuing..")
+            log.debug(e)
+
+
+def get_password(entry=None, username=None, prompt=None, always_ask=False):
+    """
+      Prompt the user for a password on stdin.
+
+      :param username: The username to get the password for. Default is the current user.
+      :param entry: The entry in the keychain. This is a caller specific key.
+      :param prompt: The entry in the keychain. This is a caller specific key.
+      :param always_ask: Force the user to enter the password every time.
+    """
+
+    password = None
+
+    if username is None:
+        username = get_username()
+
+    has_keychain = initialize_keychain()
+
+    # Unlock the user's keychain otherwise, if running under SSH, 'security(1)' will thrown
an error.
+    unlock_keychain(username)
+
+    if prompt is None:
+        prompt = "Enter %s's password: " % username
+
+    if has_keychain and entry is not None and always_ask is False:
+        password = get_password_from_keyring(entry, username)
+
+    if password is None:
+        password = getpass.getpass(prompt=prompt)
+
+    return password
+
+
+def get_password_from_keyring(entry=None, username=None):
+    """
+      :param entry: The entry in the keychain. This is a caller specific key.
+      :param username: The username to get the password for. Default is the current user.
+    """
+
+    password = None
+
+    if username is None:
+        username = get_username()
+
+    has_keychain = initialize_keychain()
+
+    # Unlock the user's keychain otherwise, if running under SSH, 'security(1)' will thrown
an error.
+    unlock_keychain(username)
+
+    if has_keychain and entry is not None:
+        try:
+            return keyring.get_password(entry, username)
+        except Exception as e:
+            log.warn("Unable to get password from keyring. Continuing..")
+            log.debug(e)
+
+    return None
+
+
+def validate_password(entry, username, check_function, password=None, retries=1, save_on_success=True,
prompt=None, **check_args):
+    """
+      Validate a password with a check function & retry if the password is incorrect.
+
+      Useful for after a user has changed their password in LDAP, but their local keychain
entry is then out of sync.
+
+      :param str entry: The keychain entry to fetch a password from.
+      :param str username: The username to authenticate
+      :param func check_function: Check function to use. Should take (username, password,
**check_args)
+      :param str password: The password to validate. If `None`, the user will be prompted.
+      :param int retries: Number of retries to prompt the user for.
+      :param bool save_on_success: Save the password if the validation was successful.
+      :param str prompt: Alternate prompt to use when asking for the user's password.
+
+      :returns: `True` on successful authentication. `False` otherwise.
+      :rtype: bool
+    """
+
+    if password is None:
+        password = get_password(entry, username, prompt)
+
+    for _ in xrange(retries + 1):
+
+        if check_function(username, password, **check_args):
+            if save_on_success:
+                save_password(entry, password, username)
+
+            return True
+
+        log.error("Couldn't successfully authenticate your username & password..")
+
+        password = get_password(entry, username, prompt, always_ask=True)
+
+    return False
+
+
+def stored_credentials():
+    with load_config(AUTH_SECTIONS, AUTH_CONFIG_DEFAULTS) as config:
+        username = config.get(AUTH_SECTION, 'username')
+        if not username:
+            return None, None
+
+    has_keychain = initialize_keychain()
+    # Unlock the user's keychain otherwise, if running under SSH, 'security(1)' will thrown
an error.
+    unlock_keychain(username)
+
+    if has_keychain:
+        try:
+            password = keyring.get_password(AUTH_SECTION, username)
+        except Exception as e:
+            return None, None
+        return username, password
+
+    return None, None

Added: incubator/public/trunk/tools/src/asf/utils/committers.py
URL: http://svn.apache.org/viewvc/incubator/public/trunk/tools/src/asf/utils/committers.py?rev=1488835&view=auto
==============================================================================
--- incubator/public/trunk/tools/src/asf/utils/committers.py (added)
+++ incubator/public/trunk/tools/src/asf/utils/committers.py Mon Jun  3 03:47:50 2013
@@ -0,0 +1,76 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+import json
+from restkit import Resource, BasicAuth
+
+from asf.utils.emails import canonical_email_address
+
+
+COMMITTERS_URL = 'https://whimsy.apache.org/roster/committer'
+
+
+class Committer(object):
+    def __init__(self, username, member, fullname, emails, urls, committees, projects, mentoring):
+        self.username = username
+        self.member = member
+        self.fullname = fullname
+        self.emails = set(emails)
+        self.urls = set(urls)
+        self.committees = set(committees)
+        self.projects = set(projects)
+        self.mentoring = set(mentoring)
+
+    def as_tuple(self):
+        return (self.username, self.member, self.fullname, tuple(sorted(self.emails)), tuple(sorted(self.urls)),
tuple(sorted(self.committees)), tuple(sorted(self.projects)), tuple(sorted(self.mentoring)))
+
+    def __eq__(self, other):
+        return self.as_tuple() == other.as_tuple()
+
+    def __hash__(self):
+        return hash(self.as_tuple())
+
+    def __repr__(self):
+        return 'Committer(%s, %s, %s, %s, %s, %s, %s, %s)' % self.as_tuple()
+
+
+COMMITTERS = {}
+
+
+def get_committer(committer, username, password):
+    global COMMITTERS
+    if committer not in COMMITTERS:
+        committer_json = json.loads(Resource('%s/%s' % (COMMITTERS_URL, committer),
+                                             filters=[BasicAuth(username, password)],
+                                             timeout=10).get(headers={'Accept': 'application/json'}).body_string())
+
+        availid = committer_json['availid']
+        member = bool(committer_json['member'])
+        fullname = committer_json['name']
+        emails = [canonical_email_address(email) for email in committer_json['emails']]
+        urls = committer_json['urls']
+        committees = committer_json['committees']
+        projects = set(committer_json['groups'])
+        if 'apsite' in projects: projects.remove('apsite')
+        if 'committers' in projects: projects.remove('committers')
+        if 'member' in projects: projects.remove('member')
+        mentoring = committer_json['auth']
+
+        COMMITTERS[committer] = Committer(availid, member, fullname, emails, urls, committees,
projects, mentoring)
+
+    return COMMITTERS[committer]

Added: incubator/public/trunk/tools/src/asf/utils/emails.py
URL: http://svn.apache.org/viewvc/incubator/public/trunk/tools/src/asf/utils/emails.py?rev=1488835&view=auto
==============================================================================
--- incubator/public/trunk/tools/src/asf/utils/emails.py (added)
+++ incubator/public/trunk/tools/src/asf/utils/emails.py Mon Jun  3 03:47:50 2013
@@ -0,0 +1,78 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+"""
+Provides access to https://id.apache.org/info/MailAlias.txt.
+"""
+from collections import defaultdict
+from restkit import BasicAuth, Resource
+
+from brownie.caching import memoize
+
+
+MAIL_ALIAS_URL = 'https://id.apache.org/info/MailAlias.txt'
+
+
+@memoize
+def canonical_email_address(email):
+    if '@' in email:
+        local, domain = email.split('@')
+        return '%s@%s' % (local, domain.lower())
+    else:
+        return email
+
+
+@memoize
+def is_apache_email_address(email):
+    return canonical_email_address(email).endswith('@apache.org')
+
+
+@memoize
+def username_from_apache_email(email):
+    if not is_apache_email_address(email):
+        raise ValueError('%s is not a valid Apache Software Foundation email' % email)
+
+    return email.split('@')[0]
+
+
+def aliases_for(apache_email, mail_aliases):
+    apache_email = canonical_email_address(apache_email)
+    if apache_email in mail_aliases:
+        return mail_aliases[apache_email]['aliases']
+    else:
+        raise ValueError('%s is not in the mail aliases file' % apache_email)
+
+
+def email_from_alias(alias_email, mail_aliases):
+    alias_email = canonical_email_address(alias_email)
+    for apache_email_address, data in mail_aliases.iteritems():
+        if alias_email in data['aliases']:
+            return apache_email_address
+    return None
+
+
+def get_mail_aliases(username, password):
+    mail_aliases = defaultdict(dict)
+    for line in Resource(MAIL_ALIAS_URL, filters=[BasicAuth(username, password)], timeout=10).get().body_stream():
+        apache_email, alias_email, member = [canonical_email_address(field.strip()) for field
in line.split(',')]
+        mail_aliases[apache_email]['member'] = bool(member)
+        if 'aliases' not in mail_aliases[apache_email]:
+            mail_aliases[apache_email]['aliases'] = set()
+        mail_aliases[apache_email]['aliases'].add(alias_email)
+
+    return mail_aliases

Added: incubator/public/trunk/tools/src/asf/utils/test.py
URL: http://svn.apache.org/viewvc/incubator/public/trunk/tools/src/asf/utils/test.py?rev=1488835&view=auto
==============================================================================
--- incubator/public/trunk/tools/src/asf/utils/test.py (added)
+++ incubator/public/trunk/tools/src/asf/utils/test.py Mon Jun  3 03:47:50 2013
@@ -0,0 +1,32 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+import nose
+
+from asf.utils.auth import stored_credentials
+
+
+def ensure_credentials_stored(function):
+    def wrapped(*args, **kwargs):
+        username, password = stored_credentials()
+        if not (username and password):
+            raise nose.SkipTest('Credentials not stored for testing')
+
+        return function(username, password, *args, **kwargs)
+
+    return nose.tools.make_decorator(function)(wrapped)



---------------------------------------------------------------------
To unsubscribe, e-mail: cvs-unsubscribe@incubator.apache.org
For additional commands, e-mail: cvs-help@incubator.apache.org


Mime
View raw message