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
Push — master ( d45b0a...2e6b2b )
by dup
02:05
created

versions.py (3 issues)

Severity
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 - 2018 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 3, 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 sys
26 1
import locale
27 1
import argparse
28 1
import os
29 1
import re
30 1
import errno
31 1
import time
32 1
import doctest
33 1
import feedparser
34 1
import yaml
35 1
import operator
36
37 1
__author__ = "Olivier Delhomme <[email protected]>"
38 1
__date__ = "06.11.2018"
39 1
__version__ = "1.5.2"
40
41
"""
42
This program checks projects versions through RSS and Atom feeds and
43
should only print those with new release version.
44
45
It implements checking for projects in github.com and freshcode.club.
46
Projects must be added to a YAML file (named by default
47
~/.config/versions/versions.yaml). One can use --file=FILENAME option
48
to specify an alternative YAML file. version.yaml is included as an
49
example in this project.
50
51
Versions uses and produces text files. Those files are cache files
52
written into ~/.local/versions directory. "*.cache" are cache files
53
containing the project list and their associated version (the latest).
54
"*.feed" are information feed cache files containing on only one line
55
the latest parsed post of the feed.
56
"""
57
58
59 1
class Conf:
60
    """
61
    Class to store configuration of the program and check version.
62
    """
63
64 1
    config_dir = ''
65 1
    local_dir = ''
66 1
    config_filename = ''
67 1
    description = {}
68 1
    options = None
69
70 1
    def __init__(self):
71
        """
72
        Inits the class
73
        """
74 1
        self.config_dir = os.path.expanduser("~/.config/versions")
75 1
        self.local_dir = os.path.expanduser("~/.local/versions")
76 1
        self.config_filename = ''  # At this stage we do not know if a filename has been set on the command line
77 1
        self.description = {}
78 1
        self.options = None
79
80
        # Make sure that the directories exists
81 1
        make_directories(self.config_dir)
82 1
        make_directories(self.local_dir)
83
84 1
        self._get_command_line_arguments()
85
86
    # End of init() function
87
88
89 1
    def load_yaml_from_config_file(self, filename):
90
        """
91
        Loads definitions from the YAML config file filename
92
        >>> conf = Conf()
93
        >>> conf.load_yaml_from_config_file('./bad_formatted.yaml')
94
        Error in configuration file ./bad_formatted.yaml at position: 9:1
95
        """
96
97 1
        config_file = codecs.open(filename, 'r', encoding='utf-8')
98
99 1
        try:
100 1
            self.description = yaml.safe_load(config_file)
101 1
        except yaml.YAMLError as err:
102 1
            if hasattr(err, 'problem_mark'):
103 1
                mark = err.problem_mark
104 1
                print(u'Error in configuration file {} at position: {}:{}'.format(filename, mark.line+1, mark.column+1))
105
            else:
106
                print(u'Error in configuration file {}'.format(filename))
107
108 1
        config_file.close()
109
110
    # End of load_yaml_from_config_file() function
111
112
113 1
    def _get_command_line_arguments(self):
114
        """
115
        Defines and gets all the arguments for the command line using
116
        argparse module. This function is called in the __init__ function
117
        of this class.
118
        """
119 1
        str_version = 'versions.py - %s' % __version__
120
121 1
        parser = argparse.ArgumentParser(description='This program checks releases and versions of programs through RSS or Atom feeds')
122
123 1
        parser.add_argument('-v', '--version', action='version', version=str_version)
124 1
        parser.add_argument('-f', '--file', action='store', dest='filename', help='Configuration file with projects to check', default='')
125 1
        parser.add_argument('-l', '--list-cache', action='store_true', dest='list_cache', help='Lists all projects and their version in cache', default=False)
126 1
        parser.add_argument('-d', '--debug', action='store_true', dest='debug', help='Starts in debug mode and prints things that may help', default=False)
127
128 1
        self.options = parser.parse_args()
129
130 1
        if self.options.filename != '':
131 1
            self.config_filename = self.options.filename
132
        else:
133 1
            self.config_filename = os.path.join(self.config_dir, 'versions.yaml')
134
135
    # End of get_command_line_arguments() function
136
137
138 1
    def extract_site_definition(self, site_name):
139
        """
140
        extracts whole site definition
141
        """
142
143 1
        if site_name in self.description:
144 1
            return self.description[site_name]
145
        else:
146
            return dict()
147
148
    # End of extract_site_definition()
149
150
151 1
    def extract_regex_from_site(self, site_name):
