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 ( f9432b...0ef6a9 )
by dup
01:13
created

get_latest_release_by_title()   B

Complexity

Conditions 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 5
c 3
b 0
f 0
dl 0
loc 26
ccs 14
cts 14
cp 1
crap 5
rs 8.0894
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 - 2017 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 1
import re
33
34 1
__author__ = "Olivier Delhomme <[email protected]>"
35 1
__date__ = "22.11.2017"
36 1
__version__ = "1.3.2"
37
38
"""
39
This program checks projects versions through 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. version.yaml is included as an
46
example in this project.
47
48
Versions uses and produces text files. Those files are cache files
49
written into ~/.local/versions directory. "*.cache" are cache files
50
containing the project list and their associated version (the latest).
51
"*.feed" are information feed cache files containing on only one line
52
the latest parsed post of the feed.
53
"""
54
55
56 1
class Conf:
57
    """
58
    Class to store configuration of the program and check version.
59
    """
60
61 1
    config_dir = ''
62 1
    local_dir = ''
63 1
    config_filename = ''
64 1
    description = {}
65 1
    options = None
66
67 1
    def __init__(self):
68
        """
69
        Inits the class
70
        """
71 1
        self.config_dir = os.path.expanduser("~/.config/versions")
72 1
        self.local_dir = os.path.expanduser("~/.local/versions")
73 1
        self.config_filename = ''  # At this stage we do not know if a filename has been set on the command line
74 1
        self.description = {}
75 1
        self.options = None
76
77
        # Make sure that the directories exists
78 1
        make_directories(self.config_dir)
79 1
        make_directories(self.local_dir)
80
81 1
        self._get_command_line_arguments()
82
83
    # End of init() function
84
85
86 1
    def load_yaml_from_config_file(self, filename):
87
        """
88
        Loads definitions from the YAML config file filename
89
        >>> conf = Conf()
90
        >>> conf.load_yaml_from_config_file('./bad_formatted.yaml')
91
        Error in configuration file ./bad_formatted.yaml at position: 9:1
92
        """
93
94 1
        config_file = codecs.open(filename, 'r', encoding='utf-8')
95
96 1
        try:
97 1
            self.description = yaml.safe_load(config_file)
98 1
        except yaml.YAMLError, err:
99 1
            if hasattr(err, 'problem_mark'):
100 1
                mark = err.problem_mark
101 1
                print(u'Error in configuration file {} at position: {}:{}'.format(filename, mark.line+1, mark.column+1))
102
            else:
103
                print(u'Error in configuration file {}'.format(filename))
104
105
106 1
        config_file.close()
107
108
    # End of load_yaml_from_config_file() function
109
110
111 1
    def _get_command_line_arguments(self):
112
        """
113
        Defines and gets all the arguments for the command line using
114
        argparse module. This function is called in the __init__ function
115
        of this class.
116
        """
117 1
        str_version = 'versions.py - %s' % __version__
118
119 1
        parser = argparse.ArgumentParser(description='This program checks releases and versions of programs through RSS or Atom feeds', version=str_version)
120
121 1
        parser.add_argument('-f', '--file', action='store', dest='filename', help='Configuration file with projects to check', default='')
122 1
        parser.add_argument('-l', '--list-cache', action='store_true', dest='list_cache', help='Lists all projects and their version in cache', default=False)
123 1
        parser.add_argument('-d', '--debug', action='store_true', dest='debug', help='Starts in debug mode and prints things that may help', default=False)
124
125 1
        self.options = parser.parse_args()
126
127 1
        if self.options.filename != '':
128 1
            self.config_filename = self.options.filename
129
        else:
130 1
            self.config_filename = os.path.join(self.config_dir, 'versions.yaml')
131
132
    # End of get_command_line_arguments() function
133
134 1
    def extract_site_definition(self, site_name):
135
        """
136
        extracts whole site definition
137
        """
138
139 1
        if site_name in self.description:
140 1
            return self.description[site_name]
