GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 7ee1f0...16656d )
by dup
01:12
created

Conf   A

Complexity

Total Complexity 3

Size/Duplication

Total Lines 57
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 57
rs 10
wmc 3

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 15 1
A load_yaml_from_config_file() 0 10 1
A _get_command_line_arguments() 0 13 1
1
#!/usr/bin/env python
2
# -*- coding: utf8 -*-
3
#
4
#  versions.py : checks releases and versions of programs through RSS
5
#                or Atom feeds and tells you
6
#
7
#  (C) Copyright 2016 Olivier Delhomme
8
#  e-mail : [email protected]
9
#
10
#  This program is free software; you can redistribute it and/or modify
11
#  it under the terms of the GNU General Public License as published by
12
#  the Free Software Foundation; either version 2, or (at your option)
13
#  any later version.
14
#
15
#  This program is distributed in the hope that it will be useful,
16
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
17
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
#  GNU General Public License for more details.
19
#
20
#  You should have received a copy of the GNU General Public License
21
#  along with this program; if not, write to the Free Software Foundation,
22
#  Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23
#
24
import feedparser
25
import yaml
26
import argparse
27
import os
28
import errno
29
30
__author__ = "Olivier Delhomme <[email protected]>"
31
__date__ = "09.06.2016"
32
__version__ = "0.0.1"
33
34
"""
35
This program checks projects versions throught RSS and Atom feeds and
36
should only print those with new release version.
37
38
It implements checking for projects in github.com and freshcode.club.
39
Projects must be added to a YAML file (named by default
40
~/.config/versions/versions.yaml). One can use --file=FILENAME option
41
to specify an alternative YAML file.
42
github projects must be listed under a "github.com:" section and
43
freshcode ones must be listed under a "freshcode.club:" section.
44
45
Versions uses and produces text files. Those files are cache files
46
written into ~/.local/versions directory. "*.cache" are cache files
47
containing the project list and their associated version (the latest).
48
"*.feed" are information feed cache files containing on only one line
49
the latest parsed post of the feed.
50
"""
51
52
53
class Conf:
54
    """
55
    Class to store configuration of the program
56
    """
57
58
    config_dir = ''
59
    local_dir = ''
60
    config_filename = ''
61
    description = {}
62
    options = None
63
64
    def __init__(self):
65
        """
66
        Inits the class
67
        """
68
        self.config_dir = os.path.expanduser("~/.config/versions")
69
        self.local_dir = os.path.expanduser("~/.local/versions")
70
        config_filename = '' # At this stage we do not know if a filename has been set on the command line
71
        description = {}
72
        options = None
73
74
        # Make sure that the directories exists
75
        make_directories(self.config_dir)
76
        make_directories(self.local_dir)
77
78
        self._get_command_line_arguments()
79
80
    # End of init() function
81
82
83
    def load_yaml_from_config_file(self, filename):
84
        """
85
        Loads definitions from the YAML config file filename
86
        """
87
88
        config_file = open(filename, 'r')
89
90
        self.description = yaml.safe_load(config_file)
91
92
        config_file.close()
93
94
    # End of load_yaml_from_config_file() function
95
96
97
    def _get_command_line_arguments(self):
98
        """
99
        Defines and gets all the arguments for the command line using
100
        argparse module. This function is called in the __init__ function
101
        of this class.
102
        """
103
104
        parser = argparse.ArgumentParser(description='This program checks releases and versions of programs through RSS or Atom feeds', version='versions - 0.0.1')
105
106
        parser.add_argument('--file', action='store', dest='filename', help='Configuration file with projects to check', default='versions.yaml')
107
108
        self.options = parser.parse_args()
109
        self.config_filename = os.path.join(self.config_dir, self.options.filename)
110
111
    # End of get_command_line_arguments() function
112
# End of Conf class
113
114
115
class FileCache:
116
    """
117
    This class should help in managing cache files
118
    """
119
120
    cache_filename = ''
121
    cache_dict = {}  # Dictionnary of projects and their associated version
122
123
    def __init__(self, local_dir, filename):
124
        """
125
        Inits the class. 'local_dir' must be a directory where we want to
126
        store the cache file named 'filename'
127
        """
128
129
        self.cache_filename = os.path.join(local_dir, filename)
130
        self.cache_dict = {}
131
        self._read_cache_file()
132
133
    # End of __init__() function
134
135
136
    def _read_cache_file(self):
137
        """
138
        Reads the cache file and puts it into a dictionnary of project with
139
        their associated version
140
        """
141
142
        if os.path.isfile(self.cache_filename):
143
144
            cache_file = open(self.cache_filename, 'r')