152
        """
153
        Extracts a regex from a site as defined in the YAML file.
154
        Returns the regex if it exists or None otherwise.
155
        """
156
157 1
        return self.extract_variable_from_site(site_name, 'regex', None)
158
159
    # End of extract_regex_from_site() function
160
161
162 1
    def extract_multiproject_from_site(self, site_name):
163
        """
164
        Extracts from a site its separator list for its multiple
165
        projects in one title. It returns None if multiproject
166
        is not defined and the list of separators instead
167
        """
168
169 1
        return self.extract_variable_from_site(site_name, 'multiproject', None)
170
171
    # End of extract…multiproject_from_site() function
172
173
174 1
    def extract_variable_from_site(self, site_name, variable, default_return):
175
        """
176
        Extracts variable from site site_name if it exists and return
177
        default_return otherwise
178
        """
179
180 1
        site_definition = self.extract_site_definition(site_name)
181
182 1
        if variable in site_definition:
183 1
            value = site_definition[variable]
184 1
            if value is None:
185 1
                print(u'Warning: no variable "{}" for site "{}".'.format(variable, site_name))
186 1
                value = default_return
187
        else:
188 1
            value = default_return
189
190 1
        return value
191
192
    # End of extract_variable_from_site() function
193
194
195 1
    def extract_project_list_from_site(self, site_name):
196
        """
197
        Extracts a project list from a site as defined in the YAML file.
198
        """
199
200 1
        return self.extract_variable_from_site(site_name, 'projects', [])
201
202
    # End of extract_project_list_from_site() function
203
204
205 1
    def extract_project_url(self, site_name):
206
        """
207
        Extracts the url definition where to check project version.
208
        """
209
210 1
        return self.extract_variable_from_site(site_name, 'url', '')
211
212
    # End of extract_project_url() function
213
214
215 1
    def extract_project_entry(self, site_name):
216
        """
217
        Extracts the entry definition (if any) of a site.
218
        """
219
220 1
        return self.extract_variable_from_site(site_name, 'entry', '')
221
222
    # End of extract_project_entry() function.
223
224
225 1
    def is_site_of_type(self, site_name, site_type):
226
        """
227
        Returns True if site_name is of type 'site_type'
228
        """
229
230 1
        site_definition = self.extract_site_definition(site_name)
231 1
        if 'type' in site_definition:
232 1
            return (site_definition['type'] == site_type)
233
        else:
234
            return False
235
236
    # End of is_site_of_type() function
237
238
239 1
    def extract_site_list(self, site_type):
240
        """
241
        Extracts all sites from a specific type (byproject or list)
242
        """
243
244 1
        all_site_list = list(self.description.keys())
245 1
        site_list = []
246 1
        for site_name in all_site_list:
247 1
            if self.is_site_of_type(site_name, site_type):
248 1
                site_list.insert(0, site_name)
249
250 1
        return site_list
251
252
    # End of extract_site_list() function
253
254
255 1
    def make_site_cache_list_name(self):
256
        """
257
        Formats list of cache filenames for all sites.
258
        """
259
260 1
        all_site_list = list(self.description.keys())
261 1
        cache_list = []
262 1
        for site_name in all_site_list:
263 1
            site_cache = u'{}.cache'.format(site_name)
264 1
            cache_list.insert(0, site_cache)
265
266 1
        return cache_list
267
268
    # End of make_site_cache_list_name() function
269
270
271 1
    def print_cache_or_check_versions(self):
272
        """
273
        Decide to pretty print projects and their associated version that
274
        are already in the cache or to check versions of that projects upon
275
        selections made at the command line
276
        """
277
278 1
        print_debug(self.options.debug, u'Loading yaml config file')
279 1
        self.load_yaml_from_config_file(self.config_filename)
280
281 1
        if self.options.list_cache is True:
282
            # Pretty prints all caches.
283 1
            cache_list = self.make_site_cache_list_name()
284 1
            print_versions_from_cache(self.local_dir, cache_list)
285
286
        else:
287
            # Checks version from online feeds
288 1
            self.check_versions()
289
290
    # End of print_list_or_check_versions() function.
291
292
293 1
    def check_versions(self):
294
        """
295
        Checks versions by parsing online feeds.
296
        """
297
298
        # Checks projects from by project sites such as github and sourceforge
299 1
        byproject_site_list = self.extract_site_list('byproject')
300
301 1
        for site_name in byproject_site_list:
302 1
            print_debug(self.options.debug, u'Checking {} projects'.format(site_name))
