From 36f2db15923382c80cd1ca1eedc5f8a78223910c Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Sun, 3 Nov 2019 16:00:56 +0300 Subject: [PATCH 01/12] Iteration 3 --- final_task/rss_reader/rss_reader.py | 181 ++++++++++++++++++++++++++++ final_task/setup.py | 16 +++ 2 files changed, 197 insertions(+) diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index e69de29..9bafdca 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -0,0 +1,181 @@ +import argparse +import feedparser +import logging +import json +import html +import sys +import datetime + +__version__ = '3.5' +args = None +limit = 3 +links = [] +print_setting = { + 'feed': [ + 'title' + ], + 'entries': [ + 'title', + 'published', + 'link', + 'summary' + ] +} + + +def get_object_feed(url: str): + try: + data = feedparser.parse(url) + # if data.status == 200: + if data.bozo: + return f'There is no rss feed at this url: {url}' + else: + return data + # else: + # return f'HTTP Status Code {data.status}' + except Exception as exc: + return f'ERROR: {exc}' + + +def to_json(data, **kwargs): + # convert data to JSON format + result = {} + if 'feed' in kwargs.keys(): + for i in kwargs['feed']: + result[i] = data['feed'][i] + if 'entries' in kwargs.keys(): + result['items'] = [] + for i in range(int(limit)): + temp = {} + for j in kwargs['entries']: + temp[j] = data['entries'][i][j] + result['items'].append(temp) + result = json.dumps(result) + return html.unescape(result) + + +def get_img(input_string): + # from string type 'somethingtext' returns ('link', 'something') + input_string = input_string[input_string.find('') < 2: + string = string[string.find('>') + 2:] + string = string[string.find('>') + 1:string.find('<')] + return key[0].upper() + key[1:] + ':' + image + html.unescape(string) + + +def set_start_setting(): + # setup start settings + global args, limit + parser = argparse.ArgumentParser() + parser.add_argument("source", help="RSS URL") + parser.add_argument("--version", help="Print version info", action="store_true") + parser.add_argument("--json", help="Print result as JSON in stdout", action="store_true") + parser.add_argument("--verbose", help="Outputs verbose status messages", action="store_true") + parser.add_argument("--limit", help="Limit news topics if this parameter provided") + parser.add_argument("--date", help="Obtaining the cached news without the Internet") + args = parser.parse_args() + if args.limit: + limit = args.limit + if args.verbose: + logging.basicConfig(stream=sys.stdout, level=logging.INFO) + else: + logging.basicConfig(filename="sample.log", level=logging.INFO) + + +def beautiful_print_data(data, **kwargs): + logging.info(type(data)) + if type(data) == feedparser.FeedParserDict: + if args.json: + data = to_json(data, **kwargs) + print(data) + else: + print() + for i in kwargs['feed']: + print(data['feed'][i]) + print() + k = 1 + for i in range(int(limit)): + for j in kwargs['entries']: + print(text_for_print(j, data['entries'][i][j])) + print() + print() + print('Links:') + for i, v in enumerate(links): + print(f'[{i + 1}]{v}') + print() + elif type(data) == dict: + print() + for i in kwargs['feed']: + print(data[i]) + print() + k = 1 + for i in range(int(limit)): + for j in kwargs['entries']: + print(text_for_print(j, data['items'][i][j])) + print() + print() + print('Links:') + for i, v in enumerate(links): + print(f'[{i + 1}]{v}') + print() + elif type(data) == str: + print(data) + + +def add_feed_to_file(json_data): + date_str = datetime.datetime.now().date().strftime('%Y%m%d') + try: + with open('cache.txt', 'r') as fin: + lines = fin.readlines() + except FileNotFoundError: + lines = [] + with open('cache.txt', 'w') as file: + if not lines: + file.write(datetime.datetime.now().date().strftime('%Y%m%d') + ' ' + str(json_data) + '\n') + for line in lines: + if date_str in line: + file.write(datetime.datetime.now().date().strftime('%Y%m%d') + ' ' + str(json_data) + '\n') + else: + file.write(line) + + +def read_feed_form_file(date_str): + with open('cache.txt', 'r') as file: + for i in file: + if date_str in i: + return json.loads(i[i.find(date_str) + len(date_str) + 1:].encode('UTF-8')) + else: + return 'Date ' + date_str + ' not found in cache.' + return None + + +def run(): + set_start_setting() + if args.version: + print(f'RSS reader version {__version__}') + elif args.date: + beautiful_print_data(read_feed_form_file(args.date), **print_setting) + else: + data = get_object_feed(args.source) + if type(data) == feedparser.FeedParserDict: + add_feed_to_file(to_json(data, **print_setting)) + beautiful_print_data(data, **print_setting) + + +if __name__ == '__main__': + run() diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..2ca7105 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup, find_packages +from os.path import join, dirname + +setup( + name="rss-reader", + version='3.5', + packages=find_packages(), + author="Vlad Protosevich", + author_email="protosevic2001@gmail.com", + install_requires=['feedparser==5.2.1'], + entry_points={ + 'console_scripts': [ + 'rss-reader = rss_parser.rss_parser:run' + ] + }, +) \ No newline at end of file From 0505252ea78eb342406cebeb03ebd2533b2f0206 Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Tue, 5 Nov 2019 22:48:42 +0300 Subject: [PATCH 02/12] Add README and bug fixes --- final_task/README.md | 41 +++++- final_task/rss_reader/__init__.py | 0 final_task/rss_reader/rss_reader.py | 208 ++++++++++++++-------------- final_task/setup.py | 2 +- 4 files changed, 144 insertions(+), 107 deletions(-) create mode 100644 final_task/rss_reader/__init__.py diff --git a/final_task/README.md b/final_task/README.md index 7af281f..a7ba83a 100644 --- a/final_task/README.md +++ b/final_task/README.md @@ -1,3 +1,38 @@ -# Your readme here -Some text. -Checkout how to write this file using *markdown*. +# RSS-READER + +## What is rss-reader? + +This is small application for watching feed on your device. It shows shot information about the latest news and keeps previous news. + +## How to instal? + +1. To install the application on your device must be python 3.7 and more. +2. Download this repository on your device. +3. Open command line(Terminal) in this directory. +4. Enter next command: +``` +python setup.py sdist +cd dist +pip install feedparser +pip install rss-reader-0.0.tar.gz +``` +5. Check workability with command: +``` +rss-reader -h +``` + +## Parameters: +``` +usage: rss-reader [-h] [--version] [--json] [--verbose] [--limit LIMIT] [--date DATE] source + +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 + --date DATE Obtaining the cached news without the Internet +``` diff --git a/final_task/rss_reader/__init__.py b/final_task/rss_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index 9bafdca..fb81558 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -1,16 +1,19 @@ import argparse +from typing import Union + import feedparser import logging import json import html import sys import datetime +import os -__version__ = '3.5' -args = None -limit = 3 -links = [] -print_setting = { +__VERSION__ = '3.5' +ARGS = None +LIMIT = 3 +LINKS = [] +PRINT_SETTING = { 'feed': [ 'title' ], @@ -23,159 +26,158 @@ } -def get_object_feed(url: str): +def get_object_feed(url: str) -> Union[str, feedparser.FeedParserDict]: try: data = feedparser.parse(url) - # if data.status == 200: - if data.bozo: - return f'There is no rss feed at this url: {url}' + if data.status == 200: + if data.bozo: + return f'ERROR: There is no rss feed at this url: {url}' + else: + return data else: - return data - # else: - # return f'HTTP Status Code {data.status}' + return f'HTTP Status Code {data.status}' except Exception as exc: return f'ERROR: {exc}' -def to_json(data, **kwargs): - # convert data to JSON format +def to_json(data) -> json: + """convert data to JSON format""" result = {} - if 'feed' in kwargs.keys(): - for i in kwargs['feed']: - result[i] = data['feed'][i] - if 'entries' in kwargs.keys(): - result['items'] = [] - for i in range(int(limit)): - temp = {} - for j in kwargs['entries']: - temp[j] = data['entries'][i][j] - result['items'].append(temp) + if isinstance(data, feedparser.FeedParserDict): + if 'feed' in PRINT_SETTING.keys(): + for feed_element in PRINT_SETTING['feed']: + result[feed_element] = text_processing(data['feed'][feed_element]) + if 'entries' in PRINT_SETTING.keys(): + result['items'] = [] + for index_news in range(LIMIT): + temp = {} + for items_element in PRINT_SETTING['entries']: + temp[items_element] = text_processing(data['entries'][index_news][items_element]) + result['items'].append(temp) + result['links'] = LINKS result = json.dumps(result) - return html.unescape(result) + return result -def get_img(input_string): - # from string type 'somethingtext' returns ('link', 'something') +def get_img(input_string: str) -> [str, str]: + """from string type 'somethingtext' returns ['link', 'something']""" input_string = input_string[input_string.find('') < 2: string = string[string.find('>') + 2:] string = string[string.find('>') + 1:string.find('<')] - return key[0].upper() + key[1:] + ':' + image + html.unescape(string) + return image + html.unescape(string) def set_start_setting(): - # setup start settings - global args, limit + """setup start settings""" + global ARGS, LIMIT parser = argparse.ArgumentParser() parser.add_argument("source", help="RSS URL") parser.add_argument("--version", help="Print version info", action="store_true") parser.add_argument("--json", help="Print result as JSON in stdout", action="store_true") parser.add_argument("--verbose", help="Outputs verbose status messages", action="store_true") - parser.add_argument("--limit", help="Limit news topics if this parameter provided") + parser.add_argument("--limit", help="Limit news topics if this parameter provided", type=int) parser.add_argument("--date", help="Obtaining the cached news without the Internet") - args = parser.parse_args() - if args.limit: - limit = args.limit - if args.verbose: + ARGS = parser.parse_args() + if ARGS.limit: + LIMIT = ARGS.limit + if ARGS.verbose: logging.basicConfig(stream=sys.stdout, level=logging.INFO) else: logging.basicConfig(filename="sample.log", level=logging.INFO) -def beautiful_print_data(data, **kwargs): - logging.info(type(data)) - if type(data) == feedparser.FeedParserDict: - if args.json: - data = to_json(data, **kwargs) - print(data) - else: - print() - for i in kwargs['feed']: - print(data['feed'][i]) - print() - k = 1 - for i in range(int(limit)): - for j in kwargs['entries']: - print(text_for_print(j, data['entries'][i][j])) - print() - print() - print('Links:') - for i, v in enumerate(links): - print(f'[{i + 1}]{v}') - print() - elif type(data) == dict: - print() - for i in kwargs['feed']: - print(data[i]) - print() - k = 1 - for i in range(int(limit)): - for j in kwargs['entries']: - print(text_for_print(j, data['items'][i][j])) - print() - print() - print('Links:') - for i, v in enumerate(links): - print(f'[{i + 1}]{v}') - print() - elif type(data) == str: - print(data) - - -def add_feed_to_file(json_data): +def add_feed_to_file(json_data: json): date_str = datetime.datetime.now().date().strftime('%Y%m%d') - try: + lines = [] + if os.path.exists('./cache.txt'): with open('cache.txt', 'r') as fin: lines = fin.readlines() - except FileNotFoundError: - lines = [] + flag = True with open('cache.txt', 'w') as file: if not lines: - file.write(datetime.datetime.now().date().strftime('%Y%m%d') + ' ' + str(json_data) + '\n') + file.write(date_str + ' ' + str(json_data) + '\n') + flag = False for line in lines: if date_str in line: - file.write(datetime.datetime.now().date().strftime('%Y%m%d') + ' ' + str(json_data) + '\n') + file.write(date_str + ' ' + str(json_data) + '\n') + flag = False else: file.write(line) + if flag: + file.write(date_str + ' ' + str(json_data) + '\n') - -def read_feed_form_file(date_str): +def read_feed_form_file(date_str: str): with open('cache.txt', 'r') as file: - for i in file: - if date_str in i: - return json.loads(i[i.find(date_str) + len(date_str) + 1:].encode('UTF-8')) + for line in file: + if date_str in line: + return json.loads(line[line.find(date_str) + len(date_str) + 1:].encode('UTF-8')) else: return 'Date ' + date_str + ' not found in cache.' - return None + + +def get_string_with_result(data: json) -> str: + """Converts json to string for print""" + result = '' + if isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, str): + result += '\n' + value + '\n\n' + continue + if isinstance(value, list) and key != 'items': + result += '\n' + edit_key(key) + '\n' + for item in value: + if isinstance(item, dict): + for key_l, value_l in item.items(): + result += edit_key(key_l) + value_l + result += '\n' + else: + result += item + result += '\n' + else: + result = data + return result + + +def edit_key(input_key: str) -> str: + if input_key == 'published': + input_key = 'Date' + elif input_key == 'summary': + input_key = 'Description' + return input_key[0].upper() + input_key[1:] + ': ' def run(): set_start_setting() - if args.version: - print(f'RSS reader version {__version__}') - elif args.date: - beautiful_print_data(read_feed_form_file(args.date), **print_setting) + if ARGS.version: + print(f'RSS reader version {__VERSION__}') + elif ARGS.date: + print(get_string_with_result(read_feed_form_file(ARGS.date))) else: - data = get_object_feed(args.source) - if type(data) == feedparser.FeedParserDict: - add_feed_to_file(to_json(data, **print_setting)) - beautiful_print_data(data, **print_setting) + data = get_object_feed(ARGS.source) + data = to_json(data) + add_feed_to_file(data) + if ARGS.json: + print(json.loads(data)) + else: + print(get_string_with_result(json.loads(data))) if __name__ == '__main__': - run() + run() \ No newline at end of file diff --git a/final_task/setup.py b/final_task/setup.py index 2ca7105..f13d3ea 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -10,7 +10,7 @@ install_requires=['feedparser==5.2.1'], entry_points={ 'console_scripts': [ - 'rss-reader = rss_parser.rss_parser:run' + 'rss-reader = rss_reader.rss_reader:run' ] }, ) \ No newline at end of file From 8cfbcc92e8b6959e61ba3f699f110a680f9d8e0b Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Fri, 8 Nov 2019 01:06:13 +0300 Subject: [PATCH 03/12] splitting into files and fixing bugs --- final_task/README.md | 2 +- final_task/__init__.py | 0 final_task/rss_reader/__init__.py | 1 + final_task/rss_reader/decorators.py | 11 ++ final_task/rss_reader/requirements.txt | 1 + final_task/rss_reader/rss_reader.py | 190 ++++++------------------ final_task/rss_reader/work_with_file.py | 36 +++++ final_task/rss_reader/work_with_json.py | 47 ++++++ final_task/rss_reader/work_with_text.py | 58 ++++++++ final_task/run_packages.py | 3 + final_task/setup.py | 5 +- 11 files changed, 206 insertions(+), 148 deletions(-) delete mode 100644 final_task/__init__.py create mode 100644 final_task/rss_reader/decorators.py create mode 100644 final_task/rss_reader/work_with_file.py create mode 100644 final_task/rss_reader/work_with_json.py create mode 100644 final_task/rss_reader/work_with_text.py create mode 100644 final_task/run_packages.py diff --git a/final_task/README.md b/final_task/README.md index a7ba83a..d71b1aa 100644 --- a/final_task/README.md +++ b/final_task/README.md @@ -14,7 +14,7 @@ This is small application for watching feed on your device. It shows shot inform python setup.py sdist cd dist pip install feedparser -pip install rss-reader-0.0.tar.gz +pip install rss-reader-3.6.tar.gz ``` 5. Check workability with command: ``` diff --git a/final_task/__init__.py b/final_task/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/final_task/rss_reader/__init__.py b/final_task/rss_reader/__init__.py index e69de29..d149c27 100644 --- a/final_task/rss_reader/__init__.py +++ b/final_task/rss_reader/__init__.py @@ -0,0 +1 @@ +__version__ = '3.6' \ No newline at end of file diff --git a/final_task/rss_reader/decorators.py b/final_task/rss_reader/decorators.py new file mode 100644 index 0000000..a8a0501 --- /dev/null +++ b/final_task/rss_reader/decorators.py @@ -0,0 +1,11 @@ +import logging + + +def functions_log(f): + def wrapper(*args, **kwargs): + logging.info(f'start function: {f.__name__}') + result = f(*args, **kwargs) + logging.info(f'end function: {f.__name__}') + return result + + return wrapper diff --git a/final_task/rss_reader/requirements.txt b/final_task/rss_reader/requirements.txt index e69de29..d579724 100644 --- a/final_task/rss_reader/requirements.txt +++ b/final_task/rss_reader/requirements.txt @@ -0,0 +1 @@ +feedparser==5.2.1 \ No newline at end of file diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index fb81558..c402099 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -1,31 +1,19 @@ import argparse from typing import Union - import feedparser import logging import json -import html import sys -import datetime -import os - -__VERSION__ = '3.5' -ARGS = None -LIMIT = 3 -LINKS = [] -PRINT_SETTING = { - 'feed': [ - 'title' - ], - 'entries': [ - 'title', - 'published', - 'link', - 'summary' - ] -} +from rss_reader.work_with_file import read_feed_form_file +from rss_reader.work_with_file import add_feed_to_file +from rss_reader.work_with_text import get_string_with_result +from rss_reader.work_with_json import to_json +from rss_reader.work_with_json import limited_json +from rss_reader.decorators import functions_log +from rss_reader import __version__ +@functions_log def get_object_feed(url: str) -> Union[str, feedparser.FeedParserDict]: try: data = feedparser.parse(url) @@ -36,148 +24,60 @@ def get_object_feed(url: str) -> Union[str, feedparser.FeedParserDict]: return data else: return f'HTTP Status Code {data.status}' + except AttributeError: + return f'ERROR: {url} - is not url(example url "https://google.com")' except Exception as exc: return f'ERROR: {exc}' -def to_json(data) -> json: - """convert data to JSON format""" - result = {} - if isinstance(data, feedparser.FeedParserDict): - if 'feed' in PRINT_SETTING.keys(): - for feed_element in PRINT_SETTING['feed']: - result[feed_element] = text_processing(data['feed'][feed_element]) - if 'entries' in PRINT_SETTING.keys(): - result['items'] = [] - for index_news in range(LIMIT): - temp = {} - for items_element in PRINT_SETTING['entries']: - temp[items_element] = text_processing(data['entries'][index_news][items_element]) - result['items'].append(temp) - result['links'] = LINKS - result = json.dumps(result) - return result - - -def get_img(input_string: str) -> [str, str]: - """from string type 'somethingtext' returns ['link', 'something']""" - input_string = input_string[input_string.find('') < 2: - string = string[string.find('>') + 2:] - string = string[string.find('>') + 1:string.find('<')] - return image + html.unescape(string) - - def set_start_setting(): """setup start settings""" - global ARGS, LIMIT parser = argparse.ArgumentParser() - parser.add_argument("source", help="RSS URL") + parser.add_argument("source", help="RSS URL", nargs='?', default='', type=str) parser.add_argument("--version", help="Print version info", action="store_true") parser.add_argument("--json", help="Print result as JSON in stdout", action="store_true") parser.add_argument("--verbose", help="Outputs verbose status messages", action="store_true") parser.add_argument("--limit", help="Limit news topics if this parameter provided", type=int) - parser.add_argument("--date", help="Obtaining the cached news without the Internet") - ARGS = parser.parse_args() - if ARGS.limit: - LIMIT = ARGS.limit - if ARGS.verbose: - logging.basicConfig(stream=sys.stdout, level=logging.INFO) + parser.add_argument("--date", help="Obtaining the cached news without the Internet", type=str) + args = parser.parse_args() + if not args.limit: + args.limit = 1000 + if args.verbose: + logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%I:%M:%S %p', + stream=sys.stdout, level=logging.DEBUG) else: - logging.basicConfig(filename="sample.log", level=logging.INFO) - - -def add_feed_to_file(json_data: json): - date_str = datetime.datetime.now().date().strftime('%Y%m%d') - lines = [] - if os.path.exists('./cache.txt'): - with open('cache.txt', 'r') as fin: - lines = fin.readlines() - flag = True - with open('cache.txt', 'w') as file: - if not lines: - file.write(date_str + ' ' + str(json_data) + '\n') - flag = False - for line in lines: - if date_str in line: - file.write(date_str + ' ' + str(json_data) + '\n') - flag = False - else: - file.write(line) - if flag: - file.write(date_str + ' ' + str(json_data) + '\n') - -def read_feed_form_file(date_str: str): - with open('cache.txt', 'r') as file: - for line in file: - if date_str in line: - return json.loads(line[line.find(date_str) + len(date_str) + 1:].encode('UTF-8')) - else: - return 'Date ' + date_str + ' not found in cache.' - - -def get_string_with_result(data: json) -> str: - """Converts json to string for print""" - result = '' - if isinstance(data, dict): - for key, value in data.items(): - if isinstance(value, str): - result += '\n' + value + '\n\n' - continue - if isinstance(value, list) and key != 'items': - result += '\n' + edit_key(key) + '\n' - for item in value: - if isinstance(item, dict): - for key_l, value_l in item.items(): - result += edit_key(key_l) + value_l - result += '\n' - else: - result += item - result += '\n' - else: - result = data - return result - - -def edit_key(input_key: str) -> str: - if input_key == 'published': - input_key = 'Date' - elif input_key == 'summary': - input_key = 'Description' - return input_key[0].upper() + input_key[1:] + ': ' + logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%I:%M:%S %p', + filename="sample.log", level=logging.DEBUG) + return args def run(): - set_start_setting() - if ARGS.version: - print(f'RSS reader version {__VERSION__}') - elif ARGS.date: - print(get_string_with_result(read_feed_form_file(ARGS.date))) - else: - data = get_object_feed(ARGS.source) + args = set_start_setting() + logging.info('the application is running') + logging.debug('args: ' + str(args)) + if args.version: + print(f'RSS reader version {__version__}') + elif args.date: + data = read_feed_form_file(args.date) + if args.json: + data = limited_json(data, args.limit) + print(json.dumps(data, ensure_ascii=False)) + else: + print(get_string_with_result(data, args.limit)) + elif args.source: + data = get_object_feed(args.source) data = to_json(data) - add_feed_to_file(data) - if ARGS.json: - print(json.loads(data)) + if 'error' not in data: + add_feed_to_file(data) + if args.json: + data = limited_json(json.loads(data), args.limit) + print(json.dumps(data, ensure_ascii=False)) else: - print(get_string_with_result(json.loads(data))) + print(get_string_with_result(json.loads(data), args.limit)) + else: + print('How work with application?\nEnter in command line: rss-reader -h') + logging.info('the application is finished') if __name__ == '__main__': - run() \ No newline at end of file + run() diff --git a/final_task/rss_reader/work_with_file.py b/final_task/rss_reader/work_with_file.py new file mode 100644 index 0000000..bc987bc --- /dev/null +++ b/final_task/rss_reader/work_with_file.py @@ -0,0 +1,36 @@ +import datetime +import os +import json +from rss_reader.decorators import functions_log + + +@functions_log +def add_feed_to_file(json_data: json): + date_str = datetime.datetime.now().date().strftime('%Y%m%d') + lines = [] + if os.path.exists('./cache.txt'): + with open('cache.txt', 'r') as fin: + lines = fin.readlines() + flag = True + with open('cache.txt', 'w') as file: + if not lines: + file.write(date_str + ' ' + str(json_data) + '\n') + flag = False + for line in lines: + if date_str in line: + file.write(date_str + ' ' + str(json_data) + '\n') + flag = False + else: + file.write(line) + if flag: + file.write(date_str + ' ' + str(json_data) + '\n') + + +@functions_log +def read_feed_form_file(date_str: str): + with open('cache.txt', 'r') as file: + for line in file: + if date_str in line: + return json.loads(line[line.find(date_str) + len(date_str) + 1:].encode('UTF-8')) + else: + return 'Date ' + date_str + ' not found in cache.' diff --git a/final_task/rss_reader/work_with_json.py b/final_task/rss_reader/work_with_json.py new file mode 100644 index 0000000..b0e089c --- /dev/null +++ b/final_task/rss_reader/work_with_json.py @@ -0,0 +1,47 @@ +import json +from feedparser import FeedParserDict +from rss_reader.decorators import functions_log +from rss_reader.work_with_text import text_processing + + +@functions_log +def to_json(data) -> json: + """convert data to JSON format""" + + structure = { + 'feed': [ + 'title' + ], + 'entries': [ + 'title', + 'published', + 'link', + 'summary' + ] + } + + result = {'title': '', 'items': [], 'links': []} + if isinstance(data, FeedParserDict): + for feed_element in structure['feed']: + result[feed_element] = text_processing(data['feed'][feed_element], result['links']) + + for item in data['entries']: + temp = {} + for items_element in structure['entries']: + temp[items_element] = text_processing(item[items_element], result['links']) + result['items'].append(temp) + elif isinstance(data, str): + result['error'] = data + result = json.dumps(result) + return result + + +@functions_log +def limited_json(data: dict, limit: int) -> dict: + result = {} + if isinstance(data, dict): + result['items'] = data['items'][:limit] + result['links'] = data['links'][:limit] + else: + result['error'] = data + return result diff --git a/final_task/rss_reader/work_with_text.py b/final_task/rss_reader/work_with_text.py new file mode 100644 index 0000000..97ef505 --- /dev/null +++ b/final_task/rss_reader/work_with_text.py @@ -0,0 +1,58 @@ +import html +from rss_reader.decorators import functions_log + + +def get_img(input_string: str) -> [str, str]: + """from string type 'somethingtext' returns ['link', 'something']""" + input_string = input_string[input_string.find('') == 1: + string = string[string.find('<') + 2:] + string = string[string.find('>') + 1:string.find('<')] + return html.unescape(image + string) + + +@functions_log +def get_string_with_result(data: dict, limit: int) -> str: + """Converts json to string for print""" + result = '' + if isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, str): + result += '\n' + value + '\n\n' + continue + if isinstance(value, list) and key != 'items' and value: + result += '\n' + edit_key(key) + '\n' + for index, item in (enumerate(value) if len(value) < limit else enumerate(value[:limit])): + if isinstance(item, dict): + for key_l, value_l in item.items(): + result += edit_key(key_l) + value_l + result += '\n' + else: + result += f'[{index + 1}] - {item}' + result += '\n' + else: + result = data + return result + + +def edit_key(input_key: str) -> str: + if input_key == 'published': + input_key = 'Date' + elif input_key == 'summary': + input_key = 'Description' + return input_key[0].upper() + input_key[1:] + ': ' diff --git a/final_task/run_packages.py b/final_task/run_packages.py new file mode 100644 index 0000000..eae15d3 --- /dev/null +++ b/final_task/run_packages.py @@ -0,0 +1,3 @@ +from rss_reader import rss_reader + +rss_reader.run() diff --git a/final_task/setup.py b/final_task/setup.py index f13d3ea..e47b79b 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,10 +1,11 @@ from setuptools import setup, find_packages from os.path import join, dirname +import rss_reader setup( name="rss-reader", - version='3.5', - packages=find_packages(), + version=rss_reader.__version__, + packages=['rss_reader'], author="Vlad Protosevich", author_email="protosevic2001@gmail.com", install_requires=['feedparser==5.2.1'], From 6bf9b5d178e7619927af4cebbc23e8e402be6fc9 Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Fri, 8 Nov 2019 21:01:09 +0300 Subject: [PATCH 04/12] Add tests and bug fixes --- final_task/rss_reader/__init__.py | 2 +- final_task/rss_reader/rss_reader.py | 6 +- final_task/rss_reader/work_with_json.py | 1 + final_task/rss_reader/work_with_text.py | 4 +- final_task/setup.py | 2 +- final_task/test/test_json.py | 40 ++++++++++ final_task/test/test_text.py | 98 +++++++++++++++++++++++++ 7 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 final_task/test/test_json.py create mode 100644 final_task/test/test_text.py diff --git a/final_task/rss_reader/__init__.py b/final_task/rss_reader/__init__.py index d149c27..5a05eaa 100644 --- a/final_task/rss_reader/__init__.py +++ b/final_task/rss_reader/__init__.py @@ -1 +1 @@ -__version__ = '3.6' \ No newline at end of file +VERSION = '3.6' \ No newline at end of file diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index c402099..71c5f88 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -10,7 +10,7 @@ from rss_reader.work_with_json import to_json from rss_reader.work_with_json import limited_json from rss_reader.decorators import functions_log -from rss_reader import __version__ +from rss_reader import VERSION @functions_log @@ -41,7 +41,7 @@ def set_start_setting(): parser.add_argument("--date", help="Obtaining the cached news without the Internet", type=str) args = parser.parse_args() if not args.limit: - args.limit = 1000 + args.limit = -1 if args.verbose: logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%I:%M:%S %p', stream=sys.stdout, level=logging.DEBUG) @@ -56,7 +56,7 @@ def run(): logging.info('the application is running') logging.debug('args: ' + str(args)) if args.version: - print(f'RSS reader version {__version__}') + print(f'RSS reader version {VERSION}') elif args.date: data = read_feed_form_file(args.date) if args.json: diff --git a/final_task/rss_reader/work_with_json.py b/final_task/rss_reader/work_with_json.py index b0e089c..5f7ccc4 100644 --- a/final_task/rss_reader/work_with_json.py +++ b/final_task/rss_reader/work_with_json.py @@ -40,6 +40,7 @@ def to_json(data) -> json: def limited_json(data: dict, limit: int) -> dict: result = {} if isinstance(data, dict): + result['title'] = data['title'] result['items'] = data['items'][:limit] result['links'] = data['links'][:limit] else: diff --git a/final_task/rss_reader/work_with_text.py b/final_task/rss_reader/work_with_text.py index 97ef505..913b850 100644 --- a/final_task/rss_reader/work_with_text.py +++ b/final_task/rss_reader/work_with_text.py @@ -10,11 +10,12 @@ def get_img(input_string: str) -> [str, str]: return [link[:link.find('"')], str_img[:str_img.find('"')]] -def text_processing(string, array_links): +def text_processing(string: str, array_links: list): """processes a string for output""" image = '' if '<' not in string: return html.unescape(string) + string += '<' if 'img' in string: image_link_and_alt_text = get_img(string) array_links.append(image_link_and_alt_text[0]) @@ -51,6 +52,7 @@ def get_string_with_result(data: dict, limit: int) -> str: def edit_key(input_key: str) -> str: + """Converts a string to a beautiful for print view ('string' -> 'String: ')""" if input_key == 'published': input_key = 'Date' elif input_key == 'summary': diff --git a/final_task/setup.py b/final_task/setup.py index e47b79b..0183eda 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -4,7 +4,7 @@ setup( name="rss-reader", - version=rss_reader.__version__, + version=rss_reader.VERSION, packages=['rss_reader'], author="Vlad Protosevich", author_email="protosevic2001@gmail.com", diff --git a/final_task/test/test_json.py b/final_task/test/test_json.py new file mode 100644 index 0000000..31473eb --- /dev/null +++ b/final_task/test/test_json.py @@ -0,0 +1,40 @@ +import unittest +from rss_reader.work_with_json import limited_json + +class TestJsonFunctions(unittest.TestCase): + def test_limited_json(self): + entering_dict = { + 'title': 'titl', + 'items': [{ + 'title': 't1', + 'published': 'date1', + 'link': 'link1', + 'summary': 'des1', + }, + { + 'title': 't2', + 'published': 'date2', + 'link': 'link2', + 'summary': 'des2' + } + ], + 'links': [ + 'first', + 'second' + ] + } + output = { + 'title': 'titl', + 'items': [{ + 'title': 't1', + 'published': 'date1', + 'link': 'link1', + 'summary': 'des1', + }], + 'links': ['first']} + self.assertEqual(limited_json(entering_dict, 1), output) + self.assertEqual(limited_json(entering_dict, 2), entering_dict) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/test/test_text.py b/final_task/test/test_text.py new file mode 100644 index 0000000..8360907 --- /dev/null +++ b/final_task/test/test_text.py @@ -0,0 +1,98 @@ +import unittest +from rss_reader.work_with_text import edit_key +from rss_reader.work_with_text import get_string_with_result +from rss_reader.work_with_text import text_processing +from rss_reader.work_with_text import get_img + + +class TestTextFunctions(unittest.TestCase): + def test_get_img(self): + entering = '

