__init__.py 5.45 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
56
57
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):
        self.config = config
Louis's avatar
Louis committed
58
        self.packages = {}
59
        for name in sorted(config['columns']):
Louis's avatar
Louis committed
60
61
            try:
                self.packages[name] = Package(name)
62
63
            except ValueError as error:
                sys.stderr.write("Ignoring package '{}' with no releases: {}.\n".format(name, error))
Louis's avatar
Louis committed
64
                continue
65

66
67
68
69
70
71
72
73
74
75
76
77
78
    @classmethod
    def from_yaml(cls, filename):
        with open(filename, 'r') as stream:
            return cls(yaml.safe_load(stream.read()))

    @classmethod
    def from_args(cls, packages=None, rows=None, users=None):
        config = {}

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

        # Processing rows
        if rows is None:
            rows = []
        rows = list(itertools.chain(*[item.split(",") for item in rows]))
        if rows:
            config['rows'] = rows

        return cls(config)
93
94
95
96

    def __iter__(self):
        yield from self.packages

Louis's avatar
Louis committed
97
98
99
100
101
102
    def values(self):
        yield from self.packages.values()

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

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
    @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")
        env.filters['as_id'] = as_id
        env.filters['color'] = random_color
        return env

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

        # Check rows
        rows = []
        for row in self.config['rows']:
            env.get_template("rows/{}.html".format(row))
            rows.append(row)

        # Render
        template = env.get_template("index.html")
        return bytes(template.render(
            rows=rows,
            packages=self.packages,
            ), "utf-8")


135
136
class Package:

Louis's avatar
Louis committed
137
    docs_api = {
138
        'readthedocs': slumber.API(base_url="http://readthedocs.io/api/v1/"),
Louis's avatar
Louis committed
139
140
        }

141
    def __init__(self, name):
Louis's avatar
Louis committed
142
        sys.stderr.write("Retrieving information about pypi package '{}'...\n".format(name))
143
144
145
146
        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))
147
        self.name = name
148
        self.info = self.rawdata["info"]
Louis's avatar
Louis committed
149
        self.releases = {}
150
151
        for version, files in self.rawdata['releases'].items():
            if not files:
Louis's avatar
Louis committed
152
                continue
153
154
155
156
            date = datetime.datetime.strptime(
                min((url['upload_time'] for url in files)),
                "%Y-%m-%dT%H:%M:%S",
                )
Louis's avatar
Louis committed
157
            self.releases[date] = {}
158
159
            self.releases[date]['downloads'] = sum((url['downloads'] for url in files))
            self.releases[date]['version'] = version
Louis's avatar
Louis committed
160
161
        if not self.releases:
            raise ValueError("Package '{}' does not exist or haven't any release.".format(name))
Louis's avatar
Louis committed
162
163
164
165
166
167
168
169
170
171
172
173
174

        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']
175

Louis's avatar
Louis committed
176
177
178
179
180
181
182
183
184
    def get_documentation(self):
        # Readthedocs
        if self.docs_api['readthedocs'].project.get(slug=self.name.lower())['objects']:
            return (
                'readthedocs', 
                None,
                )

        # Pypi docs
185
        if self.info['docs_url'] is not None:
Louis's avatar
Louis committed
186
            return ("pypi", None)
Louis's avatar
Louis committed
187
188

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