145
146
            for line in cache_file:
147
148
                line = line.strip()
149
150
                if line.count(' ') > 0:
151
                    (project, version) = line.split(' ', 1)
152
153
                elif line != '':
154
                    project = line
155
                    version = ''
156
157
                self.cache_dict[project] = version
158
159
            cache_file.close()
160
161
    # End of _read_cache_file() function
162
163
164
    def write_cache_file(self):
165
        """
166
        Owerwrites dictionnary cache to the cache file
167
        """
168
169
        cache_file = open_and_truncate_file(self.cache_filename)
170
171
        for (project, version) in self.cache_dict.iteritems():
172
            cache_file.write('%s %s\n' % (project, version))
173
174
        cache_file.close()
175
176
    # End of write_cache_file() function
177
178
179
    def update_cache_dict(self, project, version):
180
        """
181
        Updates cache dictionnary if needed
182
        """
183
184
        try:
185
            version_cache = self.cache_dict[project]
186
187
            if version != version_cache:
188
                print('%s %s' % (project, version))
189
                self.cache_dict[project] = version
190
191
        except KeyError:
192
            print('%s %s' % (project, version))
193
            self.cache_dict[project] = version
194
195
    # End of update_cache_dict() function
196
# End of FileCache class
197
198
199
class FeedCache:
200
201
    cache_filename = ''
202
    year = 2016
203
    month = 05
204
    day = 1
205
    hour = 0
206
    minute = 0
207
    date_minutes = 0
208
209
210
    def __init__(self, local_dir, filename):
211
        """
212
        Inits the class. 'local_dir' must be a directory where we want to
213
        store the cache file named 'filename'
214
        """
215
216
        self.cache_filename = os.path.join(local_dir, filename)
217
        self.year = 2016
218
        self.month = 5
219
        self.day = 1
220
        self.hour = 0
221
        self.minute = 0
222
        self.date_minutes = self._calculate_minutes(self.year, self.month, self.day, self.hour, self.minute)
223
224
    # End of __init__() function
225
226
227
    def read_cache_feed(self):
228
        """
229
        Reads the cache file which should only contain one date on the
230
        first line
231
        """
232
233
        if os.path.isfile(self.cache_filename):
234
            cache_file = open(self.cache_filename, 'r')
235
            (self.year, self.month, self.day, self.hour, self.minute) = cache_file.readline().strip().split(' ', 4)
236
            self._calculate_minutes(self.year, self.month, self.day, self.hour, self.minute)
237
            cache_file.close()
238
239
    # End of read_cache_feed() function
240
241
242
    def write_cache_feed(self):
243
        """
244
        Overwrites the cache file with values stored in this class
245
        """
246
        cache_file = open_and_truncate_file(self.cache_filename)
247
248
        cache_file.write('%s %s %s %s %s' % (self.year, self.month, self.day, self.hour, self.minute))
249
250
        cache_file.close()
251
252
    # End of write_cache_feed() function
253
254
255
    def update_cache_feed(self, date):
256
        """
257
        Updates the values stored in the class with the date which should
258
        be a time.struct_time
259
        """
260
261
        self.year = date.tm_year
262
        self.month = date.tm_mon
263
        self.day = date.tm_mday
264
        self.hour = date.tm_hour
265
        self.minute = date.tm_min
266
        self.date_minutes = self._calculate_minutes_from_date(date)
267
268
    # End of update_cache_feed() function
269
270
271
    def _calculate_minutes(self, year, mon, day, hour, mins):
272
        """
273
        Calculate a number of minutes with all parameters and returns
274
        this.
275
        """
276
277
        minutes = (year * 365 * 24 * 60) + \
278
                  (mon * 30 * 24 * 60) + \
279
                  (day * 24 * 60) + \
280
                  (hour * 60) + \
281
                  (mins)
282
283
        return minutes
284
285
    # End of _calculate_minutes() function
286
287
288
    def _calculate_minutes_from_date(self, date):
289
        """
290
        Transforms a date in a number of minutes to ease comparisons
291
        and returns this number of minutes
292
        """
293
294
        return self._calculate_minutes(date.tm_year, date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min)
295
296
    # End of _calculate_minutes() function
297
298
299
    def is_newer(self, date):
300
        """
301
        Tells wether "date" is newer than the one in the cache (returns True
302
        or not (returns False)
303
        """
304
305
        minutes = self._calculate_minutes_from_date(date)
306
307
        if minutes > self.date_minutes:
308
            return True
309
        else:
310
            return False
311
312
    # End of is_newer() function
