diff --git a/did/base.py b/did/base.py index 54a8c334..e8c807e3 100644 --- a/did/base.py +++ b/did/base.py @@ -1,192 +1,263 @@ # coding: utf-8 -""" Stats & StatsGroup, the core of the data gathering """ +""" Config, Date, User and Exceptions """ from __future__ import unicode_literals, absolute_import +import os +import codecs +import datetime import optparse +import StringIO import xmlrpclib +import ConfigParser +from dateutil.relativedelta import MO as MONDAY +from dateutil.relativedelta import relativedelta as delta from did import utils -from did import plugins +from did.utils import log # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Stats +# Constants # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class Stats(object): - """ General statistics """ - _name = None - _error = None - _enabled = None - option = None - dest = None - parent = None - stats = None - - def __init__( - self, option, name=None, parent=None, user=None, options=None): - """ Set the name, indent level and initialize data. """ - self.option = option.replace(" ", "-") - self.dest = self.option.replace("-", "_") - self._name = name - self.parent = parent - self.stats = [] - # Save user and options (get it directly or from parent) - self.options = options or getattr(self.parent, 'options', None) - if user is None and self.parent is not None: - self.user = self.parent.user - else: - self.user = user - utils.log.debug( - 'Loading {0} Stats instance for {1}'.format(option, self.user)) +# Config file location +CONFIG = os.path.expanduser("~/.did") - @property - def name(self): - """ Use docs string unless name set. """ - return self._name or self.__doc__.strip() - - def add_option(self, group): - """ Add option for self to the parser group object. """ - group.add_option( - "--{0}".format(self.option), action="store_true", help=self.name) - - def enabled(self): - """ Check whether we're enabled (or if parent is). """ - # Cache into ._enabled - if self._enabled is None: - if self.parent is not None and self.parent.enabled(): - self._enabled = True - else: - # Default to Enabled if not otherwise disabled - self._enabled = getattr(self.options, self.dest, True) - utils.log.debug("{0} Enabled? {1}".format(self.option, self._enabled)) - return self._enabled - - def fetch(self): - """ Fetch the stats (to be implemented by respective class). """ - raise NotImplementedError() - - def check(self): - """ Check the stats if enabled. """ - if not self.enabled(): - return - try: - self.fetch() - except (xmlrpclib.Fault, utils.ConfigError) as error: - utils.log.error(error) - self._error = True - # Raise the exception if debugging - if not self.options or self.options.debug: - raise - # Show the results stats (unless merging) - if self.options and not self.options.merge: - self.show() - - def header(self): - """ Show summary header. """ - # Show question mark instead of count when errors encountered - count = "? (error encountered)" if self._error else len(self.stats) - utils.item("{0}: {1}".format(self.name, count), options=self.options) - - def show(self): - """ Display indented statistics. """ - if not self._error and not self.stats: - return - self.header() - for stat in self.stats: - utils.item(stat, level=1, options=self.options) +# Default maximum width +MAX_WIDTH = 79 - def merge(self, other): - """ Merge another stats. """ - self.stats.extend(other.stats) - if other._error: - self._error = True +# Today's date +TODAY = datetime.date.today() # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Stats Group +# Exceptions # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class StatsGroup(Stats): - """ Stats group """ - - # Default order - order = 500 +class ConfigError(Exception): + """ General problem with configuration file """ + pass - def add_option(self, parser): - """ Add option group and all children options. """ - group = optparse.OptionGroup(parser, self.name) - for stat in self.stats: - stat.add_option(group) - group.add_option( - "--{0}".format(self.option), action="store_true", help="All above") - parser.add_option_group(group) +class ReportError(Exception): + """ General problem with report generation """ + pass - def check(self): - """ Check all children stats. """ - for stat in self.stats: - stat.check() - def show(self): - """ List all children stats. """ - for stat in self.stats: - stat.show() - - def merge(self, other): - """ Merge all children stats. """ - for this, other in zip(self.stats, other.stats): - this.merge(other) +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Config +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - def fetch(self): - """ Stats groups do not fetch anything """ - pass +class Config(object): + """ User config file """ + parser = None -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# User Stats -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def __init__(self, config=None, path=None): + """ + Read the config file -class UserStats(StatsGroup): - """ User statistics in one place """ + Parse config from given string (config) or file (path). + If no config or path given, default to "~/.did/config" which + can be overrided by the DID_CONFIG environment variable. + """ + # Read the config only once (unless explicitly provided) + if self.parser is not None and config is None and path is None: + return + Config.parser = ConfigParser.SafeConfigParser() + # If config provided as string, parse it directly + if config is not None: + log.info("Inspecting config file from string") + log.debug(utils.pretty(config)) + self.parser.readfp(StringIO.StringIO(config)) + return + # Check the environment for config file override + # (unless path is explicitly provided) + if path is None: + try: + directory = os.environ["DID_CONFIG"] + except KeyError: + directory = CONFIG + path = directory.rstrip("/") + "/config" + # Parse the config from file + try: + log.info("Inspecting config file '{0}'".format(path)) + self.parser.readfp(codecs.open(path, "r", "utf8")) + except IOError as error: + log.error(error) + raise ConfigError("Unable to read the config file") - def __init__(self, user=None, options=None): - """ Initialize stats objects. """ - super(UserStats, self).__init__( - option="all", user=user, options=options) - self.stats = [] - for section, statsgroup in plugins.detect(): - self.stats.append(statsgroup(option=section, parent=self)) + @property + def email(self): + """ User email(s) """ + try: + return self.parser.get("general", "email") + except ConfigParser.NoOptionError: + return [] - def add_option(self, parser): - """ Add options for each stats group. """ - for stat in self.stats: - stat.add_option(parser) + @property + def width(self): + """ Maximum width of the report """ + try: + return int(self.parser.get("general", "width")) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + return MAX_WIDTH + + def sections(self, kind=None): + """ Return all sections (optionally of given kind only) """ + result = [] + for section in self.parser.sections(): + # Selected kind only if provided + if kind is not None: + try: + section_type = self.parser.get(section, "type") + if section_type != kind: + continue + except ConfigParser.NoOptionError: + # Implicit header/footer type for backward compatibility + if (section == kind == "header" or + section == kind == "footer"): + pass + else: + continue + result.append(section) + return result + + def section(self, section, skip=None): + """ Return section items, skip selected (type/order by default) """ + if skip is None: + skip = ['type', 'order'] + return [(key, val) for key, val in self.parser.items(section) + if key not in skip] + + def item(self, section, it): + """ Return content of given item in selected section """ + for key, value in self.section(section, skip=['type']): + if key == it: + return value + raise ConfigError( + "Item '{0}' not found in section '{1}'".format(it, section)) + + @staticmethod + def example(): + """ Return config example """ + return "[general]\nemail = Name Surname \n" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Header & Footer +# Date # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class EmptyStats(Stats): - """ Custom stats group for header & footer """ - def __init__(self, option, name=None, parent=None): - Stats.__init__(self, option, name, parent) +class Date(object): + """ Date parsing for common word formats """ + + def __init__(self, date=None): + """ Parse the date string """ + if isinstance(date, datetime.date): + self.date = date + elif date is None or date.lower() == "today": + self.date = TODAY + elif date.lower() == "yesterday": + self.date = TODAY - delta(days=1) + else: + self.date = datetime.date(*[int(i) for i in date.split("-")]) + self.datetime = datetime.datetime( + self.date.year, self.date.month, self.date.day, 0, 0, 0) + + def __str__(self): + """ Ascii version of the string representation """ + return utils.ascii(unicode(self)) + + def __unicode__(self): + """ String format for printing """ + return unicode(self.date) + + @staticmethod + def this_week(): + """ Return start and end date of the current week. """ + since = TODAY + delta(weekday=MONDAY(-1)) + until = since + delta(weeks=1) + return Date(since), Date(until) + + @staticmethod + def last_week(): + """ Return start and end date of the last week. """ + since = TODAY + delta(weekday=MONDAY(-2)) + until = since + delta(weeks=1) + return Date(since), Date(until) + + @staticmethod + def this_month(): + """ Return start and end date of this month. """ + since = TODAY + delta(day=1) + until = since + delta(months=1) + return Date(since), Date(until) + + @staticmethod + def last_month(): + """ Return start and end date of this month. """ + since = TODAY + delta(day=1, months=-1) + until = since + delta(months=1) + return Date(since), Date(until) + + @staticmethod + def this_quarter(): + """ Return start and end date of this quarter. """ + since = TODAY + delta(day=1) + while since.month % 3 != 0: + since -= delta(months=1) + until = since + delta(months=3) + return Date(since), Date(until) + + @staticmethod + def last_quarter(): + """ Return start and end date of this quarter. """ + since, until = Date.this_quarter() + since = since.date - delta(months=3) + until = until.date - delta(months=3) + return Date(since), Date(until) + + @staticmethod + def this_year(): + """ Return start and end date of this fiscal year """ + since = TODAY + while since.month != 3 or since.day != 1: + since -= delta(days=1) + until = since + delta(years=1) + return Date(since), Date(until) + + @staticmethod + def last_year(): + """ Return start and end date of the last fiscal year """ + since, until = Date.this_year() + since = since.date - delta(years=1) + until = until.date - delta(years=1) + return Date(since), Date(until) - def show(self): - """ Name only for empty stats """ - utils.item(self.name, options=self.options) - def fetch(self): - """ Nothing to do for empty stats """ - pass +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# User +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +class User(object): + """ User info """ -class EmptyStatsGroup(StatsGroup): - """ Header & Footer stats group """ - def __init__(self, option, name=None, parent=None): - StatsGroup.__init__(self, option, name, parent=parent) - for opt, name in sorted(utils.Config().section(option)): - self.stats.append(EmptyStats(opt, name, parent=self)) + def __init__(self, email, name=None, login=None): + """ Set user email, name and login values. """ + if not email: + raise ReportError("Email required for user initialization.") + else: + # Extract everything from the email string provided + # eg, "My Name" + parts = utils.EMAIL_REGEXP.search(email) + if parts is None: + raise ConfigError("Invalid email address '{0}'".format(email)) + self.email = parts.groups()[1] + self.login = login or self.email.split('@')[0] + self.name = name or parts.groups()[0] or u"Unknown" + + def __unicode__(self): + """ Use name & email for string representation. """ + return u"{0} <{1}>".format(self.name, self.email) diff --git a/did/cli.py b/did/cli.py index 3edc8c23..49887080 100644 --- a/did/cli.py +++ b/did/cli.py @@ -16,8 +16,10 @@ import ConfigParser from dateutil.relativedelta import relativedelta as delta +import did.base import did.utils as utils -from did.base import UserStats +from did.stats import UserStats +from did.base import ConfigError, ReportError # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -58,7 +60,7 @@ def __init__(self, arguments=None): "--format", default="text", help="Output style, possible values: text (default) or wiki") group.add_option( - "--width", default=utils.Config().width, type="int", + "--width", default=did.base.Config().width, type="int", help="Maximum width of the report output (default: %default)") group.add_option( "--brief", action="store_true", @@ -88,7 +90,7 @@ def parse(self, arguments=None): # Enable debugging output if opt.debug: - utils.logging.set(utils.LOG_DEBUG) + utils.Logging.set(utils.LOG_DEBUG) # Enable --all if no particular stat or group selected opt.all = not any([ @@ -98,15 +100,15 @@ def parse(self, arguments=None): # Detect email addresses and split them on comma if not opt.emails: - opt.emails = utils.Config().email + opt.emails = did.base.Config().email opt.emails = utils.split(opt.emails, separator=re.compile(r"\s*,\s*")) # Time period handling if opt.since is None and opt.until is None: opt.since, opt.until, period = self.time_period(arg) else: - opt.since = utils.Date(opt.since or "1993-01-01") - opt.until = utils.Date(opt.until or "today") + opt.since = did.base.Date(opt.since or "1993-01-01") + opt.until = did.base.Date(opt.until or "today") # Make the 'until' limit inclusive opt.until.date += delta(days=1) period = "given date range" @@ -128,37 +130,37 @@ def time_period(arg): """ Detect desired time period for the argument """ since, until, period = None, None, None if "today" in arg: - since = utils.Date("today") - until = utils.Date("today") + since = did.base.Date("today") + until = did.base.Date("today") until.date += delta(days=1) period = "today" elif "year" in arg: if "last" in arg: - since, until = utils.Date.last_year() + since, until = did.base.Date.last_year() period = "the last fiscal year" else: - since, until = utils.Date.this_year() + since, until = did.base.Date.this_year() period = "this fiscal year" elif "quarter" in arg: if "last" in arg: - since, until = utils.Date.last_quarter() + since, until = did.base.Date.last_quarter() period = "the last quarter" else: - since, until = utils.Date.this_quarter() + since, until = did.base.Date.this_quarter() period = "this quarter" elif "month" in arg: if "last" in arg: - since, until = utils.Date.last_month() + since, until = did.base.Date.last_month() period = "the last month" else: - since, until = utils.Date.this_month() + since, until = did.base.Date.this_month() period = "this month" else: if "last" in arg: - since, until = utils.Date.last_week() + since, until = did.base.Date.last_week() period = "the last week" else: - since, until = utils.Date.this_week() + since, until = did.base.Date.this_week() period = "this week" return since, until, period @@ -184,9 +186,9 @@ def main(arguments=None): gathered_stats = [] # Check for user email addresses (command line or config) - users = [utils.User(email=email) for email in options.emails] + users = [did.base.User(email=email) for email in options.emails] if not users: - raise utils.ConfigError("No user email provided") + raise ConfigError("No user email provided") # Prepare team stats object for data merging team_stats = UserStats(options=options) @@ -214,7 +216,7 @@ def main(arguments=None): # Return all gathered stats objects return gathered_stats, team_stats - except (utils.ConfigError, utils.ReportError) as error: + except (ConfigError, ReportError) as error: utils.log.error(error) sys.exit(1) @@ -226,8 +228,8 @@ def main(arguments=None): utils.log.error(error) utils.log.error( "No email provided on the command line or in the config file") - utils.info( - "Create at least a minimum config file {0}:".format(utils.CONFIG)) + utils.info("Create at least a minimum config file {0}:".format( + did.base.CONFIG)) from getpass import getuser utils.info( '[general]\nemail = "My Name" <{0}@domain.com>'.format(getuser())) diff --git a/did/plugins/__init__.py b/did/plugins/__init__.py index f596fed2..0e379209 100644 --- a/did/plugins/__init__.py +++ b/did/plugins/__init__.py @@ -26,7 +26,9 @@ import sys import types -from did.utils import Config, ConfigError, log +from did.utils import log +from did.base import Config, ConfigError +from did.stats import StatsGroup, EmptyStatsGroup # Self reference and file path to this module PLUGINS = sys.modules[__name__] @@ -67,7 +69,6 @@ def detect(): as well as the option used to enable those particular stats. """ # Detect classes inherited from StatsGroup and return them sorted - from did.base import StatsGroup, EmptyStatsGroup stats = [] for plugin in load(): module = getattr(PLUGINS, plugin) diff --git a/did/plugins/bugzilla.py b/did/plugins/bugzilla.py index fc5fa461..319f86f6 100644 --- a/did/plugins/bugzilla.py +++ b/did/plugins/bugzilla.py @@ -32,8 +32,9 @@ import bugzilla import xmlrpclib -from did.base import Stats, StatsGroup -from did.utils import Config, log, pretty, ReportError +from did.base import Config, ReportError +from did.stats import Stats, StatsGroup +from did.utils import log, pretty # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/did/plugins/footer.py b/did/plugins/footer.py index cdf656b2..069e51b8 100644 --- a/did/plugins/footer.py +++ b/did/plugins/footer.py @@ -11,7 +11,7 @@ status = Status: Green | Yellow | Orange | Red """ -from did.base import EmptyStatsGroup +from did.stats import EmptyStatsGroup # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/did/plugins/gerrit.py b/did/plugins/gerrit.py index 2b7514de..d9f4926a 100644 --- a/did/plugins/gerrit.py +++ b/did/plugins/gerrit.py @@ -10,12 +10,14 @@ prefix = GR """ -from datetime import datetime import json import urllib import urlparse -from did.base import Stats, StatsGroup -from did.utils import Config, ReportError, log, pretty, TODAY +from datetime import datetime + +from did.utils import log, pretty +from did.stats import Stats, StatsGroup +from did.base import Config, ReportError, TODAY # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/did/plugins/git.py b/did/plugins/git.py index cd77ced0..aff883cf 100644 --- a/did/plugins/git.py +++ b/did/plugins/git.py @@ -18,8 +18,10 @@ import os import re import subprocess -from did.base import Stats, StatsGroup -from did.utils import Config, item, log, pretty, ReportError + +from did.base import Config, ReportError +from did.utils import item, log, pretty +from did.stats import Stats, StatsGroup # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/did/plugins/header.py b/did/plugins/header.py index 850a3072..83de3f80 100644 --- a/did/plugins/header.py +++ b/did/plugins/header.py @@ -10,7 +10,7 @@ joy = Joy of the week ;-) """ -from did.base import EmptyStatsGroup +from did.stats import EmptyStatsGroup # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/did/plugins/items.py b/did/plugins/items.py index b6939746..277b44b0 100644 --- a/did/plugins/items.py +++ b/did/plugins/items.py @@ -12,8 +12,9 @@ item3 = Project Three """ -from did.base import Stats, StatsGroup -from did.utils import Config, item +from did.utils import item +from did.base import Config +from did.stats import Stats, StatsGroup # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/did/plugins/jira.py b/did/plugins/jira.py index 374eed7f..1794b8a5 100644 --- a/did/plugins/jira.py +++ b/did/plugins/jira.py @@ -15,12 +15,13 @@ import json import urllib import urllib2 -import urllib2_kerberos -import dateutil.parser import cookielib +import dateutil.parser +import urllib2_kerberos -from did.base import Stats, StatsGroup -from did.utils import Config, log, pretty, listed, ReportError +from did.utils import log, pretty, listed +from did.base import Config, ReportError +from did.stats import Stats, StatsGroup # Default identifier width DEFAULT_WIDTH = 4 diff --git a/did/plugins/nitrate.py b/did/plugins/nitrate.py index 0a55bd9e..5071088b 100644 --- a/did/plugins/nitrate.py +++ b/did/plugins/nitrate.py @@ -10,7 +10,7 @@ from __future__ import absolute_import -from did.base import Stats, StatsGroup +from did.stats import Stats, StatsGroup from did.utils import log TEST_CASE_COPY_TAG = "TestCaseCopy" diff --git a/did/plugins/rt.py b/did/plugins/rt.py index 42ee468c..a0111887 100644 --- a/did/plugins/rt.py +++ b/did/plugins/rt.py @@ -17,8 +17,9 @@ import urlparse import kerberos -from did.base import Stats, StatsGroup -from did.utils import log, pretty, ReportError, Config +from did.utils import log, pretty +from did.base import ReportError, Config +from did.stats import Stats, StatsGroup # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/did/plugins/trac.py b/did/plugins/trac.py index ba323e1a..cbad130c 100644 --- a/did/plugins/trac.py +++ b/did/plugins/trac.py @@ -13,8 +13,9 @@ import re import xmlrpclib -from did.base import Stats, StatsGroup -from did.utils import Config, ReportError, log, pretty +from did.utils import log, pretty +from did.base import Config, ReportError +from did.stats import Stats, StatsGroup INTERESTING_RESOLUTIONS = ["canceled"] MAX_TICKETS = 1000000 diff --git a/did/plugins/wiki.py b/did/plugins/wiki.py index 453c4a04..a64b0251 100644 --- a/did/plugins/wiki.py +++ b/did/plugins/wiki.py @@ -10,8 +10,15 @@ """ import xmlrpclib -from did.base import Stats, StatsGroup -from did.utils import Config, item + +from did.utils import item +from did.base import Config +from did.stats import Stats, StatsGroup + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Wiki Stats +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~rom did.utils import item # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Wiki Stats diff --git a/did/stats.py b/did/stats.py new file mode 100644 index 00000000..ce76c5d7 --- /dev/null +++ b/did/stats.py @@ -0,0 +1,194 @@ +# coding: utf-8 + +""" Stats & StatsGroup, the core of the data gathering """ + +from __future__ import unicode_literals, absolute_import + +import optparse +import xmlrpclib + +import did.base +from did import utils +from did.utils import log + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Stats +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +class Stats(object): + """ General statistics """ + _name = None + _error = None + _enabled = None + option = None + dest = None + parent = None + stats = None + + def __init__( + self, option, name=None, parent=None, user=None, options=None): + """ Set the name, indent level and initialize data. """ + self.option = option.replace(" ", "-") + self.dest = self.option.replace("-", "_") + self._name = name + self.parent = parent + self.stats = [] + # Save user and options (get it directly or from parent) + self.options = options or getattr(self.parent, 'options', None) + if user is None and self.parent is not None: + self.user = self.parent.user + else: + self.user = user + log.debug( + 'Loading {0} Stats instance for {1}'.format(option, self.user)) + + @property + def name(self): + """ Use docs string unless name set. """ + return self._name or self.__doc__.strip() + + def add_option(self, group): + """ Add option for self to the parser group object. """ + group.add_option( + "--{0}".format(self.option), action="store_true", help=self.name) + + def enabled(self): + """ Check whether we're enabled (or if parent is). """ + # Cache into ._enabled + if self._enabled is None: + if self.parent is not None and self.parent.enabled(): + self._enabled = True + else: + # Default to Enabled if not otherwise disabled + self._enabled = getattr(self.options, self.dest, True) + log.debug("{0} Enabled? {1}".format(self.option, self._enabled)) + return self._enabled + + def fetch(self): + """ Fetch the stats (to be implemented by respective class). """ + raise NotImplementedError() + + def check(self): + """ Check the stats if enabled. """ + if not self.enabled(): + return + try: + self.fetch() + except (xmlrpclib.Fault, did.base.ConfigError) as error: + log.error(error) + self._error = True + # Raise the exception if debugging + if not self.options or self.options.debug: + raise + # Show the results stats (unless merging) + if self.options and not self.options.merge: + self.show() + + def header(self): + """ Show summary header. """ + # Show question mark instead of count when errors encountered + count = "? (error encountered)" if self._error else len(self.stats) + utils.item("{0}: {1}".format(self.name, count), options=self.options) + + def show(self): + """ Display indented statistics. """ + if not self._error and not self.stats: + return + self.header() + for stat in self.stats: + utils.item(stat, level=1, options=self.options) + + def merge(self, other): + """ Merge another stats. """ + self.stats.extend(other.stats) + if other._error: + self._error = True + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Stats Group +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +class StatsGroup(Stats): + """ Stats group """ + + # Default order + order = 500 + + def add_option(self, parser): + """ Add option group and all children options. """ + + group = optparse.OptionGroup(parser, self.name) + for stat in self.stats: + stat.add_option(group) + group.add_option( + "--{0}".format(self.option), action="store_true", help="All above") + parser.add_option_group(group) + + def check(self): + """ Check all children stats. """ + for stat in self.stats: + stat.check() + + def show(self): + """ List all children stats. """ + for stat in self.stats: + stat.show() + + def merge(self, other): + """ Merge all children stats. """ + for this, other in zip(self.stats, other.stats): + this.merge(other) + + def fetch(self): + """ Stats groups do not fetch anything """ + pass + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# User Stats +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +class UserStats(StatsGroup): + """ User statistics in one place """ + + def __init__(self, user=None, options=None): + """ Initialize stats objects. """ + super(UserStats, self).__init__( + option="all", user=user, options=options) + self.stats = [] + import did.plugins + for section, statsgroup in did.plugins.detect(): + self.stats.append(statsgroup(option=section, parent=self)) + + def add_option(self, parser): + """ Add options for each stats group. """ + for stat in self.stats: + stat.add_option(parser) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Header & Footer +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +class EmptyStats(Stats): + """ Custom stats group for header & footer """ + def __init__(self, option, name=None, parent=None): + Stats.__init__(self, option, name, parent) + + def show(self): + """ Name only for empty stats """ + utils.item(self.name, options=self.options) + + def fetch(self): + """ Nothing to do for empty stats """ + pass + + +class EmptyStatsGroup(StatsGroup): + """ Header & Footer stats group """ + def __init__(self, option, name=None, parent=None): + StatsGroup.__init__(self, option, name, parent=parent) + for opt, name in sorted(did.base.Config().section(option)): + self.stats.append(EmptyStats(opt, name, parent=self)) diff --git a/did/utils.py b/did/utils.py index f4c14f52..17225cca 100644 --- a/did/utils.py +++ b/did/utils.py @@ -7,27 +7,15 @@ import os import re import sys -import codecs import logging -import datetime -import StringIO import unicodedata -import ConfigParser from pprint import pformat as pretty -from dateutil.relativedelta import MO as MONDAY -from dateutil.relativedelta import relativedelta as delta # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Constants # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Config file location -CONFIG = os.path.expanduser("~/.did") - -# Default maximum width -MAX_WIDTH = 79 - # Coloring COLOR_ON = 1 COLOR_OFF = 0 @@ -46,9 +34,6 @@ # See: http://stackoverflow.com/questions/14010875 EMAIL_REGEXP = re.compile(r'(?:"?([^"]*)"?\s)?(?:]+)>?)') -# Date -TODAY = datetime.date.today() - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Utils @@ -294,107 +279,7 @@ def get(self): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Config -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -class Config(object): - """ User config file """ - - parser = None - - def __init__(self, config=None, path=None): - """ - Read the config file - - Parse config from given string (config) or file (path). - If no config or path given, default to "~/.did/config" which - can be overrided by the DID_CONFIG environment variable. - """ - # Read the config only once (unless explicitly provided) - if self.parser is not None and config is None and path is None: - return - Config.parser = ConfigParser.SafeConfigParser() - # If config provided as string, parse it directly - if config is not None: - log.info("Inspecting config file from string") - log.debug(pretty(config)) - self.parser.readfp(StringIO.StringIO(config)) - return - # Check the environment for config file override - # (unless path is explicitly provided) - if path is None: - try: - directory = os.environ["DID_CONFIG"] - except KeyError: - directory = CONFIG - path = directory.rstrip("/") + "/config" - # Parse the config from file - try: - log.info("Inspecting config file '{0}'".format(path)) - self.parser.readfp(codecs.open(path, "r", "utf8")) - except IOError as error: - log.error(error) - raise ConfigError("Unable to read the config file") - - @property - def email(self): - """ User email(s) """ - try: - return self.parser.get("general", "email") - except ConfigParser.NoOptionError: - return [] - - @property - def width(self): - """ Maximum width of the report """ - try: - return int(self.parser.get("general", "width")) - except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): - return MAX_WIDTH - - def sections(self, kind=None): - """ Return all sections (optionally of given kind only) """ - result = [] - for section in self.parser.sections(): - # Selected kind only if provided - if kind is not None: - try: - section_type = self.parser.get(section, "type") - if section_type != kind: - continue - except ConfigParser.NoOptionError: - # Implicit header/footer type for backward compatibility - if (section == kind == "header" or - section == kind == "footer"): - pass - else: - continue - result.append(section) - return result - - def section(self, section, skip=None): - """ Return section items, skip selected (type/order by default) """ - if skip is None: - skip = ['type', 'order'] - return [(key, val) for key, val in self.parser.items(section) - if key not in skip] - - def item(self, section, it): - """ Return content of given item in selected section """ - for key, value in self.section(section, skip=['type']): - if key == it: - return value - raise ConfigError( - "Item '{0}' not found in section '{1}'".format(it, section)) - - @staticmethod - def example(): - """ Return config example """ - return "[general]\nemail = Name Surname \n" - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Color +# Coloring # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def color(text, color=None, background=None, light=False, enabled=True): @@ -490,137 +375,6 @@ def enabled(self): return self._mode == COLOR_ON -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Exceptions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -class ConfigError(Exception): - """ General problem with configuration file """ - pass - - -class ReportError(Exception): - """ General problem with report generation """ - pass - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Date -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -class Date(object): - """ Date parsing for common word formats """ - - def __init__(self, date=None): - """ Parse the date string """ - if isinstance(date, datetime.date): - self.date = date - elif date is None or date.lower() == "today": - self.date = TODAY - elif date.lower() == "yesterday": - self.date = TODAY - delta(days=1) - else: - self.date = datetime.date(*[int(i) for i in date.split("-")]) - self.datetime = datetime.datetime( - self.date.year, self.date.month, self.date.day, 0, 0, 0) - - def __str__(self): - """ Ascii version of the string representation """ - return ascii(unicode(self)) - - def __unicode__(self): - """ String format for printing """ - return unicode(self.date) - - @staticmethod - def this_week(): - """ Return start and end date of the current week. """ - since = TODAY + delta(weekday=MONDAY(-1)) - until = since + delta(weeks=1) - return Date(since), Date(until) - - @staticmethod - def last_week(): - """ Return start and end date of the last week. """ - since = TODAY + delta(weekday=MONDAY(-2)) - until = since + delta(weeks=1) - return Date(since), Date(until) - - @staticmethod - def this_month(): - """ Return start and end date of this month. """ - since = TODAY + delta(day=1) - until = since + delta(months=1) - return Date(since), Date(until) - - @staticmethod - def last_month(): - """ Return start and end date of this month. """ - since = TODAY + delta(day=1, months=-1) - until = since + delta(months=1) - return Date(since), Date(until) - - @staticmethod - def this_quarter(): - """ Return start and end date of this quarter. """ - since = TODAY + delta(day=1) - while since.month % 3 != 0: - since -= delta(months=1) - until = since + delta(months=3) - return Date(since), Date(until) - - @staticmethod - def last_quarter(): - """ Return start and end date of this quarter. """ - since, until = Date.this_quarter() - since = since.date - delta(months=3) - until = until.date - delta(months=3) - return Date(since), Date(until) - - @staticmethod - def this_year(): - """ Return start and end date of this fiscal year """ - since = TODAY - while since.month != 3 or since.day != 1: - since -= delta(days=1) - until = since + delta(years=1) - return Date(since), Date(until) - - @staticmethod - def last_year(): - """ Return start and end date of the last fiscal year """ - since, until = Date.this_year() - since = since.date - delta(years=1) - until = until.date - delta(years=1) - return Date(since), Date(until) - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# User -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -class User(object): - """ User info """ - - def __init__(self, email, name=None, login=None): - """ Set user email, name and login values. """ - if not email: - raise ReportError("Email required for user initialization.") - else: - # Extract everything from the email string provided - # eg, "My Name" - parts = EMAIL_REGEXP.search(email) - if parts is None: - raise ConfigError("Invalid email address '{0}'".format(email)) - self.email = parts.groups()[1] - self.login = login or self.email.split('@')[0] - self.name = name or parts.groups()[0] or u"Unknown" - - def __unicode__(self): - """ Use name & email for string representation. """ - return u"{0} <{1}>".format(self.name, self.email) - - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Default Logger # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/modules.rst b/docs/modules.rst index fdc90b77..49b8873d 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -3,9 +3,18 @@ Modules =============== -Except for plugins there are two basic modules, `base`_ and -`utils`_, which handle essential part of the functionality. -The command line script `did`_ implements option handling. +The `stats`_ module contains the core of the stats gathering +functionality. Some basic functionality like exceptions, config, +user and date handling is placed in the `base`_ module. Generic +utilities can be found in the `utils`_ module. Option parsing +and other command line stuff resides in the `cli`_ module. + +stats +----- + +.. automodule:: did.stats + :members: + :undoc-members: base ---- diff --git a/tests/plugins/test_bugzilla.py b/tests/plugins/test_bugzilla.py index b37821c4..b1fcaf67 100644 --- a/tests/plugins/test_bugzilla.py +++ b/tests/plugins/test_bugzilla.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals, absolute_import import did.cli -import did.utils +import did.base CONFIG = """ @@ -21,7 +21,7 @@ def test_bugzilla_linus(): """ Check bugs filed by Linus :-) """ - did.utils.Config(CONFIG) + did.base.Config(CONFIG) did.cli.main( "--email torvalds@linux-foundation.org " "--bz-filed --until today".split()) @@ -33,5 +33,5 @@ def test_bugzilla_linus(): def test_bugzilla_week(): """ Check all stats for given week""" - did.utils.Config(CONFIG) + did.base.Config(CONFIG) did.cli.main("--bz --email psplicha@redhat.com".split()) diff --git a/tests/plugins/test_git.py b/tests/plugins/test_git.py index a8755123..afbdefac 100644 --- a/tests/plugins/test_git.py +++ b/tests/plugins/test_git.py @@ -32,7 +32,7 @@ def test_git_regular(): """ Simple git stats """ - did.utils.Config(CONFIG.format(GIT_PATH)) + did.base.Config(CONFIG.format(GIT_PATH)) stats = did.cli.main(INTERVAL)[0][0].stats[0].stats[0].stats assert any([ "8a725af - Simplify git plugin tests" in stat @@ -40,19 +40,19 @@ def test_git_regular(): def test_git_verbose(): """ Verbose git stats """ - did.utils.Config(CONFIG.format(GIT_PATH)) + did.base.Config(CONFIG.format(GIT_PATH)) stats = did.cli.main(INTERVAL + " --verbose")[0][0].stats[0].stats[0].stats assert any(["tests/plugins" in stat for stat in stats]) def test_git_nothing(): """ No stats found """ - did.utils.Config(CONFIG.format(GIT_PATH)) + did.base.Config(CONFIG.format(GIT_PATH)) stats = did.cli.main("--until 2015-01-01")[0][0].stats[0].stats[0].stats assert stats == [] def test_git_invalid(): """ Invalid git repo """ - did.utils.Config(CONFIG.format("/tmp")) + did.base.Config(CONFIG.format("/tmp")) try: did.cli.main(INTERVAL) except SystemExit: @@ -62,7 +62,7 @@ def test_git_invalid(): def test_git_non_existent(): """ Non-existent git repo """ - did.utils.Config(CONFIG.format("i-do-not-exist")) + did.base.Config(CONFIG.format("i-do-not-exist")) try: did.cli.main(INTERVAL) except SystemExit: diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 00000000..6912f491 --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,64 @@ +# coding: utf-8 + +from __future__ import unicode_literals, absolute_import + + +def test_base_import(): + # simple test that import works + from did import base + assert base + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Config +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def test_Config(): + from did.base import Config + assert Config + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Date +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def test_Date(): + from did.base import Date + assert Date + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# User +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def test_User(): + from did.base import User + assert User + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Exceptions +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def test_ConfigError(): + ''' Confirm ConfigError exception is defined ''' + from did.base import ConfigError + + try: + raise ConfigError + except ConfigError: + pass + else: + raise RuntimeError("ConfigError exception failing!") + + +def test_ReportError(): + ''' Confirm ReportError exception is defined ''' + from did.base import ReportError + + try: + raise ReportError + except ReportError: + pass + else: + raise RuntimeError("ReportError exception failing!") diff --git a/tests/test_did.py b/tests/test_did.py index ebafb621..8cb5556c 100644 --- a/tests/test_did.py +++ b/tests/test_did.py @@ -10,7 +10,7 @@ # Prepare path and config examples PATH = os.path.dirname(os.path.realpath(__file__)) -MINIMAL = did.utils.Config.example() +MINIMAL = did.base.Config.example() EXAMPLE = "".join(open(PATH + "/../examples/config").readlines()) # Substitute example git paths for real life directories EXAMPLE = re.sub(r"\S+/git/[a-z]+", PATH, EXAMPLE) @@ -21,7 +21,7 @@ def test_help_minimal(): """ Help message with minimal config """ - did.utils.Config(config=MINIMAL) + did.base.Config(config=MINIMAL) try: did.cli.main(["--help"]) except SystemExit: @@ -33,7 +33,7 @@ def test_help_minimal(): def test_help_example(): """ Help message with example config """ - did.utils.Config(config=EXAMPLE) + did.base.Config(config=EXAMPLE) try: did.cli.main(["--help"]) except SystemExit: diff --git a/tests/test_stats.py b/tests/test_stats.py index 59cba520..0ebf2529 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -7,29 +7,29 @@ def test_Stats(): # simple test that import works - from did.base import Stats + from did.stats import Stats assert Stats def test_StatsGroup(): # simple test that import works - from did.base import StatsGroup + from did.stats import StatsGroup assert StatsGroup def test_UserStats(): # simple test that import works - from did.base import UserStats + from did.stats import UserStats assert UserStats def test_EmptyStats(): # simple test that import works - from did.base import EmptyStats + from did.stats import EmptyStats assert EmptyStats def test_EmptyStatsGroup(): # simple test that import works - from did.base import EmptyStatsGroup + from did.stats import EmptyStatsGroup assert EmptyStatsGroup diff --git a/tests/test_utils.py b/tests/test_utils.py index c9b6f4e2..30a1dd25 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,7 +12,7 @@ def test_utils_import(): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# CONSTS +# Constants # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def test_email_re(): @@ -104,16 +104,7 @@ def test_Logging(): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Config -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -def test_Config(): - from did.utils import Config - assert Config - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Color +# Coloring # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def test_Coloring(): @@ -124,49 +115,3 @@ def test_Coloring(): def test_color(): from did.utils import color assert color - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Date -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -def test_Date(): - from did.utils import Date - assert Date - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# User -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -def test_User(): - from did.utils import User - assert User - - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Exceptions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -def test_ConfigError(): - ''' Confirm ConfigError exception is defined ''' - from did.utils import ConfigError - - try: - raise ConfigError - except ConfigError: - pass - else: - raise RuntimeError("ConfigError exception failing!") - - -def test_ReportError(): - ''' Confirm ReportError exception is defined ''' - from did.utils import ReportError - - try: - raise ReportError - except ReportError: - pass - else: - raise RuntimeError("ReportError exception failing!")