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 ( 16656d...1cf4cb )
by dup
01:23
created

FileCache._read_cache_file()   A

Complexity

Conditions 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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