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 ( 02f883...2616f0 )
by dup
01:12
created

print_cache_or_check_versions()   A

Complexity

Conditions 2

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 21
rs 9.3142
c 0
b 0
f 0
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__ = "30.07.2016"
32
__version__ = "0.0.2"
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('-f', '--file', action='store', dest='filename', help='Configuration file with projects to check', default='versions.yaml')
107
        parser.add_argument('-l', '--list-cache', action='store_true', dest='list_cache', help='Lists all projects and their version in cache', default=False)
108
109
        self.options = parser.parse_args()
110
        self.config_filename = os.path.join(self.config_dir, self.options.filename)
111
112
    # End of get_command_line_arguments() function
113
# End of Conf class
114
115
116
class FileCache:
117
    """
118
    This class should help in managing cache files
119
    """
120
121
    cache_filename = ''
122
    cache_dict = {}  # Dictionnary of projects and their associated version
123
124
    def __init__(self, local_dir, filename):
125
        """
126
        Inits the class. 'local_dir' must be a directory where we want to
127
        store the cache file named 'filename'
128
        """
129
130
        self.cache_filename = os.path.join(local_dir, filename)
131
        self.cache_dict = {}
132
        self._read_cache_file()
133
134
    # End of __init__() function
135
136
137
    def _return_project_and_version_from_line(self, line):
138
        """
139
        Splits the line into a project and a version if possible (the line
140
        must contain a whitespace.
141
        """
142
143
        line = line.strip()
144
145
        if line.count(' ') > 0:
146
            (project, version) = line.split(' ', 1)
147
148
        elif line != '':
149
            project = line
150
            version = ''
151
152
        return (project, version)
153
154
    # End of _return_project_and_version_from_line() function
155
156
157
    def _read_cache_file(self):
158
        """
159
        Reads the cache file and puts it into a dictionnary of project with
160
        their associated version
161
        """
162
163
        if os.path.isfile(self.cache_filename):
164
165
            cache_file = open(self.cache_filename, 'r')
166
167
            for line in cache_file:
168
169
                (project, version) = self._return_project_and_version_from_line(line)
170
171
                self.cache_dict[project] = version
172
173
            cache_file.close()
174
175
    # End of _read_cache_file() function
176
177
178
    def write_cache_file(self):
179
        """
180
        Owerwrites dictionnary cache to the cache file
181
        """
182
183
        cache_file = open_and_truncate_file(self.cache_filename)
184
185
        for (project, version) in self.cache_dict.iteritems():
186
            cache_file.write('%s %s\n' % (project, version))
187
188
        cache_file.close()
189
190
    # End of write_cache_file() function
191
192
193
    def update_cache_dict(self, project, version):
194
        """
195
        Updates cache dictionnary if needed
196
        """
197
198
        try:
199
            version_cache = self.cache_dict[project]
200
201
            if version != version_cache:
202
                print('%s %s' % (project, version))
203
                self.cache_dict[project] = version
204
205
        except KeyError:
206
            print('%s %s' % (project, version))
207
            self.cache_dict[project] = version
208
209
    # End of update_cache_dict() function
210
211
212
    def print_cache_dict(self, sitename):
213
        """
214
        Pretty prints the cache dictionary as it is recorded in the files.
215
        """
216
217
        print('%s:' % sitename)
218
219
        # Gets project and version tuple sorted by project lowered while sorting
220
        for project, version in sorted(self.cache_dict.iteritems(), key=lambda proj: proj[0].lower()):
221
            print('\t%s %s' % (project, version))
222
223
        print('')
224
225
    # End of print_cache_dict() function
226
# End of FileCache class
227
228
229
class FeedCache:
230
231
    cache_filename = ''
232
    year = 2016
233
    month = 05
234
    day = 1
235
    hour = 0
236
    minute = 0
237
    date_minutes = 0
238
239
240
    def __init__(self, local_dir, filename):
241
        """
242
        Inits the class. 'local_dir' must be a directory where we want to
243
        store the cache file named 'filename'
244
        """
245
246
        self.cache_filename = os.path.join(local_dir, filename)
247
        self.read_cache_feed()
