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

Conf.print_cache_or_check_versions()   A

Complexity

Conditions 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 18
ccs 7
cts 7
cp 1
crap 2
rs 9.4285
1
#!/usr/bin/env python
2
# -*- coding: utf8 -*-
3
#
4
#  versions.py : checks releases and versions of programs through RSS
5
#                or Atom feeds and tells you
6
#
7
#  (C) Copyright 2016 - 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 as 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')
120
121 1
        parser.add_argument('-v', '--version', action='version', version=str_version)
122 1
        parser.add_argument('-f', '--file', action='store', dest='filename', help='Configuration file with projects to check', default='')
123 1
        parser.add_argument('-l', '--list-cache', action='store_true', dest='list_cache', help='Lists all projects and their version in cache', default=False)
124 1
        parser.add_argument('-d', '--debug', action='store_true', dest='debug', help='Starts in debug mode and prints things that may help', default=False)
125
126 1
        self.options = parser.parse_args()
127
128 1
        if self.options.filename != '':
129 1
            self.config_filename = self.options.filename
130
        else:
131 1
            self.config_filename = os.path.join(self.config_dir, 'versions.yaml')
132
133
    # End of get_command_line_arguments() function
134
135 1
    def extract_site_definition(self, site_name):
136
        """
137
        extracts whole site definition
138
        """
139
140 1
        if site_name in self.description:
141 1
            return self.description[site_name]
142
        else:
143
            return dict()
144
145
    # End of extract_site_definition()
146
147
148 1
    def extract_regex_from_site(self, site_name):
149
        """
150
        Extracts a regex from a site as defined in the YAML file.
151
        Returns the regex if it exists or None otherwise.
152
        """
153
154 1
        site_definition = self.extract_site_definition(site_name)
155
156 1
        if 'regex' in site_definition:
157 1
            regex = site_definition['regex']
158
        else:
159 1
            regex = None
160
161 1
        return regex
162
163
    # End of extract_regex_from_site() function
164
165
166 1
    def extract_project_list_from_site(self, site_name):
167
        """
168
        Extracts a project list from a site as defined in the YAML file.
169
        """
170
171 1
        site_definition = self.extract_site_definition(site_name)
172
173 1
        if 'projects' in site_definition:
174 1
            project_list = site_definition['projects']
175 1
            if project_list is None:
176 1
                print(u'Warning: no project for site "{}".'.format(site_name))
177 1
                project_list = []
178
        else:
179 1
            project_list = []
180
181 1
        return project_list
182
183
    # End of extract_project_list_from_site() function
184
185
186 1
    def extract_project_url(self, site_name):
187
        """
188
        Extracts the url definition where to check project version.
189
        """
190
191 1
        site_definition = self.extract_site_definition(site_name)
192
193 1
        if 'url' in site_definition:
194 1
            project_url = site_definition['url']
195
        else:
196
            project_url = ''
197
198 1
        return project_url
199
200
    # End of extract_project_url() function
201
202
203 1
    def is_site_of_type(self, site_name, type):
204
        """
205
        Returns True if site_name is of type 'type'
206
        """
207
208 1
        site_definition = self.extract_site_definition(site_name)
209 1
        if 'type' in site_definition:
210 1
            return (site_definition['type'] == type)
211
        else:
212
            return False
213
214
    # End of is_site_of_type() function
215
216
217 1
    def extract_site_list(self, type):
218
        """
219
        Extracts all sites from a specific type (byproject or list)
220
        """
221
222 1
        all_site_list = list(self.description.keys())
223 1
        site_list = []
224 1
        for site_name in all_site_list:
225 1
            if self.is_site_of_type(site_name, type):
226 1
                site_list.insert(0, site_name)
227
228 1
        return site_list
229
230
    # End of extract_site_list() function
231
232
233 1
    def make_site_cache_list_name(self):
234
        """
235
        Formats list of cache filenames for all sites.
236
        """
237
238 1
        all_site_list = list(self.description.keys())
