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 (9fe5e2)
by dup
03:09
created

extract_site_definition()   A

Complexity

Conditions 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 9
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 9.6666
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 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__ = "20.10.2017"
36 1
__version__ = "1.0.0"
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
        line = line.strip()
154
155
        if line.count(' ') > 0:
156
            (project, version) = line.split(' ', 1)
157
158
        elif line != '':
159
            project = line
160
            version = ''
161
162
        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
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
175
176
            for line in cache_file:
177
                (project, version) = self._return_project_and_version_from_line(line)
178
                self.cache_dict[project] = version
179
180
            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(project_list, local_dir, debug, feed_url, cache_filename):
410
    """
411
    Checks project's versions on feed_url if any are defined in the yaml
412
    file under the specified tag that got the project_list passed as an argument.
413
    """
414
415 1
    site_cache = FileCache(local_dir, cache_filename)
416
417 1
    for project in project_list:
418 1
        version = get_latest_release_by_title(project, debug, feed_url)
419 1
        if version != '':
420 1
            site_cache.update_cache_dict(project, version, debug)
421
422 1
    site_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
            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, cache_filename_list, debug):
524
    """
525
    Prints all projects and their associated data from the cache
526
    """
527
    for cache_filename in cache_filename_list:
528
        site_cache = FileCache(local_dir, cache_filename)
529
        site_cache.print_cache_dict(cache_filename)
530
    
531
    freshcode_cache = FileCache(local_dir, 'freshcode.cache')
532
    freshcode_cache.print_cache_dict('Freshcode')
533
534
# End of print_versions_from_cache()
535
536
537 1
def extract_site_definition(versions_conf, site_name):
538
    """
539
    extracts whole site definition
540
    """
541
542 1
    if site_name in versions_conf.description:
543 1
        return versions_conf.description[site_name]
544
    else:
545
        return dict()
546
547
# End of extract_site_definition()
548
549
550 1
def extract_project_list_from_site_def(versions_conf, site_name):
551
    """
552
    Extracts a project list from a site by project definition
553
    """
554
555 1
    site_definition = extract_site_definition(versions_conf, site_name)
556
557 1
    if 'projects' in site_definition:
558 1
        project_list = site_definition['projects']
559
    else:
560
        project_list = []
561
562 1
    return project_list
563
564
# End of extract_project_list_from_site_def() function
565
566
567 1
def extract_project_url(versions_conf, site_name):
568
    """
569
    Extracts the url definition where to check project version.
570
    """
571
572 1
    site_definition = extract_site_definition(versions_conf, site_name)
573
574 1
    if 'url' in site_definition:
575 1
        project_url = site_definition['url']
576
    else:
577
        project_url = ''
578
579 1
    return project_url
580
581
# End of extract_project_url() function
582
583
584 1
def check_versions(versions_conf, debug):
585
    """
586
    Checks versions by parsing online feeds
587
    """
588
589
    # Checks projects from by project sites such as github and sourceforge
590 1
    site_list = ['sourceforge', 'github']
591
592 1
    for site_name in site_list:
593
594 1
        print_debug(debug, u'Checking {} projects'.format(site_name))
595 1
        project_list = extract_project_list_from_site_def(versions_conf, site_name)
596 1
        project_url = extract_project_url(versions_conf, site_name)
597 1
        site_cache = u'{}.cache'.format(site_name)
598 1
        check_versions_feeds_by_projects(project_list, versions_conf.local_dir, debug, project_url, site_cache)
599
600
    # Checks projects from freshcode.club
601 1
    print_debug(debug, u'Checking freshcode updates')
602 1
    check_versions_for_freshcode(versions_conf.description['freshcode.club'], versions_conf.local_dir, debug)
603
604
# End of check_versions() function
605
606
607 1
def print_cache_or_check_versions(versions_conf):
608
    """
609
    Decide to pretty print projects and their associated version that
610
    are already in the cache or to check versions of that projects upon
611
    selections made at the command line
612
    """
613
614 1
    debug = versions_conf.options.debug
615 1
    print_debug(debug, u'Loading yaml config file')
616 1
    versions_conf.load_yaml_from_config_file(versions_conf.config_filename)
617
618 1
    if versions_conf.options.list_cache is True:
619
        # Pretty prints all caches.
620
        print_versions_from_cache(versions_conf.local_dir, ['sourceforge.cache', 'github.cache'], debug)
621
622
    else:
623
        # Checks version from online feeds
624 1
        check_versions(versions_conf, debug)
625
626
# End of print_list_or_check_versions() function.
627
628
629 1
def main():
630
    """
631
    This is the where the program begins
632
    """
633
634 1
    versions_conf = Conf()  # Configuration options
635
636 1
    if versions_conf.options.debug:
637 1
        doctest.testmod(verbose=True)
638
639 1
    if os.path.isfile(versions_conf.config_filename):
640 1
        print_cache_or_check_versions(versions_conf)
641
642
    else:
643
        print('Error: file %s does not exist' % versions_conf.config_filename)
644
645
# End of main() function
646
647
648 1
def print_debug(debug, message):
649
    """
650
    Prints 'message' if debug mode is True
651
    """
652
653 1
    if debug:
654 1
        print(message)
655
656
# End of print_debug() function
657
658
659 1
if __name__ == "__main__":
660
    main()
661