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