3944.1.1
by Francis J. Lacoste
Use system version python2.4 for scripts. |
1 |
#!/usr/bin/python2.4
|
8687.15.22
by Karl Fogel
Add the copyright header block to the remaining .py files. |
2 |
#
|
3 |
# Copyright 2009 Canonical Ltd. This software is licensed under the
|
|
4 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
|
3485.3.3
by James Henstridge
add zope spec metadata importer |
5 |
|
6 |
# A script to import metadata about the Zope 3 specs into Launchpad
|
|
7 |
||
8 |
__metaclass__ = type |
|
9 |
||
10 |
import itertools |
|
11 |
import re |
|
12 |
import sys |
|
13 |
import urllib2 |
|
14 |
||
15 |
import _pythonpath |
|
16 |
from zope.component import getUtility |
|
17 |
from BeautifulSoup import BeautifulSoup |
|
18 |
||
19 |
from canonical.lp import initZopeless |
|
20 |
from canonical.lp.dbschema import ( |
|
21 |
SpecificationStatus, SpecificationGoalStatus, SpecificationDelivery, |
|
22 |
SpecificationPriority) |
|
23 |
from canonical.launchpad.scripts import execute_zcml_for_scripts |
|
24 |
from canonical.launchpad.interfaces import ( |
|
25 |
IPersonSet, IProductSet, ISpecificationSet) |
|
26 |
||
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
27 |
WIKI_BASE = 'http://wiki.zope.org/zope3/' |
3485.3.3
by James Henstridge
add zope spec metadata importer |
28 |
PROPOSAL_LISTS = ['Zope3Proposals', 'OldProposals', 'DraftProposals'] |
29 |
specroot = WIKI_BASE + 'Zope3Proposals' |
|
30 |
||
31 |
at_replacements = ['_at_', '(at)', '@'] |
|
32 |
author_email_pat = re.compile('[-.A-Za-z0-9]+(?:@|%s)[-.A-Za-z0-9]+' % |
|
33 |
'|'.join([re.escape(replacement) |
|
34 |
for replacement in at_replacements])) |
|
35 |
||
36 |
def getTextContent(tag): |
|
3485.3.28
by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com |
37 |
if tag is None: |
38 |
return '' |
|
3485.3.3
by James Henstridge
add zope spec metadata importer |
39 |
if isinstance(tag, basestring): |
40 |
return tag |
|
41 |
return ''.join([e for e in tag.recursiveChildGenerator() |
|
42 |
if isinstance(e, basestring)]) |
|
43 |
||
44 |
||
45 |
class ZopeSpec: |
|
46 |
||
47 |
def __init__(self, url, title, summary): |
|
48 |
self.url = url |
|
49 |
self.name = self.url.split('/')[-1] |
|
50 |
self.title = title |
|
51 |
self.summary = summary |
|
52 |
self.authors = set() |
|
53 |
self.statuses = set() |
|
54 |
||
55 |
def parseAuthorEmails(self, text): |
|
56 |
author_email_list = author_email_pat.findall(text) |
|
57 |
for author in author_email_list: |
|
58 |
# unmangle at symbol in email:
|
|
59 |
for replacement in at_replacements: |
|
60 |
author = author.replace(replacement, '@') |
|
61 |
self.authors.add(author) |
|
62 |
||
63 |
def parseStatuses(self, soup): |
|
64 |
wiki_badges = [ |
|
65 |
'IsWorkInProgress', |
|
66 |
||
67 |
'IsProposal', |
|
68 |
'IsRejectedProposal', |
|
69 |
'IsSupercededProposal', |
|
70 |
'IsRetractedProposal', |
|
71 |
'IsAcceptedProposal', |
|
72 |
'IsImplementedProposal', |
|
73 |
'IsExpiredProposal', |
|
74 |
'IsDraftProposal', |
|
75 |
||
76 |
'IsPlanned', |
|
77 |
'IsResolved', |
|
78 |
'IsImplemented', |
|
79 |
||
80 |
'IsReplaced', |
|
81 |
'IsOutdated', |
|
82 |
'IsDraft', |
|
83 |
'IsEditedDraft', |
|
84 |
'IsRoughDraft', |
|
85 |
]
|
|
86 |
for badge in wiki_badges: |
|
87 |
url = WIKI_BASE + badge |
|
88 |
if soup.fetch('a', {'href': url}): |
|
89 |
self.statuses.add(badge) |
|
90 |
||
91 |
def parseSpec(self): |
|
92 |
contents = urllib2.urlopen(self.url).read() |
|
93 |
soup = BeautifulSoup(contents) |
|
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
94 |
contentdivs = soup('div', {'class': 'content'}) |
3485.3.3
by James Henstridge
add zope spec metadata importer |
95 |
assert len(contentdivs) == 1 |
96 |
contentdiv = contentdivs[0] |
|
97 |
||
98 |
# Specification statuses are represented by "wiki badges",
|
|
99 |
# which are just hyperlinks to particular pages.
|
|
100 |
self.parseStatuses(soup) |
|
101 |
||
102 |
# There are two styles of spec. One of them has a table with
|
|
103 |
# RFC-822 style headers in it. The other has minor level headings
|
|
104 |
# with text under the heading.
|
|
105 |
tables = soup('table') |
|
106 |
# Every page has one table, for the main page layout. So, if the page
|
|
107 |
# has two tables, it means that it will be using the RFC-822 style.
|
|
108 |
if len(tables) >= 2: |
|
109 |
# This is a spec with RFC-822 style headers.
|
|
110 |
docinfo = tables[1] |
|
111 |
for row in docinfo('tr'): |
|
112 |
if len(row('th')) < 1 or len(row('td')) < 1: |
|
113 |
continue
|
|
114 |
key = row('th')[0].renderContents() |
|
115 |
if key.endswith(':'): |
|
116 |
key = key[:-1] |
|
117 |
value = row('td')[0].renderContents() |
|
118 |
||
119 |
if 'Author' in key: |
|
120 |
self.parseAuthorEmails(value) |
|
121 |
else: |
|
122 |
# This is a spec with minor level headings, or perhaps with no
|
|
123 |
# headings at all.
|
|
124 |
||
125 |
# Look for an author heading.
|
|
126 |
author_headers = soup(text=re.compile('Author.*', re.I)) |
|
127 |
if author_headers: |
|
128 |
author = author_headers[0].findNext().renderContents() |
|
129 |
self.parseAuthorEmails(author) |
|
130 |
||
131 |
@property
|
|
132 |
def lpname(self): |
|
133 |
# add dashes before capitalised words
|
|
134 |
name = re.sub(r'([^A-Z])([A-Z])', r'\1-\2', self.name) |
|
135 |
# lower case name
|
|
136 |
name = name.lower() |
|
137 |
# remove leading dashes
|
|
138 |
while name.startswith('-'): |
|
139 |
name = name[1:] |
|
140 |
# if name doesn't begin with an alphabetical character prefix it
|
|
141 |
if not name[0].isalpha(): |
|
142 |
name = 'x-' + name |
|
143 |
return name |
|
144 |
||
145 |
@property
|
|
146 |
def lpstatus(self): |
|
147 |
# implemented and accepted specs => APPROVED
|
|
148 |
for status in ['IsImplemented', |
|
149 |
'IsImplementedProposal', |
|
150 |
'IsAcceptedProposal']: |
|
151 |
if status in self.statuses: |
|
152 |
return SpecificationStatus.APPROVED |
|
3485.3.28
by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com |
153 |
# WIP => DISCUSSION
|
3485.3.3
by James Henstridge
add zope spec metadata importer |
154 |
if 'IsWorkInProgress' in self.statuses: |
3485.3.28
by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com |
155 |
return SpecificationStatus.DISCUSSION |
3485.3.3
by James Henstridge
add zope spec metadata importer |
156 |
for status in ['IsSupercededProposal', 'IsReplaced']: |
157 |
if status in self.statuses: |
|
158 |
return SpecificationStatus.SUPERSEDED |
|
159 |
for status in ['IsExpiredProposal', 'IsOutdated']: |
|
160 |
if status in self.statuses: |
|
161 |
return SpecificationStatus.OBSOLETE |
|
162 |
# draft statuses:
|
|
163 |
for status in ['IsDraftProposal', |
|
164 |
'IsDraft', |
|
165 |
'IsEditedDraft', |
|
166 |
'IsRoughDraft']: |
|
167 |
if status in self.statuses: |
|
168 |
return SpecificationStatus.DRAFT |
|
169 |
# otherwise ...
|
|
170 |
return SpecificationStatus.PENDINGREVIEW |
|
171 |
||
172 |
@property
|
|
173 |
def lpgoalstatus(self): |
|
174 |
# implemented and accepted specs => ACCEPTED
|
|
175 |
for status in ['IsImplemented', |
|
176 |
'IsImplementedProposal', |
|
177 |
'IsAcceptedProposal']: |
|
178 |
if status in self.statuses: |
|
179 |
return SpecificationGoalStatus.ACCEPTED |
|
180 |
# rejected or retracted => DECLINED
|
|
181 |
for status in ['IsRetractedProposal', 'IsRejectedProposal']: |
|
182 |
if status in self.statuses: |
|
183 |
return SpecificationGoalStatus.DECLINED |
|
184 |
||
185 |
# otherwise ...
|
|
186 |
return SpecificationGoalStatus.PROPOSED |
|
187 |
||
188 |
@property
|
|
189 |
def lpdelivery(self): |
|
190 |
for status in ['IsImplemented', |
|
191 |
'IsImplementedProposal']: |
|
192 |
if status in self.statuses: |
|
193 |
return SpecificationDelivery.IMPLEMENTED |
|
194 |
# otherwise ...
|
|
195 |
return SpecificationDelivery.UNKNOWN |
|
196 |
||
197 |
def syncSpec(self): |
|
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
198 |
zope = getUtility(IProductSet).getByName('zope') |
199 |
zope_dev = getUtility(IPersonSet).getByName('zope-dev') |
|
3485.3.3
by James Henstridge
add zope spec metadata importer |
200 |
# has the spec been created?
|
201 |
lpspec = getUtility(ISpecificationSet).getByURL(self.url) |
|
202 |
if not lpspec: |
|
203 |
lpspec = getUtility(ISpecificationSet).new( |
|
204 |
name=self.lpname, |
|
205 |
title=self.title, |
|
206 |
specurl=self.url, |
|
207 |
summary=self.summary, |
|
208 |
priority=SpecificationPriority.UNDEFINED, |
|
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
209 |
status=SpecificationStatus.NEW, |
3485.3.3
by James Henstridge
add zope spec metadata importer |
210 |
owner=zope_dev, |
211 |
product=zope) |
|
212 |
||
213 |
# synchronise
|
|
214 |
lpspec.title = self.title |
|
215 |
lpspec.summary = self.summary |
|
216 |
lpspec.status = self.lpstatus |
|
3485.3.28
by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com |
217 |
newgoalstatus = self.lpgoalstatus |
218 |
if newgoalstatus != lpspec.goalstatus: |
|
219 |
if newgoalstatus == SpecificationGoalStatus.PROPOSED: |
|
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
220 |
lpspec.proposeGoal(None, zope_dev) |
3485.3.28
by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com |
221 |
elif newgoalstatus == SpecificationGoalStatus.ACCEPTED: |
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
222 |
lpspec.acceptBy(zope_dev) |
3485.3.28
by James Henstridge
local import-zope-specs.py changes from carbon.ubuntu.com |
223 |
elif newgoalstatus == SpecificationGoalStatus.DECLINED: |
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
224 |
lpspec.declineBy(zope_dev) |
3485.3.3
by James Henstridge
add zope spec metadata importer |
225 |
lpspec.delivery = self.lpdelivery |
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
226 |
lpspec.updateLifecycleStatus(zope_dev) |
227 |
||
3485.3.3
by James Henstridge
add zope spec metadata importer |
228 |
# set the assignee to the first author email with an LP account
|
229 |
for author in sorted(self.authors): |
|
230 |
person = getUtility(IPersonSet).getByEmail(author) |
|
231 |
if person is not None: |
|
232 |
lpspec.assignee = person |
|
233 |
break
|
|
234 |
||
235 |
||
236 |
def iter_spec_urls(url=specroot): |
|
237 |
contents = urllib2.urlopen(url) |
|
238 |
soup = BeautifulSoup(contents) |
|
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
239 |
contentdivs = soup('div', {'class': 'content'}) |
3485.3.3
by James Henstridge
add zope spec metadata importer |
240 |
assert len(contentdivs) == 1 |
241 |
contentdiv = contentdivs[0] |
|
242 |
listofspecs = contentdiv('ul')[0] |
|
243 |
||
244 |
for listitem in listofspecs('li', recursive=False): |
|
245 |
anchors = listitem('a') |
|
246 |
if not anchors: |
|
247 |
continue
|
|
248 |
specanchor = anchors[0] |
|
249 |
href = specanchor['href'] |
|
250 |
# broken wiki link => ignore
|
|
3485.3.34
by James Henstridge
update import-zope-specs.py to work with current Launchpad and current Zope wiki |
251 |
if 'createform?page=' in href: |
3485.3.3
by James Henstridge
add zope spec metadata importer |
252 |
continue
|
253 |
title = getTextContent(specanchor) |
|
254 |
summary = ''.join([getTextContent(tag) |
|
255 |
for tag in specanchor.nextSiblingGenerator()]) |
|
256 |
yield ZopeSpec(href, title, summary.strip()) |
|
257 |
||
258 |
||
259 |
def main(argv): |
|
260 |
execute_zcml_for_scripts() |
|
261 |
ztm = initZopeless() |
|
262 |
||
263 |
for spec in itertools.chain(*[iter_spec_urls(WIKI_BASE + page) |
|
264 |
for page in PROPOSAL_LISTS]): |
|
265 |
# parse extra information from the spec body
|
|
266 |
spec.parseSpec() |
|
267 |
# add its metadata to LP
|
|
268 |
print 'Synchronising', spec.name |
|
269 |
ztm.begin() |
|
270 |
try: |
|
271 |
spec.syncSpec() |
|
272 |
ztm.commit() |
|
273 |
except: |
|
274 |
ztm.abort() |
|
275 |
raise
|
|
276 |
||
277 |
if __name__ == '__main__': |
|
278 |
sys.exit(main(sys.argv)) |