2 """Tell me who you are!
7 from debian_bundle import deb822
9 # Lets first assure no guarding (but annoying) warnings
11 warnings.simplefilter('ignore', FutureWarning)
12 warnings.filterwarnings('ignore', 'Module debian_bundle was already imported.*', UserWarning)
15 from ConfigParser import SafeConfigParser
16 from optparse import OptionParser, Option, OptionGroup, OptionConflictError
27 from jinja2 import Environment, PackageLoader
29 from pprint import PrettyPrinter
32 class AptListsCache(object):
33 def __init__(self, cachedir='build/cache',
36 self.cachedir = cachedir
38 if not ro_cachedirs is None:
39 self.ro_cachedirs = ro_cachedirs
41 self.ro_cachedirs = []
44 create_dir(self.cachedir)
46 def get(self, url, update=False):
47 """Looks in the cache if the file is there and takes the cached one.
48 Otherwise it is downloaded first.
50 Knows how to deal with http:// and svn:// URLs.
55 # look whether it is compressed
56 cext = url.split('.')[-1]
57 if cext in ['gz', 'bz2']:
58 target_url = url[:-1 * len(cext) -1]
60 # assume not compressed
64 # turn url into a filename -- mimik what APT does for
66 tfilename = '_'.join(target_url.split('/')[2:])
68 # if we need to download anyway do not search
70 cfilename = os.path.join(self.cachedir, tfilename)
72 # look for the uncompressed file anywhere in the cache
74 for cp in [self.cachedir] + self.ro_cachedirs:
75 if os.path.exists(os.path.join(cp, tfilename)):
76 cfilename = os.path.join(cp, tfilename)
81 cfilename = os.path.join(self.cachedir, tfilename)
84 # if updated needed -- download
86 #print 'Caching file from %s' % url
88 if url.startswith('svn://'):
90 pysvn.Client().export(url, cfilename)
91 if url.startswith('http://'):
93 tempfile, ignored = urllib.urlretrieve(url)
100 decompressor = 'bzip2'
105 "Don't know how to decompress %s files" \
108 if not decompressor is None:
109 if subprocess.call([decompressor, '-d', '-q', '-f',
111 raise RuntimeError, \
112 "Something went wrong while decompressing '%s'" \
115 # move decompressed file into cache
116 shutil.move(os.path.splitext(tempfile)[0], cfilename)
118 # XXX do we need that if explicit filename is provided?
122 fh = codecs.open(cfilename, 'r', 'utf-8')
127 def add_pkgfromtaskfile(db, urls):
128 cache = AptListsCache()
134 # loop over all stanzas
135 for stanza in deb822.Packages.iter_paragraphs(fh):
136 if stanza.has_key('Depends'):
137 pkg = stanza['Depends']
138 elif stanza.has_key('Suggests'):
139 pkg = stanza['Suggests']
143 # account for multiple packages per line
145 pkgs += [p.strip() for p in pkg.split(',')]
147 pkgs.append(pkg.strip())
150 if not db.has_key(p):
151 db[p] = get_emptydbentry()
155 def get_emptydbentry():
158 def import_blendstask(cfg, db, url):
159 cache = AptListsCache()
163 # figure out blend's task page URL, since they differ from blend to blend
164 urlsec = url.split('/')
165 blendname = urlsec[-3]
166 if blendname == 'debian-med':
167 taskpage_url = 'http://debian-med.alioth.debian.org/tasks/'
168 elif blendname == 'debian-science':
169 taskpage_url = 'http://blends.alioth.debian.org/science/tasks/'
171 raise ValueError('Unknown blend "%s"' % blendname)
172 taskpage_url += urlsec[-1]
174 for st in deb822.Packages.iter_paragraphs(fh):
175 if st.has_key('Task'):
176 task_name = st['Task']
177 task = (blendname, task_name, taskpage_url)
179 if st.has_key('Depends'):
181 elif st.has_key('Suggests'):
184 # print 'Warning: Cannot determine name of prospective package ' \
185 # '... ignoring. Dump follows:'
189 # take care of pkg lists
190 for p in pkg.split(', '):
191 if not db.has_key(p):
192 print 'Ignoring blend package "%s"' % p
198 info['tasks'] = [task]
199 if st.has_key('License'):
200 info['license'] = st['License']
201 if st.has_key('Responsible'):
202 info['responsible'] = st['Responsible']
205 if st.has_key('Pkg-Description'):
206 descr = st['Pkg-Description'].split('\n')
207 info['description'] = descr[0].strip()
208 info['long_description'] = \
209 u'\n'.join(descr[1:])
211 # charge the basic property set
212 db[p]['main']['description'] = info['description']
213 db[p]['main']['long_description'] = info['long_description']
214 if st.has_key('WNPP'):
215 db[p]['main']['debian_itp'] = st['WNPP']
216 if st.has_key('Pkg-URL'):
217 db[p]['main']['other_pkg'] = st['Pkg-URL']
218 if st.has_key('Homepage'):
219 db[p]['main']['homepage'] = st['Homepage']
222 if st.has_key('Published-Title'):
223 title = st['Published-Title']
225 # trip trailing dot -- added later
226 pub = {'title': title[:-1]}
228 pub = {'title': title}
229 if st.has_key('Published-Authors'):
230 pub['authors'] = st['Published-Authors']
231 if st.has_key('Published-Year'):
232 pub['year'] = st['Published-Year']
233 if st.has_key('Published-In'):
234 pub['in'] = st['Published-In']
235 if st.has_key('Published-URL'):
236 pub['url'] = st['Published-URL']
237 if st.has_key('Published-DOI'):
238 pub['doi'] = st['Published-DOI']
239 # need at least one URL
240 if not pub.has_key('url'):
241 pub['url'] = "http://dx.doi.org/%s" % st['Published-DOI']
243 db[p]['main']['publication'] = pub
246 if st.has_key('Registration'):
247 db[p]['main']['registration'] = st['Registration']
250 if st.has_key('Remark'):
251 # prepend a single space to make it look like a long description
252 info['remark'] = convert_longdescr(' ' + st['Remark'])
254 # only store if there isn't something already
255 if not db[p].has_key('blends'):
256 db[p]['blends'] = info
258 # just add this tasks name and id
259 db[p]['blends']['tasks'].append(task)
261 # handle pkg name aliases
262 if p in cfg.options('blend package aliases'):
263 src_entry = db[p].copy()
264 # remove original entry
266 # copy the entry into all aliases
267 for alias in cfg.get('blend package aliases', p).split():
268 print "Aliasing %s to %s" % (p, alias)
269 db[alias] = copy.deepcopy(src_entry)
274 def get_releaseinfo(rurl):
275 cache = AptListsCache()
276 # root URL of the repository
277 baseurl = '/'.join(rurl.split('/')[:-1])
278 # get the release file from the cache
279 release_file = cache.get(rurl)
281 # create parser instance
282 rp = deb822.Release(release_file)
284 # architectures on this dist
285 archs = rp['Architectures'].split()
286 components = rp['Components'].split()
287 # compile a new codename that also considers the repository label
288 # to distinguish between official and unofficial repos.
290 origin = rp['Origin']
291 codename = rp['Codename']
292 labelcode = '_'.join([rp['Label'], rp['Codename']])
297 return {'baseurl': baseurl, 'archs': archs, 'components': components,
298 'codename': codename, 'label': label, 'labelcode': labelcode,
302 def build_pkgsurl(baseurl, component, arch):
303 return '/'.join([baseurl, component, 'binary-' + arch, 'Packages.bz2'])
306 def import_release(cfg, db, rurl):
307 cache = AptListsCache()
309 ri = get_releaseinfo(rurl)
311 # compile the list of Packages files to parse and parse them
312 for c in ri['components']:
313 for a in ri['archs']:
314 # compile packages URL
315 pkgsurl = build_pkgsurl(ri['baseurl'], c, a)
317 # retrieve from cache
318 packages_file = cache.get(pkgsurl)
321 for stanza in deb822.Packages.iter_paragraphs(packages_file):
322 db = _store_pkg(cfg, db, stanza, ri['origin'], ri['codename'], c, ri['baseurl'])
325 packages_file.close()
329 def _store_pkg(cfg, db, st, origin, codename, component, baseurl):
336 # only care for known packages
337 if not db.has_key(pkg):
338 # print 'Ignoring NeuroDebian package "%s"' % pkg
341 distkey = (trans_codename(codename, cfg), 'neurodebian-' + codename)
343 if db[pkg].has_key(distkey):
344 info = db[pkg][distkey]
346 info = {'architecture': []}
349 if not st['Architecture'] in info['architecture']:
350 info['architecture'].append(st['Architecture'])
351 info['maintainer'] = st['Maintainer']
352 if st.has_key('Homepage'):
353 info['homepage'] = st['Homepage']
354 info['version'] = st['Version']
357 info['distribution'] = origin
358 info['release'] = codename
359 info['component'] = component
362 info['poolurl'] = '/'.join([os.path.dirname(st['Filename'])])
365 descr = st['Description'].replace('%', '%%').split('\n')
366 info['description'] = descr[0].strip()
367 info['long_description'] = u'\n'.join(descr[1:])
369 db[pkg][distkey] = info
371 # charge the basic property set
372 db[pkg]['main']['description'] = info['description']
373 db[pkg]['main']['long_description'] = info['long_description']
374 if st.has_key('Source'):
375 db[pkg]['main']['sv'] = "%s %s" % (st['Source'], st['Version'])
377 db[pkg]['main']['sv'] = "%s %s" % (st['Package'], st['Version'])
378 if st.has_key('Homepage'):
379 db[pkg]['main']['homepage'] = st['Homepage']
380 if st.has_key('Recommends'):
381 db[pkg]['main']['recommends'] = st['Recommends']
386 def trans_codename(codename, cfg):
387 """Translate a known codename into a release description.
389 Unknown codenames will simply be returned as is.
391 # if we know something, tell
392 if codename in cfg.options('release codenames'):
393 return cfg.get('release codenames', codename)
398 def create_dir(path):
399 if os.path.exists(path):
402 ps = path.split(os.path.sep)
404 for i in range(1,len(ps) + 1):
405 p = os.path.sep.join(ps[:i])
407 if not os.path.exists(p):
411 def dde_get(url, fail=False):
412 # enforce delay to be friendly to DDE
415 data = json.read(urllib2.urlopen(url+"?t=json").read())['r']
416 print "SUCCESS:", url
418 except urllib2.HTTPError, e:
419 print "NOINFO:", url, type(e)
421 except urllib2.URLError, e:
422 print "URLERROR:", url, type(e)
424 print "Permanant failure"
426 print "Try again after 30 seconds..."
428 return dde_get(url, fail=True)
429 except (StopIteration):
432 except json.ReadException, e:
433 print "UDD-DOWN?:", url, type(e)
437 def nitrc_get(spec, fail=False):
438 nitrc_url = 'http://www.nitrc.org/export/site/projects.json.php'
440 # change into this from python 2.6 on
441 #data = json.loads(urllib2.urlopen(nitrc_url + '?spec=%s' % spec).read())
442 data = json.read(urllib2.urlopen(nitrc_url + '?spec=%s' % spec).read())
443 print "NITRC-SUCCESS:", spec
444 except urllib2.HTTPError, e:
445 print "NITRC-NOINFO:", spec, type(e)
447 except urllib2.URLError, e:
448 print "NITRC-URLERROR:", spec, type(e)
450 print "Permanant failure"
452 print "Try again after 30 seconds..."
454 return nitrc_get(spec, fail=True)
458 def parse_nitrc(data):
461 # simplify -- there is only one project in the data
462 project = data['projects'][0]
463 nitrc_filtered = {'downloads': 0,
465 for pkg in project['packages']:
466 for release in pkg['releases']:
467 for file in release['files']:
468 nitrc_filtered['downloads'] += file['download_count']
469 return nitrc_filtered
472 def import_nitrc(cfg, db):
474 if not cfg.has_option("nitrc ids", p):
476 nitrc_spec = cfg.get("nitrc ids", p)
477 nitrc_data = nitrc_get(nitrc_spec)
478 nitrc_excerpt = parse_nitrc(nitrc_data)
479 if not nitrc_excerpt is None:
480 db[p]['nitrc'] = nitrc_excerpt
484 def import_dde(cfg, db):
485 query_url = cfg.get('dde', 'pkgquery_url')
488 q = dde_get(query_url + "/packages/all/%s" % p)
490 # copy all stuff, while preserving non-overlapping information
491 for k, v in q.iteritems():
493 # get latest popcon info for debian and ubuntu
494 # cannot use origin field itself, since it is none for few packages
496 origin = q['drc'].split()[0]
497 if origin == 'ubuntu':
498 if q.has_key('popcon'):
499 db[p]['main']['ubuntu_popcon'] = q['popcon']
500 # if we have ubuntu, need to get debian
501 q = dde_get(query_url + "/packages/prio-debian-sid/%s" % p)
502 if q and q.has_key('popcon'):
503 db[p]['main']['debian_popcon'] = q['popcon']
504 elif origin == 'debian':
505 if q.has_key('popcon'):
506 db[p]['main']['debian_popcon'] = q['popcon']
507 # if we have debian, need to get ubuntu
508 q = dde_get(query_url + "/packages/prio-ubuntu-natty/%s" % p)
509 if q and q.has_key('popcon'):
510 db[p]['main']['ubuntu_popcon'] = q['popcon']
512 print("Ignoring unkown origin '%s' for package '%s'." \
515 # now get info for package from all releases in UDD
516 q = dde_get(query_url + "/dist/p:%s" % p)
519 # hold all info about this package per distribution release
522 distkey = (trans_codename(cp['release'], cfg),
523 "%s-%s" % (cp['distribution'], cp['release']))
524 if not info.has_key(distkey):
526 # turn into a list to append others later
527 info[distkey]['architecture'] = [info[distkey]['architecture']]
528 # accumulate data for multiple over archs
530 comp = apt.VersionCompare(cp['version'],
531 info[distkey]['version'])
532 # found another arch for the same version
534 info[distkey]['architecture'].append(cp['architecture'])
535 # found newer version, dump the old ones
538 # turn into a list to append others later
539 info[distkey]['architecture'] = [info[distkey]['architecture']]
540 # simply ignore older versions
544 # finally assign the new package data
545 for k, v in info.iteritems():
550 def assure_unicode(s):
551 """Assure that argument is unicode
553 Necessary if strings are not carrying out Pythonish 'u' prefix to
554 signal UTF8 strings, but are in fact UTF8
556 if type(s) is unicode:
559 # attempt regular unicode call and if fails -- just decode it
563 except UnicodeDecodeError, e:
564 return s.decode('utf8')
566 return assure_unicode(str(s))
569 def convert_longdescr(ld):
570 ld = ld.replace('% ', '%% ')
572 for i, l in enumerate(ld):
574 ld[i] = ' #NEWLINEMARKER#'
575 # look for embedded lists
576 elif len(l) >=3 and l[:2] == ' ' and l[2] in '-*':
577 ld[i] = ' #NEWLINEMARKER# ' + l[2:]
579 ld = u' '.join([l[1:] for l in ld])
580 ld = ld.replace('#NEWLINEMARKER# ', '\n\n')
581 # cleanup any leftover (e.g. trailing markers)
582 ld = ld.replace('#NEWLINEMARKER#', '')
586 def generate_pkgpage(pkg, cfg, db, template, addenum_dir):
587 # local binding for ease of use
589 # do nothing if there is not at least the very basic stuff
590 if not pkgdb['main'].has_key('description'):
592 title = '**%s** -- %s' % (pkg, pkgdb['main']['description'])
593 underline = '*' * (len(title) + 2)
594 title = '%s\n %s\n%s' % (underline, title, underline)
596 page = template.render(
599 long_description=convert_longdescr(
600 assure_unicode(pkgdb['main']['long_description'])),
604 # the following can be replaced by something like
605 # {% include "sidebar.html" ignore missing %}
606 # in the template whenever jinja 2.2 becomes available
607 addenum = os.path.join(os.path.abspath(addenum_dir), '%s.rst' % pkg)
608 if os.path.exists(addenum):
609 page += '\n\n.. include:: %s\n' % addenum
613 def store_db(db, filename):
614 pp = PrettyPrinter(indent=2)
615 f = codecs.open(filename, 'w', 'utf-8')
616 f.write(pp.pformat(db))
620 def read_db(filename):
621 f = codecs.open(filename, 'r', 'utf-8')
625 def write_sourceslist(jinja_env, cfg, outdir):
627 create_dir(os.path.join(outdir, '_static'))
630 for release in cfg.options('release codenames'):
631 if release == 'data':
632 # no seperate list for the data archive
634 transrel = trans_codename(release, cfg)
636 for mirror in cfg.options('mirrors'):
637 listname = 'neurodebian.%s.%s.sources.list' % (release, mirror)
638 repos[transrel].append((mirror, listname))
639 lf = open(os.path.join(outdir, '_static', listname), 'w')
640 for rel in ('data', release):
641 aptcfg = '%s %s main contrib non-free\n' % (cfg.get('mirrors', mirror),
643 lf.write('deb %s' % aptcfg)
644 lf.write('#deb-src %s' % aptcfg)
647 srclist_template = jinja_env.get_template('sources_lists.rst')
648 sl = open(os.path.join(outdir, 'sources_lists'), 'w')
649 sl.write(srclist_template.render(repos=repos))
653 def write_pkgpages(jinja_env, cfg, db, outdir, addenum_dir):
655 create_dir(os.path.join(outdir, 'pkgs'))
657 # generate the TOC with all packages
658 toc_template = jinja_env.get_template('pkgs_toc.rst')
659 toc = codecs.open(os.path.join(outdir, 'pkgs.rst'), 'w', 'utf-8')
660 # this is a fragile test
661 toc.write(toc_template.render(
662 pkgs=[k for k in db.keys()
663 if not ('Datasets', 'neurodebian-data') in db[k]]))
665 # and now only for dataset packages
666 toc_template = jinja_env.get_template('datasets_toc.rst')
667 toc = codecs.open(os.path.join(outdir, 'datasets.rst'), 'w', 'utf-8')
668 # this is a fragile test
669 toc.write(toc_template.render(
670 pkgs=[k for k in db.keys()
671 if ('Datasets', 'neurodebian-data') in db[k]]))
675 # and now each individual package page
676 pkg_template = jinja_env.get_template('pkg.rst')
678 page = generate_pkgpage(p, cfg, db, pkg_template, addenum_dir)
679 # when no page is available skip this package
682 pf = codecs.open(os.path.join(outdir, 'pkgs', p + '.rst'), 'w', 'utf-8')
687 def prepOptParser(op):
688 # use module docstring for help output
689 op.usage = "%s [OPTIONS]\n\n" % sys.argv[0] + __doc__
691 op.add_option("--db",
692 action="store", type="string", dest="db",
694 help="Database file to read. Default: None")
696 op.add_option("--cfg",
697 action="store", type="string", dest="cfg",
699 help="Repository config file.")
701 op.add_option("-o", "--outdir",
702 action="store", type="string", dest="outdir",
704 help="Target directory for ReST output. Default: None")
706 op.add_option("-r", "--release-url",
707 action="append", dest="release_urls",
710 op.add_option("--pkgaddenum", action="store", dest="addenum_dir",
711 type="string", default=None, help="None")
715 op = OptionParser(version="%prog 0.0.2")
718 (opts, args) = op.parse_args()
721 print('There needs to be exactly one command')
727 print("'--cfg' option is mandatory.")
730 print("'--db' option is mandatory.")
734 cfg = SafeConfigParser()
737 # load existing db, unless renew is requested
738 if cmd == 'updatedb':
740 if cfg.has_option('packages', 'select taskfiles'):
741 db = add_pkgfromtaskfile(db, cfg.get('packages',
742 'select taskfiles').split())
744 # add additional package names from config file
745 if cfg.has_option('packages', 'select names'):
746 for p in cfg.get('packages', 'select names').split():
747 if not db.has_key(p):
748 db[p] = get_emptydbentry()
750 # get info from task files
751 if cfg.has_option('packages', 'prospective'):
752 for url in cfg.get('packages', 'prospective').split():
753 db = import_blendstask(cfg, db, url)
755 # parse NeuroDebian repository
756 if cfg.has_option('neurodebian', 'releases'):
757 for rurl in cfg.get('neurodebian', 'releases').split():
758 db = import_release(cfg, db, rurl)
760 # collect package information from DDE
761 db = import_dde(cfg, db)
762 # get info from NITRC
763 db = import_nitrc(cfg, db)
765 store_db(db, opts.db)
769 # load the db from file
770 db = read_db(opts.db)
773 jinja_env = Environment(loader=PackageLoader('neurodebian', 'templates'))
775 # generate package pages and TOC and write them to files
776 write_pkgpages(jinja_env, cfg, db, opts.outdir, opts.addenum_dir)
778 write_sourceslist(jinja_env, cfg, opts.outdir)
780 if __name__ == "__main__":