141
        else:
142
            return dict()
143
144
    # End of extract_site_definition()
145
146
147 1
    def extract_regex_from_site(self, site_name):
148
        """
149
        Extracts a regex from a site as defined in the YAML file.
150
        Returns the regex if it exists or None otherwise.
151
        """
152
153 1
        site_definition = self.extract_site_definition(site_name)
154
155 1
        if 'regex' in site_definition:
156 1
            regex = site_definition['regex']
157
        else:
158 1
            regex = None
159
160 1
        return regex
161
162
    # End of extract_regex_from_site() function
163
164
165 1
    def extract_project_list_from_site(self, site_name):
166
        """
167
        Extracts a project list from a site as defined in the YAML file.
168
        """
169
170 1
        site_definition = self.extract_site_definition(site_name)
171
172 1
        if 'projects' in site_definition:
173 1
            project_list = site_definition['projects']
174 1
            if project_list is None:
175 1
                print(u'Warning: no project for site "{}".'.format(site_name))
176 1
                project_list = []
177
        else:
178 1
            project_list = []
179
180 1
        return project_list
181
182
    # End of extract_project_list_from_site() function
183
184
185 1
    def extract_project_url(self, site_name):
186
        """
187
        Extracts the url definition where to check project version.
188
        """
189
190 1
        site_definition = self.extract_site_definition(site_name)
191
192 1
        if 'url' in site_definition:
193 1
            project_url = site_definition['url']
194
        else:
195
            project_url = ''
196
197 1
        return project_url
198
199
    # End of extract_project_url() function
200
201
202 1
    def is_site_of_type(self, site_name, type):
203
        """
204
        Returns True if site_name is of type 'type'
205
        """
206
207 1
        site_definition = self.extract_site_definition(site_name)
208 1
        if 'type' in site_definition:
209 1
            return (site_definition['type'] == type)
210
        else:
211
            return False
212
213
    # End of is_site_of_type() function
214
215
216 1
    def extract_site_list(self, type):
217
        """
218
        Extracts all sites from a specific type (byproject or list)
219
        """
220
221 1
        all_site_list = list(self.description.keys())
222 1
        site_list = []
223 1
        for site_name in all_site_list:
224 1
            if self.is_site_of_type(site_name, type):
225 1
                site_list.insert(0, site_name)
226
227 1
        return site_list
228
229
    # End of extract_site_list() function
230
231
232 1
    def make_site_cache_list_name(self):
233
        """
234
        Formats list of cache filenames for all sites.
235
        """
236
237 1
        all_site_list = list(self.description.keys())
238 1
        cache_list = []
239 1
        for site_name in all_site_list:
240 1
            site_cache = u'{}.cache'.format(site_name)
241 1
            cache_list.insert(0, site_cache)
242
243 1
        return cache_list
244
245
    # End of make_site_cache_list_name() function
246
247
248 1
    def print_cache_or_check_versions(self):
249
        """
250
        Decide to pretty print projects and their associated version that
251
        are already in the cache or to check versions of that projects upon
252
        selections made at the command line
253
        """
254
255 1
        print_debug(self.options.debug, u'Loading yaml config file')
256 1
        self.load_yaml_from_config_file(self.config_filename)
257
258 1
        if self.options.list_cache is True:
259
            # Pretty prints all caches.
260 1
            cache_list = self.make_site_cache_list_name()
261 1
            print_versions_from_cache(self.local_dir, cache_list, self.options.debug)
262
263
        else:
264
            # Checks version from online feeds
265 1
            self.check_versions()
266
267
    # End of print_list_or_check_versions() function.
268
269
270 1
    def check_versions(self):
271
        """
272
        Checks versions by parsing online feeds.
273
        """
274
275
        # Checks projects from by project sites such as github and sourceforge
276 1
        byproject_site_list = self.extract_site_list('byproject')
277
278 1
        for site_name in byproject_site_list:
279 1
            print_debug(self.options.debug, u'Checking {} projects'.format(site_name))
