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.
Passed
Branch master (6e1fb4)
by dup
01:26 queued 23s
created

check_versions()   A

Complexity

Conditions 1

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 16
ccs 7
cts 7
cp 1
crap 1
rs 9.4285
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 1
import codecs
25 1
import feedparser
26 1
import yaml
27 1
import argparse
28 1
import os
29 1
import errno
30 1
import time
31 1
import doctest
32
33
34 1
__author__ = "Olivier Delhomme <[email protected]>"
35 1
__date__ = "12.10.2017"
36 1
__version__ = "0.0.5"
37
38
"""
39
This program checks projects versions throught RSS and Atom feeds and
40
should only print those with new release version.
41
42
It implements checking for projects in github.com and freshcode.club.
43
Projects must be added to a YAML file (named by default
44
~/.config/versions/versions.yaml). One can use --file=FILENAME option
45
to specify an alternative YAML file.
46
github projects must be listed under a "github.com:" section and
47
freshcode ones must be listed under a "freshcode.club:" section.
48
49
Versions uses and produces text files. Those files are cache files
50
written into ~/.local/versions directory. "*.cache" are cache files
51
containing the project list and their associated version (the latest).
52
"*.feed" are information feed cache files containing on only one line
53
the latest parsed post of the feed.
54
"""
55
56
57 1
class Conf:
58
    """
59
    Class to store configuration of the program
60
    """
61
62 1
    config_dir = ''
63 1
    local_dir = ''
64 1
    config_filename = ''
65 1
    description = {}
66 1
    options = None
67
68 1
    def __init__(self):
69
        """
70
        Inits the class
71
        """
72 1
        self.config_dir = os.path.expanduser("~/.config/versions")
73 1
        self.local_dir = os.path.expanduser("~/.local/versions")
74 1
        self.config_filename = ''  # At this stage we do not know if a filename has been set on the command line
75 1
        self.description = {}
76 1
        self.options = None
77
78
        # Make sure that the directories exists
79 1
        make_directories(self.config_dir)
80 1
        make_directories(self.local_dir)
81
82 1
        self._get_command_line_arguments()
83
84
    # End of init() function
85
86
87 1
    def load_yaml_from_config_file(self, filename):
88
        """
89
        Loads definitions from the YAML config file filename
90
        """
91
92 1
        config_file = codecs.open(filename, 'r', encoding='utf-8')
93
94 1
        self.description = yaml.safe_load(config_file)
95
96 1
        config_file.close()
97
98
    # End of load_yaml_from_config_file() function
99
100
101 1
    def _get_command_line_arguments(self):
102
        """
103
        Defines and gets all the arguments for the command line using
104
        argparse module. This function is called in the __init__ function
105
        of this class.
106
        """
107 1
        str_version = 'versions.py - %s' % __version__
108
109 1
        parser = argparse.ArgumentParser(description='This program checks releases and versions of programs through RSS or Atom feeds', version=str_version)
110
111 1
        parser.add_argument('-f', '--file', action='store', dest='filename', help='Configuration file with projects to check', default='')
112 1
        parser.add_argument('-l', '--list-cache', action='store_true', dest='list_cache', help='Lists all projects and their version in cache', default=False)
113 1
        parser.add_argument('-d', '--debug', action='store_true', dest='debug', help='Starts in debug mode and prints things that may help', default=False)
114
115 1
        self.options = parser.parse_args()
116
117 1
        if self.options.filename != '':
118 1
            self.config_filename = self.options.filename
119
        else:
120
            self.config_filename = os.path.join(self.config_dir, 'versions.yaml')
121
122
    # End of get_command_line_arguments() function
123
# End of Conf class
124
125
126 1
class FileCache:
127
    """
128
    This class should help in managing cache files
129
    """
130
131 1
    cache_filename = ''
132 1
    cache_dict = {}  # Dictionnary of projects and their associated version
133
134 1
    def __init__(self, local_dir, filename):
135
        """
136
        Inits the class. 'local_dir' must be a directory where we want to
137
        store the cache file named 'filename'
138
        """
139
140 1
        self.cache_filename = os.path.join(local_dir, filename)
141 1
        self.cache_dict = {}
142 1
        self._read_cache_file()
143
144
    # End of __init__() function
145
146
147 1
    def _return_project_and_version_from_line(self, line):
148
        """
149
        Splits the line into a project and a version if possible (the line
150
        must contain a whitespace.
151
        """
152
153 1
        line = line.strip()
154
155 1
        if line.count(' ') > 0:
156 1
            (project, version) = line.split(' ', 1)
157
158
        elif line != '':
159
            project = line
160
            version = ''
