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
Branch master (037a2b)
by dup
01:30 queued 31s
created

lower_list_of_strings()   A

Complexity

Conditions 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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