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 ( 2616f0...5d1213 )
by dup
01:15
created

check_and_update_feed()   B

Complexity

Conditions 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 4
c 3
b 0
f 0
dl 0
loc 24
rs 8.6845
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
    project_list_low = []
444
    for project in project_list:
445
        project_list_low.insert(0, project.lower())
446
447
448
    # Checking every feed entry that are newer than the last check
449
    # and updates the dictionnary accordingly
450
    for entry in feed_list:
451
        (project, version) = entry.title.strip().split(' ', 1)
452
453
        if project.lower() in project_list_low:
454
            cache.update_cache_dict(project, version)
455
456
    cache.write_cache_file()
457
458
# End of check_and_update_feed()
459
460
461
def check_versions_for_freshcode(freshcode_project_list, local_dir):
462
    """
463
    Checks projects with freshcode's web site's RSS
464
    """
465
466
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
467
468
    url = 'http://freshcode.club/projects.rss'
469
    feed = feedparser.parse(url)
470
471
    feed_info = FeedCache(local_dir, 'freshcode.feed')
472
    feed_info.read_cache_feed()
473
474
    if len(feed.entries) > 0:
475
476
        feed_list = make_list_of_newer_feeds(feed, feed_info)
477
        check_and_update_feed(feed_list, freshcode_project_list, freshcode_cache)
478
479
        # Updating feed_info with the latest parsed feed entry date
480
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
481
482
    feed_info.write_cache_feed()
483
484
# End of check_versions_for_freshcode() function
485
486
487
def print_versions_from_cache(local_dir):
488
    """
489
    Prints all projects and their associated data from the cache
490
    """
491
492
    github_cache = FileCache(local_dir, 'github.cache')
493
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
494
495
    github_cache.print_cache_dict('Github')
496
    freshcode_cache.print_cache_dict('Freshcode')
497
498
# End of print_versions_from_cache()
499
500
501
def print_cache_or_check_versions(versions_conf):
502
    """
503
    Decide to pretty print projects and their associated version that
504
    are already in the cache or to check versions of that projects upon
505
    selections made at the command line
506
    """
507
508
    versions_conf.load_yaml_from_config_file(versions_conf.config_filename)
509
510
    if versions_conf.options.list_cache == True:
511
512
        # Pretty prints all caches.
513
        print_versions_from_cache(versions_conf.local_dir)
514
515
    else:
516
517
        # Checks projects from github
518
        check_versions_for_github_projects(versions_conf.description['github.com'], versions_conf.local_dir)
519
520
        # Checks projects from freshcode.club
521
        check_versions_for_freshcode(versions_conf.description['freshcode.club'], versions_conf.local_dir)
522
523
# End of print_list_or_check_versions() function.
524
525
526
def main():
527
    """
528
    This is the where the program begins
529
    """
530
531
    versions_conf = Conf()  # Configuration options
532
533
    if os.path.isfile(versions_conf.config_filename):
534
535
        print_cache_or_check_versions(versions_conf)
536
537
    else:
538
        print('Error: file %s does not exist' % config_filename)
539
540
# End of main() function
541
542
543
if __name__=="__main__" :
544
    main()
545