239 1
        cache_list = []
240 1
        for site_name in all_site_list:
241 1
            site_cache = u'{}.cache'.format(site_name)
242 1
            cache_list.insert(0, site_cache)
243
244 1
        return cache_list
245
246
    # End of make_site_cache_list_name() function
247
248
249 1
    def print_cache_or_check_versions(self):
250
        """
251
        Decide to pretty print projects and their associated version that
252
        are already in the cache or to check versions of that projects upon
253
        selections made at the command line
254
        """
255
256 1
        print_debug(self.options.debug, u'Loading yaml config file')
257 1
        self.load_yaml_from_config_file(self.config_filename)
258
259 1
        if self.options.list_cache is True:
260
            # Pretty prints all caches.
261 1
            cache_list = self.make_site_cache_list_name()
262 1
            print_versions_from_cache(self.local_dir, cache_list, self.options.debug)
263
264
        else:
265
            # Checks version from online feeds
266 1
            self.check_versions()
267
268
    # End of print_list_or_check_versions() function.
269
270
271 1
    def check_versions(self):
272
        """
273
        Checks versions by parsing online feeds.
274
        """
275
276
        # Checks projects from by project sites such as github and sourceforge
277 1
        byproject_site_list = self.extract_site_list('byproject')
278
279 1
        for site_name in byproject_site_list:
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.items():
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 1
            print_debug(debug, u'\t\tIn cache: {}'.format(version_cache))
396
397 1
            if version != version_cache:
398 1
                print(u'{} {}'.format(project, version))
399 1
                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.items(), 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 = 5
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 1
        regex = project['regex']
589 1
        name = project['name']
590 1
        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 a site of type 'byproject'.
600
    project must be a string that represents the project (user/repository in
601
    github for instance).
602
    """
603
604 1
    version = ''
605
606 1
    (valued, name, regex) = get_values_from_project(project)
607
608 1
    url = feed_url.format(name)
609 1
    feed = get_feed_entries_from_url(url)
610
611 1
    if feed is not None and len(feed.entries) > 0:
612 1
        version = feed.entries[0].title
613
614 1
    if valued:
615 1
        res = re.match(regex, version)
616 1
        if res:
617 1
            version = res.group(1)
618 1
        print_debug(debug, u'\tname: {}\n\tversion: {}\n\tregex: {} : {}'.format(name, version, regex, res))
619
620 1
    print_debug(debug, u'\tProject {}: {}'.format(name, version))
621
622 1
    return (name, version)
623
624
# End of get_latest_release_by_title() function
625
626
627 1
def check_versions_feeds_by_projects(project_list, local_dir, debug, feed_url, cache_filename):
628
    """
629
    Checks project's versions on feed_url if any are defined in the yaml
630
    file under the specified tag that got the project_list passed as an argument.
631
    """
632
633 1
    site_cache = FileCache(local_dir, cache_filename)
634
635 1
    for project in project_list:
636 1
        (project, version) = get_latest_release_by_title(project, debug, feed_url)
637 1
        if version != '':
638 1
            site_cache.update_cache_dict(project, version, debug)
639
640 1
    site_cache.write_cache_file()
641
642
# End of check_versions_feeds_by_projects() function
643
644
645 1
def cut_title_with_default_method(title):
646
    """
647
    Cuts title with a default method and a fallback
648
    >>> cut_title_with_default_method('versions 1.3.2')
649
    ('versions', '1.3.2')
650
    >>> cut_title_with_default_method('no_version_project')
651
    ('no_version_project', '')
652
    """
653
654 1
    try:
655 1
        (project, version) = title.strip().split(' ', 1)
656
657 1
    except ValueError:
658 1
        project = title.strip()
659 1
        version = ''
660
661 1
    return (project, version)
662
663
# End of cut_title_with_default_method() function
664
665
666 1
def cut_title_with_regex_method(title, regex):
667
    """
668
    Cuts title using a regex. If it does not success