303 1
            (project_list, project_url, cache_filename, project_entry) = self.get_infos_for_site(site_name)
304 1
            feed_filename = u'{}.feed'.format(site_name)
305 1
            check_versions_feeds_by_projects(project_list, self.local_dir, self.options.debug, project_url, cache_filename, feed_filename, project_entry)
306
307
        # Checks projects from 'list' tupe sites such as freshcode.club
308 1
        list_site_list = self.extract_site_list('list')
309 1
        for site_name in list_site_list:
310 1
            print_debug(self.options.debug, u'Checking {} updates'.format(site_name))
311 1
            (project_list, project_url, cache_filename, project_entry) = self.get_infos_for_site(site_name)
312 1
            regex = self.extract_regex_from_site(site_name)
313 1
            multiproject = self.extract_multiproject_from_site(site_name)
314 1
            feed_filename = u'{}.feed'.format(site_name)
315 1
            check_versions_for_list_sites(project_list, project_url, cache_filename, feed_filename, self.local_dir, self.options.debug, regex, multiproject)
316
317
    # End of check_versions() function
318
319
320 1
    def get_infos_for_site(self, site_name):
321
        """
322
        Returns informations about a site as a tuple
323
        (list of projects, url to check, filename of the cache)
324
        """
325
326 1
        project_list = self.extract_project_list_from_site(site_name)
327 1
        project_url = self.extract_project_url(site_name)
328 1
        project_entry = self.extract_project_entry(site_name)
329 1
        cache_filename = u'{}.cache'.format(site_name)
330
331 1
        return (project_list, project_url, cache_filename, project_entry)
332
333
    # End of get_infos_for_site() function
334
335
336
# End of Conf class
337
338
339 1
class FileCache:
340
    """
341
    This class should help in managing cache files
342
    """
343
344 1
    cache_filename = ''
345 1
    cache_dict = {}  # Dictionnary of projects and their associated version
346
347 1
    def __init__(self, local_dir, filename):
348
        """
349
        Inits the class. 'local_dir' must be a directory where we want to
350
        store the cache file named 'filename'
351
        """
352
353 1
        self.cache_filename = os.path.join(local_dir, filename)
354 1
        self.cache_dict = {}
355 1
        self._read_cache_file()
356
357
    # End of __init__() function
358
359
360 1
    def _return_project_and_version_from_line(self, line):
361
        """
362
        Splits the line into a project and a version if possible (the line
363
        must contain a whitespace.
364
        """
365
366 1
        line = line.strip()
367
368 1
        if line.count(' ') > 0:
369 1
            (project, version) = line.split(' ', 1)
370
371
        elif line != '':
372
            project = line
373
            version = ''
374
375 1
        return (project, version)
376
377
    # End of _return_project_and_version_from_line() function
378
379
380 1
    def _read_cache_file(self):
381
        """
382
        Reads the cache file and puts it into a dictionnary of project with
383
        their associated version
384
        """
385
386 1
        if os.path.isfile(self.cache_filename):
387 1
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
388
389 1
            for line in cache_file:
390 1
                (project, version) = self._return_project_and_version_from_line(line)
391 1
                self.cache_dict[project] = version
392
393 1
            cache_file.close()
394
395
    # End of _read_cache_file() function
396
397
398 1
    def write_cache_file(self):
399
        """
400
        Owerwrites dictionnary cache to the cache file
401
        """
402
403 1
        cache_file = open_and_truncate_file(self.cache_filename)
404
405 1
        for (project, version) in self.cache_dict.items():
406 1
            cache_file.write('%s %s\n' % (project, version))
407
408 1
        cache_file.close()
409
410
    # End of write_cache_file() function
411
412 1
    def print_if_newest_version(self, project, version, debug):
413
        """
414
        Prints the project and it's version if it is newer than the
415
        one in cache.
416
        """
417 1
        try:
418 1
            version_cache = self.cache_dict[project]
419 1
            print_debug(debug, u'\t\tIn cache: {}'.format(version_cache))
420
421 1
            if version != version_cache:
422 1
                print_project_version(project, version)
423
424 1
        except KeyError:
425 1
            print_project_version(project, version)
426
427
    # End of print_if_newest_version() function.
428
429
430 1
    def update_cache_dict(self, project, version, debug):
431
        """
432
        Updates cache dictionnary if needed. We always keep the latest version.
433
        """
434
435 1
        try:
436 1
            version_cache = self.cache_dict[project]
