__init__.py 5.79 KB
Newer Older
Louis's avatar
Louis committed
1
2
3
#!/usr/bin/env python3

import collections
4
import colorsys
Louis's avatar
Louis committed
5
import datetime
6
import functools
7
8
import itertools
import jinja2
9
10
11
12
import json
import operator
import os
import pkg_resources
13
import random
14
import requests
Louis's avatar
Louis committed
15
import slumber
Louis's avatar
Louis committed
16
import sys
Louis's avatar
Louis committed
17
import urllib
18
import xmlrpc.client as xmlrpclib
19
import yaml
Louis's avatar
Louis committed
20

21
22
VERSION = "0.2.0-dev"

23
_client = xmlrpclib.ServerProxy('https://pypi.python.org/pypi')
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
class Counter:
    def __init__(self):
        self.value = 0

    def count(self):
        self.value += 1
        return self.value

def as_id(string):
    return "".join(list(filter(str.isalnum, string)))

def random_color(seed, saturation, value):
    old_seed = random.random()
    random.seed(seed)
    rgb = colorsys.hsv_to_rgb(random.random(), saturation, value)
    random.seed(old_seed)
    return ("#%02x%02x%02x" % tuple([int(value * 255) for value in rgb]))

def iter_user_packages(user):
    for _, package in _client.user_packages(user):
        try:
            yield package
        except urllib.error.HTTPError as error:
            sys.stderr.write("Ignoring user '{}': {}.".format(
                user,
                str(error),
                ))

class Renderer:

    def __init__(self, config):
56
        # Processing config
57
        self.config = config
58
59
60
61
62
63
64
65
        if "columns" not in self.config:
            self.config['columns'] = list(set(itertools.chain(*[
                    package.keys()
                    for package 
                    in self.config['packages'].values()
                ])))

        # Processing packages
Louis's avatar
Louis committed
66
        self.packages = {}
67
        for name in sorted(config['packages']):
Louis's avatar
Louis committed
68
69
            try:
                self.packages[name] = Package(name)
70
71
            except Exception as error:
                sys.stderr.write("Ignoring package '{}': {}.\n".format(name, error))
Louis's avatar
Louis committed
72
                continue
73

74
75
76
77
78
79
    @classmethod
    def from_yaml(cls, filename):
        with open(filename, 'r') as stream:
            return cls(yaml.safe_load(stream.read()))

    @classmethod
80
    def from_args(cls, packages=None, columns=None, users=None):
81
82
83
84
85
86
        config = {}

        # Processing packages
        if packages is None:
            packages = []
        packages = list(itertools.chain(*[item.split(",") for item in packages]))
Louis's avatar
Louis committed
87
88
        if users is not None:
            for user in users:
89
90
                packages.extend(iter_user_packages(user))
        packages = list(set(packages))
91
        config['packages'] = {pkg: {} for pkg in set(packages)}
92

93
94
95
96
97
98
        # Processing columns
        if columns is None:
            columns = []
        columns = list(itertools.chain(*[item.split(",") for item in columns]))
        if columns:
            config['columns'] = columns
99
100

        return cls(config)
101
102
103
104

    def __iter__(self):
        yield from self.packages

Louis's avatar
Louis committed
105
106
107
108
109
110
    def values(self):
        yield from self.packages.values()

    def __getitem__(self, key):
        return self.packages[key]

111
112
113
114
115
116
117
118
119
120
121
    @staticmethod
    def _get_environment():
        TEMPLATEDIR = os.path.join(
            pkg_resources.resource_filename(__name__, 'data'),
            'templates',
            )

        env = jinja2.Environment(loader=jinja2.FileSystemLoader(TEMPLATEDIR))
        env.globals['counter'] = Counter().count
        env.globals['now'] = datetime.datetime.now()
        env.filters['datetime'] = operator.methodcaller('strftime', "%Y-%m-%d %H:%M:%S")
Louis's avatar
TODOs    
Louis committed
122
        env.filters['as_id'] = as_id # TODO: Remove?
123
124
125
126
127
128
        env.filters['color'] = random_color
        return env

    def render(self):
        env = self._get_environment()

129
130
131
132
133
        # Check columns
        columns = []
        for column in self.config['columns']:
            env.get_template("columns/{}.html".format(column))
            columns.append(column)
134
135
136

        # Render
        template = env.get_template("index.html")
Louis's avatar
Louis committed
137
        return template.render(
138
            columns=columns,
139
            packages=self.packages,
Louis's avatar
Louis committed
140
            )
141
142


143
144
class Package:

Louis's avatar
Louis committed
145
    docs_api = {
146
        'readthedocs': slumber.API(base_url="http://readthedocs.io/api/v1/"),
Louis's avatar
Louis committed
147
148
        }

149
    def __init__(self, name):
Louis's avatar
Louis committed
150
        sys.stderr.write("Retrieving information about pypi package '{}'...\n".format(name))
151
152
153
154
        try:
            self.rawdata = json.loads(requests.get("http://pypi.python.org/pypi/{}/json".format(name)).text)
        except Exception as error:
            raise Exception("Error while getting data for package '{}': {}.".format(name, error))
155
        self.name = name
156
        self.info = self.rawdata["info"]
Louis's avatar
Louis committed
157
        self.releases = {}
158
159
        for version, files in self.rawdata['releases'].items():
            if not files:
Louis's avatar
Louis committed
160
                continue
161
162
163
164
            date = datetime.datetime.strptime(
                min((url['upload_time'] for url in files)),
                "%Y-%m-%dT%H:%M:%S",
                )
Louis's avatar
Louis committed
165
            self.releases[date] = {}
166
167
            self.releases[date]['downloads'] = sum((url['downloads'] for url in files))
            self.releases[date]['version'] = version
Louis's avatar
Louis committed
168
169
        if not self.releases:
            raise ValueError("Package '{}' does not exist or haven't any release.".format(name))
Louis's avatar
Louis committed
170
171
172
173
174
175
176
177
178
179
180
181
182

        downloads = 0
        for release in sorted(self.releases):
            self.releases[release]['previous'] = downloads
            downloads += self.releases[release]['downloads']

    def iter_dates(self):
        yield from self.releases.keys()

    @property
    def total_downloads(self):
        last = sorted(self.releases.keys())[-1]
        return self.releases[last]['downloads'] + self.releases[last]['previous']
183

Louis's avatar
Louis committed
184
185
186
187
188
189
190
191
192
    def get_documentation(self):
        # Readthedocs
        if self.docs_api['readthedocs'].project.get(slug=self.name.lower())['objects']:
            return (
                'readthedocs', 
                None,
                )

        # Pypi docs
193
        if self.info['docs_url'] is not None:
Louis's avatar
Louis committed
194
            return ("pypi", None)
Louis's avatar
Louis committed
195
196

        # No documentation
Louis's avatar
Louis committed
197
        return (None, None)