161
162 1
        return (project, version)
163
164
    # End of _return_project_and_version_from_line() function
165
166
167 1
    def _read_cache_file(self):
168
        """
169
        Reads the cache file and puts it into a dictionnary of project with
170
        their associated version
171
        """
172
173 1
        if os.path.isfile(self.cache_filename):
174 1
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
175
176 1
            for line in cache_file:
177 1
                (project, version) = self._return_project_and_version_from_line(line)
178 1
                self.cache_dict[project] = version
179
180 1
            cache_file.close()
181
182
    # End of _read_cache_file() function
183
184
185 1
    def write_cache_file(self):
186
        """
187
        Owerwrites dictionnary cache to the cache file
188
        """
189
190 1
        cache_file = open_and_truncate_file(self.cache_filename)
191
192 1
        for (project, version) in self.cache_dict.iteritems():
193 1
            cache_file.write('%s %s\n' % (project, version))
194
195 1
        cache_file.close()
196
197
    # End of write_cache_file() function
198
199
200 1
    def update_cache_dict(self, project, version, debug):
201
        """
202
        Updates cache dictionnary if needed
203
        """
204
205 1
        try:
206 1
            version_cache = self.cache_dict[project]
207
            print_debug(debug, u'\t\tIn cache: {}'.format(version_cache))
208
209
            if version != version_cache:
210
                print('%s %s' % (project, version))
211
                self.cache_dict[project] = version
212
213 1
        except KeyError:
214 1
            print('%s %s' % (project, version))
215 1
            self.cache_dict[project] = version
216
217
    # End of update_cache_dict() function
218
219
220 1
    def print_cache_dict(self, sitename):
221
        """
222
        Pretty prints the cache dictionary as it is recorded in the files.
223
        """
224
225
        print('%s:' % sitename)
226
227
        # Gets project and version tuple sorted by project lowered while sorting
228
        for project, version in sorted(self.cache_dict.iteritems(), key=lambda proj: proj[0].lower()):
229
            print('\t%s %s' % (project, version))
230
231
        print('')
232
233
    # End of print_cache_dict() function
234
# End of FileCache class
235
236
237 1
class FeedCache:
238
239 1
    cache_filename = ''
240 1
    year = 2016
241 1
    month = 05
242 1
    day = 1
243 1
    hour = 0
244 1
    minute = 0
245 1
    date_minutes = 0
246
247
248 1
    def __init__(self, local_dir, filename):
249
        """
250
        Inits the class. 'local_dir' must be a directory where we want to
251
        store the cache file named 'filename'
252
        """
253
254 1
        self.cache_filename = os.path.join(local_dir, filename)
255 1
        self.read_cache_feed()
256
257
    # End of __init__() function
258
259
260 1
    def read_cache_feed(self):
261
        """
262
        Reads the cache file which should only contain one date on the
263
        first line
264
        """
265
266 1
        if os.path.isfile(self.cache_filename):
267
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
268
            (self.year, self.month, self.day, self.hour, self.minute) = cache_file.readline().strip().split(' ', 4)
269
            self.date_minutes = self._calculate_minutes(int(self.year), int(self.month), int(self.day), int(self.hour), int(self.minute))
270
            cache_file.close()
271
272
    # End of read_cache_feed() function
273
274
275 1
    def write_cache_feed(self):
276
        """
277
        Overwrites the cache file with values stored in this class
278
        """
279 1
        cache_file = open_and_truncate_file(self.cache_filename)
280
281 1
        cache_file.write('%s %s %s %s %s' % (self.year, self.month, self.day, self.hour, self.minute))
282
283 1
        cache_file.close()
284
285
    # End of write_cache_feed() function
286
287
288 1
    def update_cache_feed(self, date):
289
        """
290
        Updates the values stored in the class with the date which should
291
        be a time.struct_time
292
        """
293
294 1
        self.year = date.tm_year
295 1
        self.month = date.tm_mon
296 1
        self.day = date.tm_mday
297 1
        self.hour = date.tm_hour
298 1
        self.minute = date.tm_min
299 1
        self.date_minutes = self._calculate_minutes_from_date(date)
300
301
    # End of update_cache_feed() function
302
303
304 1
    def _calculate_minutes(self, year, mon, day, hour, mins):
305
        """
306
        Calculate a number of minutes with all parameters and returns
307
        this.
308
        >>> fc = FeedCache('localdir','filename')
309
        >>> fc._calculate_minutes(2016, 5, 1, 0, 0)
310
        1059827040
311
        """
312
313 1
        minutes = (year * 365 * 24 * 60) + \
314
                  (mon * 30 * 24 * 60) + \