Trump fumes about reports that' + \
+                ' he wanted Barr to host news conference clearing him on Ukraine callThe president is lashing out on Twitter over news first reported by the Washington Post ' + \ + 'that he wanted the attorney general to hold a press conference declaring he had broken no laws dur' + \ + 'ing the July 25 phone call in which he urged Ukraine’s new president to investigate his political ' + \ + 'rival.


' + output = ['http://l2.yimg.com/uu/api/res/1.2/fMm6_rzTShdBsnaYua7fIA--/YXBwaWQ9eXRhY2h5b247aD04Njt3PTEzMDs' + + '-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2019-11/e87f47a0-016d-11e' + + 'a-ab5f-bdce0579bae9', 'Trump fumes about reports that he wanted Barr to host news conference c' + + 'learing him on Ukraine call'] + self.assertEqual(get_img(entering), output) + + entering = 'alt' + output = ['link', 'alt'] + self.assertEqual(get_img(entering), output) + + entering = 'alt' + output = ['links', 'alt'] + self.assertNotEqual(get_img(entering), output) + + def test_text_processing(self): + links = [] + entering = 'test' + output = 'test' + self.assertEqual(text_processing(entering, links), output) + + entering = 'test_' + output = 'test' + self.assertNotEqual(text_processing(entering, links), output) + + entering = '