669
    fallback to default.
670
    >>> cut_title_with_regex_method('versions 1.3.2', '([\w]+)\s([\d\.]+)')
671
    ('versions', '1.3.2', False)
672
    >>> cut_title_with_regex_method('versions 1.3.2', '([\w]+)notgood\s([\d\.]+)')
673
    ('', '', True)
674
    """
675
676 1
    default = False
677 1
    project = ''
678 1
    version = ''
679
680 1
    res = re.match(regex, title)
681 1
    if res:
682 1
        project = res.group(1)
683 1
        version = res.group(2)
684
    else:
685 1
        default = True
686
687 1
    return (project, version, default)
688
689
# End of cut_title_with_regex_method() function
690
691
692 1
def cut_title_in_project_version(title, regex):
693
    """
694
    Cuts the title into a tuple (project, version) where possible with a regex
695
    or if there is no regex or the regex did not match cuts the title with a
696
    default method
697
    """
698 1
    default = False
699
700 1
    if regex is not None:
701 1
        (project, version, default) = cut_title_with_regex_method(title, regex)
702
    else:
703 1
        default = True
704
705 1
    if default:
706 1
        (project, version) = cut_title_with_default_method(title)
707
708 1
    return (project, version)
709
710
# End of cut_title_in_project_version() function
711
712
713 1
def make_list_of_newer_feeds(feed, feed_info, debug, regex):
714
    """
715
    Compares feed entries and keep those that are newer than the latest
716
    check we've done and inserting the newer ones in reverse order in
717
    a list to be returned
718
    """
719
720 1
    feed_list = []
721
722
    # inserting into a list in reverse order to keep the most recent
723
    # version in case of multiple release of the same project in the
724
    # feeds
725 1
    for a_feed in feed.entries:
726
727 1
        (project, version) = cut_title_in_project_version(a_feed.title, regex)
728 1
        print_debug(debug, u'\tFeed entry ({0}): project: {1:16} version: {2}'.format(time.strftime('%x %X', a_feed.published_parsed), project, version))
729
730 1
        if feed_info.is_newer(a_feed.published_parsed):
731 1
            feed_list.insert(0, a_feed)
732
733 1
    return feed_list
734
735
# End of make_list_of_newer_feeds() function
736
737
738 1
def lower_list_of_strings(project_list):
739
    """
740
    Lowers every string in the list to ease sorting and comparisons
741
    """
742
743 1
    project_list_low = [project.lower() for project in project_list]
744
745 1
    return project_list_low
746
747
# End of lower_list_of_strings() function
748
749
750 1
def check_and_update_feed(feed_list, project_list, cache, debug, regex):
751
    """
752
    Checks every feed entry in the list against project list cache and
753
    then updates the dictionnary then writes the cache file to the disk.
754
     - feed_list    is a list of feed (from feedparser module)
755
     - project_list is the list of project as read from the yaml
756
                    configuration file
757
     - cache is an initialized instance of FileCache
758
    """
759
760
    # Lowers the list before searching in it
761 1
    project_list_low = lower_list_of_strings(project_list)
762
763
    # Checking every feed entry that are newer than the last check
764
    # and updates the dictionnary accordingly
765 1
    for entry in feed_list:
766 1
        (project, version) = cut_title_in_project_version(entry.title, regex)
767 1
        print_debug(debug, u'\tChecking {0:16}: {1}'.format(project, version))
768 1
        if project.lower() in project_list_low:
769
            cache.update_cache_dict(project, version, debug)
770
771 1
    cache.write_cache_file()
772
773
# End of check_and_update_feed() function
774
775
776 1
def manage_http_status(feed, url):
777
    """
778
    Manages http status code present in feed and prints
779
    an error in case of a 3xx, 4xx or 5xx and stops
780
    doing anything for the feed by returning None.
