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 ( 6e37c8...3547c5 )
by dup
01:02
created

get_feed_entries_from_url()   A

Complexity

Conditions 2

Size

Total Lines 19

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 19
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__ = "15.11.2017"
36 1
__version__ = "1.3.0"
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: 8: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("Error in configuration file {} at position: {}:{}".format(filename, mark.line+1, mark.column+1))
102
            else:
103
                print("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
        else:
175 1
            project_list = []
176
177 1
        return project_list
178
179
    # End of extract_project_list_from_site() function
180
181
182 1
    def extract_project_url(self, site_name):
183
        """
184
        Extracts the url definition where to check project version.
185
        """
186
187 1
        site_definition = self.extract_site_definition(site_name)
188
189 1
        if 'url' in site_definition:
190 1
            project_url = site_definition['url']
191
        else:
192
            project_url = ''
193
194 1
        return project_url
195
196
    # End of extract_project_url() function
197
198
199 1
    def is_site_of_type(self, site_name, type):
200
        """
201
        Returns True if site_name is of type 'type'
202
        """
203
204 1
        site_definition = self.extract_site_definition(site_name)
205 1
        if 'type' in site_definition:
206 1
            return (site_definition['type'] == type)
207
        else:
208
            return False
209
210
    # End of is_site_of_type() function
211
212
213 1
    def extract_site_list(self, type):
214
        """
215
        Extracts all sites from a specific type (byproject or list)
216
        """
217
218 1
        all_site_list = list(self.description.keys())
219 1
        site_list = []
220 1
        for site_name in all_site_list:
221 1
            if self.is_site_of_type(site_name, type):
222 1
                site_list.insert(0, site_name)
223
       
224 1
        return site_list
225
226
    # End of extract_site_list() function
227
228
229 1
    def make_site_cache_list_name(self):
230
        """
231
        Formats list of cache filenames for all sites.
232
        """
233
234 1
        all_site_list = list(self.description.keys())
235 1
        cache_list = []
236 1
        for site_name in all_site_list:
237 1
            site_cache = u'{}.cache'.format(site_name)
238 1
            cache_list.insert(0, site_cache)
239
240 1
        return cache_list
241
242
    # End of make_site_cache_list_name() function
243
244
245 1
    def print_cache_or_check_versions(self):
246
        """
247
        Decide to pretty print projects and their associated version that
248
        are already in the cache or to check versions of that projects upon
249
        selections made at the command line
250
        """
251
252 1
        print_debug(self.options.debug, u'Loading yaml config file')
253 1
        self.load_yaml_from_config_file(self.config_filename)
254
255 1
        if self.options.list_cache is True:
256
            # Pretty prints all caches.
257 1
            cache_list = self.make_site_cache_list_name()
258 1
            print_versions_from_cache(self.local_dir, cache_list, self.options.debug)
259
260
        else:
261
            # Checks version from online feeds
262 1
            self.check_versions()
263
264
    # End of print_list_or_check_versions() function.
265
266
267 1
    def check_versions(self):
268
        """
269
        Checks versions by parsing online feeds
270
        """
271
272
        # Checks projects from by project sites such as github and sourceforge
273 1
        byproject_site_list = self.extract_site_list('byproject')
274
275 1
        for site_name in byproject_site_list:
276
277 1
            print_debug(self.options.debug, u'Checking {} projects'.format(site_name))
278 1
            (project_list, project_url, cache_filename) = self.get_infos_for_site(site_name)
279 1
            check_versions_feeds_by_projects(project_list, self.local_dir, self.options.debug, project_url, cache_filename)
280
281
        # Checks projects from 'list' tupe sites such as freshcode.club
282 1
        list_site_list = self.extract_site_list('list')
283 1
        for site_name in list_site_list:
284 1
            print_debug(self.options.debug, u'Checking {} updates'.format(site_name))
285 1
            (project_list, project_url, cache_filename) = self.get_infos_for_site(site_name)
286 1
            regex = self.extract_regex_from_site(site_name)
287 1
            feed_filename = u'{}.feed'.format(site_name)
288 1
            check_versions_for_list_sites(project_list, project_url, cache_filename, feed_filename, self.local_dir, self.options.debug, regex)
289
290
    # End of check_versions() function
291
    
292
    
293 1
    def get_infos_for_site(self, site_name):
294
        """
295
        Returns informations about a site as a tuple
296
        (list of projects, url to check, filename of the cache)
297
        """
298
299 1
        project_list = self.extract_project_list_from_site(site_name)
300 1
        project_url = self.extract_project_url(site_name)
301 1
        cache_filename = u'{}.cache'.format(site_name)
302
303 1
        return (project_list, project_url, cache_filename)
304
305
    # End of get_infos_for_site() function
306
307
308
# End of Conf class
309
310
311 1
class FileCache:
312
    """
313
    This class should help in managing cache files
314
    """
315
316 1
    cache_filename = ''
317 1
    cache_dict = {}  # Dictionnary of projects and their associated version
318
319 1
    def __init__(self, local_dir, filename):
320
        """
321
        Inits the class. 'local_dir' must be a directory where we want to
322
        store the cache file named 'filename'
323
        """
324
325 1
        self.cache_filename = os.path.join(local_dir, filename)
326 1
        self.cache_dict = {}
327 1
        self._read_cache_file()
328
329
    # End of __init__() function
330
331
332 1
    def _return_project_and_version_from_line(self, line):
333
        """
334
        Splits the line into a project and a version if possible (the line
335
        must contain a whitespace.
336
        """
337
338 1
        line = line.strip()
339
340 1
        if line.count(' ') > 0:
341 1
            (project, version) = line.split(' ', 1)
342
343
        elif line != '':
344
            project = line
345
            version = ''
346
347 1
        return (project, version)
348
349
    # End of _return_project_and_version_from_line() function
350
351
352 1
    def _read_cache_file(self):
353
        """
354
        Reads the cache file and puts it into a dictionnary of project with
355
        their associated version
356
        """
357
358 1
        if os.path.isfile(self.cache_filename):
359 1
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
360
361 1
            for line in cache_file:
362 1
                (project, version) = self._return_project_and_version_from_line(line)
363 1
                self.cache_dict[project] = version
364
365 1
            cache_file.close()
366
367
    # End of _read_cache_file() function
368
369
370 1
    def write_cache_file(self):
371
        """
372
        Owerwrites dictionnary cache to the cache file
373
        """
374
375 1
        cache_file = open_and_truncate_file(self.cache_filename)
376
377 1
        for (project, version) in self.cache_dict.iteritems():
378 1
            cache_file.write('%s %s\n' % (project, version))
379
380 1
        cache_file.close()
381
382
    # End of write_cache_file() function
383
384
385 1
    def update_cache_dict(self, project, version, debug):
386
        """
387
        Updates cache dictionnary if needed
388
        """
389
390 1
        try:
391 1
            version_cache = self.cache_dict[project]
392
            print_debug(debug, u'\t\tIn cache: {}'.format(version_cache))
393
394
            if version != version_cache:
395
                print('%s %s' % (project, version))
396
                self.cache_dict[project] = version
397
398 1
        except KeyError:
399 1
            print('%s %s' % (project, version))
400 1
            self.cache_dict[project] = version
401
402
    # End of update_cache_dict() function
403
404
405 1
    def print_cache_dict(self, sitename):
406
        """
407
        Pretty prints the cache dictionary as it is recorded in the files.
408
        """
409
410 1
        print('%s:' % sitename)
411
412
        # Gets project and version tuple sorted by project lowered while sorting
413 1
        for project, version in sorted(self.cache_dict.iteritems(), key=lambda proj: proj[0].lower()):
414 1
            print('\t%s %s' % (project, version))
415
416 1
        print('')
417
418
    # End of print_cache_dict() function
419
# End of FileCache class
420
421
422 1
class FeedCache:
423
424 1
    cache_filename = ''
425 1
    year = 2016
426 1
    month = 05
427 1
    day = 1
428 1
    hour = 0
429 1
    minute = 0
430 1
    date_minutes = 0
431
432
433 1
    def __init__(self, local_dir, filename):
434
        """
435
        Inits the class. 'local_dir' must be a directory where we want to
436
        store the cache file named 'filename'
437
        """
438
439 1
        self.cache_filename = os.path.join(local_dir, filename)
440 1
        self.read_cache_feed()
441
442
    # End of __init__() function
443
444
445 1
    def read_cache_feed(self):
446
        """
447
        Reads the cache file which should only contain one date on the
448
        first line
449
        """
450
451 1
        if os.path.isfile(self.cache_filename):
452
            cache_file = codecs.open(self.cache_filename, 'r', encoding='utf-8')
453
            (self.year, self.month, self.day, self.hour, self.minute) = cache_file.readline().strip().split(' ', 4)
454
            self.date_minutes = self._calculate_minutes(int(self.year), int(self.month), int(self.day), int(self.hour), int(self.minute))
455
            cache_file.close()
456
457
    # End of read_cache_feed() function
458
459
460 1
    def write_cache_feed(self):
461
        """
462
        Overwrites the cache file with values stored in this class
463
        """
464 1
        cache_file = open_and_truncate_file(self.cache_filename)
465
466 1
        cache_file.write('%s %s %s %s %s' % (self.year, self.month, self.day, self.hour, self.minute))
467
468 1
        cache_file.close()
469
470
    # End of write_cache_feed() function
471
472
473 1
    def update_cache_feed(self, date):
474
        """
475
        Updates the values stored in the class with the date which should
476
        be a time.struct_time
477
        """
478
479 1
        self.year = date.tm_year
480 1
        self.month = date.tm_mon
481 1
        self.day = date.tm_mday
482 1
        self.hour = date.tm_hour
483 1
        self.minute = date.tm_min
484 1
        self.date_minutes = self._calculate_minutes_from_date(date)
485
486
    # End of update_cache_feed() function
487
488
489 1
    def _calculate_minutes(self, year, mon, day, hour, mins):
490
        """
491
        Calculate a number of minutes with all parameters and returns
492
        this.
493
        >>> fc = FeedCache('localdir','filename')
494
        >>> fc._calculate_minutes(2016, 5, 1, 0, 0)
495
        1059827040
496
        """
497
498 1
        minutes = (year * 365 * 24 * 60) + \
499
                  (mon * 30 * 24 * 60) + \
500
                  (day * 24 * 60) + \
501
                  (hour * 60) + \
502
                  (mins)
503
504 1
        return minutes
505
506
    # End of _calculate_minutes() function
507
508
509 1
    def _calculate_minutes_from_date(self, date):
510
        """
511
        Transforms a date in a number of minutes to ease comparisons
512
        and returns this number of minutes
513
        """
514
515 1
        return self._calculate_minutes(date.tm_year, date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min)
516
517
    # End of _calculate_minutes() function
518
519
520 1
    def is_newer(self, date):
521
        """
522
        Tells wether "date" is newer than the one in the cache (returns True
523
        or not (returns False)
524
        """
525
526 1
        minutes = self._calculate_minutes_from_date(date)
527
528 1
        if minutes > self.date_minutes:
529 1
            return True
530
531
        else:
532
            return False
533
534
    # End of is_newer() function
535
# End of FeedCache class
536
537
538
######## Below are some utility functions used by classes above ########
539
540
541 1
def make_directories(path):
542
    """
543
    Makes all directories in path if possible. It is not an error if
544
    path already exists.
545
    """
546
547 1
    try:
548 1
        os.makedirs(path)
549
550 1
    except OSError as exc:
551
552 1
        if exc.errno != errno.EEXIST or os.path.isdir(path) is not True:
553
            raise
554
555
# End of make_directories() function
556
557
558 1
def open_and_truncate_file(filename):
559
    """
560
    Opens filename for writing truncating it to a zero length file
561
    and returns a python file object.
562
    """
563
564 1
    cache_file = codecs.open(filename, 'w', encoding='utf-8')
565 1
    cache_file.truncate(0)
566 1
    cache_file.flush()
567
568 1
    return cache_file
569
570
# End of open_and_truncate_file() function
571
####################### End of utility functions #######################
572
573
574 1
def get_values_from_project(project):
575
    """
576
    Gets the values of 'regex' and 'name' keys if found and
577
    returns a tuple (valued, name, regex)
578
    """
579
580 1
    regex = ''
581 1
    name = project
582 1
    valued = False
583
584 1
    if 'regex' in project and 'name' in project:
585
        regex = project['regex']
586
        name = project['name']
587
        valued = True
588
589 1
    return (valued, name, regex)
590
591
# End of get_values_from_project() function
592
593
594 1
def get_latest_release_by_title(project, debug, feed_url):
595
    """
596
    Gets the latest release of a program on github. program must be a
597
    string of the form user/repository.
598
    """
599
600 1
    version = ''
601
602 1
    (valued, name, regex) = get_values_from_project(project)
603
604 1
    url = feed_url.format(name)
605 1
    feed = get_feed_entries_from_url(url)
606
607 1
    if feed is not None and len(feed.entries) > 0:
608 1
        version = feed.entries[0].title
609
610 1
    if valued:
611
        res = re.match(regex, version)
612
        if res:
613
            version = res.group(1)
614
        print_debug(debug, u'\tname: {}\n\tversion: {}\n\tregex: {} : {}'.format(name, version, regex, res))
615
616 1
    print_debug(debug, u'\tProject {}: {}'.format(name, version))
617
618 1
    return (name, version)
619
620
# End of get_latest_release_by_title() function
621
622
623 1
def check_versions_feeds_by_projects(project_list, local_dir, debug, feed_url, cache_filename):
624
    """
625
    Checks project's versions on feed_url if any are defined in the yaml
626
    file under the specified tag that got the project_list passed as an argument.
627
    """
628
629 1
    site_cache = FileCache(local_dir, cache_filename)
630
631 1
    for project in project_list:
632 1
        (project, version) = get_latest_release_by_title(project, debug, feed_url)
633 1
        if version != '':
634 1
            site_cache.update_cache_dict(project, version, debug)
635
636 1
    site_cache.write_cache_file()
637
638
# End of check_versions_feeds_by_projects() function
639
640
641 1
def cut_title_in_project_version(title, regex):
642
    """
643
    Cuts the title into a tuple (project, version) where possible
644
    """
645 1
    default = False
646
647 1
    if regex is not None:
648
        res = re.match(regex, title)
649
        if res:
650
            project = res.group(1)
651
            version = res.group(2)
652
        else:
653
            default = True
654
    else:
655 1
        default = True
656
657
658 1
    if default:
659 1
        try:
660 1
            (project, version) = title.strip().split(' ', 1)
661
662
        except ValueError as val:
663
            project = title.strip()
664
            version = ''
665
666 1
    return (project, version)
667
668
# End of cut_title_in_project_version() function
669
670
671 1
def make_list_of_newer_feeds(feed, feed_info, debug, regex):
672
    """
673
    Compares feed entries and keep those that are newer than the latest
674
    check we've done and inserting the newer ones in reverse order in
675
    a list to be returned
676
    """
677
678 1
    feed_list = []
679
680
    # inserting into a list in reverse order to keep the most recent
681
    # version in case of multiple release of the same project in the
682
    # feeds
683 1
    for a_feed in feed.entries:
684
685 1
        (project, version) = cut_title_in_project_version(a_feed.title, regex)
686 1
        print_debug(debug, u'\tFeed entry ({0}): project: {1:16} version: {2}'.format(time.strftime('%x %X', a_feed.published_parsed), project, version))
687
688 1
        if feed_info.is_newer(a_feed.published_parsed):
689 1
            feed_list.insert(0, a_feed)
690
691 1
    return feed_list
692
693
# End of make_list_of_newer_feeds() function
694
695
696 1
def lower_list_of_strings(project_list):
697
    """
698
    Lowers every string in the list to ease sorting and comparisons
699
    """
700
701 1
    project_list_low = [project.lower() for project in project_list]
702
703 1
    return project_list_low
704
705
# End of lower_list_of_strings() function
706
707
708 1
def check_and_update_feed(feed_list, project_list, cache, debug, regex):
709
    """
710
    Checks every feed entry in the list against project list cache and
711
    then updates the dictionnary then writes the cache file to the disk.
712
     - feed_list    is a list of feed (from feedparser module)
713
     - project_list is the list of project as read from the yaml
714
                    configuration file
715
     - cache is an initialized instance of FileCache
716
    """
717
718
    # Lowers the list before searching in it
719 1
    project_list_low = lower_list_of_strings(project_list)
720
721
    # Checking every feed entry that are newer than the last check
722
    # and updates the dictionnary accordingly
723 1
    for entry in feed_list:
724 1
        (project, version) = cut_title_in_project_version(entry.title, regex)
725 1
        print_debug(debug, u'\tChecking {0:16}: {1}'.format(project, version))
726 1
        if project.lower() in project_list_low:
727
            cache.update_cache_dict(project, version, debug)
728
729 1
    cache.write_cache_file()
730
731
# End of check_and_update_feed() function
732
733
734 1
def get_feed_entries_from_url(url):
735
    """
736
    Gets feed entries from an url that should be an
737
    RSS or Atom feed. 
738
    >>> get_feed_entries_from_url("http://delhomme.org/notfound.html")
739
    Error 404 while fetching "http://delhomme.org/notfound.html".
740
    >>> feed = get_feed_entries_from_url("http://blog.delhomme.org/index.php?feed/atom")
741
    >>> feed.status
742
    200
743
    """
744
745 1
    feed = feedparser.parse(url)
746 1
    err = feed.status / 100
747
748 1
    if err > 2:
749 1
        print(u'Error {} while fetching "{}".'.format(feed.status, url))
750 1
        feed = None
751
752 1
    return feed
753
754
# End of get_feed_entries_from_url() function
755
756
757 1
def check_versions_for_list_sites(feed_project_list, url, cache_filename, feed_filename, local_dir, debug, regex):
758
    """
759
    Checks projects of list type sites such as freshcode's web site's RSS
760
    """
761
762 1
    freshcode_cache = FileCache(local_dir, cache_filename)
763
764 1
    feed_info = FeedCache(local_dir, feed_filename)
765 1
    feed_info.read_cache_feed()
766
767 1
    feed = get_feed_entries_from_url(url)
768
769 1
    if feed is not None:
770 1
        length = len(feed.entries)
771 1
        print_debug(debug, u'\tFound {} entries'.format(length))
772
773 1
        feed_list = make_list_of_newer_feeds(feed, feed_info, debug, regex)
774 1
        print_debug(debug, u'\tFound {} new entries (relative to {})'.format(len(feed_list), feed_info.date_minutes))
775
776 1
        check_and_update_feed(feed_list, feed_project_list, freshcode_cache, debug, regex)
777
778
        # Updating feed_info with the latest parsed feed entry date
779 1
        feed_info.update_cache_feed(feed.entries[0].published_parsed)
780
781 1
    feed_info.write_cache_feed()
782
783
# End of check_versions_for_list_sites() function
784
785
786 1
def print_versions_from_cache(local_dir, cache_filename_list, debug):
787
    """
788
    Prints all projects and their associated data from the cache
789
    """
790 1
    for cache_filename in cache_filename_list:
791 1
        site_cache = FileCache(local_dir, cache_filename)
792 1
        site_cache.print_cache_dict(cache_filename)
793
794
# End of print_versions_from_cache()
795
796
797 1
def main():
798
    """
799
    This is the where the program begins
800
    """
801
802 1
    versions_conf = Conf()  # Configuration options
803
804 1
    if versions_conf.options.debug:
805 1
        doctest.testmod(verbose=True)
806
807 1
    if os.path.isfile(versions_conf.config_filename):
808 1
        versions_conf.print_cache_or_check_versions()
809
810
    else:
811 1
        print('Error: file %s does not exist' % versions_conf.config_filename)
812
813
# End of main() function
814
815
816 1
def print_debug(debug, message):
817
    """
818
    Prints 'message' if debug mode is True
819
    """
820
821 1
    if debug:
822 1
        print(message)
823
824
# End of print_debug() function
825
826
827 1
if __name__ == "__main__":
828
    main()
829