280 1
            (project_list, project_url, cache_filename) = self.get_infos_for_site(site_name)
281 1
            check_versions_feeds_by_projects(project_list, self.local_dir, self.options.debug, project_url, cache_filename)
282
283
        # Checks projects from 'list' tupe sites such as freshcode.club
284 1
        list_site_list = self.extract_site_list('list')
285 1
        for site_name in list_site_list:
286 1
            print_debug(self.options.debug, u'Checking {} updates'.format(site_name))
287 1
            (project_list, project_url, cache_filename) = self.get_infos_for_site(site_name)
288 1
            regex = self.extract_regex_from_site(site_name)
289 1
            feed_filename = u'{}.feed'.format(site_name)
290 1
            check_versions_for_list_sites(project_list, project_url, cache_filename, feed_filename, self.local_dir, self.options.debug, regex)
291
292
    # End of check_versions() function
293
    
294
    
295 1
    def get_infos_for_site(self, site_name):
296
        """
297
        Returns informations about a site as a tuple
298
        (list of projects, url to check, filename of the cache)
299
        """
300
301 1
        project_list = self.extract_project_list_from_site(site_name)
302 1
        project_url = self.extract_project_url(site_name)
303 1
        cache_filename = u'{}.cache'.format(site_name)
304
305 1
        return (project_list, project_url, cache_filename)
306
307
    # End of get_infos_for_site() function
308
309
310
# End of Conf class
311
312
313 1
class FileCache:
314
    """
315
    This class should help in managing cache files
316
    """
317
318 1
    cache_filename = ''
319 1
    cache_dict = {}  # Dictionnary of projects and their associated version
320
321 1
    def __init__(self, local_dir, filename):
322
        """
323
        Inits the class. 'local_dir' must be a directory where we want to
324
        store the cache file named 'filename'
325
        """
326
327 1
        self.cache_filename = os.path.join(local_dir, filename)
328 1
        self.cache_dict = {}
329 1
        self._read_cache_file()
330
331
    # End of __init__() function
332
333
334 1
    def _return_project_and_version_from_line(self, line):
335
        """
336
        Splits the line into a project and a version if possible (the line
337
        must contain a whitespace.
338
        """
339
340 1
        line = line.strip()
341
342 1
        if line.count(' ') > 0:
343 1
            (project, version) = line.split(' ', 1)
344
345
        elif line != '':
346
            project = line
347
            version = ''
348
349 1
        return (project, version)
350
351
    # End of _return_project_and_version_from_line() function
352
353
354 1
    def _read_cache_file(self):
355
        """
356
        Reads the cache file and puts it into a dictionnary of project with
357
        their associated version
358
        """
359
360 1
        if os.path.isfile(self.cache_filename):
361 1
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
362
363 1
            for line in cache_file:
364 1
                (project, version) = self._return_project_and_version_from_line(line)
365 1
                self.cache_dict[project] = version
366
367 1
            cache_file.close()
368
369
    # End of _read_cache_file() function
370
371
372 1
    def write_cache_file(self):
373
        """
374
        Owerwrites dictionnary cache to the cache file
375
        """
376
377 1
        cache_file = open_and_truncate_file(self.cache_filename)
378
379 1
        for (project, version) in self.cache_dict.iteritems():
380 1
            cache_file.write('%s %s\n' % (project, version))
381
382 1
        cache_file.close()
383
384
    # End of write_cache_file() function
385
386
387 1
    def update_cache_dict(self, project, version, debug):
388
        """
389
        Updates cache dictionnary if needed
390
        """
391
392 1
        try:
393 1
            version_cache = self.cache_dict[project]
394 1
            print_debug(debug, u'\t\tIn cache: {}'.format(version_cache))
395
396 1
            if version != version_cache:
397 1
                print(u'{} {}'.format(project, version))
398 1
                self.cache_dict[project] = version
399
400 1
        except KeyError:
401 1
            print(u'{} {}'.format(project, version))
