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 ( 4e6c8a...f9432b )
by dup
01:14
created

get_feed_entries_from_url()   A

Complexity

Conditions 2

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
c 3
b 0
f 0
dl 0
loc 21
ccs 7
cts 7
cp 1
crap 2
rs 9.3142
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
            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
            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
280 1
            print_debug(self.options.debug, u'Checking {} projects'.format(site_name))
281 1
            (project_list, project_url, cache_filename) = self.get_infos_for_site(site_name)
282 1
            check_versions_feeds_by_projects(project_list, self.local_dir, self.options.debug, project_url, cache_filename)
283
284
        # Checks projects from 'list' tupe sites such as freshcode.club
285 1
        list_site_list = self.extract_site_list('list')
286 1
        for site_name in list_site_list:
287 1
            print_debug(self.options.debug, u'Checking {} updates'.format(site_name))
288 1
            (project_list, project_url, cache_filename) = self.get_infos_for_site(site_name)
289 1
            regex = self.extract_regex_from_site(site_name)
290 1
            feed_filename = u'{}.feed'.format(site_name)
291 1
            check_versions_for_list_sites(project_list, project_url, cache_filename, feed_filename, self.local_dir, self.options.debug, regex)
292
293
    # End of check_versions() function
294
    
295
    
296 1
    def get_infos_for_site(self, site_name):
297
        """
298
        Returns informations about a site as a tuple
299
        (list of projects, url to check, filename of the cache)
300
        """
301
302 1
        project_list = self.extract_project_list_from_site(site_name)
303 1
        project_url = self.extract_project_url(site_name)
304 1
        cache_filename = u'{}.cache'.format(site_name)
305
306 1
        return (project_list, project_url, cache_filename)
307
308
    # End of get_infos_for_site() function
309
310
311
# End of Conf class
312
313
314 1
class FileCache:
315
    """
316
    This class should help in managing cache files
317
    """
318
319 1
    cache_filename = ''
320 1
    cache_dict = {}  # Dictionnary of projects and their associated version
321
322 1
    def __init__(self, local_dir, filename):
323
        """
324
        Inits the class. 'local_dir' must be a directory where we want to
325
        store the cache file named 'filename'
326
        """
327
328 1
        self.cache_filename = os.path.join(local_dir, filename)
329 1
        self.cache_dict = {}
330 1
        self._read_cache_file()
331
332
    # End of __init__() function
333
334
335 1
    def _return_project_and_version_from_line(self, line):
336
        """
337
        Splits the line into a project and a version if possible (the line
338
        must contain a whitespace.
339
        """
340
341 1
        line = line.strip()
342
343 1
        if line.count(' ') > 0:
344 1
            (project, version) = line.split(' ', 1)
345
346
        elif line != '':
347
            project = line
348
            version = ''
349
350 1
        return (project, version)
351
352
    # End of _return_project_and_version_from_line() function
353
354
355 1
    def _read_cache_file(self):
356
        """
357
        Reads the cache file and puts it into a dictionnary of project with
358
        their associated version
359
        """
360
361 1
        if os.path.isfile(self.cache_filename):
362 1
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
363
364 1
            for line in cache_file:
365 1
                (project, version) = self._return_project_and_version_from_line(line)
366 1
                self.cache_dict[project] = version
367
368 1
            cache_file.close()
369
370
    # End of _read_cache_file() function
371
372
373 1
    def write_cache_file(self):
374
        """
375
        Owerwrites dictionnary cache to the cache file
376
        """
377
378 1
        cache_file = open_and_truncate_file(self.cache_filename)
379
380 1
        for (project, version) in self.cache_dict.iteritems():
381 1
            cache_file.write('%s %s\n' % (project, version))
382
383 1
        cache_file.close()
384
385
    # End of write_cache_file() function
386
387
388 1
    def update_cache_dict(self, project, version, debug):
389
        """
390
        Updates cache dictionnary if needed
391
        """
392
393 1
        try:
394 1
            version_cache = self.cache_dict[project]
