ewa.rules

Package ewa :: Module rules |
[hide private]
[frames] | no frames]

# Source Code for Module ewa.rules

      1  """
      2
      3  a rule system for ewa, used to determine what files should appear
      4  before or after a main mp3 file in a composite mp3.
      5
      6  Rules are callables that take a single "filename" parameter and return
      7  None or a generator that yields mp3 filenames (or equivalent
      8  designations) in sequence.
      9
     10  A RuleList is a rule with a list of subrules, optionally with a
     11  condition (matched against the filename).  When the RuleList is
     12  called, if the condition does not exist, or if it matches, each
     13  subrule is called on the filename until one returns something, which
     14  is the return value.
     15
     16  Rules can be marshalled to and from JSON, and from (but currently not
     17  to) the ewa rule configuration format implemented in ewa.ruleparser.
     18
     19  """
     20
     21
     22  import datetime
     23  import fnmatch
     24  import os
     25  import re
     26  from string import Template
     27  import time
     28
     29  try:
     30      import simplejson as json
     31  except ImportError:
     32      import json
     33
     34  from ewa.logutil import warn
     35
     36 -class _template(Template):
     37      idpattern='[_a-z0-9]+'
     38
     39 -class OriginalName(str):
     40      is_original=True
     41
     42 -def to_jsondata(obj):
     43      if isinstance(obj, datetime.date):
     44          return dict(year=obj.year, month=obj.month, day=obj.day)
     45      elif isinstance(obj, datetime.datetime):
     46          # tzinfo not supported
     47          return dict(year=obj.year,
     48                      month=obj.month,
     49                      day=obj.day,
     50                      hour=obj.hour,
     51                      minute=obj.minute,
     52                      second=obj.second,
     53                      microsecond=obj.microsecond)
     54
     55      elif isinstance(obj, list):
     56          return [to_jsondata(x) for x in obj]
     57      elif isinstance(obj, tuple):
     58          return tuple(to_jsondata(x) for x in obj)
     59      try:
     60          return obj.to_jsondata()
     61      except AttributeError:
     62          return obj
     63
     64 -class _jsonable(object):
     65 -    def to_jsondata(self):
     66          return dict((k, to_jsondata(v)) for k,v in self.__dict__.iteritems())
     67
     68 -class RuleList(_jsonable):
     69
     70 -    def __init__(self, rules, cond=None):
     71          self.rules=rules
     72          self.cond=cond
     73
     74 -    def __call__(self, filename):
     75          if self.cond:
     76              m=self.cond.match(filename)
     77              if not m:
     78                  return
     79          for r in self.rules:
     80              res=r(filename)
     81              if res:
     82                  return res
     83
     84
     85 -class DefaultRule(_jsonable):
     86      """
     87      this may be useful as the last rule in a rule-list;
     88      it yields the filename passed and nothing else
     89      """
     90 -    def __call__(self, filename):
     91          yield OriginalName(filename)
     92
     93 -class MatchRule(_jsonable):
     94 -    def __init__(self, matcher, pre=None, post=None):
     95          """
     96          the matcher is a callable with a "match" method.  pre and post
     97          and lists of things that go before and after the filename
     98          passed in.
     99          """
    100          self.matcher=matcher
    101          self.pre=pre or []
    102          self.post=post or []
    103
    104 -    def _gen_list(self, filename, match):
    105          if hasattr(match, 'groupdict'):
    106              # is a regex match
    107              d=dict((str(i+1), v) for i, v in enumerate(match.groups()))
    108              d.update(match.groupdict())
    109              expand=lambda s: match.expand(_template(f).safe_substitute(d))
    110          else:
    111              expand=lambda s: s
    112
    113          for f in self.pre:
    114              yield expand(f)
    115
    116          yield OriginalName(filename)
    117
    118          for f in self.post:
    119              yield expand(f)
    120
    121
    122 -    def _match(self, filename):
    123          if self.matcher is None:
    124              return True
    125          return self.matcher.match(filename)
    126
    127 -    def __call__(self, filename):
    128          m=self._match(filename)
    129          if m:
    130              return self._gen_list(filename, m)
    131
    132 -class And(_jsonable):
    133 -    def __init__(self, *submatchers):
    134        self.submatchers=submatchers
    135
    136 -    def match(self, target):
    137          res=False
    138          for m in self.submatchers:
    139              res=m.match(target)
    140              if not res:
    141                  return False
    142          return res
    143
    144 -class Or(_jsonable):
    145 -    def __init__(self, *submatchers):
    146          self.submatchers=submatchers
    147
    148 -    def match(self, target):
    149          for m in self.submatchers:
    150              res=m.match(target)
    151              if res:
    152                  return res
    153          return False
    154
    155 -class Not(_jsonable):
    156 -    def __init__(self, matcher):
    157          self.matcher=matcher
    158
    159 -    def match(self, target):
    160          return not self.matcher.match(target)
    161
    162 -class RegexMatcher(_jsonable):
    163 -    def __init__(self, regex, flags=0):
    164          self.regex=regex
    165          self.flags=flags
    166
    167 -    def match(self, target):
    168          return re.match(self.regex, target, self.flags)
    169
    170 -class GlobMatcher(_jsonable):
    171 -    def __init__(self, pattern, casesensitive=True):
    172          self.pattern=pattern
    173          self.casesensitive=casesensitive
    174
    175 -    def match(self, target):
    176          if self.casesensitive:
    177              return fnmatch.fnmatchcase(target, self.pattern)
    178          else:
    179              return fnmatch.fnmatch(target, self.pattern)
    180
    181 -def extract_datetime(target, regex=r'\d{6}', format='%m%d%y'):
    182      m=re.search(regex, target)
    183      if m:
    184          try:
    185              ttuple=time.strptime(m.group(), format)
    186          except ValueError:
    187              warn("error in time format: %s from %s", m.group(), target)
    188          else:
    189              return datetime.datetime(*ttuple[:6])
    190      return None
    191
    192 -class CurrentTimeMatch(_jsonable):
    193      """
    194      returns true if the current time falls within a
    195      datetime range
    196      """
    197
    198 -    def __init__(self,
    199                   start=datetime.datetime.min,
    200                   end=datetime.datetime.max):
    201          self.start=start
    202          self.end=end
    203
    204 -    def match(self, target):
    205          return self.start <= datetime.datetime.now() <= self.end
    206
    207
    208
    209 -class FileTimeMatch(_jsonable):
    210      """
    211      returns true if a date encoded in a string falls within
    212      a date range
    213      """
    214 -    def __init__(self,
    215                   start=datetime.datetime.min,
    216                   end=datetime.datetime.max,
    217                   dateregex=r'\d{6}',
    218                   dateformat='%m%d%y'):
    219          self.start=start
    220          self.end=end
    221          self.dateregex=dateregex
    222          self.dateformat=dateformat
    223
    224 -    def match(self, target):
    225          date=extract_datetime(target, self.dateregex, self.dateformat)
    226          if not date:
    227              return False
    228          return self.start <= date <= self.end
    229
    230 -def RegexRule(pattern, pre=None, post=None, flags=0):
    231      matcher=RegexMatcher(pattern, flags)
    232      return MatchRule(matcher, pre, post)
    233
    234 -def GlobMatchRule(pattern, pre=None, post=None, casesensitive=True):
    235      matcher=GlobMatcher(pattern, casesensitive)
    236      return MatchRule(matcher, pre, post)
    237
    238  _json_registry=dict((x.__name__, x) for x in (datetime.date,
    239                                                datetime.datetime,
    240                                                RuleList,
    241                                                DefaultRule,
    242                                                MatchRule,
    243                                                And,
    244                                                Or,
    245                                                Not,
    246                                                RegexMatcher,
    247                                                GlobMatcher,
    248                                                CurrentTimeMatch,
    249                                                FileTimeMatch))
    250
    251 -def from_jsondata(data):
    252      if isinstance(data, dict) and len(data)==1:
    253          key=data.keys()[0]
    254          if key in _json_registry:
    255              obj=_json_registry[key]
    256              kwargs=dict((str(k),from_jsondata(v)) for k,v in data[key].iteritems())
    257              return obj(**kwargs)
    258
    259      if isinstance(data, list):
    260          return [from_jsondata(x) for x in data]
    261      return data
    262
    263
    264 -def from_json(json):
    265      data=json.loads(json)
    266      return from_jsondata(data)
    267
    268 -def to_json(data):
    269      jd=to_jsondata(data)
    270      return json.dumps(jd)
    271
    272  # to avoid a circular dependency, this is a stand-in for
    273  # ewa.ruleparser.parse_file for the first run, and is the real thing
    274  # thereafter
    275 -def _parse_ewaconf(filename):
    276      global _parse_ewaconf
    277      from ewa.ruleparser import parse_file
    278      # replace ourself after the first run
    279      parse_ewaconf=parse_file
    280      return parse_file(filename)
    281
    282 -class FileRule(object):
    283
    284 -    def __init__(self, rulefile, refresh=15, format=None):
    285          self.rulefile=rulefile
    286          self._refresh=refresh
    287          if format is None:
    288              if rulefile.endswith('.json') or rulefile.endswith('.js'):
    289                  format='json'
    290              elif rulefile.endswith('.py'):
    291                  format='python'
    292              else:
    293                  format='ewaconf'
    294          self.format=format
    295          self._load_rule()
    296
    297      @staticmethod
    298 -    def _rules_from_python(pyfile):
    299          filename=os.path.abspath(pyfile)
    300          s=open(filename).read()
    301          codeobj=compile(s, filename, 'exec')
    302          env={}
    303          exec codeobj in {}, env
    304          # let a KeyError propagate
    305          return env['rules']
    306
    307 -    def _load_rule(self, mtime=None, lastchecked=None):
    308          if mtime is None:
    309              mtime=os.path.getmtime(self.rulefile)
    310          if lastchecked is None:
    311              lastchecked=time.time()
    312          self._modified=mtime
    313          if self.format=='json':
    314              self._rule=from_json(open(self.rulefile).read())
    315          elif self.format=='python':
    316              self._rule=self._rules_from_python(self.rulefile)
    317          elif self.format=='ewaconf':
    318              self._rule=_parse_ewaconf(self.rulefile)
    319          else:
    320              raise ValueError, "unrecognized format: %s" % self.format
    321          self._lastchecked=lastchecked
    322
    323 -    def _check(self):
    324          t=time.time()
    325          if (t-self._lastchecked) > self._refresh:
    326              m=os.path.getmtime(self.rulefile)
    327              if m > self._modified:
    328                  self._load_rule(m,t)
    329
    330 -    def __call__(self, filename):
    331          self._check()
    332          return self._rule(filename)
    333
    334  __all__=[
    335      'RuleList',
    336      'DefaultRule',
    337      'MatchRule',
    338      'And',
    339      'Or',
    340      'Not',
    341      'RegexMatcher',
    342      'GlobMatcher',
    343      'extract_datetime',
    344      'CurrentTimeMatch',
    345      'FileTimeMatch',
    346      'RegexRule',
    347      'GlobMatchRule',
    348      'from_json',
    349      'to_json',
    350      'FileRule'
    351      ]
    352