From 4df8deab8d24b2b16682c286e3cebdaf9292cd65 Mon Sep 17 00:00:00 2001 From: Elia Onishchouck Date: Sat, 9 Nov 2019 10:58:01 +0300 Subject: [PATCH 01/10] Initial commit. Added a simple RSS-reader functionality --- LICENSE | 21 ++++++++ MANIFEST.in | 1 + README.md | 31 +++++++++++ rss_reader/__init__.py | 0 rss_reader/rss_reader.py | 110 +++++++++++++++++++++++++++++++++++++++ rss_reader/version.py | 1 + setup.py | 33 ++++++++++++ 7 files changed, 197 insertions(+) create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 rss_reader/__init__.py create mode 100644 rss_reader/rss_reader.py create mode 100644 rss_reader/version.py create mode 100644 setup.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b4e4cc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Elia Onishchouk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..540b720 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include requirements.txt \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..737f59b --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Introduction to Python. Hometask + +RSS reader is a command-line utility which receives [RSS](wikipedia.org/wiki/RSS) URL and prints results in human-readable format. + + +Utility provides the following interface: +```shell +usage: rss_reader.py [-h] [--version] [--json] [--verbose] [--limit LIMIT] + source + +Pure Python command-line RSS reader. + +positional arguments: + source RSS URL + +optional arguments: + -h, --help show this help message and exit + --version Print version info + --json Print result as JSON in stdout + --verbose Outputs verbose status messages + --limit LIMIT Limit news topics if this parameter provided + +``` + +With the argument `--json` the program converts the news into [JSON](https://en.wikipedia.org/wiki/JSON) format.(Still in progress) + +With the argument `--limit` the program prints given number of news. + +With the argument `--verbose` the program prints all logs in stdout.(Still in progress) + +Withe the argument `--version` the program prints in stdout it's current version and complete it's work. \ No newline at end of file diff --git a/rss_reader/__init__.py b/rss_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py new file mode 100644 index 0000000..3b6cd64 --- /dev/null +++ b/rss_reader/rss_reader.py @@ -0,0 +1,110 @@ +import argparse +import feedparser +import html +from bs4 import BeautifulSoup +import json +from tqdm import tqdm + +import version + +class Item: + def __init__(self, title, date, link, description, links): + self.title=title + self.date=date + self.link=link + self.description=description + self.links=links + + +def main() -> None: + parser = argparse.ArgumentParser(description='Pure Python command-line RSS reader.') + parser.add_argument('source', type=str, help='RSS URL') + + parser.add_argument('--version', action='version', version='%(prog)s v'+version.__version__, help='Print version info') + parser.add_argument('--json', action='store_true', help='Print result as JSON in stdout') + parser.add_argument('--verbose', action='store_true', help='Outputs verbose status messages') + parser.add_argument('--limit', type=int, default=-1, help='Limit news topics if this parameter provided') + args = parser.parse_args() + NewsFeed = feedparser.parse(args.source) + if args.limit==-1 : + args.limit=len(NewsFeed.entries) + + + d=json.dumps([{"title": "title1","description": "description1","link": "link2","pubDate": "pubDate1","source": {"url1": "url1","__url2": "url2"}}, {"title": "title2","description": "description2","link": "link2","pubDate": "pubDate2","source": {"url1": "url1","__url2": "url2"}}]) + open("out.json","w").write(d) + for i in range(args.limit) : + + entry = NewsFeed.entries[i] + soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) + + print ('Title: {0}'.format(html.unescape(entry['title']))) + print ('Date: {0}'.format(entry['published'])) + print ('Link: {0}'.format(entry['link'])) + print () + + j=1 + images_links=[] + for img in soup.findAll('img') : + + if 'alt' in img.attrs: + alt=img['alt'] + if alt!='': + final=' [image {0} | {1}] '.format(j,alt) + else: + final=' [image {0}]'.format(j) + + else: + final=' [image {0}]'.format(j) + src=img['src'] + images_links.append('[{0}]: {1}'.format(j, src)) + j+=1 + + soup.find('img').replace_with(final) + j=1 + href_links=[] + for href in soup.findAll('a'): + if 'href' in href.attrs: + link=href['href'] + if href.text!='': + final=' [link {0} | {1}] '.format(j,href.text) + else: + final=' [link {0}] '.format(j) + soup.find('a').replace_with(final) + href_links.append('[{0}]: {1}'.format(j, link)) + j+=1 + j=1 + video_links=[] + for video in soup.findAll('iframe'): + if 'src' in video.attrs: + link=video['src'] + final=' [video {0}] '.format(j) + soup.find('iframe').replace_with(final) + video_links.append('[{0}]: {1}'.format(j, link)) + j+=1 + links={'images_links':images_links,'href_links':href_links,'video_links':video_links} + item=Item(html.unescape(entry['title']),entry['published'],entry['link'],soup.text,links) + print(soup.text) + print () + print () + if href_links: + print ('Links:') + for link in href_links: + print (link) + if images_links: + print () + print ('Images:') + for link in images_links: + print (link) + + if video_links: + print () + print ('Videos:') + for link in video_links: + print (link) + print () + print () + print () + +if __name__ == '__main__': + + main() diff --git a/rss_reader/version.py b/rss_reader/version.py new file mode 100644 index 0000000..13312d8 --- /dev/null +++ b/rss_reader/version.py @@ -0,0 +1 @@ +__version__="0.8" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ebc8e9e --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +import setuptools +from rss_reader import version + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="rss-reader", + version=version.__version__, + author="Elia Onishchouk", + author_email="elias0n@mail.ru", + description="A simple command-line RSS reader", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/el0ny/PythonHomework", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + entry_points={ + + 'console_scripts': [ + + 'rss_reader = rss_reader.rss_reader:main', + + ], + + }, + install_requires=['feedparser', 'bs4'], + python_requires='>=3.6', +) From 03f00f861a8c4296f97bdf1429efd29e2f3aa0f0 Mon Sep 17 00:00:00 2001 From: Elia Onishchouck Date: Tue, 12 Nov 2019 18:35:19 +0300 Subject: [PATCH 02/10] --json argument is working now --- rss_reader/rss_reader.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py index 3b6cd64..03c7b4e 100644 --- a/rss_reader/rss_reader.py +++ b/rss_reader/rss_reader.py @@ -3,10 +3,19 @@ import html from bs4 import BeautifulSoup import json -from tqdm import tqdm +#from tqdm import tqdm import version +class News_Feed: + def __init__(self, feed_title, items): + self.feed_title=feed_title + self.items=items + + def print_feed(self): + with open("data_file.json", "w") as write_file: + json.dump({"Feed":self.feed_title, "Items":[item.return_item() for item in self.items]}, write_file) + class Item: def __init__(self, title, date, link, description, links): self.title=title @@ -14,7 +23,9 @@ def __init__(self, title, date, link, description, links): self.link=link self.description=description self.links=links - + + def return_item(self): + return {"title": self.title,"description": self.description,"link": self.link,"pubDate": self.date,"source": self.links} def main() -> None: parser = argparse.ArgumentParser(description='Pure Python command-line RSS reader.') @@ -29,9 +40,8 @@ def main() -> None: if args.limit==-1 : args.limit=len(NewsFeed.entries) - - d=json.dumps([{"title": "title1","description": "description1","link": "link2","pubDate": "pubDate1","source": {"url1": "url1","__url2": "url2"}}, {"title": "title2","description": "description2","link": "link2","pubDate": "pubDate2","source": {"url1": "url1","__url2": "url2"}}]) - open("out.json","w").write(d) + news=[] + for i in range(args.limit) : entry = NewsFeed.entries[i] @@ -82,7 +92,8 @@ def main() -> None: video_links.append('[{0}]: {1}'.format(j, link)) j+=1 links={'images_links':images_links,'href_links':href_links,'video_links':video_links} - item=Item(html.unescape(entry['title']),entry['published'],entry['link'],soup.text,links) + + news.append(Item(html.unescape(entry['title']),entry['published'],entry['link'],soup.text,links)) print(soup.text) print () print () @@ -104,7 +115,8 @@ def main() -> None: print () print () print () - + newsFeed=News_Feed("URL", news) + newsFeed.print_feed(); if __name__ == '__main__': main() From 27dcea67aada456ae76b34a19cc34d6ca24e50f2 Mon Sep 17 00:00:00 2001 From: Elia Onishchouck Date: Thu, 14 Nov 2019 15:53:15 +0300 Subject: [PATCH 03/10] --json now works as intedent. main() function divided into smaller functions. --- README.md | 2 +- rss_reader/rss_reader.py | 171 ++++++++++++++++++++++----------------- rss_reader/version.py | 2 +- 3 files changed, 97 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 737f59b..0dc82d0 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ optional arguments: ``` -With the argument `--json` the program converts the news into [JSON](https://en.wikipedia.org/wiki/JSON) format.(Still in progress) +With the argument `--json` the program converts the news into [JSON](https://en.wikipedia.org/wiki/JSON) format. With the argument `--limit` the program prints given number of news. diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py index 03c7b4e..9b01ae7 100644 --- a/rss_reader/rss_reader.py +++ b/rss_reader/rss_reader.py @@ -4,17 +4,29 @@ from bs4 import BeautifulSoup import json #from tqdm import tqdm - -import version +from rss_reader import version +#import version class News_Feed: def __init__(self, feed_title, items): self.feed_title=feed_title self.items=items + + def print_to_json(self): + with open("news.json", "w") as write_file: + json.dump({"Feed":self.feed_title, "Items":[item.return_item() for item in self.items]}, write_file) + print('news.json created successfully') - def print_feed(self): - with open("data_file.json", "w") as write_file: - json.dump({"Feed":self.feed_title, "Items":[item.return_item() for item in self.items]}, write_file) + def print_to_console(self): + print ('Feed: {0}'.format(self.feed_title)) + for item in self.items: + item.print_to_console() + + def print_feed(self, json): + if(json): + self.print_to_json() + else: + self.print_to_console() class Item: def __init__(self, title, date, link, description, links): @@ -24,10 +36,32 @@ def __init__(self, title, date, link, description, links): self.description=description self.links=links + def print_to_console(self): + print ('\nTitle: {0}'.format(self.title)) + print ('Date: {0}'.format(self.date)) + print ('Link: {0} \n'.format(self.link)) + + print(self.description) + print() + if self.links['href_links']: + print ('\nLinks:') + for link in self.links['href_links']: + print (link) + + if self.links['images_links']: + print ('\nImages:') + for link in self.links['images_links']: + print (link) + + if self.links['video_links']: + print ('\nVideos:') + for link in self.links['video_links']: + print (link) + print ('\n//////////////////////////////////////////////////////////////////////////') + def return_item(self): return {"title": self.title,"description": self.description,"link": self.link,"pubDate": self.date,"source": self.links} - -def main() -> None: +def set_argparse(): parser = argparse.ArgumentParser(description='Pure Python command-line RSS reader.') parser.add_argument('source', type=str, help='RSS URL') @@ -35,7 +69,52 @@ def main() -> None: parser.add_argument('--json', action='store_true', help='Print result as JSON in stdout') parser.add_argument('--verbose', action='store_true', help='Outputs verbose status messages') parser.add_argument('--limit', type=int, default=-1, help='Limit news topics if this parameter provided') - args = parser.parse_args() + return parser.parse_args() + +def find_images(args, soup): + + image_iterator=1 + images_links=[] + for img in soup.findAll('img') : + + if 'alt' in img.attrs and img['alt']!='': + replaced_data=' [image {0} | {1}] '.format(image_iterator,img['alt']) + else: + replaced_data=' [image {0}]'.format(image_iterator) + src=img['src'] + images_links.append('[{0}]: {1}'.format(image_iterator, src)) + soup.find('img').replace_with(replaced_data) + image_iterator+=1 + return images_links + +def find_href(args,soup): + href_iterator=1 + href_links=[] + for href in soup.findAll('a') : + + if 'href' in href.attrs: + link=href['href'] + if href.text!='': + replaced_data=' [link {0} | {1}] '.format(href_iterator,href.text) + else: + replaced_data=' [link {0}] '.format(href_iterator) + href_links.append('[{0}]: {1}'.format(href_iterator, link)) + soup.find('a').replace_with(replaced_data) + href_iterator+=1 + return href_links +def find_videos(args,soup): + video_iterator=1 + video_links=[] + for video in soup.findAll('iframe'): + if 'src' in video.attrs: + link=video['src'] + replaced_data=' [video {0}] '.format(video_iterator) + video_links.append('[{0}]: {1}'.format(video_iterator, link)) + soup.find('iframe').replace_with(final) + video_iterator+=1 + +def main() -> None: + args=set_argparse(); NewsFeed = feedparser.parse(args.source) if args.limit==-1 : args.limit=len(NewsFeed.entries) @@ -46,77 +125,17 @@ def main() -> None: entry = NewsFeed.entries[i] soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) - - print ('Title: {0}'.format(html.unescape(entry['title']))) - print ('Date: {0}'.format(entry['published'])) - print ('Link: {0}'.format(entry['link'])) - print () - - j=1 - images_links=[] - for img in soup.findAll('img') : - - if 'alt' in img.attrs: - alt=img['alt'] - if alt!='': - final=' [image {0} | {1}] '.format(j,alt) - else: - final=' [image {0}]'.format(j) - - else: - final=' [image {0}]'.format(j) - src=img['src'] - images_links.append('[{0}]: {1}'.format(j, src)) - j+=1 - - soup.find('img').replace_with(final) - j=1 - href_links=[] - for href in soup.findAll('a'): - if 'href' in href.attrs: - link=href['href'] - if href.text!='': - final=' [link {0} | {1}] '.format(j,href.text) - else: - final=' [link {0}] '.format(j) - soup.find('a').replace_with(final) - href_links.append('[{0}]: {1}'.format(j, link)) - j+=1 - j=1 - video_links=[] - for video in soup.findAll('iframe'): - if 'src' in video.attrs: - link=video['src'] - final=' [video {0}] '.format(j) - soup.find('iframe').replace_with(final) - video_links.append('[{0}]: {1}'.format(j, link)) - j+=1 + + images_links=find_images(args, soup) + href_links=find_href(args, soup) + video_links=find_videos(args, soup) + links={'images_links':images_links,'href_links':href_links,'video_links':video_links} news.append(Item(html.unescape(entry['title']),entry['published'],entry['link'],soup.text,links)) - print(soup.text) - print () - print () - if href_links: - print ('Links:') - for link in href_links: - print (link) - if images_links: - print () - print ('Images:') - for link in images_links: - print (link) - - if video_links: - print () - print ('Videos:') - for link in video_links: - print (link) - print () - print () - print () - newsFeed=News_Feed("URL", news) - newsFeed.print_feed(); + + newsFeed=News_Feed(NewsFeed.feed.title, news) + newsFeed.print_feed(args.json); if __name__ == '__main__': main() diff --git a/rss_reader/version.py b/rss_reader/version.py index 13312d8..14d6530 100644 --- a/rss_reader/version.py +++ b/rss_reader/version.py @@ -1 +1 @@ -__version__="0.8" +__version__="1.0" From 46014ec304979622b8d3ec4e4b7f4065a6ae4ba7 Mon Sep 17 00:00:00 2001 From: Elia Onishchouck Date: Fri, 15 Nov 2019 00:25:37 +0300 Subject: [PATCH 04/10] --verbose atribute now prints log in console as well as --json --- README.md | 2 +- rss_reader/rss_reader.py | 51 ++++++++++++++++++++++++++-------------- rss_reader/version.py | 2 +- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 0dc82d0..a1e8622 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,6 @@ With the argument `--json` the program converts the news into [JSON](https://en. With the argument `--limit` the program prints given number of news. -With the argument `--verbose` the program prints all logs in stdout.(Still in progress) +With the argument `--verbose` the program prints all logs in stdout. Withe the argument `--version` the program prints in stdout it's current version and complete it's work. \ No newline at end of file diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py index 9b01ae7..fb1d0c7 100644 --- a/rss_reader/rss_reader.py +++ b/rss_reader/rss_reader.py @@ -1,5 +1,6 @@ import argparse import feedparser +import logging import html from bs4 import BeautifulSoup import json @@ -13,11 +14,12 @@ def __init__(self, feed_title, items): self.items=items def print_to_json(self): - with open("news.json", "w") as write_file: - json.dump({"Feed":self.feed_title, "Items":[item.return_item() for item in self.items]}, write_file) - print('news.json created successfully') + logging.info('Printing news in json format') + print(json.dumps({"Feed":self.feed_title, "Items":[item.return_item() for item in self.items]})) + def print_to_console(self): + logging.info('Printing news in console format') print ('Feed: {0}'.format(self.feed_title)) for item in self.items: item.print_to_console() @@ -72,11 +74,12 @@ def set_argparse(): return parser.parse_args() def find_images(args, soup): - - image_iterator=1 + logging.info('Starting image finding') + image_iterator=0 images_links=[] - for img in soup.findAll('img') : + for img in soup.findAll('img') : + image_iterator+=1 if 'alt' in img.attrs and img['alt']!='': replaced_data=' [image {0} | {1}] '.format(image_iterator,img['alt']) else: @@ -84,15 +87,19 @@ def find_images(args, soup): src=img['src'] images_links.append('[{0}]: {1}'.format(image_iterator, src)) soup.find('img').replace_with(replaced_data) - image_iterator+=1 + + + logging.info('Image finding finished. Found %s images', image_iterator) return images_links def find_href(args,soup): - href_iterator=1 + logging.info('Starting link finding') + href_iterator=0 href_links=[] for href in soup.findAll('a') : if 'href' in href.attrs: + href_iterator+=1 link=href['href'] if href.text!='': replaced_data=' [link {0} | {1}] '.format(href_iterator,href.text) @@ -100,42 +107,50 @@ def find_href(args,soup): replaced_data=' [link {0}] '.format(href_iterator) href_links.append('[{0}]: {1}'.format(href_iterator, link)) soup.find('a').replace_with(replaced_data) - href_iterator+=1 + logging.info('Link finding finished. Found %s links', href_iterator) return href_links + def find_videos(args,soup): - video_iterator=1 + logging.info('Starting video finding') + video_iterator=0 video_links=[] for video in soup.findAll('iframe'): if 'src' in video.attrs: + video_iterator+=1 link=video['src'] replaced_data=' [video {0}] '.format(video_iterator) video_links.append('[{0}]: {1}'.format(video_iterator, link)) soup.find('iframe').replace_with(final) - video_iterator+=1 + logging.info('Video finding finished. Found %s videos', video_iterator) + return video_links def main() -> None: args=set_argparse(); + if args.verbose: + logging.basicConfig(format='%(asctime)s %(funcName)s %(message)s', datefmt='%I:%M:%S' ,level=logging.DEBUG) + + logging.info('Application started. RSS source is %s', args.source) NewsFeed = feedparser.parse(args.source) + if args.limit==-1 : args.limit=len(NewsFeed.entries) news=[] - + logging.info('Begin processing each news') for i in range(args.limit) : - + logging.info('Parsing news number %s', i+1) entry = NewsFeed.entries[i] - soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) - + soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) images_links=find_images(args, soup) href_links=find_href(args, soup) - video_links=find_videos(args, soup) - + video_links=find_videos(args, soup) links={'images_links':images_links,'href_links':href_links,'video_links':video_links} - news.append(Item(html.unescape(entry['title']),entry['published'],entry['link'],soup.text,links)) + logging.info('News number %s has parsed', i+1) newsFeed=News_Feed(NewsFeed.feed.title, news) newsFeed.print_feed(args.json); + logging.info('Application completed') if __name__ == '__main__': main() diff --git a/rss_reader/version.py b/rss_reader/version.py index 14d6530..f11231d 100644 --- a/rss_reader/version.py +++ b/rss_reader/version.py @@ -1 +1 @@ -__version__="1.0" +__version__="1.1" From 019d68fcb09cc8454730be88859c41ecea6c414c Mon Sep 17 00:00:00 2001 From: Elia Onishchouck Date: Fri, 15 Nov 2019 13:33:52 +0300 Subject: [PATCH 05/10] Polished according to pep8 --- rss_reader/rss_reader.py | 173 ++++++++++++++++++++------------------- rss_reader/version.py | 2 +- 2 files changed, 92 insertions(+), 83 deletions(-) diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py index fb1d0c7..6789120 100644 --- a/rss_reader/rss_reader.py +++ b/rss_reader/rss_reader.py @@ -2,155 +2,164 @@ import feedparser import logging import html -from bs4 import BeautifulSoup import json -#from tqdm import tqdm -from rss_reader import version -#import version +from bs4 import BeautifulSoup +# from tqdm import tqdm +from rss_reader import version as vers +# import version as vers + class News_Feed: def __init__(self, feed_title, items): - self.feed_title=feed_title - self.items=items - + self.feed_title = feed_title + self.items = items + def print_to_json(self): logging.info('Printing news in json format') - print(json.dumps({"Feed":self.feed_title, "Items":[item.return_item() for item in self.items]})) - + print(json.dumps({"Feed": self.feed_title, "Items": [item.return_item() for item in self.items]})) def print_to_console(self): logging.info('Printing news in console format') - print ('Feed: {0}'.format(self.feed_title)) + print('Feed: {0}'.format(self.feed_title)) for item in self.items: item.print_to_console() - + def print_feed(self, json): if(json): self.print_to_json() else: self.print_to_console() + class Item: def __init__(self, title, date, link, description, links): - self.title=title - self.date=date - self.link=link - self.description=description - self.links=links - + self.title = title + self.date = date + self.link = link + self.description = description + self.links = links + def print_to_console(self): - print ('\nTitle: {0}'.format(self.title)) - print ('Date: {0}'.format(self.date)) - print ('Link: {0} \n'.format(self.link)) - + print('\nTitle: {0}'.format(self.title)) + print('Date: {0}'.format(self.date)) + print('Link: {0} \n'.format(self.link)) print(self.description) print() + if self.links['href_links']: - print ('\nLinks:') + print('\nLinks:') for link in self.links['href_links']: - print (link) - + print(link) + if self.links['images_links']: - print ('\nImages:') + print('\nImages:') for link in self.links['images_links']: - print (link) - + print(link) + if self.links['video_links']: - print ('\nVideos:') + print('\nVideos:') for link in self.links['video_links']: - print (link) - print ('\n//////////////////////////////////////////////////////////////////////////') - + print(link) + print('\n//////////////////////////////////////////////////////////////////////////') + def return_item(self): - return {"title": self.title,"description": self.description,"link": self.link,"pubDate": self.date,"source": self.links} + return {"title": self.title, "description": self.description, + "link": self.link, "pubDate": self.date, "source": self.links} + + def set_argparse(): parser = argparse.ArgumentParser(description='Pure Python command-line RSS reader.') parser.add_argument('source', type=str, help='RSS URL') - parser.add_argument('--version', action='version', version='%(prog)s v'+version.__version__, help='Print version info') + parser.add_argument('--version', action='version', version='%(prog)s v'+vers.__version__, help='Print version info') parser.add_argument('--json', action='store_true', help='Print result as JSON in stdout') parser.add_argument('--verbose', action='store_true', help='Outputs verbose status messages') parser.add_argument('--limit', type=int, default=-1, help='Limit news topics if this parameter provided') return parser.parse_args() + def find_images(args, soup): logging.info('Starting image finding') - image_iterator=0 - images_links=[] - for img in soup.findAll('img') : - - image_iterator+=1 - if 'alt' in img.attrs and img['alt']!='': - replaced_data=' [image {0} | {1}] '.format(image_iterator,img['alt']) + image_iterator = 0 + images_links = [] + for img in soup.findAll('img'): + + image_iterator += 1 + if 'alt' in img.attrs and img['alt'] != '': + replaced_data = ' [image {0} | {1}] '.format(image_iterator, img['alt']) else: - replaced_data=' [image {0}]'.format(image_iterator) - src=img['src'] + replaced_data = ' [image {0}]'.format(image_iterator) + src = img['src'] images_links.append('[{0}]: {1}'.format(image_iterator, src)) soup.find('img').replace_with(replaced_data) - - + logging.info('Image finding finished. Found %s images', image_iterator) return images_links -def find_href(args,soup): + +def find_href(args, soup): logging.info('Starting link finding') - href_iterator=0 - href_links=[] - for href in soup.findAll('a') : - + href_iterator = 0 + href_links = [] + for href in soup.findAll('a'): + if 'href' in href.attrs: - href_iterator+=1 - link=href['href'] - if href.text!='': - replaced_data=' [link {0} | {1}] '.format(href_iterator,href.text) + href_iterator += 1 + link = href['href'] + if href.text != '': + replaced_data = ' [link {0} | {1}] '.format(href_iterator, href.text) else: - replaced_data=' [link {0}] '.format(href_iterator) + replaced_data = ' [link {0}] '.format(href_iterator) href_links.append('[{0}]: {1}'.format(href_iterator, link)) soup.find('a').replace_with(replaced_data) - logging.info('Link finding finished. Found %s links', href_iterator) + logging.info('Link finding finished. Found %s links', href_iterator) return href_links -def find_videos(args,soup): + +def find_videos(args, soup): logging.info('Starting video finding') - video_iterator=0 - video_links=[] + video_iterator = 0 + video_links = [] for video in soup.findAll('iframe'): if 'src' in video.attrs: - video_iterator+=1 - link=video['src'] - replaced_data=' [video {0}] '.format(video_iterator) - video_links.append('[{0}]: {1}'.format(video_iterator, link)) + video_iterator += 1 + link = video['src'] + replaced_data = ' [video {0}] '.format(video_iterator) + video_links.append('[{0}]: {1}'.format(video_iterator, link)) soup.find('iframe').replace_with(final) logging.info('Video finding finished. Found %s videos', video_iterator) return video_links - -def main() -> None: - args=set_argparse(); + + +def main(): + args = set_argparse() if args.verbose: - logging.basicConfig(format='%(asctime)s %(funcName)s %(message)s', datefmt='%I:%M:%S' ,level=logging.DEBUG) - + logging.basicConfig(format='%(asctime)s %(funcName)s %(message)s', datefmt='%I:%M:%S', level=logging.DEBUG) + logging.info('Application started. RSS source is %s', args.source) NewsFeed = feedparser.parse(args.source) - - if args.limit==-1 : - args.limit=len(NewsFeed.entries) - news=[] + if args.limit == -1: + args.limit = len(NewsFeed.entries) + + news = [] logging.info('Begin processing each news') - for i in range(args.limit) : + for i in range(args.limit): logging.info('Parsing news number %s', i+1) entry = NewsFeed.entries[i] - soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) - images_links=find_images(args, soup) - href_links=find_href(args, soup) - video_links=find_videos(args, soup) - links={'images_links':images_links,'href_links':href_links,'video_links':video_links} - news.append(Item(html.unescape(entry['title']),entry['published'],entry['link'],soup.text,links)) + soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) + images_links = find_images(args, soup) + href_links = find_href(args, soup) + video_links = find_videos(args, soup) + links = {'images_links': images_links, 'href_links': href_links, 'video_links': video_links} + news.append(Item(html.unescape(entry['title']), entry['published'], entry['link'], soup.text, links)) logging.info('News number %s has parsed', i+1) - - newsFeed=News_Feed(NewsFeed.feed.title, news) - newsFeed.print_feed(args.json); + + newsFeed = News_Feed(NewsFeed.feed.title, news) + newsFeed.print_feed(args.json) logging.info('Application completed') + + if __name__ == '__main__': - + main() diff --git a/rss_reader/version.py b/rss_reader/version.py index f11231d..b6f6120 100644 --- a/rss_reader/version.py +++ b/rss_reader/version.py @@ -1 +1 @@ -__version__="1.1" +__version__="1.2" From a283958e20fed92661c52a4ca04fdddb155a4c55 Mon Sep 17 00:00:00 2001 From: Elia Onishchouck Date: Sun, 17 Nov 2019 22:20:25 +0300 Subject: [PATCH 06/10] Added requirements and json schema files. Now some exceptions are tracked --- json_schema.json | 91 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + rss_reader/rss_reader.py | 62 ++++++++++++++------------- rss_reader/version.py | 2 +- 4 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 json_schema.json create mode 100644 requirements.txt diff --git a/json_schema.json b/json_schema.json new file mode 100644 index 0000000..78aba63 --- /dev/null +++ b/json_schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + type": "object", + "title": "News feed json schema", + "required": [ + "Feed", + "Items" + ], + "properties": { + "Feed": { + "type": "string", + "title": "Feed", + "description": "The title of the feed" + }, + "Items": { + "type": "array", + "title": "News", + "items": { + "type": "object", + "title": "news", + "required": [ + "title", + "description", + "link", + "pubDate", + "source" + ], + "properties": { + "title": { + "type": "string", + "title": "Title", + "description": "The title of the news" + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description of the news" + }, + "link": { + "type": "string", + "title": "Link", + "description": "The origin link of the news" + }, + "pubDate": { + "type": "string", + "title": "Date", + "description": "The date this news was published" + }, + "source": { + "type": "object", + "title": "Links inside the description", + "required": [ + "images_links", + "href_links", + "video_links" + ], + "properties": { + "images_links": { + "type": "array", + "title": "Images links", + "items": { + "type": "string", + "title": "Image link", + "description": "The source of the image" + } + }, + "href_links": { + "type": "array", + "title": "Hyper references", + "items": { + "type": "string", + "title": "URL link", + "description": "The source of the hyper reference" + } + }, + "video_links": { + "type": "array", + "title": "Video links", + "items": { + "type": "string", + "title": "Video link", + "description": "The source of the video" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1c5511d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +feedparser +bs4 \ No newline at end of file diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py index 6789120..ce61ffb 100644 --- a/rss_reader/rss_reader.py +++ b/rss_reader/rss_reader.py @@ -132,34 +132,40 @@ def find_videos(args, soup): def main(): - args = set_argparse() - if args.verbose: - logging.basicConfig(format='%(asctime)s %(funcName)s %(message)s', datefmt='%I:%M:%S', level=logging.DEBUG) - - logging.info('Application started. RSS source is %s', args.source) - NewsFeed = feedparser.parse(args.source) - - if args.limit == -1: - args.limit = len(NewsFeed.entries) - - news = [] - logging.info('Begin processing each news') - for i in range(args.limit): - logging.info('Parsing news number %s', i+1) - entry = NewsFeed.entries[i] - soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) - images_links = find_images(args, soup) - href_links = find_href(args, soup) - video_links = find_videos(args, soup) - links = {'images_links': images_links, 'href_links': href_links, 'video_links': video_links} - news.append(Item(html.unescape(entry['title']), entry['published'], entry['link'], soup.text, links)) - logging.info('News number %s has parsed', i+1) - - newsFeed = News_Feed(NewsFeed.feed.title, news) - newsFeed.print_feed(args.json) - logging.info('Application completed') - - + try: + args = set_argparse() + if args.verbose: + logging.basicConfig(format='%(asctime)s %(funcName)s %(message)s', datefmt='%I:%M:%S', level=logging.DEBUG) + + logging.info('Application started. RSS source is %s', args.source) + NewsFeed = feedparser.parse(args.source) + if NewsFeed.bozo == 1: + raise Exception('The feed is not well-formed XML') + # if 'status' not in NewsFeed: + # raise Exception('An error happened such that the feed does not contain an HTTP response') + if args.limit < 0 or args.limit > len(NewsFeed.entries): + args.limit = len(NewsFeed.entries) + + news = [] + logging.info('Begin processing each news') + for i in range(args.limit): + logging.info('Parsing news number %s', i+1) + entry = NewsFeed.entries[i] + soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) + images_links = find_images(args, soup) + href_links = find_href(args, soup) + video_links = find_videos(args, soup) + links = {'images_links': images_links, 'href_links': href_links, 'video_links': video_links} + news.append(Item(html.unescape(entry['title']), entry['published'], entry['link'], html.unescape(soup.text), links)) + logging.info('News number %s has parsed', i+1) + + newsFeed = News_Feed(NewsFeed.feed.title, news) + newsFeed.print_feed(args.json) + logging.info('Application completed') + + except Exception as e: + print(e) + if __name__ == '__main__': main() diff --git a/rss_reader/version.py b/rss_reader/version.py index b6f6120..0edf6c0 100644 --- a/rss_reader/version.py +++ b/rss_reader/version.py @@ -1 +1 @@ -__version__="1.2" +__version__="1.3" From ee431b9b065873e2e6fb3dedb12ca5e9801b7e91 Mon Sep 17 00:00:00 2001 From: Elia Onishchouck Date: Thu, 28 Nov 2019 15:23:10 +0300 Subject: [PATCH 07/10] Now news can be cached and loaded using --date argument --- .gitignore | 3 + rss_reader/rss_reader.py | 153 +++++++++++++++++++++++++++------------ rss_reader/version.py | 2 +- 3 files changed, 111 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index 894a44c..fa532a7 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# PyCharm +.idea \ No newline at end of file diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py index ce61ffb..11a53d5 100644 --- a/rss_reader/rss_reader.py +++ b/rss_reader/rss_reader.py @@ -3,45 +3,69 @@ import logging import html import json +from pathlib import Path from bs4 import BeautifulSoup -# from tqdm import tqdm -from rss_reader import version as vers -# import version as vers +# from rss_reader import version as vers +import version as vers -class News_Feed: +class NewsFeed: + """Base class for news feed""" def __init__(self, feed_title, items): self.feed_title = feed_title self.items = items - def print_to_json(self): + def print_to_json(self, limit): logging.info('Printing news in json format') - print(json.dumps({"Feed": self.feed_title, "Items": [item.return_item() for item in self.items]})) + print(json.dumps(self.create_json(0, limit))) - def print_to_console(self): + def create_json(self, is_cached, limit): + return {'Feed': self.feed_title, 'Items': [item.return_item(is_cached) for item in self.items[:limit]]} + + def print_to_console(self, limit): logging.info('Printing news in console format') print('Feed: {0}'.format(self.feed_title)) - for item in self.items: + for item in self.items[:limit]: item.print_to_console() + logging.info('Printed %s news', limit) - def print_feed(self, json): + def print_feed(self, json, limit): + if limit > len(self.items) or limit < 0: + limit = len(self.items) if(json): - self.print_to_json() + self.print_to_json(limit) else: - self.print_to_console() + self.print_to_console(limit) + + def save_news(self, limit): + logging.info('Saving news') + news_to_save = self.create_json(1, limit)['Items'] + existing_news = load_from_cache() + news_to_save += [item for item in existing_news if not item in news_to_save] + with open('cache.json', 'w') as json_file: + json.dump(news_to_save, json_file) class Item: - def __init__(self, title, date, link, description, links): - self.title = title - self.date = date - self.link = link - self.description = description - self.links = links + """ + Class for single news item from news feed + Attributes of the class are: + title a + pubDate a + link a + description a + links a + date_string a + source a + """ + def __init__(self, news_dict): + for key in news_dict: + setattr(self, key, news_dict[key]) + def print_to_console(self): print('\nTitle: {0}'.format(self.title)) - print('Date: {0}'.format(self.date)) + print('Date: {0}'.format(self.pubDate)) print('Link: {0} \n'.format(self.link)) print(self.description) print() @@ -62,9 +86,14 @@ def print_to_console(self): print(link) print('\n//////////////////////////////////////////////////////////////////////////') - def return_item(self): - return {"title": self.title, "description": self.description, - "link": self.link, "pubDate": self.date, "source": self.links} + def return_item(self, is_cached): + item_content = {'title': self.title, 'description': self.description, + 'link': self.link, 'pubDate': self.pubDate, 'links': self.links} + if is_cached: + item_content['date_string'] = self.date_string + item_content['source'] = self.source + + return item_content def set_argparse(): @@ -75,6 +104,7 @@ def set_argparse(): parser.add_argument('--json', action='store_true', help='Print result as JSON in stdout') parser.add_argument('--verbose', action='store_true', help='Outputs verbose status messages') parser.add_argument('--limit', type=int, default=-1, help='Limit news topics if this parameter provided') + parser.add_argument('--date', type=str, help='Shows news of specific date') return parser.parse_args() @@ -131,6 +161,26 @@ def find_videos(args, soup): return video_links +def read_from_cache(date, source): + cached_news = load_from_cache() + dated_news = [] + for news in cached_news: + if news['source'] == source and news['date_string'] == date: + dated_news.append(Item(news)) + return dated_news + + +def load_from_cache(): + logging.info('Loading from cache') + cached_news = [] + if Path('cache.json').is_file(): + with open("cache.json") as cache: + data = cache.read() + cached_news = json.loads(data) + logging.info('Loaded %s news', len(cached_news)) + return cached_news + + def main(): try: args = set_argparse() @@ -138,34 +188,45 @@ def main(): logging.basicConfig(format='%(asctime)s %(funcName)s %(message)s', datefmt='%I:%M:%S', level=logging.DEBUG) logging.info('Application started. RSS source is %s', args.source) - NewsFeed = feedparser.parse(args.source) - if NewsFeed.bozo == 1: - raise Exception('The feed is not well-formed XML') - # if 'status' not in NewsFeed: - # raise Exception('An error happened such that the feed does not contain an HTTP response') - if args.limit < 0 or args.limit > len(NewsFeed.entries): - args.limit = len(NewsFeed.entries) - - news = [] - logging.info('Begin processing each news') - for i in range(args.limit): - logging.info('Parsing news number %s', i+1) - entry = NewsFeed.entries[i] - soup = html.unescape(BeautifulSoup(entry['summary'], "html.parser")) - images_links = find_images(args, soup) - href_links = find_href(args, soup) - video_links = find_videos(args, soup) - links = {'images_links': images_links, 'href_links': href_links, 'video_links': video_links} - news.append(Item(html.unescape(entry['title']), entry['published'], entry['link'], html.unescape(soup.text), links)) - logging.info('News number %s has parsed', i+1) - - newsFeed = News_Feed(NewsFeed.feed.title, news) - newsFeed.print_feed(args.json) + args.source = args.source.rstrip('/') + + if args.date: + news = read_from_cache(args.date, args.source) + if not news: + raise Exception('The are no news of {0} from {1} stored'.format(args.source, args.date)) + news_feed = NewsFeed('Cached news', news) + else: + parsed_feed = feedparser.parse(args.source) + if parsed_feed.bozo == 1: + raise Exception('The feed is not well-formed XML. Details are {0}'.format(parsed_feed.bozo_exception)) + # if 'status' not in NewsFeed: + # raise Exception('An error happened such that the feed does not contain an HTTP response') + news = [] + logging.info('Begin processing each news') + for i in range(len(parsed_feed.entries)): + logging.info('Parsing news number %s', i + 1) + entry = parsed_feed.entries[i] + soup = html.unescape(BeautifulSoup(entry['summary'], 'html.parser')) + images_links = find_images(args, soup) + href_links = find_href(args, soup) + video_links = find_videos(args, soup) + links = {'images_links': images_links, 'href_links': href_links, 'video_links': video_links} + date_string = ''.join(map(str, entry.published_parsed[:3])) + dict_news = {'title': html.unescape(entry['title']), 'pubDate': entry['published'], 'link': entry['link'], + 'description': html.unescape(soup.text), 'links': links, 'date_string': date_string, + 'source': args.source} + news.append(Item(dict_news)) + logging.info('News number %s has parsed', i + 1) + news_feed = NewsFeed(parsed_feed.feed.title, news) + news_feed.save_news(len(parsed_feed.entries)) + news_feed.print_feed(args.json, args.limit) + logging.info('Application completed') - + except Exception as e: print(e) - + + if __name__ == '__main__': main() diff --git a/rss_reader/version.py b/rss_reader/version.py index 0edf6c0..8388621 100644 --- a/rss_reader/version.py +++ b/rss_reader/version.py @@ -1 +1 @@ -__version__="1.3" +__version__="1.5" From c0b02b59515559b30ea34349e01e321c6a7b43ea Mon Sep 17 00:00:00 2001 From: Elia Onishchouk Date: Sun, 1 Dec 2019 18:44:49 +0300 Subject: [PATCH 08/10] Prefinal commit. The program can convert news into fb2 and html format. Docstrings were added --- README.md | 14 +- rss_reader/rss_reader.py | 511 +++++++++++++++++++++++++++++++++++---- rss_reader/version.py | 2 +- 3 files changed, 477 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index a1e8622..76a85ac 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ RSS reader is a command-line utility which receives [RSS](wikipedia.org/wiki/RSS Utility provides the following interface: ```shell usage: rss_reader.py [-h] [--version] [--json] [--verbose] [--limit LIMIT] + [--date DATE] [--to-html TO_HTML] [--to-fb2 TO_FB2] source Pure Python command-line RSS reader. @@ -19,7 +20,10 @@ optional arguments: --json Print result as JSON in stdout --verbose Outputs verbose status messages --limit LIMIT Limit news topics if this parameter provided - + --date DATE Shows news of specific date + --to-html TO_HTML Convert news into html format and save to a specified + path + --to-fb2 TO_FB2 Convert news into fb2 format and save to a specified path ``` With the argument `--json` the program converts the news into [JSON](https://en.wikipedia.org/wiki/JSON) format. @@ -28,4 +32,10 @@ With the argument `--limit` the program prints given number of news. With the argument `--verbose` the program prints all logs in stdout. -Withe the argument `--version` the program prints in stdout it's current version and complete it's work. \ No newline at end of file +With the argument `--version` the program prints in stdout it's current version and complete it's work. + +With the argument `--date` the program prints or saves news of source from specific date stored if there are any. + +With the argument `--to-html` the program saves news from source to the given path as a html file. + +With the argument `--to-fb2` the program saves news from source to the given path as a fb2 file. \ No newline at end of file diff --git a/rss_reader/rss_reader.py b/rss_reader/rss_reader.py index 11a53d5..6899f9f 100644 --- a/rss_reader/rss_reader.py +++ b/rss_reader/rss_reader.py @@ -3,67 +3,225 @@ import logging import html import json +import urllib +from base64 import b64encode from pathlib import Path from bs4 import BeautifulSoup -# from rss_reader import version as vers -import version as vers +from rss_reader import version as vers +# import version as vers + + +class Converter: + """This class is used to convert news feed to either html or fb2 version""" + + def __init__(self, news_feed): + """ + Constructor of Converter class. It assigns encoding value additionally + :param NewsFeed news_feed: A NewsFeed object that contains news feed + """ + + self.news_feed = news_feed + self.encoding = self.news_feed.items[0].encoding + + def convert_to_html(self, path, limit, date): + """ + This function creates a html file with news feed + + :param str path: The path where new file will be saved + :param int limit: The number of news to be saved + :param str date: Optional: if exists than resulting html file will only contain news from specific date + """ + + logging.info('Converting to html') + path_object = Path(path) + path_object.mkdir(parents=True, exist_ok=True) + path_object /= 'news feed.html' + with path_object.open('w', encoding=self.encoding) as html_file: + html_file.write(self.create_html(limit, date)) + + logging.info('Converting to html successful') + + def convert_to_fb2(self, path, limit): + """ + This function creates a fb2 file with news feed + + :param str path: The path where new file will be saved + :param int limit: The number of news to be saved + """ + + logging.info('Converting to fb2') + path_object = Path(path) + path_object.mkdir(parents=True, exist_ok=True) + path_object /= 'news feed.fb2' + with path_object.open('w', encoding=self.encoding) as fb2_file: + fb2_file.write(self.create_fb2(limit)) + + logging.info('Converting to fb2 successful') + + def create_html(self, limit, date): + """ + This function creates a html version of news feed + + :param int limit: The number of news to be created + :param str date: Optional: if exists than resulting html implementation will only contain news from specific date + :return: a html like news feed + :rtype: str + """ + + logging.info('Creating html text') + limit = checking_limit(limit, self.news_feed.items) + news = '\n'.join([item.create_div(date) for item in self.news_feed.items[:limit]]) # ???? + + return """ + + + +

News Feed

+ {0} + + + """.format(news) + + def create_fb2(self, limit): + """ + This function creates a fb2 version of news feed + + :param int limit: The number of news to be created + :return: a fb2 like news feed + :rtype: str + """ + + logging.info('Creating fb2 text') + limit = checking_limit(limit, self.news_feed.items) + news = ''.join([item.create_section() for item in self.news_feed.items[:limit]]) + binaries = ''.join([item.create_binary() for item in self.news_feed.items[:limit]]) + + return """ + + + + newspapers + RSS Reader + el0ny + + + {myprog} + {vers} + + + + {news} + + {binaries} + + """.format(myprog=__name__, vers=vers.__version__, + news=news, encoding=self.encoding, + binaries=binaries) class NewsFeed: """Base class for news feed""" + def __init__(self, feed_title, items): + """ + This constructor only initializes two values, nothing else + + :param str feed_title: The title of news feed + :param list items: A list of Item objects, basically, a list of news + """ + self.feed_title = feed_title self.items = items - def print_to_json(self, limit): - logging.info('Printing news in json format') - print(json.dumps(self.create_json(0, limit))) + def print_feed(self, _json, limit): + """ + This function allows to print news in cmd either in json or str format + + :param bool _json: If true than the news will be in json format, otherwise in str format + :param int limit: The number of news to be printed + """ + + limit = checking_limit(limit, self.items) + if _json: + self.print_to_json(limit) + else: + self.print_to_console(limit) def create_json(self, is_cached, limit): + """ + This function allows to create json like dict of news + + :param bool is_cached: If true then json will be ready to be saved, otherwise, to be printed + :param int limit: The number of news to be printed + :return: A json like dict of news + :rtype: dict + """ + return {'Feed': self.feed_title, 'Items': [item.return_item(is_cached) for item in self.items[:limit]]} + def print_to_json(self, limit): + """ + This function allows to print news in cmd in json format + + :param int limit: The number of news to be printed + """ + + logging.info('Printing news in json format') + print(json.dumps(self.create_json(0, limit))) + def print_to_console(self, limit): + """ + This function allows to print news in cmd in str format + + :param int limit: The number of news to be printed + """ + logging.info('Printing news in console format') print('Feed: {0}'.format(self.feed_title)) for item in self.items[:limit]: item.print_to_console() logging.info('Printed %s news', limit) - def print_feed(self, json, limit): - if limit > len(self.items) or limit < 0: - limit = len(self.items) - if(json): - self.print_to_json(limit) - else: - self.print_to_console(limit) - def save_news(self, limit): + """ + This function allows to save news in a json file in homedirectory/rss_reader_cache/cache.json + + :param int limit: The number of news to be saved + """ logging.info('Saving news') news_to_save = self.create_json(1, limit)['Items'] existing_news = load_from_cache() news_to_save += [item for item in existing_news if not item in news_to_save] - with open('cache.json', 'w') as json_file: + path = Path.home().joinpath('rss_reader_cache') + cache_file = "cache.json" + path.mkdir(parents=True, exist_ok=True) + filepath = path / cache_file + with filepath.open('w') as json_file: json.dump(news_to_save, json_file) - + logging.info('Saving news successful') class Item: """ Class for single news item from news feed - Attributes of the class are: - title a - pubDate a - link a - description a - links a - date_string a - source a + Attributes of the class can vary depend on if this item is created from loading from cache, or from parsed feed + They are: + str title News title + str pubDate Published date in it's original form + str link Link to the news + str description Description of the news + dict links A dict with href, image, video links + str date_string (optional: only from cache) Published date in YYYYMMDD format + str source (optional: only from cache) Rss source + str encoding (optional: only from cache) Encoding of the news """ def __init__(self, news_dict): for key in news_dict: setattr(self, key, news_dict[key]) - def print_to_console(self): + """ + This function allows to print one news item in console + """ print('\nTitle: {0}'.format(self.title)) print('Date: {0}'.format(self.pubDate)) print('Link: {0} \n'.format(self.link)) @@ -86,32 +244,234 @@ def print_to_console(self): print(link) print('\n//////////////////////////////////////////////////////////////////////////') + def create_div(self, date): + """ + This function creates a div block of news needed for html convertation + + :param str date: Optional: if exists than resulting div implementation will only contain news from specific date + :return: A string representation of div block of news + :rtype: str + """ + + return """ +
+

{title}

+ {pubDate} +

+

{description}

+ Read More +


+
+ """.format(title=html.escape(self.title), pubDate=self.pubDate, + description=self.insert_hrefs(self.description, date), link=self.link) + + def create_section(self): + """ + This function creates a section block of news needed for fb2 convertation + + :return: A string representation of section block of news + :rtype: str + """ + + logging.info('Creating section') + description = html.escape(self.description) + return """ +
+ <p>{title}</p> +

{pubDate}

+

{description}

+
+ """.format(title=html.escape(self.title), pubDate=self.pubDate, + description=self.insert_hrefs_fb2(self.description)) + + def insert_hrefs(self, description, date): + """ + This function inserts href links in description needed for html convertation + + :param str description: The original description of news + :param str date: Optional: if exists than resulting description will only contain news from specific date + :return: A description with inserted href links + :rtype: str + """ + + logging.info('href inserted') + description = self.insert_images(html.escape(description), date) + description = self.insert_videos(description) + for href_link in self.links['href_links']: + href_raw = description[description.find(' [link '):description.find(']', description.find(' [link '))+1] + href_content = href_raw[href_raw.find(' | ')+3:len(href_raw)-1] + href_html = '{content}'.format(href=href_link[href_link.find(': ')+2:], content=href_content) + description = description.replace(href_raw, href_html) + return description + + def insert_images(self, description, date): + """ + This function inserts images in description needed for html convertation + + :param str description: The original description of news + :param str date: Optional: if exists than resulting description will only contain news from specific date + :return: A description with inserted image links + :rtype: str + """ + logging.info('Image inserted') + for image_link in self.links['images_links']: + image_raw = description[description.find(' [image '):description.find(']', description.find(' [image '))+1] + image_alt = image_raw[image_raw.find(' | ') + 3:len(image_raw) - 1] + source = image_link[image_link.find(': ') + 2:] + logging.info(source) + if date: + image_name = source.split('/')[-1] + image_name = image_name.translate(str.maketrans('', '', '.?><"*:|')) + '.jpg' + path = Path.home().joinpath('rss_reader_cache/image') + source = path / image_name + image_html = '{alt}'.format(src=source, alt=image_alt) + description = description.replace(image_raw, image_html) + return description + + def insert_videos(self, description): + """ + This function inserts video links in description needed for html convertation + (I thought that I can convert them into full videos, but then I realised that it was a bad idea, + so I decided to just keep that part, although it isn't necessary anymore + + :param str description: The original description of news + :param str date: Optional: if exists than resulting description will only contain news from specific date + :return: A description with inserted video links + :rtype: str + """ + logging.info('Video inserted') + for video_link in self.links['video_links']: + video_href = description[description.find(' [video '):description.find(']', description.find(' [video '))+1] + logging.info(video_href) + source = video_link[video_link.find(': ') + 2:] + image_html = '{content}'.format(src=source, content=video_href[1:]) + description = description.replace(video_href, image_html) + return description + + def create_binary(self): + """ + This function creates a with b64 images needed for fb2 convertation + + :return: A string in format with images inside + """ + + logging.info('Creating binaries') + binaries = '' + if not self.links['images_links']: + return '' + for image_link in self.links['images_links']: + source = image_link[image_link.find(': ') + 2:] + logging.info(source) + image_name = source.split('/')[-1] + if source == '': + image_name = '.jpg' + encoded_string = '/9j/4AAQSkZJRgABAQEAXgBeAAD/4RpwRXhpZgAATU0AKgAAAAgABgALAAIAAAAmAAAIYgESAAMAAAABAAEAAAExAAIAAAAmAAAIiAEyAAIAAAAUAAAIrodpAAQAAAABAAAIwuocAAcAAAgMAAAAVgAAEUYc6gAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFdpbmRvd3MgUGhvdG8gRWRpdG9yIDEwLjAuMTAwMTEuMTYzODQAV2luZG93cyBQaG90byBFZGl0b3IgMTAuMC4xMDAxMS4xNjM4NAAyMDE5OjExOjMwIDE0OjQ2OjAyAAAGkAMAAgAAABQAABEckAQAAgAAABQAABEwkpEAAgAAAAMwMQAAkpIAAgAAAAMwMQAAoAEAAwAAAAEAAQAA6hwABwAACAwAAAkQAAAAABzqAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAxOToxMTozMCAxNDo0MTo1MQAyMDE5OjExOjMwIDE0OjQxOjUxAAAAAAYBAwADAAAAAQAGAAABGgAFAAAAAQAAEZQBGwAFAAAAAQAAEZwBKAADAAAAAQAC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MTE8L3htcDpDcmVhdGVEYXRlPjwvcmRmOkRlc2NyaXB0aW9uPjwvcmRmOlJERj48L3g6eG1wbWV0YT4NCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDw/eHBhY2tldCBlbmQ9J3cnPz7/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCABWAFYBAREA/8QAHAAAAwADAQEBAAAAAAAAAAAABAUGAgMHAQgA/8QAOxAAAQMDAgMFBQYDCQAAAAAAAQIDBAAFEQYhBxIxEyJBUWEIMkJxgRQVFiNSwZKhsRckcoKRotHS8P/aAAgBAQAAPwD6O3Ua3NMc2+KPYiZxtR7UDPhRSLcfKs/u75f6VqctxA6UI9Bx4Uvfi48KBcZ5TsKw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' + else: + logging.info('Image name %s', image_name) + image_name = image_name.translate(str.maketrans('', '', '.?><"*:|')) + '.jpg' + path = Path.home().joinpath('rss_reader_cache/image') + source = path / image_name + with open(source, "rb") as image_file: + encoded_string = b64encode(image_file.read()).decode() + + binaries += '{data}'\ + .format(src=image_name, data=encoded_string) + return binaries + + def insert_hrefs_fb2(self, description): + """ + This function allows find and insert links into description + (That is also a rudimental function. Originally I wanted to make hrefs to web links, which are stored as + notes. But something went wrong and not all rss were working correctly. So now it just makes links that are + empty) + + :param str description: Were to find those hrefs + :return: Resulting description with inserted href links + :rtype: str + """ + + logging.info('href inserted') + description = self.insert_images_fb2(html.escape(description)) + for href_link in self.links['href_links']: + href_raw = description[description.find(' [link '):description.find(']', description.find(' [link '))+1] + href_content = href_raw[href_raw.find(' | ')+3:len(href_raw)-1] + href_fb2 = '{content}'.format(href=href_link[href_link.find(': ')+2:], content=href_content) + description = description.replace(href_raw, href_fb2) + return description + + def insert_images_fb2(self, description): + """ + This function allows find and insert links to images into description + + :param str description: Were to find those images + :return: Resulting description with inserted image links + :rtype: str + """ + logging.info('Image inserted') + for image_link in self.links['images_links']: + image_raw = description[description.find(' [image '):description.find(']', description.find(' [image '))+1] + image_alt = image_raw[image_raw.find(' | ') + 3:len(image_raw) - 1] + source = image_link[image_link.find(': ') + 2:] + image_name = source.split('/')[-1] + image_name = image_name.translate(str.maketrans('', '', '.?><"*:|')) + '.jpg' + image_html = '{alt}'.format(src=image_name, alt=image_alt) + description = description.replace(image_raw, image_html) + return description + def return_item(self, is_cached): + """ + This function returns the content of this object as a dict + + :param bool is_cached: If true than the result dict will be able to be cached + :return: A dict with this object's content + :rtype: dict + """ + item_content = {'title': self.title, 'description': self.description, 'link': self.link, 'pubDate': self.pubDate, 'links': self.links} if is_cached: item_content['date_string'] = self.date_string item_content['source'] = self.source - + item_content['encoding'] = self.encoding return item_content def set_argparse(): + """ + This function allows to get needed parameters from command line + + :return: An object with all needed parameters inside + """ + parser = argparse.ArgumentParser(description='Pure Python command-line RSS reader.') parser.add_argument('source', type=str, help='RSS URL') - parser.add_argument('--version', action='version', version='%(prog)s v'+vers.__version__, help='Print version info') + parser.add_argument('--version', action='version', version='%(prog)s v'+vers.__version__, + help='Print version info') parser.add_argument('--json', action='store_true', help='Print result as JSON in stdout') parser.add_argument('--verbose', action='store_true', help='Outputs verbose status messages') parser.add_argument('--limit', type=int, default=-1, help='Limit news topics if this parameter provided') parser.add_argument('--date', type=str, help='Shows news of specific date') + parser.add_argument('--to-html', dest='to_html', type=str, + help='Convert news into html format and save to a specified path') + parser.add_argument('--to-fb2', dest='to_fb2', type=str, + help='Convert news into fb2 format and save to a specified path') return parser.parse_args() -def find_images(args, soup): +def find_images(soup): + """ + This function allows to extract image links from parsed feed + + :param bs4.BeautifulSoup soup: A beautifulsoup representation of parsed news feed + :return: A list of found image links + :rtype: list + """ + logging.info('Starting image finding') image_iterator = 0 images_links = [] + for img in soup.findAll('img'): image_iterator += 1 @@ -120,6 +480,17 @@ def find_images(args, soup): else: replaced_data = ' [image {0}]'.format(image_iterator) src = img['src'] + + if src != '': + image_name = src.split('/')[-1] + image_name = image_name.translate(str.maketrans('', '', '.?><"*:|')) + '.jpg' + path = Path.home().joinpath('rss_reader_cache/image') + path.mkdir(parents=True, exist_ok=True) + filepath = path / image_name + if filepath.is_file(): + logging.info('Image already exists') + else: + urllib.request.urlretrieve(src, filepath) images_links.append('[{0}]: {1}'.format(image_iterator, src)) soup.find('img').replace_with(replaced_data) @@ -127,12 +498,19 @@ def find_images(args, soup): return images_links -def find_href(args, soup): +def find_href(soup): + """ + This function allows to extract href links from parsed feed + + :param bs4.BeautifulSoup soup: A beautifulsoup representation of parsed news feed + :return: A list of found href links + :rtype: list + """ + logging.info('Starting link finding') href_iterator = 0 href_links = [] for href in soup.findAll('a'): - if 'href' in href.attrs: href_iterator += 1 link = href['href'] @@ -141,12 +519,20 @@ def find_href(args, soup): else: replaced_data = ' [link {0}] '.format(href_iterator) href_links.append('[{0}]: {1}'.format(href_iterator, link)) - soup.find('a').replace_with(replaced_data) + href.replace_with(replaced_data) logging.info('Link finding finished. Found %s links', href_iterator) return href_links -def find_videos(args, soup): +def find_videos(soup): + """ + This function allows to extract