248
249
    # End of __init__() function
250
251
252
    def read_cache_feed(self):
253
        """
254
        Reads the cache file which should only contain one date on the
255
        first line
256
        """
257
258
        if os.path.isfile(self.cache_filename):
259
            cache_file = open(self.cache_filename, 'r')
260
            (self.year, self.month, self.day, self.hour, self.minute) = cache_file.readline().strip().split(' ', 4)
261
            self.date_minutes = self._calculate_minutes(self.year, self.month, self.day, self.hour, self.minute)
262
            cache_file.close()
263
264
    # End of read_cache_feed() function
265
266
267
    def write_cache_feed(self):
268
        """
269
        Overwrites the cache file with values stored in this class
270
        """
271
        cache_file = open_and_truncate_file(self.cache_filename)
272
273
        cache_file.write('%s %s %s %s %s' % (self.year, self.month, self.day, self.hour, self.minute))
274
275
        cache_file.close()
276
277
    # End of write_cache_feed() function
278
279
280
    def update_cache_feed(self, date):
281
        """
282
        Updates the values stored in the class with the date which should
283
        be a time.struct_time
284
        """
285
286
        self.year = date.tm_year
287
        self.month = date.tm_mon
288
        self.day = date.tm_mday
289
        self.hour = date.tm_hour
290
        self.minute = date.tm_min
291
        self.date_minutes = self._calculate_minutes_from_date(date)
292
293
    # End of update_cache_feed() function
294
295
296
    def _calculate_minutes(self, year, mon, day, hour, mins):
297
        """
298
        Calculate a number of minutes with all parameters and returns
299
        this.
300
        """
301
302
        minutes = (year * 365 * 24 * 60) + \
303
                  (mon * 30 * 24 * 60) + \
304
                  (day * 24 * 60) + \
305
                  (hour * 60) + \
306
                  (mins)
307
308
        return minutes
309
310
    # End of _calculate_minutes() function
311
312
313
    def _calculate_minutes_from_date(self, date):
314
        """
315
        Transforms a date in a number of minutes to ease comparisons
316
        and returns this number of minutes
317
        """
318
319
        return self._calculate_minutes(date.tm_year, date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min)
320
321
    # End of _calculate_minutes() function
322
323
324
    def is_newer(self, date):
325
        """
326
        Tells wether "date" is newer than the one in the cache (returns True
327
        or not (returns False)
328
        """
329
330
        minutes = self._calculate_minutes_from_date(date)
331
332
        if minutes > self.date_minutes:
333
            return True
334
        else:
335
            return False
336
337
    # End of is_newer() function
338
# End of FeedCache class
339
340
341
######## Below are some utility functions used by classes above ########
342
343
def make_directories(path):
344
    """
345
    Makes all directories in path if possible. It is not an error if
346
    path already exists.
347
    """
348
349
    try:
350
        os.makedirs(path)
351
352
    except OSError as exc:
353
354
        if exc.errno != errno.EEXIST or os.path.isdir(path) != True:
355
            raise
356
357
# End of make_directories() function
358
359
360
def open_and_truncate_file(filename):
361
    """
362
    Opens filename for writing truncating it to a zero length file
363
    and returns a python file object.
364
    """
365
366
    cache_file = open(filename, 'w')
367
    cache_file.truncate(0)
368
    cache_file.flush()
369
370
    return cache_file
371
372
# End of open_and_truncate_file() function
373
####################### End of utility functions #######################
374
375
376
def get_latest_github_release(program):
377
    """
378
    Gets the latest release of a program on github. program must be a
379
    string of the form user/repository.
380
    """
381
382
    version = ''
383
    url = 'https://github.com/' + program + '/releases.atom'
384
    feed = feedparser.parse(url)
385
386
    if len(feed.entries) > 0:
387
        version = feed.entries[0].title
388
389
    return version
390
391
# End of get_latest_github_release() function
392
393
394
def check_versions_for_github_projects(github_project_list, local_dir):
395
    """
396
    Checks project's versions on github if any are defined in the yaml
397
    file under the github.com tag.
398
    """
399
400
    github_cache = FileCache(local_dir, 'github.cache')