437 1
            print_debug(debug, u'\t\tUpdating cache with in cache: {} / new ? version {}'.format(version_cache, version))
438
439 1
            if version != version_cache:
440 1
                self.cache_dict[project] = version
441
442 1
        except KeyError:
443 1
            self.cache_dict[project] = version
444
445
    # End of update_cache_dict() function
446
447
448 1
    def print_cache_dict(self, sitename):
449
        """
450
        Pretty prints the cache dictionary as it is recorded in the files.
451
        """
452
453 1
        print(u'{}:'.format(sitename))
454
455
        # Gets project and version tuple sorted by project lowered while sorting
456 1
        for project, version in sorted(self.cache_dict.items(), key=lambda proj: proj[0].lower()):
457 1
            print(u'\t{} {}'.format(project, version))
458
459 1
        print('')
460
461
    # End of print_cache_dict() function
462
# End of FileCache class
463
464
465 1
class FeedCache:
466
467 1
    cache_filename = ''
468 1
    year = 2016
469 1
    month = 5
470 1
    day = 1
471 1
    hour = 0
472 1
    minute = 0
473 1
    date_minutes = 0
474
475
476 1
    def __init__(self, local_dir, filename):
477
        """
478
        Inits the class. 'local_dir' must be a directory where we want to
479
        store the cache file named 'filename'
480
        """
481
482 1
        self.cache_filename = os.path.join(local_dir, filename)
483 1
        self.read_cache_feed()
484
485
    # End of __init__() function
486
487
488 1
    def read_cache_feed(self):
489
        """
490
        Reads the cache file which should only contain one date on the
491
        first line
492
        """
493
494 1
        if os.path.isfile(self.cache_filename):
495 1
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
496 1
            (self.year, self.month, self.day, self.hour, self.minute) = cache_file.readline().strip().split(' ', 4)
497 1
            self.date_minutes = self._calculate_minutes(int(self.year), int(self.month), int(self.day), int(self.hour), int(self.minute))
498 1
            cache_file.close()
499
500
    # End of read_cache_feed() function
501
502
503 1
    def write_cache_feed(self):
504
        """
505
        Overwrites the cache file with values stored in this class
506
        """
507 1
        cache_file = open_and_truncate_file(self.cache_filename)
508
509 1
        cache_file.write('%s %s %s %s %s' % (self.year, self.month, self.day, self.hour, self.minute))
510
511 1
        cache_file.close()
512
513
    # End of write_cache_feed() function
514
515
516 1
    def update_cache_feed(self, date):
517
        """
518
        Updates the values stored in the class with the date which should
519
        be a time.struct_time
520
        """
521
522 1
        self.year = date.tm_year
523 1
        self.month = date.tm_mon
524 1
        self.day = date.tm_mday
525 1
        self.hour = date.tm_hour
526 1
        self.minute = date.tm_min
527 1
        self.date_minutes = self._calculate_minutes_from_date(date)
528
529
    # End of update_cache_feed() function
530
531
532 1
    def _calculate_minutes(self, year, mon, day, hour, mins):
533
        """
534
        Calculate a number of minutes with all parameters and returns
535
        this.
536
        >>> fc = FeedCache('localdir','filename')
537
        >>> fc._calculate_minutes(2016, 5, 1, 0, 0)
538
        1059827040
539
        """
540
541 1
        minutes = (year * 365 * 24 * 60) + \
542
                  (mon * 30 * 24 * 60) + \
543
                  (day * 24 * 60) + \
544
                  (hour * 60) + \
545
                  (mins)
546
547 1
        return minutes
548
549
    # End of _calculate_minutes() function
550
551
552 1
    def _calculate_minutes_from_date(self, date):
553
        """
554
        Transforms a date in a number of minutes to ease comparisons
555
        and returns this number of minutes
556
        """
557
558 1
        return self._calculate_minutes(date.tm_year, date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min)
559
560
    # End of _calculate_minutes() function
561
562
563 1
    def is_newer(self, date):
564
        """
565
        Tells wether "date" is newer than the one in the cache (returns True
566
        or not (returns False)
567
        """
568
569 1
        minutes = self._calculate_minutes_from_date(date)
570
571 1
        if minutes > self.date_minutes:
572 1
            return True
573
574
        else:
575 1
            return False
576
577
    # End of is_newer() function
578
# End of FeedCache class
579
580
581
######## Below are some utility functions used by classes above ########
582
583
584 1
def make_directories(path):
585
    """
586
    Makes all directories in path if possible. It is not an error if
587
    path already exists.
588
    """