test' + output = 'test' + self.assertEqual(text_processing(entering, links), output) + + entering = '<>test<>' + output = 'test' + self.assertEqual(text_processing(entering, links), output) + + entering = 'texttest<>' + output = '[image 1: text][1] test' + self.assertEqual(text_processing(entering, links), output) + self.assertEqual(links, ['link']) + + def test_edit_key(self): + entering = 'key' + output = 'Key: ' + self.assertEqual(edit_key(entering), output) + + entering = 'published' + output = 'Date: ' + self.assertEqual(edit_key(entering), output) + + entering = 'summary' + output = 'Description: ' + self.assertEqual(edit_key(entering), output) + + def test_get_string_with_result(self): + entering_dict = { + 'title': 'titl', + 'items': [{ + 'title': 't1', + 'published': 'date1', + 'link': 'link1', + 'summary': 'des1', + }, + { + 'title': 't2', + 'published': 'date2', + 'link': 'link2', + 'summary': 'des2' + } + ], + 'links': [ + 'first', + 'second' + ] + } + output = '\ntitl\n\nTitle: t1\nDate: date1\nLink: link1\nDescription: des1\n\n' + \ + 'Title: t2\nDate: date2\nLink: link2\nDescription: des2\n\n' + \ + '\nLinks: \n[1] - first\n[2] - second\n' + self.assertEqual(get_string_with_result(entering_dict, 2), output) + + +if __name__ == '__main__': + unittest.main() From 5cf536a925e5e9e566e7057db665e82833c19d17 Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Fri, 8 Nov 2019 21:45:43 +0300 Subject: [PATCH 05/12] Fix ModuleNotFoundError --- final_task/rss_reader/rss_reader.py | 35 ++++++++++++++----------- final_task/rss_reader/work_with_file.py | 6 ++--- final_task/rss_reader/work_with_json.py | 12 ++++----- final_task/rss_reader/work_with_text.py | 4 +-- final_task/run_packages.py | 3 --- final_task/setup.py | 1 + final_task/test/test_json.py | 11 +++++--- final_task/test/test_text.py | 33 ++++++++++++----------- 8 files changed, 56 insertions(+), 49 deletions(-) delete mode 100644 final_task/run_packages.py diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index 71c5f88..bedac66 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -4,16 +4,19 @@ import logging import json import sys -from rss_reader.work_with_file import read_feed_form_file -from rss_reader.work_with_file import add_feed_to_file -from rss_reader.work_with_text import get_string_with_result -from rss_reader.work_with_json import to_json -from rss_reader.work_with_json import limited_json -from rss_reader.decorators import functions_log -from rss_reader import VERSION +import os +sys.path.append(os.path.abspath(os.path.dirname(__file__))) -@functions_log + +import work_with_file +import work_with_text +import work_with_json +import decorators +import __init__ + + +@decorators.functions_log def get_object_feed(url: str) -> Union[str, feedparser.FeedParserDict]: try: data = feedparser.parse(url) @@ -56,24 +59,24 @@ def run(): logging.info('the application is running') logging.debug('args: ' + str(args)) if args.version: - print(f'RSS reader version {VERSION}') + print(f'RSS reader version {__init__.VERSION}') elif args.date: - data = read_feed_form_file(args.date) + data = work_with_file.read_feed_form_file(args.date) if args.json: - data = limited_json(data, args.limit) + data = work_with_json.limited_json(data, args.limit) print(json.dumps(data, ensure_ascii=False)) else: - print(get_string_with_result(data, args.limit)) + print(work_with_text.get_string_with_result(data, args.limit)) elif args.source: data = get_object_feed(args.source) - data = to_json(data) + data = work_with_json.to_json(data) if 'error' not in data: - add_feed_to_file(data) + work_with_file.add_feed_to_file(data) if args.json: - data = limited_json(json.loads(data), args.limit) + data = work_with_json.limited_json(json.loads(data), args.limit) print(json.dumps(data, ensure_ascii=False)) else: - print(get_string_with_result(json.loads(data), args.limit)) + print(work_with_text.get_string_with_result(json.loads(data), args.limit)) else: print('How work with application?\nEnter in command line: rss-reader -h') logging.info('the application is finished') diff --git a/final_task/rss_reader/work_with_file.py b/final_task/rss_reader/work_with_file.py index bc987bc..3554041 100644 --- a/final_task/rss_reader/work_with_file.py +++ b/final_task/rss_reader/work_with_file.py @@ -1,10 +1,10 @@ import datetime import os import json -from rss_reader.decorators import functions_log +import decorators -@functions_log +@decorators.functions_log def add_feed_to_file(json_data: json): date_str = datetime.datetime.now().date().strftime('%Y%m%d') lines = [] @@ -26,7 +26,7 @@ def add_feed_to_file(json_data: json): file.write(date_str + ' ' + str(json_data) + '\n') -@functions_log +@decorators.functions_log def read_feed_form_file(date_str: str): with open('cache.txt', 'r') as file: for line in file: diff --git a/final_task/rss_reader/work_with_json.py b/final_task/rss_reader/work_with_json.py index 5f7ccc4..4bd2ff0 100644 --- a/final_task/rss_reader/work_with_json.py +++ b/final_task/rss_reader/work_with_json.py @@ -1,10 +1,10 @@ import json from feedparser import FeedParserDict -from rss_reader.decorators import functions_log -from rss_reader.work_with_text import text_processing +import decorators +import work_with_text -@functions_log +@decorators.functions_log def to_json(data) -> json: """convert data to JSON format""" @@ -23,12 +23,12 @@ def to_json(data) -> json: result = {'title': '', 'items': [], 'links': []} if isinstance(data, FeedParserDict): for feed_element in structure['feed']: - result[feed_element] = text_processing(data['feed'][feed_element], result['links']) + result[feed_element] = work_with_text.text_processing(data['feed'][feed_element], result['links']) for item in data['entries']: temp = {} for items_element in structure['entries']: - temp[items_element] = text_processing(item[items_element], result['links']) + temp[items_element] = work_with_text.text_processing(item[items_element], result['links']) result['items'].append(temp) elif isinstance(data, str): result['error'] = data @@ -36,7 +36,7 @@ def to_json(data) -> json: return result -@functions_log +@decorators.functions_log def limited_json(data: dict, limit: int) -> dict: result = {} if isinstance(data, dict): diff --git a/final_task/rss_reader/work_with_text.py b/final_task/rss_reader/work_with_text.py index 913b850..b087982 100644 --- a/final_task/rss_reader/work_with_text.py +++ b/final_task/rss_reader/work_with_text.py @@ -1,5 +1,5 @@ import html -from rss_reader.decorators import functions_log +import decorators def get_img(input_string: str) -> [str, str]: @@ -27,7 +27,7 @@ def text_processing(string: str, array_links: list): return html.unescape(image + string) -@functions_log +@decorators.functions_log def get_string_with_result(data: dict, limit: int) -> str: """Converts json to string for print""" result = '' diff --git a/final_task/run_packages.py b/final_task/run_packages.py deleted file mode 100644 index eae15d3..0000000 --- a/final_task/run_packages.py +++ /dev/null @@ -1,3 +0,0 @@ -from rss_reader import rss_reader - -rss_reader.run() diff --git a/final_task/setup.py b/final_task/setup.py index 0183eda..c004481 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -5,6 +5,7 @@ setup( name="rss-reader", version=rss_reader.VERSION, + include_package_data=True, packages=['rss_reader'], author="Vlad Protosevich", author_email="protosevic2001@gmail.com", diff --git a/final_task/test/test_json.py b/final_task/test/test_json.py index 31473eb..00f835d 100644 --- a/final_task/test/test_json.py +++ b/final_task/test/test_json.py @@ -1,5 +1,10 @@ import unittest -from rss_reader.work_with_json import limited_json +import sys +import os + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'rss_reader')) +import work_with_json + class TestJsonFunctions(unittest.TestCase): def test_limited_json(self): @@ -32,8 +37,8 @@ def test_limited_json(self): 'summary': 'des1', }], 'links': ['first']} - self.assertEqual(limited_json(entering_dict, 1), output) - self.assertEqual(limited_json(entering_dict, 2), entering_dict) + self.assertEqual(work_with_json.limited_json(entering_dict, 1), output) + self.assertEqual(work_with_json.limited_json(entering_dict, 2), entering_dict) if __name__ == '__main__': diff --git a/final_task/test/test_text.py b/final_task/test/test_text.py index 8360907..ed9eb42 100644 --- a/final_task/test/test_text.py +++ b/final_task/test/test_text.py @@ -1,8 +1,9 @@ import unittest -from rss_reader.work_with_text import edit_key -from rss_reader.work_with_text import get_string_with_result -from rss_reader.work_with_text import text_processing -from rss_reader.work_with_text import get_img +import os +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'rss_reader')) +import work_with_text class TestTextFunctions(unittest.TestCase): @@ -21,51 +22,51 @@ def test_get_img(self): '-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2019-11/e87f47a0-016d-11e' + 'a-ab5f-bdce0579bae9', 'Trump fumes about reports that he wanted Barr to host news conference c' + 'learing him on Ukraine call'] - self.assertEqual(get_img(entering), output) + self.assertEqual(work_with_text.get_img(entering), output) entering = 'alt' output = ['link', 'alt'] - self.assertEqual(get_img(entering), output) + self.assertEqual(work_with_text.get_img(entering), output) entering = 'alt' output = ['links', 'alt'] - self.assertNotEqual(get_img(entering), output) + self.assertNotEqual(work_with_text.get_img(entering), output) def test_text_processing(self): links = [] entering = 'test' output = 'test' - self.assertEqual(text_processing(entering, links), output) + self.assertEqual(work_with_text.text_processing(entering, links), output) entering = 'test_' output = 'test' - self.assertNotEqual(text_processing(entering, links), output) + self.assertNotEqual(work_with_text.text_processing(entering, links), output) entering = '