402 1
            self.cache_dict[project] = version
403
404
    # End of update_cache_dict() function
405
406
407 1
    def print_cache_dict(self, sitename):
408
        """
409
        Pretty prints the cache dictionary as it is recorded in the files.
410
        """
411
412 1
        print(u'{}:'.format(sitename))
413
414
        # Gets project and version tuple sorted by project lowered while sorting
415 1
        for project, version in sorted(self.cache_dict.iteritems(), key=lambda proj: proj[0].lower()):
416 1
            print(u'\t{} {}'.format(project, version))
417
418 1
        print('')
419
420
    # End of print_cache_dict() function
421
# End of FileCache class
422
423
424 1
class FeedCache:
425
426 1
    cache_filename = ''
427 1
    year = 2016
428 1
    month = 05
429 1
    day = 1
430 1
    hour = 0
431 1
    minute = 0
432 1
    date_minutes = 0
433
434
435 1
    def __init__(self, local_dir, filename):
436
        """
437
        Inits the class. 'local_dir' must be a directory where we want to
438
        store the cache file named 'filename'
439
        """
440
441 1
        self.cache_filename = os.path.join(local_dir, filename)
442 1
        self.read_cache_feed()
443
444
    # End of __init__() function
445
446
447 1
    def read_cache_feed(self):
448
        """
449
        Reads the cache file which should only contain one date on the
450
        first line
451
        """
452
453 1
        if os.path.isfile(self.cache_filename):
454
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
455
            (self.year, self.month, self.day, self.hour, self.minute) = cache_file.readline().strip().split(' ', 4)
456
            self.date_minutes = self._calculate_minutes(int(self.year), int(self.month), int(self.day), int(self.hour), int(self.minute))
457
            cache_file.close()
458
459
    # End of read_cache_feed() function
460
461
462 1
    def write_cache_feed(self):
463
        """
464
        Overwrites the cache file with values stored in this class
465
        """
466 1
        cache_file = open_and_truncate_file(self.cache_filename)
467
468 1
        cache_file.write('%s %s %s %s %s' % (self.year, self.month, self.day, self.hour, self.minute))
469
470 1
        cache_file.close()
471
472
    # End of write_cache_feed() function
473
474
475 1
    def update_cache_feed(self, date):
476
        """
477
        Updates the values stored in the class with the date which should
478
        be a time.struct_time
479
        """
480
481 1
        self.year = date.tm_year
482 1
        self.month = date.tm_mon
483 1
        self.day = date.tm_mday
484 1
        self.hour = date.tm_hour
485 1
        self.minute = date.tm_min
486 1
        self.date_minutes = self._calculate_minutes_from_date(date)
487
488
    # End of update_cache_feed() function
489
490
491 1
    def _calculate_minutes(self, year, mon, day, hour, mins):
492
        """
493
        Calculate a number of minutes with all parameters and returns
494
        this.
495
        >>> fc = FeedCache('localdir','filename')
496
        >>> fc._calculate_minutes(2016, 5, 1, 0, 0)
497
        1059827040
498
        """
499
500 1
        minutes = (year * 365 * 24 * 60) + \
501
                  (mon * 30 * 24 * 60) + \
502
                  (day * 24 * 60) + \
503
                  (hour * 60) + \
504
                  (mins)
505
506 1
        return minutes
507
508
    # End of _calculate_minutes() function
509
510
511 1
    def _calculate_minutes_from_date(self, date):
512
        """
513
        Transforms a date in a number of minutes to ease comparisons
514
        and returns this number of minutes
515
        """
516
517 1
        return self._calculate_minutes(date.tm_year, date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min)
518
519
    # End of _calculate_minutes() function
520
521
522 1
    def is_newer(self, date):
523
        """
524
        Tells wether "date" is newer than the one in the cache (returns True
525
        or not (returns False)
526
        """
527
528 1
        minutes = self._calculate_minutes_from_date(date)
529
530 1
        if minutes > self.date_minutes:
531 1
            return True
532
533
        else:
534
            return False
535
536
    # End of is_newer() function
537
# End of FeedCache class
538
539
540
######## Below are some utility functions used by classes above ########
541
542
543 1
def make_directories(path):
544
    """
545
    Makes all directories in path if possible. It is not an error if
546
    path already exists.
547
    """
548
549 1
    try:
550 1
        os.makedirs(path)
551
552 1
    except OSError as exc:
553
554 1
        if exc.errno != errno.EEXIST or os.path.isdir(path) is not True:
555
            raise
556
557
# End of make_directories() function
558
559
560 1
def open_and_truncate_file(filename):
561
    """
562
    Opens filename for writing truncating it to a zero length file
563
    and returns a python file object.
564
    """
565
566 1
    cache_file = codecs.open(filename, 'w', encoding='utf-8')
567 1
    cache_file.truncate(0)
568 1
    cache_file.flush()
569
570 1
    return cache_file
571
572
# End of open_and_truncate_file() function
573
####################### End of utility functions #######################
574
575
576 1
def get_values_from_project(project):
577
    """
578
    Gets the values of 'regex' and 'name' keys if found and
579
    returns a tuple (valued, name, regex)
580
    """
581
582 1
    regex = ''
583 1
    name = project
584 1
    valued = False
585
586 1
    if 'regex' in project and 'name' in project:
587 1
        regex = project['regex']
588 1
        name = project['name']
589 1
        valued = True
590
591 1
    return (valued, name, regex)
592
593
# End of get_values_from_project() function
594
595
596 1
def get_latest_release_by_title(project, debug, feed_url):
597
    """
598
    Gets the latest release of a program on a site of type 'byproject'.
599
    project must be a string that represents the project (user/repository in
600
    github for instance).
601
    """
602
603 1
    version = ''
604
605 1
    (valued, name, regex) = get_values_from_project(project)
606
607 1
    url = feed_url.format(name)
608 1
    feed = get_feed_entries_from_url(url)
609
610 1
    if feed is not None and len(feed.entries) > 0:
611 1
        version = feed.entries[0].title
612
613 1
    if valued:
614 1
        res = re.match(regex, version)
615 1
        if res:
616 1
            version = res.group(1)
617 1
        print_debug(debug, u'\tname: {}\n\tversion: {}\n\tregex: {} : {}'.format(name, version, regex, res))
618
619 1
    print_debug(debug, u'\tProject {}: {}'.format(name, version))
620
621 1
    return (name, version)
622
623
# End of get_latest_release_by_title() function
624
625
626 1
def check_versions_feeds_by_projects(project_list, local_dir, debug, feed_url, cache_filename):
627
    """
628
    Checks project's versions on feed_url if any are defined in the yaml
629
    file under the specified tag that got the project_list passed as an argument.
630
    """
631
632 1
    site_cache = FileCache(local_dir, cache_filename)
633
634 1
    for project in project_list:
635 1
        (project, version) = get_latest_release_by_title(project, debug, feed_url)
636 1
        if version != '':
637 1
            site_cache.update_cache_dict(project, version, debug)
638
639 1
    site_cache.write_cache_file()
640
641
# End of check_versions_feeds_by_projects() function
642
643
644 1
def cut_title_with_default_method(title):
645
    """
646
    Cuts title with a default method and a fallback
647
    >>> cut_title_with_default_method('versions 1.3.2')
648
    ('versions', '1.3.2')
649
    >>> cut_title_with_default_method('no_version_project')
650
    ('no_version_project', '')
651
    """
652
653 1
    try:
654 1
        (project, version) = title.strip().split(' ', 1)
655
656 1
    except ValueError:
657 1
        project = title.strip()
658 1
        version = ''
659
660 1
    return (project, version)