313
# End of FeedCache class
314
315
316
######## Below are some utility functions used by classes above ########
317
318
def make_directories(path):
319
    """
320
    Makes all directories in path if possible. It is not an error if
321
    path already exists.
322
    """
323
324
    try:
325
        os.makedirs(path)
326
327
    except OSError as exc:
328
329
        if exc.errno != errno.EEXIST or os.path.isdir(path) != True:
330
            raise
331
332
# End of make_directories() function
333
334
335
def open_and_truncate_file(filename):
336
    """
337
    Opens filename for writing truncating it to a zero length file
338
    and returns a python file object.
339
    """
340
341
    cache_file = open(filename, 'w')
342
    cache_file.truncate(0)
343
    cache_file.flush()
344
345
    return cache_file
346
347
# End of open_and_truncate_file() function
348
####################### End of utility functions #######################
349
350
351
def get_latest_github_release(program):
352
    """
353
    Gets the latest release of a program on github. program must be a
354
    string of the form user/repository.
355
    """
356
357
    version = ''
358
    url = 'https://github.com/' + program + '/releases.atom'
359
    feed = feedparser.parse(url)
360
361
    if len(feed.entries) > 0:
362
        version = feed.entries[0].title
363
364
    return version
365
366
# End of get_latest_github_release() function
367
368
369
def check_versions_for_github_projects(github_project_list, local_dir):
370
    """
371
    Checks project's versions on github if any are defined in the yaml
372
    file under the github.com tag.
373
    """
374
375
    github_cache = FileCache(local_dir, 'github.cache')
376
377
    for project in github_project_list:
378
379
        version = get_latest_github_release(project)
380
        github_cache.update_cache_dict(project, version)
381
382
    github_cache.write_cache_file()
383
384
# End of check_versions_for_github_projects() function
385
386
387
def make_list_of_newer_feeds(feed, feed_info):
388
    """
389
    Compares feed entries and keep those that are newer than the latest
390
    check we've done and inserting the newer ones in reverse order in
391
    a list to be returned
392
    """
393
394
    feed_list = []
395
396
    # inserting into a list in reverse order to keep the most recent
397
    # version in case of multiple release of the same project in the
398
    # feeds
399
    for a_feed in feed.entries:
400
        if feed_info.is_newer(a_feed.published_parsed):
401
            feed_list.insert(0, a_feed)
402
403
    return feed_list
404
405
# End of make_list_of_newer_feeds() function
406
407
408
def check_and_update_feed(feed_list, project_list, cache):
409
    """
410
    Checks every feed entry in the list against project list cache and
411
    then updates the dictionnary if needed.
412
     - feed_list    is a list of feed (from feedparser module)
413
     - project_list is the list of project as read from the yaml
414
                    configuration file
415
     - cache is an initialized instance of FileCache
416
    """
417
418
    # Checking every feed entry that are newer than the last check
419
    # and updates the dictionnary accordingly
420
    for entry in feed_list:
421
        (project, version) = entry.title.strip().split(' ', 1)
422
423
        if project in project_list:
424
            cache.update_cache_dict(project, version)
425
426
    cache.write_cache_file()
427
428
# End of check_and_update_feed()
429
430
431
def check_versions_for_freshcode(freshcode_project_list, local_dir):
432
    """
433
    Checks projects with freshcode's web site's RSS
434
    """
435
436
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
437
438
    url = 'http://freshcode.club/projects.rss'
439
    feed = feedparser.parse(url)
440
441
    feed_info = FeedCache(local_dir, 'freshcode.feed')
442
    feed_info.read_cache_feed()
443
444
    if len(feed.entries) > 0:
445
446
        feed_list = make_list_of_newer_feeds(feed, feed_info)
447
        check_and_update_feed(feed_list, freshcode_project_list, freshcode_cache)
448
449
        # Updating feed_info with the latest parsed feed entry date
450
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
451
452
    feed_info.write_cache_feed()
453
454
# End of check_versions_for_freshcode() function
455
456
457
def main():
458
    """
459
    This is the where the program begins
460
    """
461
462
    versions_conf = Conf()  # Configuration options
463
464
    if os.path.isfile(versions_conf.config_filename):
465
466
        versions_conf.load_yaml_from_config_file(versions_conf.config_filename)
467
468
        # Checks projects from github
469
        check_versions_for_github_projects(versions_conf.description['github.com'], versions_conf.local_dir)
470
471
        # Checks projects from freshcode.club
472
        check_versions_for_freshcode(versions_conf.description['freshcode.club'], versions_conf.local_dir)
473
474
    else:
475
        print('Error: file %s does not exist' % config_filename)
476
477
# End of main() function
478
479
480
if __name__=="__main__" :
481
    main()
482