test' output = 'test' - self.assertEqual(text_processing(entering, links), output) + self.assertEqual(work_with_text.text_processing(entering, links), output) entering = '<>test<>' output = 'test' - self.assertEqual(text_processing(entering, links), output) + self.assertEqual(work_with_text.text_processing(entering, links), output) entering = 'texttest<>' output = '[image 1: text][1] test' - self.assertEqual(text_processing(entering, links), output) + self.assertEqual(work_with_text.text_processing(entering, links), output) self.assertEqual(links, ['link']) def test_edit_key(self): entering = 'key' output = 'Key: ' - self.assertEqual(edit_key(entering), output) + self.assertEqual(work_with_text.edit_key(entering), output) entering = 'published' output = 'Date: ' - self.assertEqual(edit_key(entering), output) + self.assertEqual(work_with_text.edit_key(entering), output) entering = 'summary' output = 'Description: ' - self.assertEqual(edit_key(entering), output) + self.assertEqual(work_with_text.edit_key(entering), output) def test_get_string_with_result(self): entering_dict = { @@ -91,7 +92,7 @@ def test_get_string_with_result(self): output = '\ntitl\n\nTitle: t1\nDate: date1\nLink: link1\nDescription: des1\n\n' + \ 'Title: t2\nDate: date2\nLink: link2\nDescription: des2\n\n' + \ '\nLinks: \n[1] - first\n[2] - second\n' - self.assertEqual(get_string_with_result(entering_dict, 2), output) + self.assertEqual(work_with_text.get_string_with_result(entering_dict, 2), output) if __name__ == '__main__': From f53c82961767c114cc5450ced8f3ecdae5c77107 Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Fri, 15 Nov 2019 20:04:28 +0300 Subject: [PATCH 06/12] 4 Iteration --- final_task/MANIFEST.in | 1 + final_task/README.md | 58 +++++++++++++++--- final_task/rss_reader/18223.ttf | Bin 0 -> 119796 bytes final_task/rss_reader/__init__.py | 2 +- final_task/rss_reader/rss_reader.py | 39 ++++++++---- .../{work_with_json.py => work_with_dict.py} | 5 +- final_task/rss_reader/work_with_file.py | 21 ++++--- final_task/rss_reader/work_with_html.py | 30 +++++++++ final_task/rss_reader/work_with_pdf.py | 55 +++++++++++++++++ final_task/rss_reader/work_with_text.py | 2 +- final_task/setup.py | 5 +- 11 files changed, 180 insertions(+), 38 deletions(-) create mode 100644 final_task/MANIFEST.in create mode 100644 final_task/rss_reader/18223.ttf rename final_task/rss_reader/{work_with_json.py => work_with_dict.py} (91%) create mode 100644 final_task/rss_reader/work_with_html.py create mode 100644 final_task/rss_reader/work_with_pdf.py diff --git a/final_task/MANIFEST.in b/final_task/MANIFEST.in new file mode 100644 index 0000000..f52d324 --- /dev/null +++ b/final_task/MANIFEST.in @@ -0,0 +1 @@ +include rss_reader\18223.ttf \ No newline at end of file diff --git a/final_task/README.md b/final_task/README.md index d71b1aa..a663a1c 100644 --- a/final_task/README.md +++ b/final_task/README.md @@ -14,7 +14,9 @@ This is small application for watching feed on your device. It shows shot inform python setup.py sdist cd dist pip install feedparser -pip install rss-reader-3.6.tar.gz +pip install rss-reader-4.0.tar.gz +pip install requests +pip install fpdf ``` 5. Check workability with command: ``` @@ -23,16 +25,54 @@ rss-reader -h ## Parameters: ``` -usage: rss-reader [-h] [--version] [--json] [--verbose] [--limit LIMIT] [--date DATE] source +rss-reader -h +usage: rss-reader [-h] [--version] [--json] [--verbose] [--limit LIMIT] [--date DATE] [--to-html TO_HTML] [--to-pdf TO_PDF] [source] positional arguments: - source RSS URL + 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 - --date DATE Obtaining the cached news without the Internet + -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 + --date DATE Obtaining the cached news without the Internet + --to-html TO_HTML The argument gets the path where the HTML news will be saved + --to-pdf TO_PDF The argument gets the path where the PDF news will be saved + ``` + +##JSON format + +``` +{ + "title": "Yahoo News - Latest News & Headlines", + "items": [ + { + "title": "Sorry, Hillary: Democrats don't need a savior", + "published": "Wed, 13 Nov 2019 14:42:53 -0500", + "link": "https://news.yahoo.com/sorry-hillary-democrats-dont-need-a-savior-194253123.html", + "summary": "[image 1: Sorry, Hillary: Democrats don't need a savior][1] With the Iowa caucuses fast approaching, Hillary Clinton is just the latest in the colorful cast of characters who seem to have surveyed the sprawling Democratic field, sensed something lacking and decided that “something” might be them." + }, + { + "title": "Immigration officer blows whistle on 'morally objectionable' Trump asylum policy", + "published": "Wed, 13 Nov 2019 12:09:02 -0500", + "link": "https://news.yahoo.com/immigration-officer-blows-whistle-on-morally-objectionable-trump-asylum-policy-170902774.html", + "summary": "[image 2: Immigration officer blows whistle on 'morally objectionable' Trump asylum policy][2] A new anonymous whistleblower has accused the Trump administration of requiring U.S. asylum officers to enforce an illegal and immoral policy “clearly designed to further this administration's racist agenda of keeping Hispanic and Latino populations from entering the United States.”" + } + ], + "links": [ + "http://l.yimg.com/uu/api/res/1.2/xq3Ser6KXPfV6aeoxbq9Uw--/YXBwaWQ9eXRhY2h5b247aD04Njt3PTEzMDs-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2019-11/14586fd0-064d-11ea-b7df-7288f8d8c1a7", + "http://l.yimg.com/uu/api/res/1.2/yNVwYmqKaLb3EZLc7wAnTw--/YXBwaWQ9eXRhY2h5b247aD04Njt3PTEzMDs-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2019-11/173727c0-0637-11ea-afa6-2c8c926c8f7d" + ] +} +``` + +* "title" - source name\ +* "items" - list with dictionary which contains one news information\ + * "title" - headline news + * "published" - date publication + * "link" - link + * "summary" - description +* "links" - this is a list with links to the image of the I-th news \ No newline at end of file diff --git a/final_task/rss_reader/18223.ttf b/final_task/rss_reader/18223.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3124ba09ccdfe38f9e8d46b5da2622c662191690 GIT binary patch literal 119796 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 json: +def to_dict(data) -> dict: """convert data to JSON format""" structure = { @@ -32,12 +32,11 @@ def to_json(data) -> json: result['items'].append(temp) elif isinstance(data, str): result['error'] = data - result = json.dumps(result) return result @decorators.functions_log -def limited_json(data: dict, limit: int) -> dict: +def limited_dict(data: dict, limit: int) -> dict: result = {} if isinstance(data, dict): result['title'] = data['title'] diff --git a/final_task/rss_reader/work_with_file.py b/final_task/rss_reader/work_with_file.py index 3554041..ef19b7d 100644 --- a/final_task/rss_reader/work_with_file.py +++ b/final_task/rss_reader/work_with_file.py @@ -5,10 +5,11 @@ @decorators.functions_log -def add_feed_to_file(json_data: json): +def add_feed_to_file(dict_data: dict): + json_data = json.dumps(dict_data) date_str = datetime.datetime.now().date().strftime('%Y%m%d') lines = [] - if os.path.exists('./cache.txt'): + if os.path.exists('cache.txt'): with open('cache.txt', 'r') as fin: lines = fin.readlines() flag = True @@ -28,9 +29,13 @@ def add_feed_to_file(json_data: json): @decorators.functions_log def read_feed_form_file(date_str: str): - with open('cache.txt', 'r') as file: - for line in file: - if date_str in line: - return json.loads(line[line.find(date_str) + len(date_str) + 1:].encode('UTF-8')) - else: - return 'Date ' + date_str + ' not found in cache.' + if os.path.exists('cache.txt'): + with open('cache.txt', 'r') as file: + for line in file: + if date_str in line: + return json.loads(line[line.find(date_str) + len(date_str) + 1:].encode('UTF-8')) + else: + return 'Date ' + date_str + ' not found in cache.' + else: + return 'Cache is empty. Please launch the app from the URL to the news site.\n' + \ + 'EXAMPLE: rss-reader https://news.yahoo.com/rss' diff --git a/final_task/rss_reader/work_with_html.py b/final_task/rss_reader/work_with_html.py new file mode 100644 index 0000000..5a93dab --- /dev/null +++ b/final_task/rss_reader/work_with_html.py @@ -0,0 +1,30 @@ +import os +import datetime + + +def write_to_html_file(data: dict, path: str): + if os.path.isdir(path): + date_str = datetime.datetime.now().date().strftime('%Y%m%d') + filename = path + os.path.sep + date_str + '_' + data['title'][:data['title'].find(' ')].replace(':', '') + '.html' + with open(filename, 'w', encoding="utf-8") as file: + file.write(text_processing_for_html(data)) + return f'the recording has been completed in the file:\n{filename}' + else: + return f'{path} is not found' + +def text_processing_for_html(data: dict): + style = '' + result = '' + data['title'] + '' + style + '