395
            print_debug(debug, u'\t\tIn cache: {}'.format(version_cache))
396
397
            if version != version_cache:
398
                print(u'{} {}'.format(project, version))
399
                self.cache_dict[project] = version
400
401 1
        except KeyError:
402 1
            print(u'{} {}'.format(project, version))
403 1
            self.cache_dict[project] = version
404
405
    # End of update_cache_dict() function
406
407
408 1
    def print_cache_dict(self, sitename):
409
        """
410
        Pretty prints the cache dictionary as it is recorded in the files.
411
        """
412
413 1
        print(u'{}:'.format(sitename))
414
415
        # Gets project and version tuple sorted by project lowered while sorting
416 1
        for project, version in sorted(self.cache_dict.iteritems(), key=lambda proj: proj[0].lower()):
417 1
            print(u'\t{} {}'.format(project, version))
418
419 1
        print('')
420
421
    # End of print_cache_dict() function
422
# End of FileCache class
423
424
425 1
class FeedCache:
426
427 1
    cache_filename = ''
428 1
    year = 2016
429 1
    month = 05
430 1
    day = 1
431 1
    hour = 0
432 1
    minute = 0
433 1
    date_minutes = 0
434
435
436 1
    def __init__(self, local_dir, filename):
437
        """
438
        Inits the class. 'local_dir' must be a directory where we want to
439
        store the cache file named 'filename'
440
        """
441
442 1
        self.cache_filename = os.path.join(local_dir, filename)
443 1
        self.read_cache_feed()
444
445
    # End of __init__() function
446
447
448 1
    def read_cache_feed(self):
449
        """
450
        Reads the cache file which should only contain one date on the
451
        first line
452
        """
453
454 1
        if os.path.isfile(self.cache_filename):
455
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
456
            (self.year, self.month, self.day, self.hour, self.minute) = cache_file.readline().strip().split(' ', 4)
457
            self.date_minutes = self._calculate_minutes(int(self.year), int(self.month), int(self.day), int(self.hour), int(self.minute))
458
            cache_file.close()
459
460
    # End of read_cache_feed() function
461
462
463 1
    def write_cache_feed(self):
464
        """
465
        Overwrites the cache file with values stored in this class
466
        """
467 1
        cache_file = open_and_truncate_file(self.cache_filename)
468
469 1
        cache_file.write('%s %s %s %s %s' % (self.year, self.month, self.day, self.hour, self.minute))
470
471 1
        cache_file.close()
472
473
    # End of write_cache_feed() function
474
475
476 1
    def update_cache_feed(self, date):
477
        """
478
        Updates the values stored in the class with the date which should
479
        be a time.struct_time
480
        """
481
482 1
        self.year = date.tm_year
483 1
        self.month = date.tm_mon
484 1
        self.day = date.tm_mday
485 1
        self.hour = date.tm_hour
486 1
        self.minute = date.tm_min
487 1
        self.date_minutes = self._calculate_minutes_from_date(date)
488
489
    # End of update_cache_feed() function
490
491
492 1
    def _calculate_minutes(self, year, mon, day, hour, mins):
493
        """
494
        Calculate a number of minutes with all parameters and returns
495
        this.
496
        >>> fc = FeedCache('localdir','filename')
497
        >>> fc._calculate_minutes(2016, 5, 1, 0, 0)
498
        1059827040
499
        """
500
501 1
        minutes = (year * 365 * 24 * 60) + \
502
                  (mon * 30 * 24 * 60) + \
503
                  (day * 24 * 60) + \
504
                  (hour * 60) + \
505
                  (mins)
506
507 1
        return minutes
508
509
    # End of _calculate_minutes() function
510
511
512 1
    def _calculate_minutes_from_date(self, date):
513
        """
514
        Transforms a date in a number of minutes to ease comparisons
515
        and returns this number of minutes
516
        """
517
518 1
        return self._calculate_minutes(date.tm_year, date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min)
519
520
    # End of _calculate_minutes() function