661
662
# End of cut_title_with_default_method() function
663
664
665 1
def cut_title_with_regex_method(title, regex):
666
    """
667
    Cuts title using a regex. If it does not success
668
    fallback to default.
669
    >>> cut_title_with_regex_method('versions 1.3.2', '([\w]+)\s([\d\.]+)')
670
    ('versions', '1.3.2', False)
671
    >>> cut_title_with_regex_method('versions 1.3.2', '([\w]+)notgood\s([\d\.]+)')
672
    ('', '', True)
673
    """
674
675 1
    default = False
676 1
    project = ''
677 1
    version = ''
678
679 1
    res = re.match(regex, title)
680 1
    if res:
681 1
        project = res.group(1)
682 1
        version = res.group(2)
683
    else:
684 1
        default = True
685
686 1
    return (project, version, default)
687
688
# End of cut_title_with_regex_method() function
689
690
691 1
def cut_title_in_project_version(title, regex):
692
    """
693
    Cuts the title into a tuple (project, version) where possible with a regex
694
    or if there is no regex or the regex did not match cuts the title with a
695
    default method
696
    """
697 1
    default = False
698
699 1
    if regex is not None:
700 1
        (project, version, default) = cut_title_with_regex_method(title, regex)
701
    else:
702 1
        default = True
703
704 1
    if default:
705 1
        (project, version) = cut_title_with_default_method(title)
706
707 1
    return (project, version)
708
709
# End of cut_title_in_project_version() function
710
711
712 1
def make_list_of_newer_feeds(feed, feed_info, debug, regex):
713
    """
714
    Compares feed entries and keep those that are newer than the latest
715
    check we've done and inserting the newer ones in reverse order in
716
    a list to be returned
717
    """
718
719 1
    feed_list = []
720
721
    # inserting into a list in reverse order to keep the most recent
722
    # version in case of multiple release of the same project in the
723
    # feeds
724 1
    for a_feed in feed.entries:
725
726 1
        (project, version) = cut_title_in_project_version(a_feed.title, regex)
727 1
        print_debug(debug, u'\tFeed entry ({0}): project: {1:16} version: {2}'.format(time.strftime('%x %X', a_feed.published_parsed), project, version))
728
729 1
        if feed_info.is_newer(a_feed.published_parsed):
730 1
            feed_list.insert(0, a_feed)
731
732 1
    return feed_list
733
734
# End of make_list_of_newer_feeds() function
735
736
737 1
def lower_list_of_strings(project_list):
738
    """
739
    Lowers every string in the list to ease sorting and comparisons
740
    """
741
742 1
    project_list_low = [project.lower() for project in project_list]
743
744 1
    return project_list_low
745
746
# End of lower_list_of_strings() function
747
748
749 1
def check_and_update_feed(feed_list, project_list, cache, debug, regex):
750
    """
751
    Checks every feed entry in the list against project list cache and
752
    then updates the dictionnary then writes the cache file to the disk.
753
     - feed_list    is a list of feed (from feedparser module)
754
     - project_list is the list of project as read from the yaml
755
                    configuration file
756
     - cache is an initialized instance of FileCache
757
    """
758
759
    # Lowers the list before searching in it
760 1
    project_list_low = lower_list_of_strings(project_list)
761
762
    # Checking every feed entry that are newer than the last check
763
    # and updates the dictionnary accordingly
764 1
    for entry in feed_list:
765 1
        (project, version) = cut_title_in_project_version(entry.title, regex)
766 1
        print_debug(debug, u'\tChecking {0:16}: {1}'.format(project, version))
767 1
        if project.lower() in project_list_low:
768
            cache.update_cache_dict(project, version, debug)
769
770 1
    cache.write_cache_file()
771
772
# End of check_and_update_feed() function
773
774
775 1
def manage_http_status(feed, url):
776
    """
777
    Manages http status code present in feed and prints
778
    an error in case of a 3xx, 4xx or 5xx and stops 
779
    doing anything for the feed by returning None.
780
    """
781
782 1
    err = feed.status / 100
783
784 1
    if err > 2:
785 1
        print(u'Error {} while fetching "{}".'.format(feed.status, url))