315
                  (day * 24 * 60) + \
316
                  (hour * 60) + \
317
                  (mins)
318
319 1
        return minutes
320
321
    # End of _calculate_minutes() function
322
323
324 1
    def _calculate_minutes_from_date(self, date):
325
        """
326
        Transforms a date in a number of minutes to ease comparisons
327
        and returns this number of minutes
328
        """
329
330 1
        return self._calculate_minutes(date.tm_year, date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min)
331
332
    # End of _calculate_minutes() function
333
334
335 1
    def is_newer(self, date):
336
        """
337
        Tells wether "date" is newer than the one in the cache (returns True
338
        or not (returns False)
339
        """
340
341 1
        minutes = self._calculate_minutes_from_date(date)
342
343 1
        if minutes > self.date_minutes:
344 1
            return True
345
346
        else:
347
            return False
348
349
    # End of is_newer() function
350
# End of FeedCache class
351
352
353
######## Below are some utility functions used by classes above ########
354
355
356 1
def make_directories(path):
357
    """
358
    Makes all directories in path if possible. It is not an error if
359
    path already exists.
360
    """
361
362 1
    try:
363 1
        os.makedirs(path)
364
365
    except OSError as exc:
366
367
        if exc.errno != errno.EEXIST or os.path.isdir(path) is not True:
368
            raise
369
370
# End of make_directories() function
371
372
373 1
def open_and_truncate_file(filename):
374
    """
375
    Opens filename for writing truncating it to a zero length file
376
    and returns a python file object.
377
    """
378
379 1
    cache_file = codecs.open(filename, 'w', encoding='utf-8')
380 1
    cache_file.truncate(0)
381 1
    cache_file.flush()
382
383 1
    return cache_file
384
385
# End of open_and_truncate_file() function
386
####################### End of utility functions #######################
387
388
389 1
def get_latest_release_by_title(program, debug, feed_url):
390
    """
391
    Gets the latest release of a program on github. program must be a
392
    string of the form user/repository.
393
    """
394
395 1
    version = ''
396 1
    url = feed_url.format(program)
397 1
    feed = feedparser.parse(url)
398
399 1
    if len(feed.entries) > 0:
400 1
        version = feed.entries[0].title
401
402 1
    print_debug(debug, u'\tProject {}: {}'.format(program, version))
403
404 1
    return version
405
406
# End of get_latest_github_release() function
407
408
409 1
def check_versions_feeds_by_projects(github_project_list, local_dir, debug, feed_url):
410
    """
411
    Checks project's versions on github if any are defined in the yaml
412
    file under the github.com tag.
413
    """
414
415 1
    github_cache = FileCache(local_dir, 'github.cache')
416
417 1
    for project in github_project_list:
418 1
        version = get_latest_release_by_title(project, debug, feed_url)
419 1
        if version != '':
420 1
            github_cache.update_cache_dict(project, version, debug)
421
422 1
    github_cache.write_cache_file()
423
424
# End of check_versions_for_github_projects() function
425
426
427 1
def make_list_of_newer_feeds(feed, feed_info, debug):
428
    """
429
    Compares feed entries and keep those that are newer than the latest
430
    check we've done and inserting the newer ones in reverse order in
431
    a list to be returned
432
    """
433
434 1
    feed_list = []
435
436
    # inserting into a list in reverse order to keep the most recent
437
    # version in case of multiple release of the same project in the
438
    # feeds
439 1
    for a_feed in feed.entries:
440 1
        (project, version) = a_feed.title.strip().split(' ', 1)
441 1
        print_debug(debug, u'\tFeed entry ({0}): project: {1:16} version: {2}'.format(time.strftime('%x %X', a_feed.published_parsed), project, version))
442 1
        if feed_info.is_newer(a_feed.published_parsed):
443 1
            feed_list.insert(0, a_feed)
444
445 1
    return feed_list
446
447
# End of make_list_of_newer_feeds() function
448
449
450 1
def lower_list_of_strings(project_list):
451
    """
452
    Lowers every string in the list to ease sorting and comparisons
453
    """
454
455 1
    project_list_low = [project.lower() for project in project_list]
456
457 1
    return project_list_low
458
459
# End of lower_list_of_strings() function
460
461
462 1
def check_and_update_feed(feed_list, project_list, cache, debug):
463
    """
464
    Checks every feed entry in the list against project list cache and
465
    then updates the dictionnary then writes the cache file to the disk.
466
     - feed_list    is a list of feed (from feedparser module)
467
     - project_list is the list of project as read from the yaml
468
                    configuration file
469
     - cache is an initialized instance of FileCache
470
    """