401
402
    for project in github_project_list:
403
404
        version = get_latest_github_release(project)
405
        github_cache.update_cache_dict(project, version)
406
407
    github_cache.write_cache_file()
408
409
# End of check_versions_for_github_projects() function
410
411
412
def make_list_of_newer_feeds(feed, feed_info):
413
    """
414
    Compares feed entries and keep those that are newer than the latest
415
    check we've done and inserting the newer ones in reverse order in
416
    a list to be returned
417
    """
418
419
    feed_list = []
420
421
    # inserting into a list in reverse order to keep the most recent
422
    # version in case of multiple release of the same project in the
423
    # feeds
424
    for a_feed in feed.entries:
425
        if feed_info.is_newer(a_feed.published_parsed):
426
            feed_list.insert(0, a_feed)
427
428
    return feed_list
429
430
# End of make_list_of_newer_feeds() function
431
432
433
def check_and_update_feed(feed_list, project_list, cache):
434
    """
435
    Checks every feed entry in the list against project list cache and
436
    then updates the dictionnary then writes the cache file to the disk.
437
     - feed_list    is a list of feed (from feedparser module)
438
     - project_list is the list of project as read from the yaml
439
                    configuration file
440
     - cache is an initialized instance of FileCache
441
    """
442
443
    # Checking every feed entry that are newer than the last check
444
    # and updates the dictionnary accordingly
445
    for entry in feed_list:
446
        (project, version) = entry.title.strip().split(' ', 1)
447
448
        if project in project_list:
449
            cache.update_cache_dict(project, version)
450
451
    cache.write_cache_file()
452
453
# End of check_and_update_feed()
454
455
456
def check_versions_for_freshcode(freshcode_project_list, local_dir):
457
    """
458
    Checks projects with freshcode's web site's RSS
459
    """
460
461
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
462
463
    url = 'http://freshcode.club/projects.rss'
464
    feed = feedparser.parse(url)
465
466
    feed_info = FeedCache(local_dir, 'freshcode.feed')
467
    feed_info.read_cache_feed()
468
469
    if len(feed.entries) > 0:
470
471
        feed_list = make_list_of_newer_feeds(feed, feed_info)
472
        check_and_update_feed(feed_list, freshcode_project_list, freshcode_cache)
473
474
        # Updating feed_info with the latest parsed feed entry date
475
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
476
477
    feed_info.write_cache_feed()
478
479
# End of check_versions_for_freshcode() function
480
481
482
def print_versions_from_cache(local_dir):
483
    """
484
    Prints all projects and their associated data from the cache
485
    """
486
487
    github_cache = FileCache(local_dir, 'github.cache')
488
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
489
490
    github_cache.print_cache_dict('Github')
491
    freshcode_cache.print_cache_dict('Freshcode')
492
493
# End of print_versions_from_cache()
494
495
496
def print_cache_or_check_versions(versions_conf):
497
    """
498
    Decide to pretty print projects and their associated version that
499
    are already in the cache or to check versions of that projects upon
500
    selections made at the command line
501
    """
502
503
    versions_conf.load_yaml_from_config_file(versions_conf.config_filename)
504
505
    if versions_conf.options.list_cache == True:
506
507
        # Pretty prints all caches.
508
        print_versions_from_cache(versions_conf.local_dir)
509
510
    else:
511
512
        # Checks projects from github
513
        check_versions_for_github_projects(versions_conf.description['github.com'], versions_conf.local_dir)
514
515
        # Checks projects from freshcode.club
516
        check_versions_for_freshcode(versions_conf.description['freshcode.club'], versions_conf.local_dir)
517
518
# End of print_list_or_check_versions() function.
519
520
521
def main():
522
    """
523
    This is the where the program begins
524
    """
525
526
    versions_conf = Conf()  # Configuration options
527
528
    if os.path.isfile(versions_conf.config_filename):
529
530
        print_cache_or_check_versions(versions_conf)
531
532
    else:
533
        print('Error: file %s does not exist' % config_filename)
534
535
# End of main() function
536
537
538
if __name__=="__main__" :
539
    main()
540