' \ + + data['title'] + '


' + for index_news, dict_news in enumerate(data['items']): + result += '

' + dict_news['title'] + '

' + result += dict_news['published'] + '
' + result += '' + \
+                  dict_news['summary'][dict_news['summary'].find(': ') + 1:dict_news['summary'].find(']')] + '
' + result += dict_news['summary'][dict_news['summary'].rfind(']') + 1:] + result += '

' + return result + + diff --git a/final_task/rss_reader/work_with_pdf.py b/final_task/rss_reader/work_with_pdf.py new file mode 100644 index 0000000..2b4d776 --- /dev/null +++ b/final_task/rss_reader/work_with_pdf.py @@ -0,0 +1,55 @@ +import os +import datetime +from fpdf import FPDF +import requests + + +def write_to_pdf_file(data: dict, path: str): + if os.path.isdir(path): + date_str = datetime.datetime.now().date().strftime('%Y%m%d') + filename = path + os.path.sep + date_str + '_' + data['title'][:data['title'].find(' ')].replace(':', '').replace('.', '') + '.pdf' + pdf = FPDF() + effective_page_width = pdf.w - 2 * pdf.l_margin + pdf.compress = False + pdf.add_page() + pdf.add_font("my", '', '18223.ttf', uni=True) + pdf.set_font("my", size=30) + pdf.cell(w=0, txt=data['title']) + pdf.set_font("my", size=20) + pdf.ln(30) + pdf.set_line_width(1) + pdf.set_draw_color(255, 0, 0) + for index_news, news_dict in enumerate(data['items']): + pdf.line(20, pdf.get_y() - 10, effective_page_width, pdf.get_y() - 10) + pdf.multi_cell(effective_page_width, 10, news_dict['title']) + if news_dict['summary'][0] == '[' and data['links'][index_news]: + p = requests.get(data['links'][index_news]) + if p.status_code == 200 and data['links'][index_news].find('.gif') == -1: + out = open('img' + str(index_news) + '.jpg', "wb") + out.write(p.content) + out.close() + try: + pdf.image('img' + str(index_news) + '.jpg') + except RuntimeError: + image_str = news_dict['summary'][ + news_dict['summary'].find(':') + 1:news_dict['summary'].find(']')] + pdf.multi_cell(effective_page_width, 10, image_str) + os.remove('img' + str(index_news) + '.jpg') + else: + image_str = news_dict['summary'][news_dict['summary'].find(':') + 1:news_dict['summary'].find(']')] + pdf.multi_cell(effective_page_width, 10, image_str) + pdf.multi_cell(effective_page_width, 10, news_dict['published']) + pdf.multi_cell(effective_page_width, 10, news_dict['summary'][news_dict['summary'].rfind(']') + 2:]) + pdf.ln(40) + try: + pdf.output(filename, 'F') + except PermissionError: + return f'close file:\n{filename}' + + + return f'the recording has been completed in the file:\n{filename}' + else: + return f'{path} is not found' + + + diff --git a/final_task/rss_reader/work_with_text.py b/final_task/rss_reader/work_with_text.py index b087982..c6b7fcd 100644 --- a/final_task/rss_reader/work_with_text.py +++ b/final_task/rss_reader/work_with_text.py @@ -28,7 +28,7 @@ def text_processing(string: str, array_links: list): @decorators.functions_log -def get_string_with_result(data: dict, limit: int) -> str: +def get_string_with_result(data: dict, limit=-1) -> str: """Converts json to string for print""" result = '' if isinstance(data, dict): diff --git a/final_task/setup.py b/final_task/setup.py index c004481..9e7578c 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,12 +1,10 @@ from setuptools import setup, find_packages -from os.path import join, dirname import rss_reader setup( name="rss-reader", version=rss_reader.VERSION, - include_package_data=True, - packages=['rss_reader'], + packages=find_packages(), author="Vlad Protosevich", author_email="protosevic2001@gmail.com", install_requires=['feedparser==5.2.1'], @@ -15,4 +13,5 @@ 'rss-reader = rss_reader.rss_reader:run' ] }, + include_package_data=True, ) \ No newline at end of file From 5f560f17dfc8375836eb313b7960892f11a3e577 Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Sat, 16 Nov 2019 01:09:10 +0300 Subject: [PATCH 07/12] Iteration 5 --- final_task/README.md | 7 +-- final_task/rss_reader/__init__.py | 2 +- final_task/rss_reader/rss_reader.py | 4 ++ final_task/rss_reader/work_with_colorize.py | 22 ++++++++++ final_task/rss_reader/work_with_text.py | 48 +++++++++++++-------- final_task/setup.py | 2 +- 6 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 final_task/rss_reader/work_with_colorize.py diff --git a/final_task/README.md b/final_task/README.md index a663a1c..fd19532 100644 --- a/final_task/README.md +++ b/final_task/README.md @@ -4,7 +4,7 @@ This is small application for watching feed on your device. It shows shot information about the latest news and keeps previous news. -## How to instal? +## How to install? 1. To install the application on your device must be python 3.7 and more. 2. Download this repository on your device. @@ -14,9 +14,10 @@ This is small application for watching feed on your device. It shows shot inform python setup.py sdist cd dist pip install feedparser -pip install rss-reader-4.0.tar.gz +pip install rss-reader-5.0.tar.gz pip install requests pip install fpdf +pip install colored ``` 5. Check workability with command: ``` @@ -40,7 +41,7 @@ optional arguments: --date DATE Obtaining the cached news without the Internet --to-html TO_HTML The argument gets the path where the HTML news will be saved --to-pdf TO_PDF The argument gets the path where the PDF news will be saved - + --colorize Colorize text ``` ##JSON format diff --git a/final_task/rss_reader/__init__.py b/final_task/rss_reader/__init__.py index a2da3e8..0c8f0af 100644 --- a/final_task/rss_reader/__init__.py +++ b/final_task/rss_reader/__init__.py @@ -1 +1 @@ -VERSION = '4.0' \ No newline at end of file +VERSION = '5.0' \ No newline at end of file diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index 9ff79e6..a5bedb1 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -14,6 +14,7 @@ import work_with_dict import work_with_html import work_with_pdf +import work_with_colorize import decorators import __init__ @@ -46,6 +47,7 @@ def set_start_setting(): parser.add_argument("--date", help="Obtaining the cached news without the Internet", type=str) parser.add_argument("--to-html", help="The argument gets the path where the HTML news will be saved", type=str) parser.add_argument("--to-pdf", help="The argument gets the path where the PDF news will be saved", type=str) + parser.add_argument("--colorize", help="Colorize text", action="store_true") args = parser.parse_args() if not args.limit: args.limit = -1 @@ -90,6 +92,8 @@ def run(): work_with_html.write_to_html_file(data, args.to_html) elif args.to_pdf: work_with_pdf.write_to_pdf_file(data, args.to_pdf) + elif args.colorize: + work_with_colorize.colorize_text(data) else: print(work_with_text.get_string_with_result(data, args.limit)) logging.info('the application is finished') diff --git a/final_task/rss_reader/work_with_colorize.py b/final_task/rss_reader/work_with_colorize.py new file mode 100644 index 0000000..66c81c5 --- /dev/null +++ b/final_task/rss_reader/work_with_colorize.py @@ -0,0 +1,22 @@ +import colored + +def colorize_text(data: dict): + yellow = colored.fg(11) + red = colored.fg(9) + green = colored.fg(82) + pink = colored.fg(200) + blue = colored.fg(20) + description_color = colored.fg(14) + default = colored.fg(230) + print() + if 'error' in data.keys(): + print(('{0} ' + data['error'] + ' {1}').format(red, default)) + print(('{0} ' + data['title'] + '\n').format(yellow)) + for index_news, dict_news in enumerate(data['items']): + print(('{0}Title: {1}' + dict_news['title']).format(green, red)) + print(('{0}Date: {1}' + dict_news['published']).format(green, pink)) + print(('{0}Link: {1}' + dict_news['link']).format(green, blue)) + print(('{0}Description: {1}' + dict_news['summary'] + '{2}\n').format(green, description_color, default)) + print('{0}Links:'.format(colored.fg(89))) + for index_links, link in enumerate(data['links']): + print(('{0}[' + str(index_links+1) + '] - {1}' + link + '{2}').format(green, blue, default)) diff --git a/final_task/rss_reader/work_with_text.py b/final_task/rss_reader/work_with_text.py index c6b7fcd..c14f5df 100644 --- a/final_task/rss_reader/work_with_text.py +++ b/final_task/rss_reader/work_with_text.py @@ -30,24 +30,36 @@ def text_processing(string: str, array_links: list): @decorators.functions_log def get_string_with_result(data: dict, limit=-1) -> str: """Converts json to string for print""" - result = '' - if isinstance(data, dict): - for key, value in data.items(): - if isinstance(value, str): - result += '\n' + value + '\n\n' - continue - if isinstance(value, list) and key != 'items' and value: - result += '\n' + edit_key(key) + '\n' - for index, item in (enumerate(value) if len(value) < limit else enumerate(value[:limit])): - if isinstance(item, dict): - for key_l, value_l in item.items(): - result += edit_key(key_l) + value_l - result += '\n' - else: - result += f'[{index + 1}] - {item}' - result += '\n' - else: - result = data + result = '\n' + if 'error' in data.keys(): + return result + data['error'] + result += data['title'] + '\n\n' + for index_news, dict_news in enumerate(data['items']): + result += 'Title: ' + dict_news['title'] + '\n' + result += 'Date: ' + dict_news['published'] + '\n' + result += 'Link: ' + dict_news['link'] + '\n' + result += 'Description: ' + dict_news['summary'] + '\n' + result += '\n' + result += '\nLinks:\n' + for index_links, link in enumerate(data['links']): + result += '[' + str(index_links+1) + '] - ' + link + '\n' + # if isinstance(data, dict): + # for key, value in data.items(): + # if isinstance(value, str): + # result += '\n' + value + '\n\n' + # continue + # if isinstance(value, list) and key != 'items' and value: + # result += '\n' + edit_key(key) + '\n' + # for index, item in (enumerate(value) if len(value) < limit else enumerate(value[:limit])): + # if isinstance(item, dict): + # for key_l, value_l in item.items(): + # result += edit_key(key_l) + value_l + # result += '\n' + # else: + # result += f'[{index + 1}] - {item}' + # result += '\n' + # else: + # result = data return result diff --git a/final_task/setup.py b/final_task/setup.py index 9e7578c..e7c9696 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -7,7 +7,7 @@ packages=find_packages(), author="Vlad Protosevich", author_email="protosevic2001@gmail.com", - install_requires=['feedparser==5.2.1'], + install_requires=['feedparser==5.2.1', 'colored', 'fpdf', 'requests'], entry_points={ 'console_scripts': [ 'rss-reader = rss_reader.rss_reader:run' From 0b07b3aadb820f74442e69241e015cddd4314b0b Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Sun, 17 Nov 2019 19:37:02 +0300 Subject: [PATCH 08/12] Bug fix and edit logic cached --- final_task/README.md | 52 +++++++++- final_task/rss_reader/MyException.py | 3 + final_task/rss_reader/__init__.py | 2 +- final_task/rss_reader/requirements.txt | 5 +- final_task/rss_reader/rss_reader.py | 66 ++++++------- final_task/rss_reader/work_with_colorize.py | 17 ++-- final_task/rss_reader/work_with_dict.py | 15 +-- final_task/rss_reader/work_with_file.py | 75 ++++++++++----- final_task/rss_reader/work_with_html.py | 7 +- final_task/rss_reader/work_with_pdf.py | 100 ++++++++++++-------- final_task/rss_reader/work_with_text.py | 22 +---- final_task/test/test_json.py | 6 +- final_task/test/test_text.py | 2 +- 13 files changed, 224 insertions(+), 148 deletions(-) create mode 100644 final_task/rss_reader/MyException.py diff --git a/final_task/README.md b/final_task/README.md index fd19532..122cf38 100644 --- a/final_task/README.md +++ b/final_task/README.md @@ -14,10 +14,10 @@ This is small application for watching feed on your device. It shows shot inform python setup.py sdist cd dist pip install feedparser -pip install rss-reader-5.0.tar.gz pip install requests pip install fpdf pip install colored +pip install rss-reader-5.1.tar.gz ``` 5. Check workability with command: ``` @@ -76,4 +76,52 @@ optional arguments: * "published" - date publication * "link" - link * "summary" - description -* "links" - this is a list with links to the image of the I-th news \ No newline at end of file +* "links" - this is a list with links to the image of the I-th news + +##How is data keeping? + +Data keep in local file which is located in directory: +Windows:`C:\Users\User\AppData\Local\Programs\Python\Python38-32\Lib\site-packages\rss_reader` +Linux:\ +macOS: + +There is only json string in the file. JSON format: +``` +{ + "20191117" : { + "title" : "News by Sun, 17 Nov 2019", + "links" : [ + "https://img.tyt.by/thumbnails/n/buryakina/0e/5/lidiya_ermoshina_20170208_bur_tutby_phsl_-9760.jpg", + "https://img.tyt.by/thumbnails/n/buryakina/0e/5/lidiya_ermoshina_20170208_bur_tutby_phsl_-9760.jpg" + ], + "items" : [ + { + "link" : "https://news.tut.by/economics/661603.html?utm_campaign=news-feed&utm_medium=rss&utm_source=rss-news", + "published" : "Sun, 17 Nov 2019 18:03:00 +0300", + "summary" : "[image 1: Фото: Дарья Бурякина, TUT.BY][1] В воскресенье утром Лукашенко сказал, что ему известны 6 случаев провокаций во время кампании и выборов, и заявил, что что со всеми жестко разберутся.", + "title" : "Глава ЦИК об инцидентах в избиркомах: Думаю, эти граждане себя точно так же ведут на кухне с женой" + }, + { + "link" : "https://news.tut.by/economics/661603.html?utm_campaign=news-feed&utm_medium=rss&utm_source=rss-news", + "published" : "Sun, 17 Nov 2019 18:03:00 +0300", + "summary" : "[image 1: Фото: Дарья Бурякина, TUT.BY][1] В воскресенье утром Лукашенко сказал, что ему известны 6 случаев провокаций во время кампании и выборов, и заявил, что что со всеми жестко разберутся.", + "title" : "Глава ЦИК об инцидентах в избиркомах: Думаю, эти граждане себя точно так же ведут на кухне с женой" + } + ] + }, + "20191116" : { + "title" : "News by Sat, 16 Nov 2019", + "links" : [ + "https://img.tyt.by/thumbnails/n/buryakina/08/6/dinamo-torpedo_20191026_bur_tutby_phsl-2466.jpg", + ], + "items" : [ + { + "link" : "https://sport.tut.by/news/hockey/661569.html?utm_campaign=news-feed&utm_medium=rss&utm_source=rss-news", + "published" : "Sat, 16 Nov 2019 23:02:00 +0300", + "summary" : "[image 62: Фото: Дарья Бурякина, TUT.BY][62] Главный тренер омского «Авангарда» Боб Хартли недоволен действиями защитника минского «Динамо» Романа Дюкова. Напомним, что после столкновения с Андреем Стасем Дюков долго не мог подняться, после чего судьи удалили игрока «Авангарда» до конца матча.", + "title" : "«Актеры должны быть в Голливуде, а не на льду!» Тренер «Авангарда» считает хоккеиста «Динамо» симулянтом" + }, + ] + } +} +``` \ No newline at end of file diff --git a/final_task/rss_reader/MyException.py b/final_task/rss_reader/MyException.py new file mode 100644 index 0000000..23fe9a0 --- /dev/null +++ b/final_task/rss_reader/MyException.py @@ -0,0 +1,3 @@ +class MyException(Exception): + def __init__(self, message): + self.message = message \ No newline at end of file diff --git a/final_task/rss_reader/__init__.py b/final_task/rss_reader/__init__.py index 0c8f0af..bf46bbf 100644 --- a/final_task/rss_reader/__init__.py +++ b/final_task/rss_reader/__init__.py @@ -1 +1 @@ -VERSION = '5.0' \ No newline at end of file +VERSION = '5.1' \ No newline at end of file diff --git a/final_task/rss_reader/requirements.txt b/final_task/rss_reader/requirements.txt index d579724..6788592 100644 --- a/final_task/rss_reader/requirements.txt +++ b/final_task/rss_reader/requirements.txt @@ -1 +1,4 @@ -feedparser==5.2.1 \ No newline at end of file +feedparser==5.2.1 +colored +fpdf +requests diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index a5bedb1..544cc43 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -15,6 +15,7 @@ import work_with_html import work_with_pdf import work_with_colorize +import MyException import decorators import __init__ @@ -25,15 +26,15 @@ def get_object_feed(url: str) -> Union[str, feedparser.FeedParserDict]: data = feedparser.parse(url) if data.status == 200: if data.bozo: - return f'ERROR: There is no rss feed at this url: {url}' + raise MyException.MyException(f'There is no rss feed at this url: {url}') else: return data else: - return f'HTTP Status Code {data.status}' + raise MyException.MyException(f'HTTP Status Code {data.status}') except AttributeError: - return f'ERROR: {url} - is not url(example url "https://google.com")' + raise MyException.MyException(f'{url} - is not url(example url "https://google.com")') except Exception as exc: - return f'ERROR: {exc}' + raise MyException.MyException(exc) def set_start_setting(): @@ -62,40 +63,41 @@ def set_start_setting(): def run(): os.chdir(os.path.abspath(os.path.dirname(__file__))) - print(os.getcwd()) args = set_start_setting() logging.info('the application is running') logging.debug('args: ' + str(args)) + logging.info(os.getcwd()) data = None - if args.version: - print(f'RSS reader version {__init__.VERSION}') - elif args.date: - data = work_with_file.read_feed_form_file(args.date) - elif args.source: - data = get_object_feed(args.source) - data = work_with_dict.to_dict(data) - if 'error' not in data: + try: + if args.version: + print(f'RSS reader version {__init__.VERSION}') + elif args.date: + data = work_with_file.read_feed_form_file(args.date) + elif args.source: + data = get_object_feed(args.source) + data = work_with_dict.to_dict(data) work_with_file.add_feed_to_file(data) - else: - print('How work with application?\nEnter in command line: rss-reader -h') - exit(1) + else: + raise MyException.MyException('How work with application?\nEnter in command line: rss-reader -h') - logging.debug(type(data)) - logging.debug(data) - if args.limit: - data = work_with_dict.limited_dict(data, args.limit) - if data.get('error', False): - print(data['error']) - elif args.json: - print(json.dumps(data, ensure_ascii=False, indent=4)) - elif args.to_html: - work_with_html.write_to_html_file(data, args.to_html) - elif args.to_pdf: - work_with_pdf.write_to_pdf_file(data, args.to_pdf) - elif args.colorize: - work_with_colorize.colorize_text(data) - else: - print(work_with_text.get_string_with_result(data, args.limit)) + logging.debug(type(data)) + logging.debug(data) + if args.limit: + data = work_with_dict.limited_dict(data, args.limit) + if args.json: + print(json.dumps(data, ensure_ascii=False, indent=4)) + elif args.to_html: + result = work_with_html.write_to_html_file(data, args.to_html) + elif args.to_pdf: + result = work_with_pdf.write_to_pdf_file(data, args.to_pdf) + elif args.colorize: + result = work_with_colorize.colorize_text(data) + else: + result = work_with_text.get_string_with_result(data, args.limit) + print(result) + except MyException.MyException as exc: + print('ERROR:') + print(exc) logging.info('the application is finished') diff --git a/final_task/rss_reader/work_with_colorize.py b/final_task/rss_reader/work_with_colorize.py index 66c81c5..db13127 100644 --- a/final_task/rss_reader/work_with_colorize.py +++ b/final_task/rss_reader/work_with_colorize.py @@ -1,5 +1,6 @@ import colored + def colorize_text(data: dict): yellow = colored.fg(11) red = colored.fg(9) @@ -9,14 +10,12 @@ def colorize_text(data: dict): description_color = colored.fg(14) default = colored.fg(230) print() - if 'error' in data.keys(): - print(('{0} ' + data['error'] + ' {1}').format(red, default)) - print(('{0} ' + data['title'] + '\n').format(yellow)) + print(f"{yellow} {data['title']} {default}") for index_news, dict_news in enumerate(data['items']): - print(('{0}Title: {1}' + dict_news['title']).format(green, red)) - print(('{0}Date: {1}' + dict_news['published']).format(green, pink)) - print(('{0}Link: {1}' + dict_news['link']).format(green, blue)) - print(('{0}Description: {1}' + dict_news['summary'] + '{2}\n').format(green, description_color, default)) - print('{0}Links:'.format(colored.fg(89))) + print(f"{green}Title: {red}{data['title']} ") + print(f"{green}Title: {pink}{data['published']} ") + print(f"{green}Title: {blue}{data['link']} ") + print(f"{green}Title: {description_color}{data['summary']}{default} ") + print(f'{colored.fg(89)}Links:') for index_links, link in enumerate(data['links']): - print(('{0}[' + str(index_links+1) + '] - {1}' + link + '{2}').format(green, blue, default)) + print(f'{green}[{index_links+1}] - {blue}{default}') diff --git a/final_task/rss_reader/work_with_dict.py b/final_task/rss_reader/work_with_dict.py index d9693b5..f410cf5 100644 --- a/final_task/rss_reader/work_with_dict.py +++ b/final_task/rss_reader/work_with_dict.py @@ -7,7 +7,6 @@ @decorators.functions_log def to_dict(data) -> dict: """convert data to JSON format""" - structure = { 'feed': [ 'title' @@ -30,18 +29,14 @@ def to_dict(data) -> dict: for items_element in structure['entries']: temp[items_element] = work_with_text.text_processing(item[items_element], result['links']) result['items'].append(temp) - elif isinstance(data, str): - result['error'] = data return result @decorators.functions_log def limited_dict(data: dict, limit: int) -> dict: - result = {} - if isinstance(data, dict): - result['title'] = data['title'] - result['items'] = data['items'][:limit] - result['links'] = data['links'][:limit] - else: - result['error'] = data + result = { + 'title': data['title'], + 'items': data['items'][:limit], + 'links': data['links'][:limit] + } return result diff --git a/final_task/rss_reader/work_with_file.py b/final_task/rss_reader/work_with_file.py index ef19b7d..78173c6 100644 --- a/final_task/rss_reader/work_with_file.py +++ b/final_task/rss_reader/work_with_file.py @@ -2,40 +2,65 @@ import os import json import decorators +import MyException @decorators.functions_log def add_feed_to_file(dict_data: dict): - json_data = json.dumps(dict_data) - date_str = datetime.datetime.now().date().strftime('%Y%m%d') - lines = [] if os.path.exists('cache.txt'): - with open('cache.txt', 'r') as fin: - lines = fin.readlines() - flag = True + with open('cache.txt', 'r') as read_file: + dict_with_date = json.load(read_file) + else: + dict_with_date = {} + for news_index, news_dict in enumerate(dict_data['items']): + date = get_date(news_dict['published']) + if date in dict_with_date.keys(): + links_on_news = [] + for dict_news in dict_with_date[date]['items']: + links_on_news.append(dict_news['link']) + if news_dict['link'] not in links_on_news: + dict_with_date[date]['items'].append(news_dict) + dict_with_date[date]['links'].append(dict_data['links'][news_index]) + else: + dict_with_date[date] = { + 'title': f"News by {news_dict['published'][:news_dict['published'].find(':') - 2]}", + 'items': [news_dict], + 'links': [dict_data['links'][news_index]], + } with open('cache.txt', 'w') as file: - if not lines: - file.write(date_str + ' ' + str(json_data) + '\n') - flag = False - for line in lines: - if date_str in line: - file.write(date_str + ' ' + str(json_data) + '\n') - flag = False - else: - file.write(line) - if flag: - file.write(date_str + ' ' + str(json_data) + '\n') + json.dump(dict_with_date, file) @decorators.functions_log def read_feed_form_file(date_str: str): if os.path.exists('cache.txt'): - with open('cache.txt', 'r') as file: - for line in file: - if date_str in line: - return json.loads(line[line.find(date_str) + len(date_str) + 1:].encode('UTF-8')) - else: - return 'Date ' + date_str + ' not found in cache.' + with open('cache.txt', 'r') as read_file: + dict_with_date = json.load(read_file) + if date_str in dict_with_date.keys(): + return dict_with_date[date_str] + else: + raise MyException.MyException('Date ' + date_str + ' not found in cache.') + else: - return 'Cache is empty. Please launch the app from the URL to the news site.\n' + \ - 'EXAMPLE: rss-reader https://news.yahoo.com/rss' + raise MyException.MyException('Cache is empty. Please launch the app from the URL to the news site.\n' + \ + 'EXAMPLE: rss-reader https://news.yahoo.com/rss') + + +def get_date(input_str: str) -> str: + result = input_str[input_str.find(',') + 2: input_str.find(':') - 2].strip(' ') + result = result.split(' ') + months = { + 'Jan': '01', + 'Feb': '02', + 'Mar': '03', + 'Apr': '04', + 'May': '05', + 'June': '06', + 'July': '07', + 'Aug': '08', + 'Sept': '09', + 'Oct': '10', + 'Nov': '11', + 'Dec': '12', + } + return result[2] + months[result[1]] + result[0] diff --git a/final_task/rss_reader/work_with_html.py b/final_task/rss_reader/work_with_html.py index 5a93dab..e72bb1b 100644 --- a/final_task/rss_reader/work_with_html.py +++ b/final_task/rss_reader/work_with_html.py @@ -1,16 +1,19 @@ import os import datetime +import MyException def write_to_html_file(data: dict, path: str): if os.path.isdir(path): date_str = datetime.datetime.now().date().strftime('%Y%m%d') - filename = path + os.path.sep + date_str + '_' + data['title'][:data['title'].find(' ')].replace(':', '') + '.html' + filename = os.path.join(path, date_str + '_' + + data['title'][:data['title'].find(' ')].replace(':', '').replace('.', '') + '.html') with open(filename, 'w', encoding="utf-8") as file: file.write(text_processing_for_html(data)) return f'the recording has been completed in the file:\n{filename}' else: - return f'{path} is not found' + raise MyException.MyException(f'{path} is not found') + def text_processing_for_html(data: dict): style = '' - result = '' + data['title'] + '' + style + '

