Changeset 1048

Show
Ignore:
Timestamp:
08/26/08 07:06:03 (19 months ago)
Author:
vasi
Message:

combine popcon and edebtags

Location:
bin
Files:
1 removed
1 modified

Legend:

Unmodified
Added
Removed
  • bin/popcon

    r1047 r1048  
    11#!/usr/bin/ruby -w 
    22 
     3module CachedURI 
     4        KeepDays = 30 
     5        def name; self.class.name; end 
     6         
     7        def cachedb; File.join(ENV['HOME'], '.' + name); end 
     8        def age(file); (Time.now - File.mtime(file)) / 86400; end 
     9         
     10        def open_handling_gzip(uri, &block) 
     11                require 'open-uri' 
     12                open(uri) do |io| 
     13                        return block[io] unless io.respond_to?(:content_type) && 
     14                                (io.content_type == 'application/x-gzip' || 
     15                                        io.content_encoding.include?('x-gzip')) 
     16                         
     17                        require 'zlib' 
     18                        begin 
     19                                rdr = Zlib::GzipReader.new(io) 
     20                                return block[rdr] 
     21                        ensure 
     22                                rdr.close 
     23                        end 
     24                end 
     25        end 
     26         
     27        def fetch 
     28                @uri ||= uri 
     29                open_handling_gzip(@uri) { |io| process(io) } 
     30        end 
     31         
     32        def initialize(opts = {}) 
     33                @nocache = nil 
     34                opts.each { |k,v| instance_variable_set("@#{k}", v) } 
     35                 
     36                @cachedb ||= cachedb 
     37                @age ||= KeepDays 
     38                 
     39                cache_file = @cachedb + '.db' 
     40                valid = !@nocache && File.exists?(cache_file) && age(cache_file) < @age 
     41                if @nocache 
     42                        @data = {} 
     43                else 
     44                        require 'dbm' 
     45                        File.unlink(cache_file) if !valid && File.exists?(cache_file) 
     46                        @data = DBM.open(@cachedb, 0666) 
     47                end 
     48                fetch unless valid 
     49        end 
     50end 
     51 
    352class Popcon 
    4         MaxAge = 7 
     53        include CachedURI 
    554         
    655        def distro_detect; IO.popen('lsb release -is 2>/dev/null').read; end 
    7          
    856        def uri 
    9                 dir = @uri || case (@distro || distro_detect).downcase 
     57                require 'uri' 
     58                dir = case (@distro || distro_detect).downcase 
    1059                        when 'ubuntu': 'http://popcon.ubuntu.com/' 
    1160                        else 'http://popcon.debian.org/' # default to debian 
     
    1463        end 
    1564         
    16         def cache_file; @cache || File.join(ENV['HOME'], '.popcondb'); end 
    17         def age(file); (Time.now - File.mtime(file)) / 86400; end 
    18          
    19         def fetch 
    20                 require 'net/http' 
    21                 require 'stringio' 
    22                 require 'zlib' 
    23                 gz = Net::HTTP.get(uri) 
    24                 str = Zlib::GzipReader.new(StringIO.new(gz)).read 
    25                  
     65        def process(io) 
    2666                # Fields: rank name inst vote old recent no_files (maintainer) 
    27                 ret = {} 
    28                 str.each_line do |line| 
     67                io.each do |line| 
    2968                        next if line[0] == ?#   # comments 
    3069                        break if line[0] == ?-  # totals 
    3170                        fields = line.split(' ', 8) 
    3271                        name, votes = fields.values_at(1, 3) 
    33                         ret[name] = votes.to_i 
    34                 end 
    35                 return ret 
    36         end 
    37  
     72                        @data[name] = votes.to_i 
     73                end 
     74        end 
     75 
     76        def initialize(opts = {}); @distro = nil; super; end 
     77        def popularity(pkgname); (@data[pkgname] || 0).to_i; end 
     78end 
     79 
     80class Debtags 
     81        include CachedURI 
     82        def uri; 'http://debtags.alioth.debian.org/tags/tags-current.gz'; end 
     83        def process(io) 
     84                bytag, bypkg = {}, {} 
     85                io.each do |line| 
     86                        md = /^(\S+):\s*(.*)/.match(line) or next 
     87                        name, tags = md[1], md[2].split(/,\s*/) 
     88                        (bypkg[name] ||= []).concat(tags) 
     89                        tags.each { |t| (bytag[t] ||= []) << name } 
     90                end 
     91                [['tag', bytag], ['pkg', bypkg]].each do |n, h| 
     92                        h.each do |k, v| 
     93                                @data["%s,%s" % [n, k]] = h[k].uniq.join(',') 
     94                        end 
     95                end 
     96        end 
     97        def tags(pkg); (@data["pkg,#{pkg}"] || '').split(','); end 
     98        def tagged_with(tag); (@data["tag,#{tag}"] || '').split(','); end 
     99end 
     100 
     101class Package 
     102        attr_reader :name 
     103        def initialize(db, name) 
     104                @db = db 
     105                @name = name 
     106        end 
     107         
     108        def popularity; @db.popcon.popularity(name); end 
     109        def tags; @db.debtags.tags(name); end 
     110end 
     111 
     112class Package::DB 
    38113        def initialize(opts = {}) 
    39                 @cache = @uri = @distro = @age = @nocache = nil 
    40114                opts.each { |k,v| instance_variable_set("@#{k}", v) } 
    41                  
    42                 cache = cache_file 
    43                 if @nocache || !File.exists?(cache) || age(cache) > (@age || MaxAge) 
    44                         @data = fetch 
    45                         File.open(cache, 'w') { |f| Marshal.dump(@data, f) } unless @nocache 
    46                 else 
    47                         @data = File.open(cache) { |f| Marshal.load(f) } 
    48                 end 
    49         end 
    50          
    51         def popularity(pkgname); @data[pkgname] || 0; end 
    52 end 
    53  
    54 class Package 
    55         attr_reader :name, :popularity, :line 
    56         def initialize(popcon, name, line) 
    57                 @name = name 
    58                 @popularity = popcon.popularity(name) 
    59                 @line = line || '' 
    60         end 
    61          
    62         def to_s; "%5d  %s%s" % [popularity, name, line]; end 
    63          
    64         class UnparseableLine < Exception; end 
    65         def self.from_line(popcon, l) 
    66                 md = l.match(/^(\S+)(.*)$/) or raise UnparseableLine.new(l) 
    67                 return new(popcon, *md.captures) 
    68         end 
    69          
    70         def self.from_enums(popcon, *enums) 
    71                 ret = [] 
    72                 enums.each do |e| 
    73                         next if e.respond_to?(:tty?) && e.tty? 
    74                         e.each { |x| ret << from_line(popcon, x) } 
    75                 end 
    76                 return ret 
    77         end 
    78         def self.print_all(list) 
    79                 list.sort_by { |x| -x.popularity }.each { |x| puts x.to_s } 
    80         end 
    81         def self.print_enums(popcon, *enums) 
    82                 print_all(from_enums(popcon, *enums)) 
    83         end 
    84 end 
    85  
    86 require 'optparse' 
    87  
    88 options = {} 
    89 OptionParser.new do |op| 
    90         op.banner = <<USAGE 
    91 Usage: popcon [OPTIONS] PKG1 PKG2 ... 
    92        stdin | popcon [OPTIONS] 
    93 Sort packages by popularity contest data. 
    94 USAGE 
    95         op.separator '' 
    96          
    97         op.on('-c', '--cache FILE', 'Cache data in this file') \ 
    98                 { |x| options[:cache] = x } 
    99         op.on('-n', '--nocache', "Don't use a cache") \ 
    100                 { |x| options[:nocache] = x } 
    101         op.on('-a', '--age DAYS', 'Refresh cache if older than this') \ 
    102                 { |x| options[:age] = x.to_i } 
    103                  
    104         op.on('-d', '--distro DISTRO', 'Use popularity data from this distro') \ 
    105                 { |x| options[:distro] = x } 
    106         op.on('-u', '--uri URI', 'Use popularity data from this URI') \ 
    107                 { |x| options[:uri] = x } 
    108 end.parse! 
    109          
    110 popcon = Popcon.new(options) 
    111 Package.print_enums(popcon, ARGV, STDIN) 
     115        end 
     116         
     117        def debtags; @debtags ||= Debtags.new; end 
     118        def popcon; @popcon ||= Popcon.new; end 
     119         
     120        def package(name); Package.new(self, name); end 
     121        def tagged_with(*tags) 
     122                names = debtags.tagged_with(tags.shift) 
     123                tags.each { |t| names = names & debtags.tagged_with(t) } 
     124                names.map { |n| package(n) } 
     125        end 
     126end 
     127 
     128require 'delegate' 
     129class AnnotatedPackage < DelegateClass(Package) 
     130        attr_reader :annotation 
     131        def initialize(pkg, annotation) 
     132                super(pkg) 
     133                @annotation = annotation || '' 
     134        end 
     135         
     136        class DB < Package::DB 
     137                class UnparseableLine < Exception; end 
     138                def package(name, ann = nil) 
     139                        AnnotatedPackage.new(super(name), ann) 
     140                end 
     141                 
     142                def package_from_line(line) 
     143                        md = line.match(/^(\S+)(.*)$/) or return nil 
     144                        return package(*md.captures) 
     145                end 
     146                 
     147                def packages_from_input(*enums) 
     148                        ret = [] 
     149                        enums.each do |e| 
     150                                next if e.respond_to?(:tty?) && e.tty? 
     151                                e.each do |line| 
     152                                        p = package_from_line(line) or next 
     153                                        ret << p 
     154                                end 
     155                        end 
     156                        return ret 
     157                end 
     158        end 
     159end      
     160 
     161module DebianPackageScript 
     162        def self.unindent(s) 
     163                ind = s.match(/\A[\t ]*/)[0] 
     164                s.gsub(Regexp.new("^#{Regexp.escape(ind)}"), '') 
     165        end 
     166         
     167        def self.db_from_opts(cache_class, usage, opts = {}, &extra_cli_options) 
     168                require 'optparse' 
     169                options = {} 
     170                OptionParser.new do |parser| 
     171                        parser.banner = usage 
     172                        parser.separator '' 
     173                         
     174                        parser.on('-c', '--cache FILE', 'Cache data in this file') \ 
     175                                { |x| options[:cache] = x } 
     176                        parser.on('-n', '--nocache', "Don't use a cache") \ 
     177                                { |x| options[:nocache] = x } 
     178                        parser.on('-a', '--age DAYS', 'Refresh cache if older than this') \ 
     179                                { |x| options[:age] = x.to_i }                           
     180                        parser.on('-u', '--uri URI', 'Fetch data from this URI') \ 
     181                                { |x| options[:uri] = x } 
     182                                 
     183                        extra_cli_options[parser, options] if extra_cli_options 
     184                end.parse! 
     185                 
     186                cache = cache_class.new(options) 
     187                db_class = opts[:db_class] || Package::DB 
     188                return db_class.new(cache_class.name.downcase => cache) 
     189        end 
     190         
     191        # Popcon annotator 
     192        def self.do_popcon 
     193                usage = unindent(<<-USAGE) 
     194                        Usage: popcon [OPTIONS] PKG1 PKG2 ... 
     195                                   stdin | popcon [OPTIONS] 
     196                        Sort packages by popularity contest data. 
     197                USAGE 
     198                pkgs = db_from_opts(Popcon, usage, 
     199                                :db_class => AnnotatedPackage::DB) do |p, o| 
     200                        p.on('-d', '--distro DISTRO', 
     201                                'Use popularity data from this distro') { |x| o[:distro] = x } 
     202                end 
     203                pkgs.packages_from_input(ARGV, STDIN). 
     204                                sort_by { |p| -p.popularity }.each do |p| 
     205                        puts "%6d  %s%s" % [p.popularity, p.name, p.annotation] 
     206                end 
     207        end 
     208         
     209        # Ersatz debtags 
     210        def self.do_edebtags 
     211                usage = unindent(<<-USAGE) 
     212                        Usage: edebtags [OPTIONS] TAG1 TAG2 ... 
     213                        Get a list of Debian packages corresponding to the given tags. 
     214                USAGE 
     215                pkgs = db_from_opts(Debtags, usage) 
     216                pkgs.tagged_with(*ARGV).map { |p| p.name }.sort.each { |n| puts n } 
     217        end 
     218         
     219        def self.run 
     220                case File.basename($0) 
     221                        when 'edebtags': do_edebtags 
     222                        else do_popcon 
     223                end 
     224        end 
     225end 
     226 
     227DebianPackageScript.run if $0 == __FILE__