589
590 1
    try:
591 1
        os.makedirs(path)
592
593 1
    except OSError as exc:
594
595 1
        if exc.errno != errno.EEXIST or os.path.isdir(path) is not True:
596
            raise
597
598
# End of make_directories() function
599
600
601 1
def open_and_truncate_file(filename):
602
    """
603
    Opens filename for writing truncating it to a zero length file
604
    and returns a python file object.
605
    """
606
607 1
    cache_file = codecs.open(filename, 'w', encoding='utf-8')
608 1
    cache_file.truncate(0)
609 1
    cache_file.flush()
610
611 1
    return cache_file
612
613
# End of open_and_truncate_file() function
614
####################### End of utility functions #######################
615
616
617 1
def get_values_from_project(project):
618
    """
619
    Gets the values of 'regex' and 'name' keys if found and
620
    returns a tuple (valued, name, regex, entry)
621
    """
622
623 1
    regex = ''
624 1
    entry = ''
625 1
    name = project
626 1
    valued = False
627
628 1
    if type(project) is dict:
629 1
        if 'name' in project:
630 1
            name = project['name']
631
632 1
        if 'regex' in project:
633 1
            regex = project['regex']
634 1
            valued = True
635
636 1
        if 'entry' in project:
637 1
            entry = project['entry']
638 1
            valued = True
639
640 1
    return (valued, name, regex, entry)
641
642
# End of get_values_from_project() function
643
644
645 1
def format_project_feed_filename(feed_filename, name):
646
    """
647
    Returns a valid filename formatted based on feed_filename (the site name)
648
    and name the name of the project
649
    """
650
651 1
    (root, ext) = os.path.splitext(feed_filename)
652 1
    norm_name = name.replace('/', '_')
653
654 1
    filename = "{}_{}{}".format(root, norm_name, ext)
655
656 1
    return filename
657
658
# End of format_project_feed_filename() function
659
660
661 1
def is_entry_last_checked(entry):
662
    """
663
    Returns true if entry is equal to last checked and
664
    false otherwise.
665
    >>> is_entry_last_checked('last checked')
666
    True
667
    >>> is_entry_last_checked('')
668
    False
669
    >>> is_entry_last_checked('latest')
670
    False
671
    """
672
673 1
    return entry == 'last checked'
674
675
# End of is_entry_last_checked() function
676
677
678 1
def sort_feed_list(feed_list, feed):
679
    """
680
    Sorts the feed list with the right attribute which depends on the feed.
681
    sort is reversed because feed_list is build by inserting ahead when
682
    parsing the feed from the most recent to the oldest entry.
683
    Returns a sorted list (by date) the first entry is the newest one.
684
    """
685
686 1
    if feed.entries[0]:
687 1
        if 'published_parsed' in feed.entries[0]:
688
            feed_list = sorted(feed_list, key=operator.attrgetter('published_parsed'), reverse=True)
689 1
        elif 'updated_parsed' in feed.entries[0]:
690 1
            feed_list = sorted(feed_list, key=operator.attrgetter('updated_parsed'), reverse=True)
691
692 1
    return feed_list
693
694
# End of sort_feed_list() function
695
696
697 1
def get_releases_filtering_feed(debug, local_dir, filename, feed, entry):
698
    """
699
    Filters the feed and returns a list of releases with one
700
    or more elements
701
    """
702
703 1
    feed_list = []
704
705 1
    if is_entry_last_checked(entry):
706 1
        feed_info = FeedCache(local_dir, filename)
707 1
        feed_info.read_cache_feed()
708 1
        feed_list = make_list_of_newer_feeds(feed, feed_info, debug)
709 1
        feed_list = sort_feed_list(feed_list, feed)
710
711
        # Updating feed_info with the latest parsed feed entry date
712 1
        if len(feed_list) >= 1:
713 1
            published_date = get_entry_published_date(feed_list[0])
714 1
            feed_info.update_cache_feed(published_date)
715
716 1
        feed_info.write_cache_feed()
717
718
    else:
719 1
        feed_list.insert(0, feed.entries[0])
720
721 1
    return feed_list
722
723
724 1
def get_latest_release_by_title(project, debug, feed_url, local_dir, feed_filename, project_entry):
725
    """
726
    Gets the latest release or the releases between the last checked time of
727
    a program on a site of type 'byproject'.
728
    project must be a string that represents the project (user/repository in
729
    github for instance).
730
    Returns a tuple which contains the name of the project, a list of versions
731
    and a boolean that indicates if we checked by last checked time (True) or
732
    by release (False).
733
    """