' \ + result = '' + \ + '' + data['title'] + '' + style + '

' \ + data['title'] + '


' for index_news, dict_news in enumerate(data['items']): result += '

' + dict_news['title'] + '

' From a8774630181c8cdc225d66c6422f1a819cca9b10 Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Sat, 23 Nov 2019 14:16:11 +0300 Subject: [PATCH 11/12] Add tests --- final_task/README.md | 35 +++++++++++++++- final_task/rss_reader/rss_reader.py | 21 +--------- final_task/rss_reader/work_with_dict.py | 15 ++++--- final_task/rss_reader/work_with_feedparser.py | 30 ++++++++++++++ final_task/rss_reader/work_with_pdf.py | 2 +- final_task/rss_reader/work_with_text.py | 2 +- .../test/{test_json.py => test_dict.py} | 22 +++++----- final_task/test/test_html.py | 40 +++++++++++++++++++ final_task/test/test_rss.xml | 29 ++++++++++++++ final_task/test/test_rssfile_to_dict.py | 10 +++++ 10 files changed, 163 insertions(+), 43 deletions(-) create mode 100644 final_task/rss_reader/work_with_feedparser.py rename final_task/test/{test_json.py => test_dict.py} (70%) create mode 100644 final_task/test/test_html.py create mode 100644 final_task/test/test_rss.xml create mode 100644 final_task/test/test_rssfile_to_dict.py diff --git a/final_task/README.md b/final_task/README.md index c0d32a3..0cbd240 100644 --- a/final_task/README.md +++ b/final_task/README.md @@ -44,6 +44,39 @@ optional arguments: --colorize Colorize text ``` +##How using? + +Open command line(terminal) and enter command: `rss-reader https://news.yahoo.com/rss --limit 2`. +You will see something like this: + +``` +Yahoo News - Latest News & Headlines + +Title: After Sondland bombshell, Democrats look to expand Trump probe to Pompeo, others +Date: Thu, 21 Nov 2019 16:12:24 -0500 +Link: https://news.yahoo.com/after-sondland-bombshell-democrats-look-to-expand-trump-probe-to-pompeo-others-211224656.html +Description: [image: After Sondland bombshell, Democrats look to expand Trump probe to Pompeo, others] House Democrats, emboldened after Ambassador Gordon Sondland provided stunning testimony Wednesday, are debating whether to expand the investigation to other Trump administration officials. +Link on image: http://l1.yimg.com/uu/api/res/1.2/audgsHkjxGCXTuCGEgaejw--/YXBwaWQ9eXRhY2h5b247aD04Njt3PTEzMDs-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2019-11/eb56ad70-0c98-11ea-955d-14c81854d8a0 + +Title: Exclusive: Acting Homeland Security chief Chad Wolf makes unannounced visit to privately funded border wall +Date: Thu, 21 Nov 2019 17:38:03 -0500 +Link: https://news.yahoo.com/acting-homeland-security-chief-chad-wolf-makes-unannounced-visit-to-privately-funded-border-wall-223803492.html +Description: [image: Exclusive: Acting Homeland Security chief Chad Wolf makes unannounced visit to privately funded border wall] Acting Homeland Security Secretary Chad Wolf made an unpublicized visit Wednesday to the site of a privately constructed border wall during his first official trip to the southern border, Yahoo News has confirmed. +Link on image: http://l2.yimg.com/uu/api/res/1.2/QqpSY1ue.EJVLS1afFZDkg--/YXBwaWQ9eXRhY2h5b247aD04Njt3PTEzMDs-/https://media-mbst-pub-ue1.s3.amazonaws.com/creatr-uploaded-images/2019-11/4dcbf870-0cad-11ea-bb6a-0080c3ca4a87 +``` + +In this example, you entered the url from which you wanted to see the news. + +You can also view the news on a specific date with the command `rss-reader --date 20190123` `20190123` is January 23, 2019. + +You can save any kind of news(selected by date or from the site) into two types of PDF or HTML file by `--to-pdf` and `--to-html` commands with a path argument (the path can be both local and global). +Example: `rss-reader --date 20190123 --to-pdf D:\` after executing the program, a message with the path to the pdf file will be displayed. + +``` +The recording has been completed in the file: +D:\20191123_Yahoo.pdf +``` + ##JSON format ``` @@ -123,4 +156,4 @@ There is only json string in the file. JSON format: ] } } -``` \ No newline at end of file +``` diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index 86e5fda..75f7e8a 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -16,27 +16,10 @@ import work_with_pdf import work_with_colorize import RssReaderException -import decorators +import work_with_feedparser import __init__ -@decorators.functions_log -def get_object_feed(url: str) -> Union[str, feedparser.FeedParserDict]: - try: - data = feedparser.parse(url) - if data.status == 200: - if data['version']: - return data - else: - raise RssReaderException.ConnectException(f'There is no rss feed at this url: {url}') - else: - raise RssReaderException.ConnectException(f'HTTP Status Code {data.status}') - except AttributeError: - raise RssReaderException.ConnectException(f'{url} - is not url(example url "https://google.com")') - except Exception as exc: - raise RssReaderException.ConnectException(exc) - - def set_start_setting(): """setup start settings""" parser = argparse.ArgumentParser() @@ -74,7 +57,7 @@ def run(): elif args.date: data = work_with_file.read_feed_form_file(args.date) elif args.source: - data = get_object_feed(args.source) + data = work_with_feedparser.get_object_feed(args.source) data = work_with_dict.to_dict(data) work_with_file.add_feed_to_file(data) else: diff --git a/final_task/rss_reader/work_with_dict.py b/final_task/rss_reader/work_with_dict.py index 3b7b800..8fc5e2a 100644 --- a/final_task/rss_reader/work_with_dict.py +++ b/final_task/rss_reader/work_with_dict.py @@ -22,16 +22,15 @@ def to_dict(data) -> dict: result = {'title': '', 'items': []} links_on_image = [] if isinstance(data, FeedParserDict): - for feed_element in structure['feed']: - result[feed_element] = work_with_text.text_processing(data['feed'][feed_element]) + result['title'] = work_with_text.text_processing(data['feed']['title']) for item in data['entries']: - temp = {} - for items_element in structure['entries']: - if items_element == 'summary': - temp[items_element] = work_with_text.text_processing(item[items_element], links_on_image) - else: - temp[items_element] = work_with_text.text_processing(item[items_element]) + temp = { + 'title': work_with_text.text_processing(item['title']), + 'published': work_with_text.text_processing(item['published']), + 'link': work_with_text.text_processing(item['link']), + 'summary': work_with_text.text_processing(item['summary'], links_on_image) + } result['items'].append(temp) for index_link, link in enumerate(links_on_image): if link: diff --git a/final_task/rss_reader/work_with_feedparser.py b/final_task/rss_reader/work_with_feedparser.py new file mode 100644 index 0000000..a6bcb4e --- /dev/null +++ b/final_task/rss_reader/work_with_feedparser.py @@ -0,0 +1,30 @@ +import decorators +import RssReaderException +import feedparser +import os + + +@decorators.functions_log +def get_object_feed(url: str) -> feedparser.FeedParserDict: + try: + data = feedparser.parse(url) + try: + if data.status == 200: + if data['version']: + return data + else: + raise RssReaderException.ConnectException(f'There is no rss feed at this url: {url}') + else: + raise RssReaderException.ConnectException(f'HTTP Status Code {data.status}') + except AttributeError: + if os.path.isfile(url): + if data['version']: + return data + else: + raise RssReaderException.ConnectException(f'There is no rss feed at this url: {url}') + else: + raise RssReaderException.ConnectException(f'HTTP Status Code {data.status}') + except AttributeError: + raise RssReaderException.ConnectException(f'{url} - is not url(example url "https://google.com")') + except Exception as exc: + raise RssReaderException.ConnectException(exc) \ No newline at end of file diff --git a/final_task/rss_reader/work_with_pdf.py b/final_task/rss_reader/work_with_pdf.py index caf71c2..bc9e38c 100644 --- a/final_task/rss_reader/work_with_pdf.py +++ b/final_task/rss_reader/work_with_pdf.py @@ -34,7 +34,7 @@ def write_to_pdf(data: dict, filename: str): if news_dict['contain_image']: download_image_and_paste_in_pdf(pdf, news_dict, index_news) pdf.multi_cell(effective_page_width, 10, news_dict['published']) - pdf.multi_cell(effective_page_width, 10, news_dict['summary'][news_dict['summary'].rfind(']') + 2:]) + pdf.multi_cell(effective_page_width, 10, news_dict['summary'][news_dict['summary'].rfind(']') + 1:]) pdf.set_font("TimesNewRoman", size=15) pdf.ln(5) pdf.multi_cell(effective_page_width, 10, 'Link on news:\n' + news_dict['link']) diff --git a/final_task/rss_reader/work_with_text.py b/final_task/rss_reader/work_with_text.py index 096b933..c98849b 100644 --- a/final_task/rss_reader/work_with_text.py +++ b/final_task/rss_reader/work_with_text.py @@ -22,7 +22,7 @@ def text_processing(string: str, array_links=None): if 'img' in string: image_link_and_alt_text = get_img(string) array_links.append(image_link_and_alt_text[0]) - image = '[image: ' + image_link_and_alt_text[1] + ']' + image = '[image: ' + image_link_and_alt_text[1] + '] ' string = string[string.find('<') + 1:] while string.find('<') - string.find('>') == 1: string = string[string.find('<') + 2:] diff --git a/final_task/test/test_json.py b/final_task/test/test_dict.py similarity index 70% rename from final_task/test/test_json.py rename to final_task/test/test_dict.py index e54b9fe..25108b4 100644 --- a/final_task/test/test_json.py +++ b/final_task/test/test_dict.py @@ -6,26 +6,22 @@ import work_with_dict -class TestJsonFunctions(unittest.TestCase): - def test_limited_json(self): +class TestDictFunctions(unittest.TestCase): + def test_limited_dict(self): entering_dict = { 'title': 'titl', 'items': [{ - 'title': 't1', - 'published': 'date1', - 'link': 'link1', - 'summary': 'des1', - }, + 'title': 't1', + 'published': 'date1', + 'link': 'link1', + 'summary': 'des1', + }, { 'title': 't2', 'published': 'date2', 'link': 'link2', 'summary': 'des2' } - ], - 'links': [ - 'first', - 'second' ] } output = { @@ -35,8 +31,8 @@ def test_limited_json(self): 'published': 'date1', 'link': 'link1', 'summary': 'des1', - }], - 'links': ['first']} + }] + } self.assertEqual(work_with_dict.limited_dict(entering_dict, 1), output) self.assertEqual(work_with_dict.limited_dict(entering_dict, 2), entering_dict) diff --git a/final_task/test/test_html.py b/final_task/test/test_html.py new file mode 100644 index 0000000..6521e6d --- /dev/null +++ b/final_task/test/test_html.py @@ -0,0 +1,40 @@ +import unittest +import sys +import os + +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'rss_reader')) +import work_with_html + + +class TestHtmlFunctions(unittest.TestCase): + def test_something(self): + entering_dict = { + 'title': 'titl', + 'items': [{ + 'title': 't1', + 'published': 'date1', + 'link': 'link1', + 'summary': 'des1', + 'contain_image': False, + + }, + { + 'title': 't2', + 'published': 'date2', + 'link': 'link2', + 'summary': '[image: alt]des2', + 'contain_image': True, + 'link_on_image': 'imgLink', + } + ] + } + output = 'titl