781
    """
782
783 1
    err = feed.status / 100
784
785 1
    if err > 2:
786 1
        print(u'Error {} while fetching "{}".'.format(feed.status, url))
787 1
        feed = None
788
789 1
    return feed
790
791
# End of manage_http_status() function
792
793
794 1
def manage_non_http_errors(feed, url):
795
    """
796
    Tries to manage non http errors and gives
797
    a message to the user.
798
    """
799
800 1
    if feed.bozo:
801 1
        if feed.bozo_exception:
802 1
            exc = feed.bozo_exception
803 1
            if hasattr(exc, 'reason'):
804 1
                message = exc.reason
805
            else:
806
                message = 'unaddressed'
807
808 1
            print(u'Error {} while fetching "{}".'.format(message, url))
809
810
        else:
811
            print(u'Error while fetching url "{}".'.format(url))
812
813
# End of manage_non_http_errors() function
814
815
816 1
def get_feed_entries_from_url(url):
817
    """
818
    Gets feed entries from an url that should be an
819
    RSS or Atom feed.
820
    >>> get_feed_entries_from_url("http://delhomme.org/notfound.html")
821
    Error 404 while fetching "http://delhomme.org/notfound.html".
822
    >>> feed = get_feed_entries_from_url("http://blog.delhomme.org/index.php?feed/atom")
823
    >>> feed.status
824
    200
825
    """
826
827 1
    feed = feedparser.parse(url)
828
829 1
    if 'status' in feed:
830 1
        feed = manage_http_status(feed, url)
831
    else:
832
        # An error happened such that the feed does not contain an HTTP response
833 1
        manage_non_http_errors(feed, url)
834 1
        feed = None
835
836 1
    return feed
837
838
# End of get_feed_entries_from_url() function
839
840
841 1
def check_versions_for_list_sites(feed_project_list, url, cache_filename, feed_filename, local_dir, debug, regex):
842
    """
843
    Checks projects of 'list' type sites such as freshcode's web site's RSS
844
    """
845
846 1
    freshcode_cache = FileCache(local_dir, cache_filename)
847
848 1
    feed_info = FeedCache(local_dir, feed_filename)
849 1
    feed_info.read_cache_feed()
850
851 1
    feed = get_feed_entries_from_url(url)
852
853 1
    if feed is not None:
854 1
        print_debug(debug, u'\tFound {} entries'.format(len(feed.entries)))
855 1
        feed_list = make_list_of_newer_feeds(feed, feed_info, debug, regex)
856 1
        print_debug(debug, u'\tFound {} new entries (relative to {})'.format(len(feed_list), feed_info.date_minutes))
857
858 1
        check_and_update_feed(feed_list, feed_project_list, freshcode_cache, debug, regex)
859
860
        # Updating feed_info with the latest parsed feed entry date
861 1
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
862
863 1
    feed_info.write_cache_feed()
864
865
# End of check_versions_for_list_sites() function
866
867
868 1
def print_versions_from_cache(local_dir, cache_filename_list, debug):
869
    """
870
    Prints all projects and their associated data from the cache
871
    """
872 1
    for cache_filename in cache_filename_list:
873 1
        site_cache = FileCache(local_dir, cache_filename)
874 1
        site_cache.print_cache_dict(cache_filename)
875
876
# End of print_versions_from_cache()
877
878
879 1
def main():
880
    """
881
    This is the where the program begins
882
    """
883
884 1
    versions_conf = Conf()  # Configuration options
885
886 1
    if versions_conf.options.debug:
887 1
        doctest.testmod(verbose=True)
888
889 1
    if os.path.isfile(versions_conf.config_filename):
890 1
        versions_conf.print_cache_or_check_versions()
891
892
    else:
893 1
        print(u'Error: file {} does not exist'.format(versions_conf.config_filename))
894
895
# End of main() function
896
897
898 1
def print_debug(debug, message):
899
    """
900
    Prints 'message' if debug mode is True
901
    """
902
903 1
    if debug:
904 1
        print(message)
905
906
# End of print_debug() function
907
908
909 1
if __name__ == "__main__":
910
    main()
911