734
735 1
    feed_list = []
736
737 1
    (valued, name, regex, entry) = get_values_from_project(project)
738
    
739 1
    if is_entry_last_checked(project_entry):
740 1
        last_checked = True
741 1
        entry = project_entry
742
    else:
743 1
        last_checked = is_entry_last_checked(entry)
744 1
    filename = format_project_feed_filename(feed_filename, name)
745
746 1
    url = feed_url.format(name)
747 1
    feed = get_feed_entries_from_url(url)
748
749 1
    if feed is not None and len(feed.entries) > 0:
750 1
        feed_list = get_releases_filtering_feed(debug, local_dir, filename, feed, entry)
751
752 1
        if valued and regex != '':
753
            # Here we match the whole list against the regex and replace the
754
            # title's entry of the result of that match upon success.
755 1
            for entry in feed_list:
756 1
                res = re.match(regex, entry.title)
757
                # Here we should make a new list with the matched entries and leave tho other ones
758 1
                if res:
759 1
                    entry.title = res.group(1)
760 1
                print_debug(debug, u'\tname: {}\n\tversion: {}\n\tregex: {} : {}'.format(name, entry.title, regex, res))
761
762 1
        print_debug(debug, u'\tProject {}: {}'.format(name, entry.title))
763
764 1
    return (name, feed_list, last_checked)
765
766
# End of get_latest_release_by_title() function
767
768
769 1
def print_project_version(project, version):
770
    """
771
    Prints to the standard output project name and it's version.
772
    """
773
774 1
    print(u'{} {}'.format(project, version))
775
776
# End of print_project_version() function
777
778
779 1
def check_versions_feeds_by_projects(project_list, local_dir, debug, feed_url, cache_filename, feed_filename, project_entry):
780
    """
781
    Checks project's versions on feed_url if any are defined in the yaml
782
    file under the specified tag that got the project_list passed as an argument.
783
    """
784
785 1
    site_cache = FileCache(local_dir, cache_filename)
786
787 1
    for project in project_list:
788 1
        (name, feed_list, last_checked) = get_latest_release_by_title(project, debug, feed_url, local_dir, feed_filename, project_entry)
789
790
791 1
        if len(feed_list) >= 1:
792
            # Updating the cache with the latest version (the first entry)
793 1
            version = feed_list[0].title
794
795 1
            if not last_checked:
796
                # printing only for latest release as last checked is
797
                # already filtered and to be printed entirely
798 1
                site_cache.print_if_newest_version(name, version, debug)
799
800 1
            site_cache.update_cache_dict(name, version, debug)
801
802 1
            if not last_checked:
803
                # we already printed this.
804 1
                del feed_list[0]
805
806 1
        for entry in feed_list:
807 1
            print_project_version(name, entry.title)
808
809 1
    site_cache.write_cache_file()
810
811
# End of check_versions_feeds_by_projects() function
812
813
814 1
def cut_title_with_default_method(title):
815
    """
816
    Cuts title with a default method and a fallback
817
    >>> cut_title_with_default_method('versions 1.3.2')
818
    ('versions', '1.3.2')
819
    >>> cut_title_with_default_method('no_version_project')
820
    ('no_version_project', '')
821
    """
822
823 1
    try:
824 1
        (project, version) = title.strip().split(' ', 1)
825
826 1
    except ValueError:
827 1
        project = title.strip()
828 1
        version = ''
829
830 1
    return (project, version)
831
832
# End of cut_title_with_default_method() function
833
834
835 1
def cut_title_with_regex_method(title, regex):
836
    """
837
    Cuts title using a regex. If it does not success
838
    fallback to default.
839
    >>> cut_title_with_regex_method('versions 1.3.2', '([\w]+)\s([\d\.]+)')
840
    ('versions', '1.3.2', False)
841
    >>> cut_title_with_regex_method('versions 1.3.2', '([\w]+)notgood\s([\d\.]+)')
842
    ('', '', True)
843
    """
844
845 1
    default = False
846 1
    project = ''
847 1
    version = ''
848
849 1
    res = re.match(regex, title)
850 1
    if res:
851 1
        project = res.group(1)
852 1
        version = res.group(2)
853
    else:
854 1
        default = True
855
856 1
    return (project, version, default)
857
858
# End of cut_title_with_regex_method() function
859
860
861 1
def cut_title_in_project_version(title, regex):
862
    """
863
    Cuts the title into a tuple (project, version) where possible with a regex
864
    or if there is no regex or the regex did not match cuts the title with a
865
    default method
866
    """
