-
Notifications
You must be signed in to change notification settings - Fork 0
/
processfeed.py
executable file
·298 lines (239 loc) · 8.18 KB
/
processfeed.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#!/usr/bin/env python
#-*- coding: UTF-8 -*-
"""
Very sexy script to process feed using simple rules
"""
from ConfigParser import SafeConfigParser
from optparse import OptionParser
import feedparser
import logging
import os
import re
import sys
try:
from collections import OrderedDict
except ImportError:
from odict import odict as OrderedDict
APP_NAME = "processfeed"
LOG_FILE = os.path.expanduser("~/.%s.log" % APP_NAME)
CONFIG_FILES = [os.path.expanduser("~/.%s" % APP_NAME),
os.path.expanduser("~/%s.ini" % APP_NAME)]
VERBOSE = 20
def write(text, destination):
"""
Short cut to append text to destination and flush that
"""
DEBUG("""write::writing "%s" to %s""" % (text, destination))
fileo = open(destination, "a")
fileo.write("%s\n" % text.encode("UTF-8", "replace"))
fileo.close()
def get_depth():
"""
Returns the current recursion level. Nice to look and debug
"""
def exist_frame(number):
"""
True if frame number exists
"""
try:
if sys._getframe(number):
return True
except ValueError:
return False
maxn = 1
minn = 0
while exist_frame(maxn):
minn = maxn
maxn *= 2
middle = (minn + maxn) / 2
while minn < middle:
if exist_frame(middle):
minn = middle
else:
maxn = middle
middle = (minn + maxn) / 2
return max(minn - 4, 0)
def ident(func, identation=" "):
"""
Decorates func to add identation prior arg[0]
"""
def decorated(message, *args, **kwargs):
newmessage = "%s%s" % (identation * (get_depth() - 1), message)
return func(newmessage, *args, **kwargs)
return decorated
def read(filename):
"""
Short cut to read a file, split the comments and return lines
"""
DEBUG("""read::reading "%s""" % filename)
try:
fileo = open(filename)
lines = [unicode(line.split(" #")[0].strip(), "latin-1")
for line in fileo.readlines()]
except IOError:
WARNING("%s does not exist" % filename)
lines = []
DEBUG("""read::readed %d lines""" % len(lines))
return lines
def get_entry_id(action, entry):
"""
Returns a unique action/entry name for consistency
"""
for key in ("id", "link"):
if key in entry:
idstring = entry[key]
break
else:
ERROR("Entry cannot be identified, please report this bug."
"Dump:\n%s" % entry.keys())
entry_id = "%s::%s" % (action["_name"], idstring)
DEBUG("get_entry_id::return %s" % entry_id)
return entry_id
def get_entries(feed):
"""
Fetch and parse the feed and returns that in cronological order.
"""
feed = feedparser.parse(feed)
if "bozo_exception" in feed:
WARNING("Bozo exception: %s %s"
% ("feed", feed["bozo_exception"]))
return []
else:
entries = feed["entries"][::-1]
MOREINFO("Readed %d entries" % len(entries))
return entries
def process_entry(action, entry):
"""
Executes the action rules with entry
"""
DEBUG("Processfeed::process_entry::%s" % entry["_id"])
safe_globals = {}
safe_globals["__builtins__"] = globals()["__builtins__"]
safe_globals["re"] = re
safe_locals = {}
safe_locals["entry"] = entry
result = None
del(action["feed"]) #FIXME: Fetching must be explicit
for name, expression in action.iteritems():
DEBUG("Processfeed::process_entry::%s = %s" % (name, expression))
if name.startswith("_"):
safe_locals[name] = expression
else:
result = eval(expression, safe_globals, safe_locals)
safe_locals[name] = result
MOREINFO("%s = %s" % (name, safe_locals[name]))
if name.startswith("assert"):
if not safe_locals[name]:
INFO("Ignored because assert is false.")
break
write("%s #(%s) %s" % (entry["_id"], result, entry["title"]), LOG_FILE)
return result
class Processfeed(object):
"""
The statefull main class
"""
def __init__(self, config):
"""
A rules set manager
config: a ConfigParse/SafeConfigParser instance
"""
self._config = config
sections = config.sections()
self.actions = OrderedDict()
for section in sections:
if section.startswith("ACTION"):
self.actions[section] = OrderedDict()
self.actions[section]["_name"] = section[7:]
for key, value in config.items(section, True):
self.actions[section][key] = value
else:
MOREINFO("""Section "%s" is ignored""" % section)
self.history = read(LOG_FILE)
def get_news(self, action, entries=None):
"""
Filter out the entries already processed. If not entries will fetch
these.
"""
if not entries:
DEBUG("Processfeed::get_news::fetching entries")
entries = get_entries(action["feed"])
new_entries = []
for entry in entries:
entry["_id"] = get_entry_id(action, entry)
if entry["_id"] in self.history:
DEBUG("Processfeed::get_news::%s in history" % entry["_id"])
else:
MOREINFO("%s is a novelty" % entry["_id"])
new_entries.append(entry)
if not new_entries:
INFO("Without novelties")
return new_entries
def process_action(self, action):
"""
Run the rules of action with the oldest novelty
"""
INFO(action["_name"])
new_entries = self.get_news(action)
if new_entries:
return process_entry(action, new_entries[0])
def process_all_actions(self):
"""
Run .process_action with each and every one of actions
"""
for action in self.actions.itervalues():
self.process_action(action)
def get_options():
"""
Parse the arguments
"""
# Instance the parser and define the usage message
optparser = OptionParser(usage="""
%prog [-vqdsc]""", version="%prog .2")
# Define the options and the actions of each one
optparser.add_option("-s", "--section", help=("Process only the given "
"section"), action="store", dest="section")
optparser.add_option("-c", "--config", help=("Uses the given conf file "
"inteast of the default"), action="store", dest="conffile")
optparser.add_option("-l", "--log", help=("Uses the given log file "
"inteast of the default"), action="store", dest="logfile")
optparser.add_option("-v", "--verbose", action="count", dest="verbose",
help="Increment verbosity")
optparser.add_option("-q", "--quiet", action="count", dest="quiet",
help="Decrement verbosity")
# Define the default options
optparser.set_defaults(verbose=0, quiet=0, logfile=LOG_FILE,
conffile="")
# Process the options
options, args = optparser.parse_args()
return options, args
def get_config(conf_file=None):
"""
Read config files
"""
config = SafeConfigParser(None, OrderedDict)
read_from = conf_file or CONFIG_FILES
files = config.read(read_from)
DEBUG("get_config::readed %s" % files)
return config
def main(options, args):
"""The main routine"""
# Read the config values from the config files
config = get_config(options.conffile)
processfeed = Processfeed(config)
processfeed.process_all_actions()
if __name__ == "__main__":
# == Reading the options of the execution ==
options, args = get_options()
VERBOSE = (options.quiet - options.verbose) * 10 + 30
format_str = "%(message)s"
logging.basicConfig(format=format_str, level=VERBOSE)
logger = logging.getLogger()
DEBUG = ident(logger.debug) # For developers
MOREINFO = ident(logger.info) # Plus info
INFO = ident(logger.warning) # Default
WARNING = ident(logger.error) # Non critical errors
ERROR = ident(logger.critical) # Critical (will break)
DEBUG("get_options::options: %s" % options)
DEBUG("get_options::args: %s" % args)
DEBUG("Verbose level: %s" % VERBOSE)
exit(main(options, args))