521
522
523 1
    def is_newer(self, date):
524
        """
525
        Tells wether "date" is newer than the one in the cache (returns True
526
        or not (returns False)
527
        """
528
529 1
        minutes = self._calculate_minutes_from_date(date)
530
531 1
        if minutes > self.date_minutes:
532 1
            return True
533
534
        else:
535
            return False
536
537
    # End of is_newer() function
538
# End of FeedCache class
539
540
541
######## Below are some utility functions used by classes above ########
542
543
544 1
def make_directories(path):
545
    """
546
    Makes all directories in path if possible. It is not an error if
547
    path already exists.
548
    """
549
550 1
    try:
551 1
        os.makedirs(path)
552
553 1
    except OSError as exc:
554
555 1
        if exc.errno != errno.EEXIST or os.path.isdir(path) is not True:
556
            raise
557
558
# End of make_directories() function
559
560
561 1
def open_and_truncate_file(filename):
562
    """
563
    Opens filename for writing truncating it to a zero length file
564
    and returns a python file object.
565
    """
566
567 1
    cache_file = codecs.open(filename, 'w', encoding='utf-8')
568 1
    cache_file.truncate(0)
569 1
    cache_file.flush()
570
571 1
    return cache_file
572
573
# End of open_and_truncate_file() function
574
####################### End of utility functions #######################
575
576
577 1
def get_values_from_project(project):
578
    """
579
    Gets the values of 'regex' and 'name' keys if found and
580
    returns a tuple (valued, name, regex)
581
    """
582
583 1
    regex = ''
584 1
    name = project
585 1
    valued = False
586
587 1
    if 'regex' in project and 'name' in project:
588
        regex = project['regex']
589
        name = project['name']
590
        valued = True
591
592 1
    return (valued, name, regex)
593
594
# End of get_values_from_project() function
595
596
597 1
def get_latest_release_by_title(project, debug, feed_url):
598
    """
599
    Gets the latest release of a program on github. program must be a
600
    string of the form user/repository.
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
        res = re.match(regex, version)
615
        if res:
616
            version = res.group(1)
617
        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
        (project, version, default) = cut_title_with_regex_method(title)
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
        length = len(feed.entries)
854 1
        print_debug(debug, u'\tFound {} entries'.format(length))
855
856 1
        feed_list = make_list_of_newer_feeds(feed, feed_info, debug, regex)
857 1
        print_debug(debug, u'\tFound {} new entries (relative to {})'.format(len(feed_list), feed_info.date_minutes))
858
859 1
        check_and_update_feed(feed_list, feed_project_list, freshcode_cache, debug, regex)
860
861
        # Updating feed_info with the latest parsed feed entry date
862 1
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
863
864 1
    feed_info.write_cache_feed()
865
866
# End of check_versions_for_list_sites() function
867
868
869 1
def print_versions_from_cache(local_dir, cache_filename_list, debug):
870
    """
871
    Prints all projects and their associated data from the cache
872
    """
873 1
    for cache_filename in cache_filename_list:
874 1
        site_cache = FileCache(local_dir, cache_filename)
875 1
        site_cache.print_cache_dict(cache_filename)
876
877
# End of print_versions_from_cache()
878
879
880 1
def main():
881
    """
882
    This is the where the program begins
883
    """
884
885 1
    versions_conf = Conf()  # Configuration options
886
887 1
    if versions_conf.options.debug:
888 1
        doctest.testmod(verbose=True)
889
890 1
    if os.path.isfile(versions_conf.config_filename):
891 1
        versions_conf.print_cache_or_check_versions()
892
893
    else:
894 1
        print(u'Error: file {} does not exist'.format(versions_conf.config_filename))
895
896
# End of main() function
897
898
899 1
def print_debug(debug, message):
900
    """
901
    Prints 'message' if debug mode is True
902
    """
903
904 1
    if debug:
905 1
        print(message)
906
907
# End of print_debug() function
908
909
910 1
if __name__ == "__main__":
911
    main()
912