867 1
    default = False
868
869 1
    if regex is not None:
870 1
        (project, version, default) = cut_title_with_regex_method(title, regex)
871
    else:
872 1
        default = True
873
874 1
    if default:
875 1
        (project, version) = cut_title_with_default_method(title)
876
877 1
    return (project, version)
0 ignored issues
show
The variable version does not seem to be defined for all execution paths.
Loading history...
The variable project does not seem to be defined for all execution paths.
Loading history...
878
879
# End of cut_title_in_project_version() function
880
881
882
883 1
def get_entry_published_date(entry):
884
    """
885
    Returns the published date of an entry.
886
    Selects the right field to do so
887
    """
888
889 1
    if 'published_parsed' in entry:
890 1
        published_date = entry.published_parsed
891 1
    elif 'updated_parsed' in entry:
892 1
        published_date = entry.updated_parsed
893
    elif 'pubDate' in entry:    # rss-0.91.dtd (netscape)
894
        published_date = entry.pubDate
895
896 1
    return published_date
0 ignored issues
show
The variable published_date does not seem to be defined for all execution paths.
Loading history...
897
898
# End of get_entry_published_date() function
899
900
901 1
def make_list_of_newer_feeds(feed, feed_info, debug):
902
    """
903
    Compares feed entries and keep those that are newer than the latest
904
    check we've done and inserting the newer ones in reverse order in
905
    a list to be returned
906
    """
907
908 1
    feed_list = []
909
910
    # inserting into a list in reverse order to keep the most recent
911
    # version in case of multiple release of the same project in the
912
    # feeds
913 1
    for a_feed in feed.entries:
914
915 1
        if a_feed:
916 1
            published_date = get_entry_published_date(a_feed)
917
918 1
            print_debug(debug, u'\tFeed entry ({0}): Feed title: "{1:16}"'.format(time.strftime('%x %X', published_date), a_feed.title))
919
920 1
            if feed_info.is_newer(published_date):
921 1
                feed_list.insert(0, a_feed)
922
        else:
923 1
            print(u'Warning: empty feed in {}'.format(feed))
924
925 1
    return feed_list
926
927
# End of make_list_of_newer_feeds() function
928
929
930 1
def lower_list_of_strings(project_list):
931
    """
932
    Lowers every string in the list to ease sorting and comparisons
933
    """
934
935 1
    project_list_low = [project.lower() for project in project_list]
936
937 1
    return project_list_low
938
939
# End of lower_list_of_strings() function
940
941
942 1
def split_multiproject_title_into_list(title, multiproject):
943
    """
944
    Splits title into a list of projects according to multiproject being
945
    a list of separators
946
    """
947
948 1
    if multiproject is not None:
949 1
        titles = re.split(multiproject, title)
950
    else:
951 1
        titles = [title]
952
953 1
    return titles
954
955
# End of split_multiproject_title_into_list() function
956
957
958
959
960 1
def check_and_update_feed(feed_list, project_list, cache, debug, regex, multiproject):
961
    """
962
    Checks every feed entry in the list against project list cache and
963
    then updates the dictionnary then writes the cache file to the disk.
964
     - feed_list    is a list of feed (from feedparser module)
965
     - project_list is the list of project as read from the yaml
966
                    configuration file
967
     - cache is an initialized instance of FileCache
968
    """
969
970
    # Lowers the list before searching in it
971 1
    project_list_low = lower_list_of_strings(project_list)
972
973
    # Checking every feed entry that are newer than the last check
974
    # and updates the dictionnary accordingly
975 1
    for entry in feed_list:
976
977 1
        titles = split_multiproject_title_into_list(entry.title, multiproject)
978
979 1
        for title in titles:
980 1
            (project, version) = cut_title_in_project_version(title, regex)
981 1
            print_debug(debug, u'\tChecking {0:16}: {1}'.format(project, version))
982 1
            if project.lower() in project_list_low:
983 1
                cache.print_if_newest_version(project, version, debug)
984 1
                cache.update_cache_dict(project, version, debug)
985
986 1
    cache.write_cache_file()
987
988
# End of check_and_update_feed() function
989
990
991 1
def manage_http_status(feed, url):
992
    """
993
    Manages http status code present in feed and prints
994
    an error in case of a 3xx, 4xx or 5xx and stops
995
    doing anything for the feed by returning None.
996
    """
997
998 1
    err = feed.status / 100
