d082adaad369c43d955202aa426a34bc79c46443
1 """Base class for a front page source, as well as a handful of specific
5 from collections
import namedtuple
8 from subprocess
import PIPE
13 from spline
.lib
import helpers
15 def max_age_to_datetime(max_age
):
16 """``max_age`` is specified in config as a number of seconds old. This
17 function takes that number and returns a corresponding datetime object.
22 seconds
= int(max_age
)
27 """Represents a source to be polled for updates. Sources are populated
28 directly from the configuration file.
33 A name to identify this specific source.
36 Name of a Fugue icon to show next to the name.
39 A URL where the full history of this source can be found.
42 The maximum number of items from this source to show at a time.
46 Items older than this age (in seconds) will be excluded. Optional.
48 Additionally, subclasses **must** define a ``template`` property -- a path
49 to a Mako template that knows how to render an update from this source.
50 The template will be passed one parameter: the update object, ``update``.
53 def __init__(self
, title
, icon
, link
, limit
=None, max_age
=None):
57 self
.limit
= int(limit
)
58 self
.max_age
= max_age_to_datetime(max_age
)
60 def poll(self
, global_limit
, global_max_age
):
61 """Public wrapper that takes care of reconciling global and source item
64 Subclasses should implement ``_poll``, below.
67 limit
= min(self
.limit
, global_limit
)
69 # Latest max age wins. Note that either could be None, but that's
70 # fine, because None is less than everything else
71 max_age
= max(self
.max_age
, global_max_age
)
73 return self
._poll(limit
, max_age
)
75 def _poll(self
, limit
, max_age
):
76 """Implementation of polling for updates. Must return an iterable.
77 Each element should be an object with ``source`` and ``time``
78 properties. A namedtuple works well.
80 raise NotImplementedError
83 FrontPageRSS
= namedtuple('FrontPageRSS', ['source', 'time', 'entry', 'content'])
84 class FeedSource(Source
):
85 """Represents an RSS or Atom feed.
93 template
= '/front_page/rss.mako'
97 def __init__(self
, feed_url
, **kwargs
):
98 kwargs
.setdefault('title', None)
99 super(FeedSource
, self
).__init__(**kwargs
)
101 self
.feed_url
= feed_url
103 def _poll(self
, limit
, max_age
):
104 feed
= feedparser
.parse(self
.feed_url
)
107 self
.title
= feed
.feed
.title
110 for entry
in feed
.entries
[:limit
]:
111 # Grab a date -- Atom has published, RSS usually just has updated.
112 # Both come out as time tuples, which datetime.datetime() can read
114 timestamp_tuple
= entry
.published_parsed
115 except AttributeError:
116 timestamp_tuple
= entry
.updated_parsed
117 timestamp
= datetime
.datetime(*timestamp_tuple
[:6])
119 if max_age
and timestamp
< max_age
:
120 # Entries should be oldest-first, so we can bail after the first
124 # Try to find something to show! Default to the summary, if there is
125 # one, or try to generate one otherwise
127 if 'summary' in entry
:
128 # If there be a summary, cheerfully trust that it's actually a
130 content
= entry
.summary
131 elif 'content' in entry
:
132 # Full content is way too much, especially for my giant blog posts.
133 # Cut this down to some arbitrary number of characters, then feed
134 # it to lxml.html to fix tag nesting
135 broken_html
= entry
.content
[0].value
[:self
.SUMMARY_LENGTH
]
136 fragment
= lxml
.html
.fromstring(broken_html
)
138 # Insert an ellipsis at the end of the last node with text
139 last_text_node
= None
140 last_tail_node
= None
141 # Need to find the last node with a tail, OR the last node with
143 for node
in fragment
.iter():
145 last_tail_node
= node
146 last_text_node
= None
148 last_text_node
= node
149 last_tail_node
= None
151 if last_text_node
is not None:
152 last_text_node
.text
+= '...'
153 if last_tail_node
is not None:
154 last_tail_node
.tail
+= '...'
157 content
= lxml
.html
.tostring(fragment
)
159 content
= helpers
.literal(content
)
161 update
= FrontPageRSS(
167 updates
.append(update
)
172 FrontPageGit
= namedtuple('FrontPageGit', ['source', 'time', 'log', 'tag'])
173 FrontPageGitCommit
= namedtuple('FrontPageGitCommit',
174 ['hash', 'author', 'time', 'subject', 'repo'])
176 class GitSource(Source
):
177 """Represents a git repository.
179 The main repository is checked for annotated tags, and an update is
180 considered to be the list of commits between them. If any other
181 repositories are listed and have the same tags, their commits will be
187 Space-separated list of repositories. These must be repository PATHS,
188 not arbitrary git URLs. Only the first one will be checked for the
192 A list of names for the repositories, in parallel with ``repo_paths``.
193 Used for constructing gitweb URLs and identifying the repositories.
196 Base URL to a gitweb installation, so commit ids can be linked to the
200 Optional. A shell glob pattern used to filter the tags.
203 template
= '/front_page/git.mako'
205 def __init__(self
, repo_paths
, repo_names
, gitweb
, tag_pattern
=None, **kwargs
):
206 kwargs
.setdefault('title', None)
207 super(GitSource
, self
).__init__(**kwargs
)
209 # Repo stuff can be space-delimited lists
210 self
.repo_paths
= repo_paths
.split()
211 self
.repo_names
= repo_names
.split()
214 self
.tag_pattern
= tag_pattern
216 def _poll(self
, limit
, max_age
):
217 # Fetch the main repo's git tags
218 git_dir
= '--git-dir=' + self
.repo_paths
[0]
225 args
.append(self
.tag_pattern
)
227 git_output
, _
= subprocess
.Popen(args
, stdout
=PIPE
).communicate()
228 tags
= git_output
.strip().split('\n')
230 # Tags come out in alphabetical order, which means earliest first. Reverse
231 # it to make the slicing easier
233 # Only history from tag to tag is actually interesting, so get the most
234 # recent $limit tags but skip the earliest
235 interesting_tags
= tags
[:-1][:limit
]
238 for tag
, since_tag
in zip(interesting_tags
, tags
[1:]):
239 # Get the date when this tag was actually created.
240 # 'raw' format gives unixtime followed by timezone offset
245 '--format=%(taggerdate:raw)',
248 tag_timestamp
, _
= subprocess
.Popen(args
, stdout
=PIPE
).communicate()
249 tag_unixtime
, tag_timezone
= tag_timestamp
.split(None, 1)
250 tagged_timestamp
= datetime
.datetime
.fromtimestamp(int(tag_unixtime
))
252 if max_age
and tagged_timestamp
< max_age
:
257 for repo_path
, repo_name
in zip(self
.repo_paths
, self
.repo_names
):
258 # Grab an easily-parsed history: fields delimited by nulls.
259 # Hash, author's name, commit timestamp, subject.
262 '--git-dir=' + repo_path
,
264 '--pretty=%h%x00%an%x00%at%x00%s',
265 "{0}..{1}".format(since_tag
, tag
),
267 proc
= subprocess
.Popen(git_log_args
, stdout
=PIPE
)
268 for line
in proc
.stdout
:
269 hash, author
, time
, subject
= line
.strip().split('\x00')
274 time
= datetime
.datetime
.fromtimestamp(int(time
)),
280 update
= FrontPageGit(
282 time
= tagged_timestamp
,
286 updates
.append(update
)