From 1d4200445e164db90baca90d94af09fe7f0d2f5f Mon Sep 17 00:00:00 2001 From: Andrey Nenuzhny Date: Mon, 18 Nov 2019 14:01:18 +0300 Subject: [PATCH] Restructured news to pass mypy cheks. - Default bot is ready - Yahoo bot is ready - Tut bot is ready - Test are done --- README.md | 7 + dist/rss-reader-2.0.tar.gz | Bin 7165 -> 7213 bytes dist/rss-reader-2.0/PKG-INFO | 87 ++++++++ dist/rss-reader-2.0/README.md | 71 ++++++ dist/rss-reader-2.0/rss_reader/__init__.py | 1 + dist/rss-reader-2.0/rss_reader/__main__.py | 2 + .../rss_reader/bots/__init__.py | 0 .../rss-reader-2.0/rss_reader/bots/default.py | 5 + dist/rss-reader-2.0/rss_reader/bots/tut.py | 79 +++++++ dist/rss-reader-2.0/rss_reader/bots/yahoo.py | 46 ++++ dist/rss-reader-2.0/rss_reader/rss.py | 148 +++++++++++++ .../rss_reader/utils/DataStructures.py | 19 ++ .../rss_reader/utils/RssInterface.py | 204 ++++++++++++++++++ .../rss_reader/utils/__init__.py | 0 dist/rss-reader-2.0/setup.cfg | 4 + dist/rss-reader-2.0/setup.py | 39 ++++ dist/rss_reader-2.0-py3-none-any.whl | Bin 10398 -> 10430 bytes rss_reader/__main__.py | 4 +- rss_reader/bots/tut.py | 70 +++--- rss_reader/bots/yahoo.py | 20 +- rss_reader/utils/DataStructures.py | 55 +++++ rss_reader/utils/RssInterface.py | 88 ++++---- tests/test_default_bot.py | 12 +- tests/test_tut.py | 6 +- tests/test_yahoo.py | 15 +- 25 files changed, 879 insertions(+), 103 deletions(-) create mode 100644 dist/rss-reader-2.0/PKG-INFO create mode 100644 dist/rss-reader-2.0/README.md create mode 100644 dist/rss-reader-2.0/rss_reader/__init__.py create mode 100644 dist/rss-reader-2.0/rss_reader/__main__.py create mode 100644 dist/rss-reader-2.0/rss_reader/bots/__init__.py create mode 100755 dist/rss-reader-2.0/rss_reader/bots/default.py create mode 100755 dist/rss-reader-2.0/rss_reader/bots/tut.py create mode 100755 dist/rss-reader-2.0/rss_reader/bots/yahoo.py create mode 100755 dist/rss-reader-2.0/rss_reader/rss.py create mode 100644 dist/rss-reader-2.0/rss_reader/utils/DataStructures.py create mode 100644 dist/rss-reader-2.0/rss_reader/utils/RssInterface.py create mode 100644 dist/rss-reader-2.0/rss_reader/utils/__init__.py create mode 100644 dist/rss-reader-2.0/setup.cfg create mode 100644 dist/rss-reader-2.0/setup.py diff --git a/README.md b/README.md index 5a97dd6..0fb0d0b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,13 @@ Wheel package for the second iteration task Actual packages you may find in the './dist' repo if you don't want to generate it manually. +Installing: +```python3 -m pip install ./dist/rss_reader-2.0-py3-none-any.whl``` +OR +``` +python3 -m pip install -r requirements.txt +pip install ./dist/rss-reader-2.0.tar.gz +``` diff --git a/dist/rss-reader-2.0.tar.gz b/dist/rss-reader-2.0.tar.gz index d26db1cf7d6ecd5aebaafd70b1436379ada0b5c0..50e060b6ee879635431f18c6f7757c35acb5db1e 100644 GIT binary patch literal 7213 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 literal 7165 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.7 +Description-Content-Type: text/markdown +Provides-Extra: tests diff --git a/dist/rss-reader-2.0/README.md b/dist/rss-reader-2.0/README.md new file mode 100644 index 0000000..5a97dd6 --- /dev/null +++ b/dist/rss-reader-2.0/README.md @@ -0,0 +1,71 @@ +# Rss reader hometask for EpamTrainee +Python RSS-reader. + +Version 2 +```shell +usage: rss.py [-h] [--verbose] [--limit LIMIT] [--json] [-v] [--width WIDTH] + url +OR + +python3 -m rss_reader ... (After installing package) + +Description: +Rss reader. Just enter rss url from your favorite site and app will print +latest news. + +positional arguments: + url url of rss + +optional arguments: + -h, --help show this help message and exit + --verbose Outputs verbose status messages + --limit LIMIT Limit news topics if this parameter provided + --json Print result as JSON in stdout + -v, --version Print version info + --width WIDTH Define a screen width to display news + +``` + +## Code description +* Code uses `argparse` module. +* Codebase covered with unit tests with at 90% coverage. +* Any mistakes are printed in human-readable error explanation. +Exception tracebacks in stdout are prohibited. +* Docstrings are mandatory for all methods, classes, functions and modules. +* Code corresponds to `pep8` (used `pycodestyle` utility for self-check). +* Feedparser module is used for rss parsing; +* There are several bots for parsing news: default bor for unimplemented RSS urls and + custom bots (yahoo, tut) with detailed approach to parsing. + +## Code self-checking +Use ./pycodestyle.sh to check the code corresponding to `pep8` +(pycodestyle package must be installed) + +## Testing +Tests are available at `https://github.com/Nenu1985/PythonHomework` +Launching: +``` +./make_tests.sh +``` +- to pass test with coverage +(nose and coverage packages must be installed) + +## Version 2: Distribution +Utility wrapes into distribution package with setuptools. +This package exports CLI utility named rss-reader. + +To generate distribution package (setuptool and wheel must be installed): +Launch: +``` python3 setup.py sdist bdist_wheel``` +In the ./dist repo you'll find a .tar and .whl files. + +Wheel package for the second iteration task +(maybe is discarded but it works) on the Google Drive: +```https://drive.google.com/file/d/1RbMYxvpEXTx77Dk61xPkwSChD_jTf0jf/view?usp=sharin``` + +Actual packages you may find in the './dist' repo if you don't want to generate it manually. + + + + + diff --git a/dist/rss-reader-2.0/rss_reader/__init__.py b/dist/rss-reader-2.0/rss_reader/__init__.py new file mode 100644 index 0000000..4e5a49d --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/__init__.py @@ -0,0 +1 @@ +from . import bots, utils \ No newline at end of file diff --git a/dist/rss-reader-2.0/rss_reader/__main__.py b/dist/rss-reader-2.0/rss_reader/__main__.py new file mode 100644 index 0000000..347db46 --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/__main__.py @@ -0,0 +1,2 @@ +from rss_reader import rss +rss.main() diff --git a/dist/rss-reader-2.0/rss_reader/bots/__init__.py b/dist/rss-reader-2.0/rss_reader/bots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dist/rss-reader-2.0/rss_reader/bots/default.py b/dist/rss-reader-2.0/rss_reader/bots/default.py new file mode 100755 index 0000000..e2acb55 --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/bots/default.py @@ -0,0 +1,5 @@ +from ..utils.RssInterface import BaseRssBot + + +class Bot(BaseRssBot): + pass diff --git a/dist/rss-reader-2.0/rss_reader/bots/tut.py b/dist/rss-reader-2.0/rss_reader/bots/tut.py new file mode 100755 index 0000000..9dc5971 --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/bots/tut.py @@ -0,0 +1,79 @@ +#!/usr/local/opt/python/bin/python3.7 + +import bs4 +import feedparser +import typing + +from rss_reader.utils.RssInterface import BaseRssBot + + +class Bot(BaseRssBot): + + def _get_news_as_dict(self, feed: feedparser.FeedParserDict) -> typing.Dict[str, typing.Any]: + """ + Returns str containing formatted news from internal attr self.feed + + :return: str with news + """ + news = {'feed': feed.get('feed').get('title'), + 'items': []} + + for i, item in enumerate(feed.get('items')[:self.limit]): + news_item = { + 'title': item.get('title'), + 'link': item.get('link', ''), + 'published': item.get('published', ''), + 'imgs': [img.get('url') for img in item.get('media_content')], + 'links': [link.get('href') for link in item.get('links')], + 'authors': [author.get('name') for author in item.get('authors')], + 'tags': [tag.get('term') for tag in item.get('tags', '')], + 'html': item.get('summary') + } + self._parse_news_item(news_item) + news['items'].append(news_item) + self.logger.info(f'Feedparser object is converted into dictionary') + return news + + def _parse_news_item(self, news_item: typing.Dict[str, typing.Any]) -> None: + """ + Forms a human readable string from news_item and adds it to the news_item dict + :param news_item: news_item content + :return: extend news_item dict with human readable news content + """ + self.logger.info(f'Extending {news_item.get("title")}') + + out_str = '' + out_str += f"\nTitle: {news_item.get('title', '')}\n" \ + f"Date: {news_item.get('published', '')}\n" \ + f"Link: {news_item.get('link', '')}\n" + out_str += f"Authors: {', '.join(news_item.get('authors'))}\n" + out_str += f"Tags: {', '.join(news_item.get('tags'))}\n" + + html = bs4.BeautifulSoup(news_item.get('html'), "html.parser") + + links = news_item.get('links') + imgs = news_item.get('imgs') + + for tag in html.descendants: + if tag.name == 'a': + pass + elif tag.name == 'img': + src = tag.attrs.get('src') + # src = src.replace('thumbnails/', '') + if src in imgs: + img_idx = imgs.index(src) + len(links) + 1 + else: + imgs.append(src) + img_idx = len(imgs) + len(links) + + out_str += f'\n[image {img_idx}: {tag.attrs.get("alt")}][{img_idx}]' + elif tag.name == 'p': + out_str += '\n' + tag.text + elif tag.name == 'br': + out_str += '\n' + out_str += f'\n{html.getText()}\n' + out_str += 'Links:\n' + out_str += '\n'.join([f'[{i + 1}]: {link} (link)' for i, link in enumerate(links)]) + '\n' + out_str += '\n'.join([f'[{i + len(links) + 1}]: {link} (image)' for i, link in enumerate(imgs)]) + '\n' + + news_item['human_text'] = out_str diff --git a/dist/rss-reader-2.0/rss_reader/bots/yahoo.py b/dist/rss-reader-2.0/rss_reader/bots/yahoo.py new file mode 100755 index 0000000..dcd2e61 --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/bots/yahoo.py @@ -0,0 +1,46 @@ +import bs4 + +from rss_reader.utils.RssInterface import BaseRssBot + + +class Bot(BaseRssBot): + + def _parse_news_item(self, news_item: dict) -> None: + """ + Forms a human readable string from news_item and adds it to the news_item dict + :param news_item: news_item content + :return: extend news_item dict with human readable news content + """ + + out_str = '' + out_str += f"\nTitle: {news_item.get('title', '')}\n" \ + f"Date: {news_item.get('published', '')}\n" \ + f"Link: {news_item.get('link', '')}\n" + + html = bs4.BeautifulSoup(news_item.get('html'), "html.parser") + + links = news_item.get('links') + imgs = news_item.get('imgs') + + for tag in html.descendants: + if tag.name == 'a': + pass + elif tag.name == 'img': + src = tag.attrs.get('src') + if src in imgs: + img_idx = imgs.index(src) + len(links) + 1 + else: + imgs.append(src) + img_idx = len(imgs) + len(links) + out_str += f'\n[image {img_idx}: {tag.attrs.get("title")}][{img_idx}]' + elif tag.name == 'p': + out_str += '\n' + tag.text + elif tag.name == 'br': + out_str += '\n' + + # out_str += Color('{autocyan}Links:{/autocyan}\n') + out_str += 'Links:\n' + out_str += '\n'.join([f'[{i + 1}]: {link} (link)' for i, link in enumerate(links)]) + '\n' + out_str += '\n'.join([f'[{i + len(links) + 1}]: {link} (image)' for i, link in enumerate(imgs)]) + '\n' + + news_item['human_text'] = out_str diff --git a/dist/rss-reader-2.0/rss_reader/rss.py b/dist/rss-reader-2.0/rss_reader/rss.py new file mode 100755 index 0000000..4365dce --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/rss.py @@ -0,0 +1,148 @@ +""" +Main module. Launches the rss reader and output the result +""" +import argparse +import getpass +import logging +import sys +from importlib import import_module + +from .utils import RssInterface +from .utils.RssInterface import RssException +from .utils.DataStructures import ConsoleArgs + +PROG_VERSION = 2.0 + + +def logger_init(level=None): + """Logger initialisation + + All logs are printed into ./main.log file + Other logs in regards of the 'level' are printed into console + """ + + level = level or logging.CRITICAL + logger = logging.getLogger(getpass.getuser()) + logger.setLevel(level=level) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + + # Logging into file + fh = logging.FileHandler("main.log", encoding="utf-8") # 6 + fh.setLevel(logging.INFO) # 7 + fh.setFormatter(formatter) + + # Logging into console + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + console_handler.setLevel(level) + + # logger.addHandler(fh) + logger.addHandler(console_handler) + return logger + + +def get_bot_instance(url: str, logger: logging.Logger) -> RssInterface: + """ + Choosing an appropriate bot to the url + :param url: url, contained rss feed + :return: Bot class inherited from RssInterface, appropriate to the url + """ + if url.find('news.yahoo.com/rss') + 1: + bot = import_module('rss_reader.bots.yahoo').Bot + logger.info('Yahoo bot is loaded') + elif url.find('news.tut.by/rss') + 1: + bot = import_module('rss_reader.bots.tut').Bot + logger.info('Tut.by bot is loaded') + else: + bot = import_module('rss_reader.bots.default').Bot + logger.info('Default bot is loaded') + return bot + + +def args_parser() -> ConsoleArgs: + """Parsing console args and returning args class""" + + PARSER = argparse.ArgumentParser( + description=''' + Rss reader. + Just enter rss url from your favorite site and app will print + latest news. + ''' + ) + + PARSER.add_argument('url', type=str, + help='url of rss', + ) + + PARSER.add_argument('--verbose', + help='Outputs verbose status messages', + action='store_true', + ) + PARSER.add_argument('--limit', + help='Limit news topics if this parameter provided', + default=10, + type=int, + ) + PARSER.add_argument('--json', + help='Print result as JSON in stdout', + action='store_true', + ) + PARSER.add_argument('-v', '--version', + help='Print version info', + action='version', + version=f'%(prog)s {PROG_VERSION}' + ) + PARSER.add_argument('--width', + help='Define a screen width to display news', + default=120, + type=int, + ) + + ARGS = PARSER.parse_args() + + return ConsoleArgs( + url=ARGS.url, + limit=ARGS.limit, + width=ARGS.width, + json=ARGS.json, + verbose=ARGS.verbose, + ) + + +def main() -> None: + """ + Main func calls rss reader + + :return: None + """ + # url: str, limit: int, width: int, json: bool, verbose: bool + args = args_parser() + # Logger initialisation depends on verbose param + if args.verbose: + logger = logger_init(level=logging.DEBUG) + else: + logger = logger_init() + + logger.info(f'Lets start! Url={args.url}') + + # Get appropriate to the url bot class + bot = get_bot_instance(args.url, logger) + + try: + rss_reader = bot(url=args.url, limit=args.limit, logger=logger, width=args.width) + if args.json: + news = rss_reader.get_json() + else: + news = rss_reader.get_news() + except RssException as ex: + print(ex.args[0]) + + except Exception as ex: + print(f'Unhandled exception!\n{ex}\nExiting...') + else: + print(news) + logger.debug('Quit application with succeed result') + + +if __name__ == "__main__": + main() diff --git a/dist/rss-reader-2.0/rss_reader/utils/DataStructures.py b/dist/rss-reader-2.0/rss_reader/utils/DataStructures.py new file mode 100644 index 0000000..430e291 --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/utils/DataStructures.py @@ -0,0 +1,19 @@ +import attr + + +@attr.s(frozen=True) +class ConsoleArgs: + """ + Structured class to store console args + + : url: news rss url + : limit: limit of printed news + : width: width of the screen to print the news + : json: bool flag to print in json format + : verbose: bool flag to set logger level + """ + url: str = attr.ib() + limit: int = attr.ib(default=10) + width: int = attr.ib(default=120) + json: bool = attr.ib(default=False) + verbose: bool = attr.ib(default=False) diff --git a/dist/rss-reader-2.0/rss_reader/utils/RssInterface.py b/dist/rss-reader-2.0/rss_reader/utils/RssInterface.py new file mode 100644 index 0000000..fe258d3 --- /dev/null +++ b/dist/rss-reader-2.0/rss_reader/utils/RssInterface.py @@ -0,0 +1,204 @@ +from abc import ABCMeta, abstractmethod, abstractproperty +import json +import feedparser +import logging +from typing import Union, Dict, List, Iterable, Optional, Any + +import bs4 +from textwrap import wrap +from terminaltables import SingleTable + + +class RssException(Exception): + """ + Custom Exception class raised by RssBots classes + """ + pass + + +class RssBotInterface(metaclass=ABCMeta): + """ + Interface for Rss reader classes. Mandatory methods are: get_news(), get_json() + and internal parser's methods for particular cases of each bot + """ + + def __init__(self, url: str, limit: int, width: int, logger: logging.Logger): + self.limit = limit + self.logger = logger + self.url = url + self.screen_width = width + feed = self._parse_raw_rss() + self.news = self._get_news_as_dict(feed) # news as dict + self.logger.info(f'Bot initialization is completed') + + @abstractmethod + def get_news(self) -> str: + """ + Returns str containing formatted news from internal attr self.feed + + :return: str with news + """ + + def get_json(self) -> str: + """ + Return json formatted news + + :return: json formatted string + """ + self.logger.info(f'Returning news in JSON format') + return json.dumps(self.news, indent=4) + + @abstractmethod + def _parse_raw_rss(self) -> feedparser.FeedParserDict: + """ + Parsing news by url + + Result stores into internal attribute self.feed + :return: None + """ + + @abstractmethod + def _get_news_as_dict(self, feed: feedparser.FeedParserDict) -> Dict[str, Union[str, List[str]]]: + """ + Returns str containing formatted news from internal attr self.feed + + :return: str with news + """ + + @abstractmethod + def _parse_news_item(self, news_item: Dict[str, Union[str, List[str]]]): + pass + + +class BaseRssBot(RssBotInterface): + """ + Base class for rss reader bots. Implements base interface + """ + + def _parse_raw_rss(self) -> feedparser.FeedParserDict: + """ + Parsing news by url + + Result stores into internal attribute self.feed + :return: FeedParserDict object with news + """ + + self.logger.info(f'Lets to grab news from {self.url}') + + feed = feedparser.parse(self.url) + + self.logger.debug(f'Got feedparser object') + + if feed.get('bozo_exception'): + # + exception = feed.get('bozo_exception') + self.logger.warning(f'Having an exception while parsing xml: {exception}') + + exception_sting = f'\tError while parsing xml: \n {exception}\n\tBad rss feed. Check your url\n\n' \ + f'\tTry to use one of this as example:\n' \ + f'\ttut_by_rss = "https://news.tut.by/rss/index.rss"\n' \ + f'\tgoogle_rss = "https://news.google.com/news/rss"\n' \ + f'\tyahoo = "https://news.yahoo.com/rss/"' + raise RssException(exception_sting) + + self.logger.info(f'well formed xml = {feed.get("bozo")}\n' + f'url= {feed.get("url")}\n' + f'title= {feed.get("channel")["title"]}\n' + f'description= {feed.get("channel")["description"]}\n' + f'link to recent changes= {feed.get("channel")["link"]}\n' + ) + return feed + + def get_news(self) -> str: + """ + Returns str containing formatted news from internal attr self.feed + + :return: str with news + """ + table = [['Feed', f"{self.news.get('feed', '')}\n"]] + news_items = self.news.get('items') + for n, item in enumerate(news_items): + # table.append([1, item.get('human_text')]) + initial_news_item = item.get('human_text', '') + splitted_by_paragraphs = initial_news_item.split('\n') + for i, line in enumerate(splitted_by_paragraphs): + if len(line) > self.screen_width: + wrapped = wrap(line, self.screen_width) + del splitted_by_paragraphs[i] + for wrapped_line in wrapped: + splitted_by_paragraphs.insert(i, wrapped_line) + i += 1 + + table.append([str(n + 1), '\n'.join(splitted_by_paragraphs)]) + + table_inst = SingleTable(table) + table_inst.inner_heading_row_border = False + table_inst.inner_row_border = True + + self.logger.info(f'Print formatted news') + + return table_inst.table + + def _get_news_as_dict(self, feed: feedparser.FeedParserDict) \ + -> Dict[str, Union[str, List[str]]]: + """ + Returns str containing formatted news from internal attr self.feed + + :return: str with news + """ + news = {'feed': feed.get('feed', '').get('title', ''), + 'items': []} + + for i, item in enumerate(feed.get('items')[:self.limit]): + news_item = { + 'title': item.get('title', ''), + 'link': item.get('link', ''), + 'published': item.get('published', ''), + 'imgs': [img.get('url', '') for img in item.get('media_content', '')], + 'links': [link.get('href', '') for link in item.get('links', '')], + 'html': item.get('summary', '') + } + self._parse_news_item(news_item) + news['items'].append(news_item) + + self.logger.info(f'Feedparser object is converted into dictionary') + return news + + def _parse_news_item(self, news_item: Dict[str, Union[str, List[str]]]) -> None: + """ + Forms a human readable string from news_item and adds it to the news_item dict + :param news_item: news_item content + :return: extend news_item dict with human readable news content + """ + self.logger.info(f'Extending {news_item.get("title", "")}') + out_str = '' + out_str += f"\nTitle: {news_item.get('title', '')}\n" \ + f"Date: {news_item.get('published', '')}\n" \ + f"Link: {news_item.get('link', '')}\n" + + html = bs4.BeautifulSoup(news_item.get('html', ''), "html.parser") + + links = news_item.get('links', '') + imgs = news_item.get('imgs', '') + + for tag in html.descendants: + if tag.name == 'a': + link = tag.attrs.get('href', '') + if link not in links: + links.append(link) + self.logger.warning(f'Link {link} isn\'t found') + elif tag.name == 'img': + src = tag.attrs.get('src', '') + if src in imgs: + img_idx = imgs.index(src) + len(links) + 1 + else: + imgs.append(src) + img_idx = len(imgs) + len(links) + out_str += f'\n[image {img_idx}: {tag.attrs.get("title")}][{img_idx}]' + + out_str += f'{html.getText()}\n' + out_str += 'Links:\n' + out_str += '\n'.join([f'[{i + 1}]: {link} (link)' for i, link in enumerate(links)]) + '\n' + out_str += '\n'.join([f'[{i + len(links) + 1}]: {link} (image)' for i, link in enumerate(imgs)]) + '\n' + + news_item['human_text'] = out_str diff --git a/dist/rss-reader-2.0/rss_reader/utils/__init__.py b/dist/rss-reader-2.0/rss_reader/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dist/rss-reader-2.0/setup.cfg b/dist/rss-reader-2.0/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/dist/rss-reader-2.0/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/dist/rss-reader-2.0/setup.py b/dist/rss-reader-2.0/setup.py new file mode 100644 index 0000000..ba1fe3f --- /dev/null +++ b/dist/rss-reader-2.0/setup.py @@ -0,0 +1,39 @@ +"""Utils to export CLI rss_reader module +using: 'python3 setup.py sdist bdist_wheel' +""" +import setuptools +from rss_reader.rss import PROG_VERSION + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="rss-reader", # Replace with your own username + version=str(PROG_VERSION), + author="Andrey Nenuzhny", + author_email="nenuzhny85@gmail.com", + description="Rss reader Epam task", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Nenu1985/PythonHomework", + + packages=setuptools.find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), + + install_requires=['bs4', + 'html2text', + 'feedparser', + 'terminaltables'], + + extras_require={ # Optional + 'tests': ['nose', 'coverage'], + }, + + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: Unix", + ], + + zip_safe=False, + python_requires='>=3.7', +) diff --git a/dist/rss_reader-2.0-py3-none-any.whl b/dist/rss_reader-2.0-py3-none-any.whl index ee67edf5ff47f78ff411578d8c5a370dc3d6c2c9..bfef2a585c716561b5d74b5fa42bc339c90635aa 100644 GIT binary patch delta 2525 zcmZXWc{J2*8^>qtS;kn$mJDNTBV;YK(L`S3LPd#mvIoRN1DxPq|w~5gg+OCcE9baxL>A5(dIzm{|l5 z6&xU3D!JAc#uSoT!zn^x;tM&n#N>Hl6IekYZY~fA4*0<(fL~(5LZVk1abtCaL|=oX zimx;TI)gyPwgXb);zT(iFL>VSUS;UJQSK$LKt!cy1>2YNOTn^S^Bqq%qZ+)Iofv`o zFX7lC{YA@BL9f&p-gtd1tvS?7!hu=Z!qU{l*xqxkXQNf?ZHzP> zoZlw%8MP<}W%9>A3UCf|Jv)pmPj`9cC(3?sulA~q%d$(_1EL?@;kBzPk;1Q?94D}{ zveu82J^xOi0y^6{QIbNa=zn^$AWaLM%+_Fa?B`gS2}6mFYM?Y_3oJIjC<`CVI{DE1 zrwrg-)Ep-1bkAEUXqtaIHDAra3R7k+J`)jbIa4$Caq81vvE z_6_55VjXFVu{E>ND&dlQniT%Amxc{bdz6+(Bo;0w2R-wuC7uG4@-0Z!r44JA3+jjf zsX%94=tQ_W<3l9+h6eZSle~BB6X{M9LFwI%zs@Lf?#fsg&`vuol}azx#V~QR$E8La zfusfud5L_UMl`GEVz;RJ`R}uA4FlU|Bdflhj&KV`w@5+>JyK5t4ao1qXm%A$)Qoj& z%<*xL$9UN6tC-4|u+bC{LhG`xNK&h%4975){bWQg;S6uSoa==IAq;62e_STkv)^|@ zFo5a^8hU4&=$RdVcg*1ibDMCz>DPYVCP4vrdm>{cPfOr==r@h5C36e?vq_N@1+QWO zV@wpJGRfD>UUfr%_V&_y;YW{rfsVUVb^^B9d~YnyYv+CxwkW7h4nlz~(;=uN)_NID zHl6&TrGZnlCfKK2ND_~gIYx8AY+%MIWW*W_!Q#i?)AEnaDj?_e7SPIxRf!q)$bpnb zqM0Z^o0{Ii!c&4-KH(!dU{ z-iL5rLI;|H1zLaTwIu+bvgNn#qQ)koZ@Q^nt#dOk)jhm45%P&q0XSA>+M_5Fb(5sz!ce!Ktoa*N^ zTsV?2@q3tJXh{1DUUttW)cv|Z&d(XadJZKd%|AIQJ%y$j==R{v*!`h3;`VUX+eY1k zs|}$W3qD)aq4jI^4Z6yMk`#!X3dNFfr$YD-&DP`5Hdnv}cK4FHjP0HMvpvf5>&qPP zmaGy;tAvCZ_~nAObiFSq^7fqNAeLQA%*9?g`|JKb>%T#Ll+3*)$^I^fv3*pMhvk3g zj=Mh)FGW1eZvYCn;y8L_c+Ij4xDH1VhZ~df&`>rHVkYY=g@Gh8TM7hf!<;Mr1LPzykIqx+PwQba9-H?W#F8qa!evEib<9i6z98(FCy zViI3&pslL}_i9PF(dq2?^a4F^FZjAySz;6w3@}xo0lRI=U7toUi2M9Olj#9 zZw2LXJiDA$v002iAR1gUuwyiY5*$mpPwMW+g{9T~Lru3n-rpqpr=-?esbn4Z_Q zVa@mLygGPLM1(B~jX446rf`M)T2Nb*oN J>=DR;{sZQ$iA4Ya delta 2470 zcmZXWc{J4f8^?#qK7<*@&R`g1DZ8R!a4~i=2$hl;6UErwk(-??UrUxOSu;i{%UH6E z5Xx9Wk?=!COi{vhb^CFD=bn4+`F+lL&gY!xeBSTppU?9?uP%oYhfFsC;h0-?)3gIg zBnSjjJSt(JTVC`uR%Mue0i~>VPunF-IhM7mf7mOFqUWcjrsK6bo_jTRk%nJ-+FZ<5&(8y-9NL^+WQme5LDBaO} zK6kF8Lt99*5h3@Se|cLkr>@QvD!A11_Eqx^?(tz-snkP~U8dhbLkLCA)2qyP{ZQMr zPdYWk{gZ6o&bqf=l7Cx>7Bu8dQ{&^T9L%9JKby5LDIZ=f0ti$R#8#3asVY9fbM`z|h2FJ)hr zCAB-v%1StlVzEnCXYUtd?wV#btUP%n+8o|A?B*2o%x^O%O0yaxb767p;}};X1>^!H zR$YZF)%wqlU~}oEjWw5A#BVT5dvaIeawD=`Q|cFtBb-?mb7Jh%u9`u4V%>r%x$XLB za80(p)}6am40EP(e=rN(Rs7u9M|E1$B>n5~io#;amZwd-N#4*>B1PrNZy{9^uOd~x zy(l4e2K-X!d4&q!lQErMz#w>r)RZpEiga;^-2QqI*1uGO^j-}vnJmP;I>9NLS#4rY zcia(pqql-;OdrqUkuOdH&!+$U8Ff}TUr<$jiIAxv+oz(5yCj(5fNtL)u2yLJk$jLz zI!(ff>%5m+0$L9Z{@A*-9=Nv~yC1U^IZ@f$aS6D-Ugc!Xym`Hq`=YBOyK(=38D3>8 z_zknjHl@5#idyXH6%cXbNpi|_(zUVvtg3=DhiGz5g~Uy{%CC7WYJoP#{BFu%aJ6h7Fl=NU1?*5A^$v^Ha zSJewvEAp;ZMtXhd3cA(cV~xQr3ZLkwRn!Jc#5lo$1*FCC<3N4}Z{cR_%A$c6INB z7#7XqiYPPRk~3a{gYJxW{7Yj=SHBKYccF){JAGx6;j$BYx3g;+)SWEq7QwO}bB`d~ zm@B4RWt{i4>oa~nMd(@*A2uY_X2#4;yVi5nZ-cuJV;i|-?%c#Y$F$^9pr;PCp2G|eW*$}&mWL>1UJd0!2RjsZ9(nbS%|xQ6XaShX|bev%9=oq8R|H_TbkF> z^=Y$zG|a4OTA?Ib?)wlrG?d)#{eqAeI(q`5m^!@ob+o1aJA97IQFfp|q)47XbHG}4 zi@IE{gTqf??`a%r8M2hUN-))$S z1$6niVSyYiQ=MBq=zT*e_XO!e$hT!UU@vda`!5^;DDt|}ua16kI32~&cLWoA{2-9P z5pn!|D(}<+V5AXCAz*^EV4Y$Iw%8?rTBI_IJur#HuvYT`y*y&T83kn)6Tnl!js*`; z6l_>&a9|!T1&Ap^jyDCRD8c$C9LPe516LH`GXKXef6qF)EAMoP{q^vfBJ5Z?bR-Rs zIqsasS(uqw+hbW+Vc`EE(Xr3_#Y7?ABOT9CDg4Xl1mZ6rTa@TOTN;QGX1#FSM*y`b Les&G{W5s^~0IqXs diff --git a/rss_reader/__main__.py b/rss_reader/__main__.py index 74bd27a..347db46 100644 --- a/rss_reader/__main__.py +++ b/rss_reader/__main__.py @@ -1,2 +1,2 @@ -from rss_reader.rss import main -main() +from rss_reader import rss +rss.main() diff --git a/rss_reader/bots/tut.py b/rss_reader/bots/tut.py index 9dc5971..5266d73 100755 --- a/rss_reader/bots/tut.py +++ b/rss_reader/bots/tut.py @@ -1,58 +1,70 @@ #!/usr/local/opt/python/bin/python3.7 - +import attr import bs4 import feedparser import typing from rss_reader.utils.RssInterface import BaseRssBot +from ..utils.DataStructures import NewsItem, News + + +@attr.s +class TutNewItem(NewsItem): + tags: typing.List[str] = attr.ib() + authors: typing.List[str] = attr.ib() class Bot(BaseRssBot): - def _get_news_as_dict(self, feed: feedparser.FeedParserDict) -> typing.Dict[str, typing.Any]: + def _get_news(self, feed: feedparser.FeedParserDict) -> News: """ Returns str containing formatted news from internal attr self.feed :return: str with news """ - news = {'feed': feed.get('feed').get('title'), - 'items': []} + news_items = [] for i, item in enumerate(feed.get('items')[:self.limit]): - news_item = { - 'title': item.get('title'), - 'link': item.get('link', ''), - 'published': item.get('published', ''), - 'imgs': [img.get('url') for img in item.get('media_content')], - 'links': [link.get('href') for link in item.get('links')], - 'authors': [author.get('name') for author in item.get('authors')], - 'tags': [tag.get('term') for tag in item.get('tags', '')], - 'html': item.get('summary') - } - self._parse_news_item(news_item) - news['items'].append(news_item) - self.logger.info(f'Feedparser object is converted into dictionary') + + news_items.append(TutNewItem( + title=item.get('title', ''), + link=item.get('link', ''), + published=item.get('published', ''), + imgs=[img.get('url', '') for img in item.get('media_content', '')], + links=[link.get('href', '') for link in item.get('links', '')], + html=item.get('summary', ''), + authors=[author.get('name', '') for author in item.get('authors', '')], + tags=[tag.get('term', '') for tag in item.get('tags', '')], + )) + + news = News( + feed=feed.get('feed', '').get('title', ''), + link=feed.get('feed', '').get('link', ''), + items=news_items, + ) + self.logger.info(f'Feedparser object is converted into news_item obj with TUT news') + return news - def _parse_news_item(self, news_item: typing.Dict[str, typing.Any]) -> None: + def _parse_news_item(self, news_item: NewsItem) -> str: """ Forms a human readable string from news_item and adds it to the news_item dict :param news_item: news_item content - :return: extend news_item dict with human readable news content + :return: human readable news content """ - self.logger.info(f'Extending {news_item.get("title")}') + self.logger.info(f'_parse_news_item_tut.by Extending {news_item.title}') out_str = '' - out_str += f"\nTitle: {news_item.get('title', '')}\n" \ - f"Date: {news_item.get('published', '')}\n" \ - f"Link: {news_item.get('link', '')}\n" - out_str += f"Authors: {', '.join(news_item.get('authors'))}\n" - out_str += f"Tags: {', '.join(news_item.get('tags'))}\n" + out_str += f"\nTitle: {news_item.title}\n" \ + f"Date: {news_item.published}\n" \ + f"Link: {news_item.link}\n" + out_str += f"Authors: {', '.join(news_item.authors)}\n" + out_str += f"Tags: {', '.join(news_item.tags)}\n" - html = bs4.BeautifulSoup(news_item.get('html'), "html.parser") + html = bs4.BeautifulSoup(news_item.html, "html.parser") - links = news_item.get('links') - imgs = news_item.get('imgs') + links = news_item.links + imgs = news_item.imgs for tag in html.descendants: if tag.name == 'a': @@ -76,4 +88,4 @@ def _parse_news_item(self, news_item: typing.Dict[str, typing.Any]) -> None: out_str += '\n'.join([f'[{i + 1}]: {link} (link)' for i, link in enumerate(links)]) + '\n' out_str += '\n'.join([f'[{i + len(links) + 1}]: {link} (image)' for i, link in enumerate(imgs)]) + '\n' - news_item['human_text'] = out_str + return out_str diff --git a/rss_reader/bots/yahoo.py b/rss_reader/bots/yahoo.py index dcd2e61..7ac45e1 100755 --- a/rss_reader/bots/yahoo.py +++ b/rss_reader/bots/yahoo.py @@ -1,26 +1,26 @@ import bs4 from rss_reader.utils.RssInterface import BaseRssBot - +from ..utils.DataStructures import NewsItem class Bot(BaseRssBot): - def _parse_news_item(self, news_item: dict) -> None: + def _parse_news_item(self, news_item: NewsItem) -> str: """ Forms a human readable string from news_item and adds it to the news_item dict :param news_item: news_item content - :return: extend news_item dict with human readable news content + :return: human readable news content """ out_str = '' - out_str += f"\nTitle: {news_item.get('title', '')}\n" \ - f"Date: {news_item.get('published', '')}\n" \ - f"Link: {news_item.get('link', '')}\n" + out_str += f"\nTitle: {news_item.title}\n" \ + f"Date: {news_item.published}\n" \ + f"Link: {news_item.link}\n" - html = bs4.BeautifulSoup(news_item.get('html'), "html.parser") + html = bs4.BeautifulSoup(news_item.html, "html.parser") - links = news_item.get('links') - imgs = news_item.get('imgs') + links = news_item.links + imgs = news_item.imgs for tag in html.descendants: if tag.name == 'a': @@ -43,4 +43,4 @@ def _parse_news_item(self, news_item: dict) -> None: out_str += '\n'.join([f'[{i + 1}]: {link} (link)' for i, link in enumerate(links)]) + '\n' out_str += '\n'.join([f'[{i + len(links) + 1}]: {link} (image)' for i, link in enumerate(imgs)]) + '\n' - news_item['human_text'] = out_str + return out_str diff --git a/rss_reader/utils/DataStructures.py b/rss_reader/utils/DataStructures.py index 430e291..c233c27 100644 --- a/rss_reader/utils/DataStructures.py +++ b/rss_reader/utils/DataStructures.py @@ -1,4 +1,18 @@ import attr +from json import JSONEncoder +import typing + + +def _default(self, obj): + """ monkey-patches json module when it's imported so + JSONEncoder.default() automatically checks for a special "to_json()" + method and uses it to encode the object if found. + """ + return getattr(obj.__class__, "to_json", _default.default)(obj) + + +_default.default = JSONEncoder.default # Save unmodified default. +JSONEncoder.default = _default # Replace it. @attr.s(frozen=True) @@ -17,3 +31,44 @@ class ConsoleArgs: width: int = attr.ib(default=120) json: bool = attr.ib(default=False) verbose: bool = attr.ib(default=False) + + +@attr.s() +class NewsItem: + """ + Based structured class to store a news item + + : title: news' item title + : link: news' link + : published: string date + : imgs: list of links to imgs + : links: list of links to news' refs + : html: html content of the news + """ + + title: str = attr.ib() + link: str = attr.ib() + published: str = attr.ib() + imgs: typing.List[str] = attr.ib() + links: typing.List[str] = attr.ib() + html: str = attr.ib() + + def to_json(self): + return self.__dict__ + + +@attr.s(frozen=True) +class News: + """ + Based structured class to store all news + + : title: feed's title + : link: feed's link + : news_items: news_items + """ + feed: str = attr.ib() + link: str = attr.ib() + items: typing.List[NewsItem] = attr.ib() + + def to_json(self): + return self.__dict__ diff --git a/rss_reader/utils/RssInterface.py b/rss_reader/utils/RssInterface.py index fe258d3..8703892 100644 --- a/rss_reader/utils/RssInterface.py +++ b/rss_reader/utils/RssInterface.py @@ -1,12 +1,14 @@ -from abc import ABCMeta, abstractmethod, abstractproperty +from abc import ABCMeta, abstractmethod +import bs4 import json -import feedparser -import logging -from typing import Union, Dict, List, Iterable, Optional, Any +from typing import Union, Dict, List -import bs4 -from textwrap import wrap +import logging +import feedparser from terminaltables import SingleTable +from textwrap import wrap + +from ..utils.DataStructures import NewsItem, News class RssException(Exception): @@ -28,8 +30,10 @@ def __init__(self, url: str, limit: int, width: int, logger: logging.Logger): self.url = url self.screen_width = width feed = self._parse_raw_rss() - self.news = self._get_news_as_dict(feed) # news as dict + # self.news = self._get_news_as_dict(feed) # news as dict + self.news = self._get_news(feed) self.logger.info(f'Bot initialization is completed') + self.human_text = '' @abstractmethod def get_news(self) -> str: @@ -57,14 +61,6 @@ def _parse_raw_rss(self) -> feedparser.FeedParserDict: :return: None """ - @abstractmethod - def _get_news_as_dict(self, feed: feedparser.FeedParserDict) -> Dict[str, Union[str, List[str]]]: - """ - Returns str containing formatted news from internal attr self.feed - - :return: str with news - """ - @abstractmethod def _parse_news_item(self, news_item: Dict[str, Union[str, List[str]]]): pass @@ -115,11 +111,11 @@ def get_news(self) -> str: :return: str with news """ - table = [['Feed', f"{self.news.get('feed', '')}\n"]] - news_items = self.news.get('items') + table = [['Feed', f"Title: {self.news.feed}\nLink: {self.news.link}"]] + news_items = self.news.items for n, item in enumerate(news_items): # table.append([1, item.get('human_text')]) - initial_news_item = item.get('human_text', '') + initial_news_item = self._parse_news_item(item) splitted_by_paragraphs = initial_news_item.split('\n') for i, line in enumerate(splitted_by_paragraphs): if len(line) > self.screen_width: @@ -139,47 +135,49 @@ def get_news(self) -> str: return table_inst.table - def _get_news_as_dict(self, feed: feedparser.FeedParserDict) \ - -> Dict[str, Union[str, List[str]]]: + def _get_news(self, feed: feedparser.FeedParserDict) -> News: """ Returns str containing formatted news from internal attr self.feed :return: str with news """ - news = {'feed': feed.get('feed', '').get('title', ''), - 'items': []} - - for i, item in enumerate(feed.get('items')[:self.limit]): - news_item = { - 'title': item.get('title', ''), - 'link': item.get('link', ''), - 'published': item.get('published', ''), - 'imgs': [img.get('url', '') for img in item.get('media_content', '')], - 'links': [link.get('href', '') for link in item.get('links', '')], - 'html': item.get('summary', '') - } - self._parse_news_item(news_item) - news['items'].append(news_item) - - self.logger.info(f'Feedparser object is converted into dictionary') + news_items = [] + + for i, item in enumerate(feed.get('items', '')[:self.limit]): + news_items.append(NewsItem( + title=item.get('title', ''), + link=item.get('link', ''), + published=item.get('published', ''), + imgs=[img.get('url', '') for img in item.get('media_content', '')], + links=[link.get('href') for link in item.get('links', '')], + html=item.get('summary', ''), + )) + + news = News( + feed=feed.get('feed', '').get('title', ''), + link=feed.get('feed', '').get('link', ''), + items=news_items, + ) + self.logger.info(f'_get_news(): Feedparser object is converted into news_item obj with Default news') + return news - def _parse_news_item(self, news_item: Dict[str, Union[str, List[str]]]) -> None: + def _parse_news_item(self, news_item: NewsItem) -> str: """ Forms a human readable string from news_item and adds it to the news_item dict :param news_item: news_item content :return: extend news_item dict with human readable news content """ - self.logger.info(f'Extending {news_item.get("title", "")}') + self.logger.info(f'Extending {news_item.title}') out_str = '' - out_str += f"\nTitle: {news_item.get('title', '')}\n" \ - f"Date: {news_item.get('published', '')}\n" \ - f"Link: {news_item.get('link', '')}\n" + out_str += f"\nTitle: {news_item.title}\n" \ + f"Date: {news_item.published}\n" \ + f"Link: {news_item.link}\n" - html = bs4.BeautifulSoup(news_item.get('html', ''), "html.parser") + html = bs4.BeautifulSoup(news_item.html, "html.parser") - links = news_item.get('links', '') - imgs = news_item.get('imgs', '') + links = news_item.links + imgs = news_item.imgs for tag in html.descendants: if tag.name == 'a': @@ -201,4 +199,4 @@ def _parse_news_item(self, news_item: Dict[str, Union[str, List[str]]]) -> None: out_str += '\n'.join([f'[{i + 1}]: {link} (link)' for i, link in enumerate(links)]) + '\n' out_str += '\n'.join([f'[{i + len(links) + 1}]: {link} (image)' for i, link in enumerate(imgs)]) + '\n' - news_item['human_text'] = out_str + return out_str diff --git a/tests/test_default_bot.py b/tests/test_default_bot.py index 6baad59..1443cfb 100644 --- a/tests/test_default_bot.py +++ b/tests/test_default_bot.py @@ -25,13 +25,13 @@ def test_bot_limit(self): self.assertEqual(self.bot_google.limit, 10) def test_bot_feed(self): - self.assertEqual(self.bot_google.news.get('feed'), 'Top stories - Google News') + self.assertEqual(self.bot_google.news.feed, 'Top stories - Google News') def test_bot_news_count(self): - self.assertEqual(len(self.bot_google.news.get('items')), 10) + self.assertEqual(len(self.bot_google.news.items), 10) def test_bot_json_length(self): - self.assertEqual(len(self.bot_google.get_json()), 66635) + self.assertEqual(len(self.bot_google.get_json()), 31210) def test_bot_reddit_news_length(self): self.assertEqual(len(self.bot_google.get_news()), 45755) @@ -40,13 +40,13 @@ def test_bot_reddit_limit(self): self.assertEqual(self.bot_reddit.limit, 3) def test_bot_reddit_feed(self): - self.assertEqual(self.bot_reddit.news.get('feed'), 'World News') + self.assertEqual(self.bot_reddit.news.feed, 'World News') def test_bot_reddit_news_count(self): - self.assertEqual(len(self.bot_reddit.news.get('items')), 3) + self.assertEqual(len(self.bot_reddit.news.items), 3) def test_bot_reddit_json_length(self): - self.assertEqual(len(self.bot_reddit.get_json()), 5069) + self.assertEqual(len(self.bot_reddit.get_json()), 2759) def test_bot_reddit_news_length(self): self.assertEqual(len(self.bot_reddit.get_news()), 5549) diff --git a/tests/test_tut.py b/tests/test_tut.py index d68ee41..1800e64 100644 --- a/tests/test_tut.py +++ b/tests/test_tut.py @@ -23,13 +23,13 @@ def test_bot_limit(self): self.assertEqual(self.bot.limit, 7) def test_bot_feed(self): - self.assertEqual(self.bot.news.get('feed'), 'TUT.BY: Новости ТУТ - Главные новости') + self.assertEqual(self.bot.news.feed, 'TUT.BY: Новости ТУТ - Главные новости') def test_bot_news_count(self): - self.assertEqual(len(self.bot.news.get('items')), 7) + self.assertEqual(len(self.bot.news.items), 7) def test_bot_json_length(self): - self.assertEqual(len(self.bot.get_json()), 34377) + self.assertEqual(len(self.bot.get_json()), 18116) def test_bot_reddit_news_length(self): self.assertEqual(len(self.bot.get_news()), 20006) diff --git a/tests/test_yahoo.py b/tests/test_yahoo.py index 1226619..14b1a68 100644 --- a/tests/test_yahoo.py +++ b/tests/test_yahoo.py @@ -16,7 +16,6 @@ from rss_reader.utils.RssInterface import RssException - class TestMainModule(unittest.TestCase): def setUp(self) -> None: url = './tests/data/yahoo_news.xml' @@ -26,21 +25,21 @@ def test_bot_limit(self): self.assertEqual(self.bot.limit, 2) def test_bot_feed(self): - self.assertEqual(self.bot.news.get('feed'), 'Yahoo News - Latest News & Headlines') + self.assertEqual(self.bot.news.feed, 'Yahoo News - Latest News & Headlines') def test_bot_news_count(self): - self.assertEqual(len(self.bot.news.get('items')), 2) + self.assertEqual(len(self.bot.news.items), 2) def test_bot_json_length(self): - self.assertEqual(len(self.bot.get_json()), 5422) + self.assertEqual(len(self.bot.get_json()), 3336) def test_bot_reddit_news_length(self): self.assertEqual(len(self.bot.get_news()), 5951) def test_human_text(self): - item = self.bot.news.get('items')[0] - self.assertEqual(item.get('title'), 'Israel kills Islamic Jihad commander, rockets rain from Gaza') - self.assertEqual(len(item.get('human_text')), 1035) + item = self.bot.news.items[0] + self.assertEqual(item.title, 'Israel kills Islamic Jihad commander, rockets rain from Gaza') + self.assertEqual(len(self.bot._parse_news_item(item)), 1035) def test_raising_exception(self): url = 'asdf' @@ -57,5 +56,5 @@ def test_main_output_news(self): with open('./tests/data/yahoo.txt', 'r') as f: out_str = f.read() - self.assertEqual(len(out_str), 565) + self.assertEqual(len(out_str), 599) self.assertGreater(out_str.find('INFO'), 4)