786 1
        feed = None
787
788 1
    return feed
789
790
# End of manage_http_status() function
791
792
793 1
def manage_non_http_errors(feed, url):
794
    """
795
    Tries to manage non http errors and gives
796
    a message to the user.
797
    """
798
799 1
    if feed.bozo:
800 1
        if feed.bozo_exception:
801 1
            exc = feed.bozo_exception
802 1
            if hasattr(exc, 'reason'):
803 1
                message = exc.reason
804
            else:
805
                message = 'unaddressed'
806
807 1
            print(u'Error {} while fetching "{}".'.format(message, url))
808
809
        else:
810
            print(u'Error while fetching url "{}".'.format(url))
811
812
# End of manage_non_http_errors() function
813
814
815 1
def get_feed_entries_from_url(url):
816
    """
817
    Gets feed entries from an url that should be an
818
    RSS or Atom feed.
819
    >>> get_feed_entries_from_url("http://delhomme.org/notfound.html")
820
    Error 404 while fetching "http://delhomme.org/notfound.html".
821
    >>> feed = get_feed_entries_from_url("http://blog.delhomme.org/index.php?feed/atom")
822
    >>> feed.status
823
    200
824
    """
825
826 1
    feed = feedparser.parse(url)
827
828 1
    if 'status' in feed:
829 1
        feed = manage_http_status(feed, url)
830
    else:
831
        # An error happened such that the feed does not contain an HTTP response
832 1
        manage_non_http_errors(feed, url)
833 1
        feed = None
834
835 1
    return feed
836
837
# End of get_feed_entries_from_url() function
838
839
840 1
def check_versions_for_list_sites(feed_project_list, url, cache_filename, feed_filename, local_dir, debug, regex):
841
    """
842
    Checks projects of 'list' type sites such as freshcode's web site's RSS
843
    """
844
845 1
    freshcode_cache = FileCache(local_dir, cache_filename)
846
847 1
    feed_info = FeedCache(local_dir, feed_filename)
848 1
    feed_info.read_cache_feed()
849
850 1
    feed = get_feed_entries_from_url(url)
851
852 1
    if feed is not None:
853 1
        print_debug(debug, u'\tFound {} entries'.format(len(feed.entries)))
854 1
        feed_list = make_list_of_newer_feeds(feed, feed_info, debug, regex)
855 1
        print_debug(debug, u'\tFound {} new entries (relative to {})'.format(len(feed_list), feed_info.date_minutes))
856
857 1
        check_and_update_feed(feed_list, feed_project_list, freshcode_cache, debug, regex)
858
859
        # Updating feed_info with the latest parsed feed entry date
860 1
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
861
862 1
    feed_info.write_cache_feed()
863
864
# End of check_versions_for_list_sites() function
865
866
867 1
def print_versions_from_cache(local_dir, cache_filename_list, debug):
868
    """
869
    Prints all projects and their associated data from the cache
870
    """
871 1
    for cache_filename in cache_filename_list:
872 1
        site_cache = FileCache(local_dir, cache_filename)
873 1
        site_cache.print_cache_dict(cache_filename)
874
875
# End of print_versions_from_cache()
876
877
878 1
def main():
879
    """
880
    This is the where the program begins
881
    """
882
883 1
    versions_conf = Conf()  # Configuration options
884
885 1
    if versions_conf.options.debug:
886 1
        doctest.testmod(verbose=True)
887
888 1
    if os.path.isfile(versions_conf.config_filename):
889 1
        versions_conf.print_cache_or_check_versions()
890
891
    else:
892 1
        print(u'Error: file {} does not exist'.format(versions_conf.config_filename))
893
894
# End of main() function
895
896
897 1
def print_debug(debug, message):
898
    """
899
    Prints 'message' if debug mode is True
900
    """
901
902 1
    if debug:
903 1
        print(message)
904
905
# End of print_debug() function
906
907
908 1
if __name__ == "__main__":
909
    main()
910