471
472
    # Lowers the list before searching in it
473 1
    project_list_low = lower_list_of_strings(project_list)
474
475
    # Checking every feed entry that are newer than the last check
476
    # and updates the dictionnary accordingly
477 1
    for entry in feed_list:
478 1
        (project, version) = entry.title.strip().split(' ', 1)
479 1
        print_debug(debug, u'\tChecking {0:16}: {1}'.format(project, version))
480
481 1
        if project.lower() in project_list_low:
482 1
            cache.update_cache_dict(project, version, debug)
483
484 1
    cache.write_cache_file()
485
486
# End of check_and_update_feed()
487
488
489 1
def check_versions_for_freshcode(freshcode_project_list, local_dir, debug):
490
    """
491
    Checks projects with freshcode's web site's RSS
492
    """
493
494 1
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
495
496 1
    url = 'http://freshcode.club/projects.rss'
497 1
    feed = feedparser.parse(url)
498
499 1
    feed_info = FeedCache(local_dir, 'freshcode.feed')
500 1
    feed_info.read_cache_feed()
501
502 1
    length = len(feed.entries)
503
504 1
    if length > 0:
505 1
        print_debug(debug, u'\tFound {} entries'.format(length))
506
507 1
        feed_list = make_list_of_newer_feeds(feed, feed_info, debug)
508 1
        print_debug(debug, u'\tFound {} new entries (relative to {})'.format(len(feed_list), feed_info.date_minutes))
509
510 1
        check_and_update_feed(feed_list, freshcode_project_list, freshcode_cache, debug)
511
512
        # Updating feed_info with the latest parsed feed entry date
513 1
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
514
515
    else:
516
        print_debug(debug, u'No entries found in feed')
517
518 1
    feed_info.write_cache_feed()
519
520
# End of check_versions_for_freshcode() function
521
522
523 1
def print_versions_from_cache(local_dir, debug):
524
    """
525
    Prints all projects and their associated data from the cache
526
    """
527
528
    github_cache = FileCache(local_dir, 'github.cache')
529
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
530
531
    github_cache.print_cache_dict('Github')
532
    freshcode_cache.print_cache_dict('Freshcode')
533
534
# End of print_versions_from_cache()
535
536
537 1
def check_versions(versions_conf, debug):
538
    """
539
    Checks versions by parsing online feeds
540
    """
541
542
    # Checks projects from sourceforge
543 1
    print_debug(debug, u'Checking sourceforge prolects')
544 1
    check_versions_feeds_by_projects(versions_conf.description['sourceforge.net'], versions_conf.local_dir, debug, 'https://sourceforge.net/projects/{}/rss?path=/')
545
546
    # Checks projects from github
547 1
    print_debug(debug, u'Checking github prolects')
548 1
    check_versions_feeds_by_projects(versions_conf.description['github.com'], versions_conf.local_dir, debug, 'https://github.com/{}/releases.atom')
549
550
    # Checks projects from freshcode.club
551 1
    print_debug(debug, u'Checking freshcode updates')
552 1
    check_versions_for_freshcode(versions_conf.description['freshcode.club'], versions_conf.local_dir, debug)
553
554
555 1
def print_cache_or_check_versions(versions_conf):
556
    """
557
    Decide to pretty print projects and their associated version that
558
    are already in the cache or to check versions of that projects upon
559
    selections made at the command line
560
    """
561
562 1
    debug = versions_conf.options.debug
563 1
    print_debug(debug, u'Loading yaml config file')
564 1
    versions_conf.load_yaml_from_config_file(versions_conf.config_filename)
565
566 1
    if versions_conf.options.list_cache is True:
567
        # Pretty prints all caches.
568
        print_versions_from_cache(versions_conf.local_dir, debug)
569
570
    else:
571 1
        check_versions(versions_conf, debug)
572
573
# End of print_list_or_check_versions() function.
574
575
576 1
def main():
577
    """
578
    This is the where the program begins
579
    """
580
581 1
    versions_conf = Conf()  # Configuration options
582
583 1
    if versions_conf.options.debug:
584 1
        doctest.testmod(verbose=True)
585
586 1
    if os.path.isfile(versions_conf.config_filename):
587 1
        print_cache_or_check_versions(versions_conf)
588
589
    else:
590
        print('Error: file %s does not exist' % versions_conf.config_filename)
591
592
# End of main() function
593
594
595 1
def print_debug(debug, message):
596
    """
597
    Prints 'message' if debug mode is True
598
    """
599
600 1
    if debug:
601 1
        print(message)
602
603
# End of print_debug() function
604
605
606 1
if __name__ == "__main__":
607
    main()
608