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