999
1000 1
    if err > 2:
1001 1
        print(u'Error {} while fetching "{}".'.format(feed.status, url))
1002 1
        feed = None
1003
1004 1
    return feed
1005
1006
# End of manage_http_status() function
1007
1008
1009 1
def manage_non_http_errors(feed, url):
1010
    """
1011
    Tries to manage non http errors and gives
1012
    a message to the user.
1013
    """
1014
1015 1
    if feed.bozo:
1016 1
        if feed.bozo_exception:
1017 1
            exc = feed.bozo_exception
1018 1
            if hasattr(exc, 'reason'):
1019 1
                message = exc.reason
1020
            else:
1021
                message = 'unaddressed'
1022
1023 1
            print(u'Error {} while fetching "{}".'.format(message, url))
1024
1025
        else:
1026
            print(u'Error while fetching url "{}".'.format(url))
1027
1028
# End of manage_non_http_errors() function
1029
1030
1031 1
def get_feed_entries_from_url(url):
1032
    """
1033
    Gets feed entries from an url that should be an
1034
    RSS or Atom feed.
1035
    >>> get_feed_entries_from_url("http://delhomme.org/notfound.html")
1036
    Error 404 while fetching "http://delhomme.org/notfound.html".
1037
    >>> feed = get_feed_entries_from_url("http://blog.delhomme.org/index.php?feed/atom")
1038
    >>> feed.status
1039
    200
1040
    """
1041
1042 1
    feed = feedparser.parse(url)
1043
1044 1
    if 'status' in feed:
1045 1
        feed = manage_http_status(feed, url)
1046
    else:
1047
        # An error happened such that the feed does not contain an HTTP response
1048 1
        manage_non_http_errors(feed, url)
1049 1
        feed = None
1050
1051 1
    return feed
1052
1053
# End of get_feed_entries_from_url() function
1054
1055
1056 1
def check_versions_for_list_sites(feed_project_list, url, cache_filename, feed_filename, local_dir, debug, regex, multiproject):
1057
    """
1058
    Checks projects of 'list' type sites such as freshcode's web site's RSS
1059
    """
1060
1061 1
    freshcode_cache = FileCache(local_dir, cache_filename)
1062
1063 1
    feed_info = FeedCache(local_dir, feed_filename)
1064 1
    feed_info.read_cache_feed()
1065
1066 1
    feed = get_feed_entries_from_url(url)
1067
1068 1
    if feed is not None:
1069 1
        print_debug(debug, u'\tFound {} entries'.format(len(feed.entries)))
1070 1
        feed_list = make_list_of_newer_feeds(feed, feed_info, debug)
1071 1
        print_debug(debug, u'\tFound {} new entries (relative to {})'.format(len(feed_list), feed_info.date_minutes))
1072
1073 1
        check_and_update_feed(feed_list, feed_project_list, freshcode_cache, debug, regex, multiproject)
1074
1075
        # Updating feed_info with the latest parsed feed entry date
1076 1
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
1077
1078 1
    feed_info.write_cache_feed()
1079
1080
# End of check_versions_for_list_sites() function
1081
1082
1083 1
def print_versions_from_cache(local_dir, cache_filename_list):
1084
    """
1085
    Prints all projects and their associated data from the cache
1086
    """
1087 1
    for cache_filename in cache_filename_list:
1088 1
        site_cache = FileCache(local_dir, cache_filename)
1089 1
        site_cache.print_cache_dict(cache_filename)
1090
1091
# End of print_versions_from_cache()
1092
1093
1094 1
def main():
1095
    """
1096
    This is the where the program begins
1097
    """
1098
1099 1
    if sys.version_info[0] == 2:
1100
        sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)
1101
1102 1
    versions_conf = Conf()  # Configuration options
1103
1104 1
    if versions_conf.options.debug:
1105 1
        doctest.testmod(verbose=True)
1106
1107 1
    if os.path.isfile(versions_conf.config_filename):
1108 1
        versions_conf.print_cache_or_check_versions()
1109
1110
    else:
1111 1
        print(u'Error: file {} does not exist'.format(versions_conf.config_filename))
1112
1113
# End of main() function
1114
1115
1116 1
def print_debug(debug, message):
1117
    """
1118
    Prints 'message' if debug mode is True
1119
    """
1120
1121 1
    if debug:
1122 1
        print(u'{}'.format(message))
1123
1124
# End of print_debug() function
1125
1126
1127 1
if __name__ == "__main__":
1128
    main()
1129