titl


' + \ + 't1

date1
des1

t2

date2
' +\ + ' alt
des2

' + self.assertEqual(work_with_html.text_processing_for_html(entering_dict), output) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/test/test_rss.xml b/final_task/test/test_rss.xml new file mode 100644 index 0000000..cba1a28 --- /dev/null +++ b/final_task/test/test_rss.xml @@ -0,0 +1,29 @@ + + + Head title + + + Title item1 + + + <p><a href="link_A"><img src="link_img" alt="alt_text"></a>des1</p<<br clear="all"> + + + link1 + + Thu, 1 Nov 2019 16:12:24 -0500 + + + + Title item2 + + + des2 + + + link2 + + Thu, 2 Nov 2019 16:12:24 -0500 + + + diff --git a/final_task/test/test_rssfile_to_dict.py b/final_task/test/test_rssfile_to_dict.py new file mode 100644 index 0000000..58e5dfe --- /dev/null +++ b/final_task/test/test_rssfile_to_dict.py @@ -0,0 +1,10 @@ +import unittest + + +class MyTestCase(unittest.TestCase): + def test_something(self): + self.assertEqual(True, False) + + +if __name__ == '__main__': + unittest.main() From a92dded61314abd376ba93e45adb7d974bac2032 Mon Sep 17 00:00:00 2001 From: Vlad Protosevich Date: Sun, 24 Nov 2019 11:54:21 +0300 Subject: [PATCH 12/12] Fix bugs --- final_task/MANIFEST.in | 3 ++- final_task/rss_reader/VERSION.txt | 1 + final_task/rss_reader/requirements.txt | 6 ++--- final_task/rss_reader/rss_reader.py | 2 +- final_task/setup.py | 6 ++--- final_task/test/test_rssfile_to_dict.py | 33 ++++++++++++++++++++++--- final_task/test/test_text.py | 15 +++++------ 7 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 final_task/rss_reader/VERSION.txt diff --git a/final_task/MANIFEST.in b/final_task/MANIFEST.in index 66d93ec..01b3e2b 100644 --- a/final_task/MANIFEST.in +++ b/final_task/MANIFEST.in @@ -1 +1,2 @@ -include rss_reader\TimesNewRoman.ttf \ No newline at end of file +include rss_reader\TimesNewRoman.ttf +include rss_reader\VERSION.txt \ No newline at end of file diff --git a/final_task/rss_reader/VERSION.txt b/final_task/rss_reader/VERSION.txt new file mode 100644 index 0000000..11aa145 --- /dev/null +++ b/final_task/rss_reader/VERSION.txt @@ -0,0 +1 @@ +5.3 \ No newline at end of file diff --git a/final_task/rss_reader/requirements.txt b/final_task/rss_reader/requirements.txt index 6788592..83d8f52 100644 --- a/final_task/rss_reader/requirements.txt +++ b/final_task/rss_reader/requirements.txt @@ -1,4 +1,4 @@ feedparser==5.2.1 -colored -fpdf -requests +colored==1.4.0 +fpdf=1.7.2 +requests==2.22.0 \ No newline at end of file diff --git a/final_task/rss_reader/rss_reader.py b/final_task/rss_reader/rss_reader.py index 75f7e8a..7d09669 100644 --- a/final_task/rss_reader/rss_reader.py +++ b/final_task/rss_reader/rss_reader.py @@ -64,7 +64,7 @@ def run(): raise RssReaderException.RssReaderException('How work with application?\nEnter in command line: rss-reader -h') if args.version: - result = f'RSS reader version {__init__.VERSION}' + result = f'RSS reader version {open("VERSION.txt").readline()}' else: if args.limit: data = work_with_dict.limited_dict(data, args.limit) diff --git a/final_task/setup.py b/final_task/setup.py index e7c9696..491b8d0 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,13 +1,13 @@ from setuptools import setup, find_packages -import rss_reader +import os setup( name="rss-reader", - version=rss_reader.VERSION, + version=open(os.path.join(os.getcwd(), "rss_reader", "VERSION.txt")).readline(), packages=find_packages(), author="Vlad Protosevich", author_email="protosevic2001@gmail.com", - install_requires=['feedparser==5.2.1', 'colored', 'fpdf', 'requests'], + install_requires=['feedparser==5.2.1', 'colored==1.4.0', 'fpdf==1.7.2', 'requests==2.22.0'], entry_points={ 'console_scripts': [ 'rss-reader = rss_reader.rss_reader:run' diff --git a/final_task/test/test_rssfile_to_dict.py b/final_task/test/test_rssfile_to_dict.py index 58e5dfe..e897868 100644 --- a/final_task/test/test_rssfile_to_dict.py +++ b/final_task/test/test_rssfile_to_dict.py @@ -1,9 +1,36 @@ import unittest +import os +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'rss_reader')) -class MyTestCase(unittest.TestCase): - def test_something(self): - self.assertEqual(True, False) +import work_with_feedparser +import work_with_dict + + +class TestRssToDict(unittest.TestCase): + def test_rss_to_dict(self): + data = work_with_feedparser.get_object_feed(os.path.abspath('test_rss.xml')) + output = { + 'title': 'Head title', + 'items': [{ + 'title': 'Title item1', + 'published': 'Thu, 1 Nov 2019 16:12:24 -0500', + 'link': 'link1', + 'summary': '[image: alt_text] des1', + 'contain_image': True, + 'link_on_image': 'link_img', + }, + { + 'title': 'Title item2', + 'published': 'Thu, 2 Nov 2019 16:12:24 -0500', + 'link': 'link2', + 'summary': 'des2', + 'contain_image': False, + } + ] + } + self.assertEqual(work_with_dict.to_dict(data), output) if __name__ == '__main__': diff --git a/final_task/test/test_text.py b/final_task/test/test_text.py index 6f3d807..cdda46a 100644 --- a/final_task/test/test_text.py +++ b/final_task/test/test_text.py @@ -51,9 +51,8 @@ def test_text_processing(self): self.assertEqual(work_with_text.text_processing(entering, links), output) entering = 'texttest<>' - output = '[image 1: text][1] test' + output = '[image: text] test' self.assertEqual(work_with_text.text_processing(entering, links), output) - self.assertEqual(links, ['link']) def test_edit_key(self): entering = 'key' @@ -76,22 +75,20 @@ def test_get_string_with_result(self): 'published': 'date1', 'link': 'link1', 'summary': 'des1', + 'contain_image': False, }, { 'title': 't2', 'published': 'date2', 'link': 'link2', - 'summary': 'des2' + 'summary': 'des2', + 'contain_image': True, + 'link_on_image': 'linkOnImg' } ], - 'links': [ - 'first', - 'second' - ] } output = '\ntitl\n\nTitle: t1\nDate: date1\nLink: link1\nDescription: des1\n\n' + \ - 'Title: t2\nDate: date2\nLink: link2\nDescription: des2\n\n' + \ - '\nLinks:\n[1] - first\n[2] - second\n' + 'Title: t2\nDate: date2\nLink: link2\nDescription: des2\nLink on image: linkOnImg\n\n' self.assertEqual(work_with_text.get_string_with_result(entering_dict, 2), output)