diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..0f3aab2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. +I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions +or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index b6e4761..ea52ded 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ parts/ sdist/ var/ @@ -85,10 +83,12 @@ ipython_config.py .python-version # pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. +# According to pypa/pipenv#598, it is recommended to include +# Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific +# dependencies or dependencies having no cross-platform support, +# pipenv may install dependencies that don't work, or not install +# all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow @@ -127,3 +127,14 @@ dmypy.json # Pyre type checker .pyre/ + +# Exclude IntelliJ files, they will be recreated from the build files +.idea/* +# But keep dictionaries to have less false positives in spellcheck inspection +!.idea/dictionaries + +# PyCharm files +.DS_Store + +# Application log files +histogramer/.logs/ diff --git a/.idea/dictionaries/jim_molecule.xml b/.idea/dictionaries/jim_molecule.xml new file mode 100644 index 0000000..b029c5e --- /dev/null +++ b/.idea/dictionaries/jim_molecule.xml @@ -0,0 +1,14 @@ + + + + addopts + asctime + asyncio + histogramer + histogrammer + komissarov + levelname + pytest + + + \ No newline at end of file diff --git a/README.md b/README.md index 6914698..2c8ab32 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,43 @@ -## Welcome to GitHub Pages - -You can use the [editor on GitHub](https://github.com/jim-molecule/histogramer/edit/master/README.md) to maintain and preview the content for your website in Markdown files. - -Whenever you commit to this repository, GitHub Pages will run [Jekyll](https://jekyllrb.com/) to rebuild the pages in your site, from the content in your Markdown files. - -### Markdown - -Markdown is a lightweight and easy-to-use syntax for styling your writing. It includes conventions for - -```markdown -Syntax highlighted code block - -# Header 1 -## Header 2 -### Header 3 - -- Bulleted -- List - -1. Numbered -2. List - -**Bold** and _Italic_ and `Code` text - -[Link](url) and ![Image](src) -``` - -For more details see [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/). - -### Jekyll Themes - -Your Pages site will use the layout and styles from the Jekyll theme you have selected in your [repository settings](https://github.com/jim-molecule/histogramer/settings). The name of this theme is saved in the Jekyll `_config.yml` configuration file. - -### Support or Contact - -Having trouble with Pages? Check out our [documentation](https://help.github.com/categories/github-pages-basics/) or [contact support](https://github.com/contact) and we’ll help you sort it out. +# Compatibility # +Histogramer has tested using `python 3.6.8` virtual environment in: + * Windows 10 OS + * macOS Catalina + +# Description # +This tool analyze text files in a directory (which was specified by user) +and it's sub folders. Statistics by words count is gathering +for each text file was found. Then a histogram will be building +by this statistics. + + # Example # +![](examples/histogram.png) + +# Installation # +* #### Using a `*.whl` dist: #### + * Download the latest `*.whl` version from a + [releases page](https://github.com/jim-molecule/histogramer/releases) + * Install histogramer: `pip3 install --upgrade path_to_wheel.whl` + +* #### Using sources: #### + * Remove dist files from project root: + * Windows: `RMDIR /Q/S build dist histogramer.egg-info` + * Mac: `rm -r build dist histogramer.egg-info` + * Install wheel: `pip3 install wheel` + * Build dist: `python setup.py bdist_wheel` + * Install histogramer: `pip3 install --upgrade path_to_wheel.whl` + +# Issues # +Please, report about any issues to an +[issues page](https://github.com/jim-molecule/histogramer/issues/new/choose) +with `~/.logs` folder's files attached. + +# Testing # +For run all tests, please, use `pytest ./histogramer/tests` from project root. +Pytest options are placed in `~/histogramer/tests/pytest.ini` file. + +# Usage # +Run a `python -m histogramer --help` script. + +# Virtual environment # +For main usage `~/requirements_main.txt` should be installed. +For testing: `~/requirements_tests.txt`. diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 2f7efbe..0000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-minimal \ No newline at end of file diff --git a/examples/histogram.png b/examples/histogram.png new file mode 100644 index 0000000..fd9a78e Binary files /dev/null and b/examples/histogram.png differ diff --git a/histogramer/__init__.py b/histogramer/__init__.py new file mode 100644 index 0000000..35fe764 --- /dev/null +++ b/histogramer/__init__.py @@ -0,0 +1,3 @@ +""" +Init for 'histogramer' python package. +""" diff --git a/histogramer/__main__.py b/histogramer/__main__.py new file mode 100644 index 0000000..5cc57c1 --- /dev/null +++ b/histogramer/__main__.py @@ -0,0 +1,22 @@ +""" +Histogramer main module. +""" +import asyncio + +from histogramer.src.helpers.args_helper import parse_arguments +from histogramer.src.helpers.log_helper import init_logger +from histogramer.src.histogram import process_text_files, show_histogram + + +async def main(): + """ + Run Histogrammer. + :return: None. + """ + arguments = await parse_arguments() + logger = await init_logger(folder_name=".logs", root_path=arguments.log) + words_count = await process_text_files("*.txt", logger, arguments.path) + await show_histogram(logger, words_count) + + +{"__main__": lambda: asyncio.run(main())}.get(__name__, lambda: None)() diff --git a/histogramer/src/__init__.py b/histogramer/src/__init__.py new file mode 100644 index 0000000..35fe764 --- /dev/null +++ b/histogramer/src/__init__.py @@ -0,0 +1,3 @@ +""" +Init for 'histogramer' python package. +""" diff --git a/histogramer/src/helpers/__init__.py b/histogramer/src/helpers/__init__.py new file mode 100644 index 0000000..5ca822d --- /dev/null +++ b/histogramer/src/helpers/__init__.py @@ -0,0 +1,3 @@ +""" +Init for 'helpers' python package. +""" diff --git a/histogramer/src/helpers/args_helper.py b/histogramer/src/helpers/args_helper.py new file mode 100644 index 0000000..5d7ec31 --- /dev/null +++ b/histogramer/src/helpers/args_helper.py @@ -0,0 +1,56 @@ +""" +Helps to work with argument parser. +""" +import argparse +import os + + +def __raise_error(path): + """ + Raise NotADirectoryError. + :param path: Directory which should exists. + :return: None. + """ + raise NotADirectoryError(f"directory '{path}' not exists") + + +def get_dir_type(path): + """ + Validate that directory exists. + :param path: Directory which should exists. + :return: Path or NotADirectoryError if directory not exists. + """ + {False: lambda: __raise_error(path)}.get( + os.path.isdir(path) or path == "0", lambda: None)() + return path + + +async def parse_arguments(raw_args=None): + """ + Parse arguments. + :param raw_args: Arguments for arg parser. + :return: Parsed arguments. + """ + parser = argparse.ArgumentParser(description="please, provide root path" + " in which (and it's sub " + "folders) text files " + "will be processed for " + "histogram building") + parser.add_argument("-p", + action="store", + default="", + dest="path", + help="root path in which (and it's sub " + "folders) text files will be processed", + required=True, + type=get_dir_type) + parser.add_argument("-l", + action="store", + default=os.getcwd(), + dest="log", + help="path to store logs. Use '0' " + "if you don't want to store them. " + "Default value: ~/.logs/", + required=False, + type=get_dir_type) + return parser.parse_args(raw_args) diff --git a/histogramer/src/helpers/datetime_helper.py b/histogramer/src/helpers/datetime_helper.py new file mode 100644 index 0000000..7723518 --- /dev/null +++ b/histogramer/src/helpers/datetime_helper.py @@ -0,0 +1,23 @@ +""" +Helps to work with datetime objects. +""" +from datetime import timedelta + + +async def datetime_to_str(datetime_obj): + """ + Convert datetime object to formatted string. + :param datetime_obj: Datetime object. + :return: Formatted string. + """ + return datetime_obj.isoformat(sep=" ", timespec="milliseconds") + + +async def get_duration(start, end): + """ + Get event duration. + :param start: Datetime when an event started. + :param end: Datetime when an event finished. + :return: Time period as formatted string. + """ + return round(number=timedelta.total_seconds(end - start), ndigits=3) diff --git a/histogramer/src/helpers/log_helper.py b/histogramer/src/helpers/log_helper.py new file mode 100644 index 0000000..29dc1ce --- /dev/null +++ b/histogramer/src/helpers/log_helper.py @@ -0,0 +1,60 @@ +""" +Helps to work with console & file logger. +""" +import logging +import os +import shutil +from logging.handlers import RotatingFileHandler +from pathlib import Path + + +def _add_rotating_file_handler(folder_name, logger, log_formatter, root_path): + """ + Add rotating file handler to the logger instance for logging to a file. + :param folder_name: Name of the folder where logs will be stored. + :param logger: Instance of logger. + :param log_formatter: Format of log messages. + :param root_path: Path to the log folder. + :return: None. + """ + path = os.path.join(root_path, folder_name) + file_name = os.path.join(path, ".histogramer") + {True: lambda: shutil.rmtree(path, ignore_errors=True)}.get( + os.path.isdir(file_name) + and Path(file_name).stat().st_size >= 5 * (1024 ** 2), + lambda: None)() + # create folder for file logs if not exists + Path(path).mkdir(parents=True, exist_ok=True) + + rotating_file_handler = RotatingFileHandler(filename=file_name) + rotating_file_handler.setFormatter(fmt=log_formatter) + rotating_file_handler.setLevel(level=logging.INFO) + logger.addHandler(hdlr=rotating_file_handler) + + +async def init_logger(folder_name, root_path): + """ + Configure logger for logging events in console (and in a file, optional). + :param folder_name: Name of the folder where logs will be stored. + :param root_path: Path to the log folder. + :return: Instance of logger. + """ + log_formatter = logging.Formatter("[%(asctime)s] " + "[%(threadName)s] " + "[%(levelname)s] " + "%(message)s") + logger = logging.getLogger() + logger.setLevel(level=logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(fmt=log_formatter) + console_handler.setLevel(level=logging.ERROR) + logger.addHandler(hdlr=console_handler) + + {"0": lambda: None}.get( + root_path, + lambda: _add_rotating_file_handler(folder_name, + logger, + log_formatter, + root_path))() + return logger diff --git a/histogramer/src/helpers/random_helper.py b/histogramer/src/helpers/random_helper.py new file mode 100644 index 0000000..9cd964b --- /dev/null +++ b/histogramer/src/helpers/random_helper.py @@ -0,0 +1,16 @@ +""" +Helps to generate random objects. +""" +import secrets +import string + + +async def get_random_string(string_length=10): + """ + Generate a random string of fixed length. + :param string_length: Length of string. + :return: Random string of fixed length. + """ + + return "".join(secrets.choice(string.ascii_lowercase) + for _ in range(string_length)) diff --git a/histogramer/src/histogram.py b/histogramer/src/histogram.py new file mode 100644 index 0000000..e344c2f --- /dev/null +++ b/histogramer/src/histogram.py @@ -0,0 +1,105 @@ +""" +Implementation of main functions for histogram building. +""" +import sys +from datetime import datetime +from multiprocessing import Pool +from pathlib import Path + +import matplotlib.pyplot as plt +import seaborn +from halo import Halo + +from histogramer.src.helpers.datetime_helper import ( + datetime_to_str, + get_duration +) + + +def _exit_if_empty_data(logger): + """ + Write log message and exit from application. + :param logger: Instance of logger. + :return: None. + """ + logger.warning("there is no data for a histogram building") + sys.exit() + + +def _count_words(file): + """ + Count words number in the file. + :param file: Path to the file which will be processed. + :return: Words count in the current file or error message. + """ + try: + return len(file.read_text().split()) + except (IOError, UnicodeDecodeError) as exception: + return f"Can't read '{file}'. Error: {exception}" + + +async def process_text_files(extension, logger, path): + """ + Calculate words count for each file (with specified extension) in that dir + and it's sub folders. + :param extension: Only files with such extension will be processed. + :param logger: Instance of logger. + :param path: Root directory in which (and it's sub folders) files will + be processed. + :return: List of numbers where each number equals words count + in the file. + """ + with Halo("Processing text files...") as spinner: + start_time = datetime.utcnow() + with Pool() as pool: + words_count = [] + for result in pool.imap_unordered(_count_words, + Path(path).rglob(extension)): + {True: lambda r=result: logger.warning(r), + False: lambda r=result: words_count.append(r) + }.get(isinstance(result, str), lambda: None)() + spinner.text = f"{len(words_count)} files processed" + end_time = datetime.utcnow() + spinner.succeed(f"[{await datetime_to_str(end_time)}] " + + f"{len(words_count)} files successfully processed for" + + f" {await get_duration(start_time, end_time)} " + "seconds.") + return words_count + + +async def show_histogram(logger, words_count): + """ + Show a histogram using words count by text files. + :param logger: Instance of logger. + :param words_count: List of numbers where each number equals words count + in the file. + :return: None. + """ + {True: lambda: _exit_if_empty_data(logger)}.get( + len(words_count) == 0, lambda: None)() + + start_time = datetime.utcnow() + message = f"[{await datetime_to_str(start_time)}] Building histogram..." + with Halo(text=message) as spinner: + plt.figure("histogramer", + dpi=75, + facecolor=(0, 0, 0), + figsize=(16, 10)) + plt.style.use(style="dark_background") + plt.xlabel(xlabel="Words Count") + plt.ylabel(ylabel="Files Count") + plt.title(label="Bar Chart for Words Count in Files", fontsize=22) + seaborn.set() + seaborn.distplot(a=words_count, kde=False) + plt.grid(alpha=0.1, which="both", linestyle="--") + plt.grid(alpha=0.08, which="minor", linestyle="-.") + plt.xticks(rotation=45) + plt.tight_layout() + + end_time = datetime.utcnow() + spinner.succeed(f"[{await datetime_to_str(end_time)}] " + + "Histogram successfully built for " + + f"{await get_duration(start_time, end_time)} " + "seconds.") + logger.info(msg="Histogram successfully built") + plt.show() diff --git a/histogramer/tests/__init__.py b/histogramer/tests/__init__.py new file mode 100644 index 0000000..b234b64 --- /dev/null +++ b/histogramer/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Init for 'tests' python package. +""" diff --git a/histogramer/tests/pytest.ini b/histogramer/tests/pytest.ini new file mode 100644 index 0000000..d0314e0 --- /dev/null +++ b/histogramer/tests/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +addopts = -k test -m "not skip" -n auto --reruns 1 --reruns-delay 0 +markers = + args_helper + asyncio_helper + datetime_helper + log_helper + random_helper + serial + skip diff --git a/histogramer/tests/test_args_helper.py b/histogramer/tests/test_args_helper.py new file mode 100644 index 0000000..296283b --- /dev/null +++ b/histogramer/tests/test_args_helper.py @@ -0,0 +1,105 @@ +""" +Tests for args_helper module. +""" +import os +import sys + +import pytest + +from histogramer.src.helpers.args_helper import get_dir_type, parse_arguments +from histogramer.src.helpers.random_helper import get_random_string + + +@pytest.mark.args_helper +@pytest.mark.asyncio +async def test_get_arguments_valid_path(): + """ + Invoke get_arguments function with valid path argument. + :return: None. + """ + path = os.getcwd() + actual = await parse_arguments(["-p", path]) + assert isinstance(actual.path, str) + assert actual.path == path + + +@pytest.mark.args_helper +@pytest.mark.asyncio +async def test_get_arguments_valid_log(): + """ + Invoke get_arguments function with valid path and log arguments. + :return: None. + """ + path = os.getcwd() + actual = await parse_arguments(["-p", path, "-l", path]) + assert isinstance(actual.path, str) + assert isinstance(actual.log, str) + assert actual.log == path + assert actual.path == path + + +@pytest.mark.args_helper +@pytest.mark.asyncio +async def test_get_arguments_invalid_path(): + """ + Invoke get_arguments function with invalid path argument. + :return: None. + """ + args = ["-p", await get_random_string()] + with pytest.raises(NotADirectoryError): + await parse_arguments(args) + + +@pytest.mark.args_helper +@pytest.mark.asyncio +async def test_get_arguments_invalid_log(): + """ + Invoke get_arguments function with invalid log argument. + :return: None. + """ + args = ["-p", os.getcwd(), "-l", await get_random_string()] + with pytest.raises(NotADirectoryError): + await parse_arguments(args) + + +@pytest.mark.args_helper +@pytest.mark.serial +@pytest.mark.asyncio +async def test_get_arguments_no_arguments(): + """ + Invoke get_arguments function without arguments. + :return: None. + """ + with open(os.devnull, "w") as file: + try: + sys.stderr = file + with pytest.raises(SystemExit): + await parse_arguments() + finally: + sys.stderr = sys.__stderr__ + + +@pytest.mark.args_helper +@pytest.mark.asyncio +@pytest.mark.parametrize("path", ["0", os.getcwd()]) +async def test_dir_type_positive(path): + """ + Invoke dir_type function with valid path argument. + :param path: Path to directory. + :return: None. + """ + actual = get_dir_type(path) + assert isinstance(actual, str) + assert actual == path + + +@pytest.mark.args_helper +@pytest.mark.asyncio +async def test_dir_type_negative(): + """ + Invoke dir_type function with invalid path argument. + :return: None. + """ + path = await get_random_string() + with pytest.raises(NotADirectoryError): + get_dir_type(path) diff --git a/histogramer/tests/test_datetime_helper.py b/histogramer/tests/test_datetime_helper.py new file mode 100644 index 0000000..4213b96 --- /dev/null +++ b/histogramer/tests/test_datetime_helper.py @@ -0,0 +1,87 @@ +""" +Tests for datetime_helper module. +""" +from datetime import datetime, timedelta + +import pytest + +from histogramer.src.helpers.datetime_helper import (datetime_to_str, + get_duration) + +_DATETIME_STR = "2020-02-14 12:44:21.625037" + + +@pytest.mark.datetime_helper +@pytest.mark.asyncio +async def test_datetime_to_str_positive(): + """ + Invoke datetime_to_str function with valid datetime_obj argument. + :return: None. + """ + actual = await datetime_to_str(datetime_obj=datetime.utcnow()) + assert isinstance(actual, str) + + +@pytest.mark.datetime_helper +@pytest.mark.asyncio +@pytest.mark.parametrize("datetime_obj", [None, _DATETIME_STR]) +async def test_datetime_to_str_negative(datetime_obj): + """ + Invoke datetime_to_str function with invalid datetime_obj argument. + :param datetime_obj: Argument type of datetime. + :return: None. + """ + with pytest.raises(AttributeError): + await datetime_to_str(datetime_obj=datetime_obj) + + +@pytest.mark.datetime_helper +@pytest.mark.asyncio +async def test_get_duration_positive(): + """ + Invoke get_duration function with valid start and end arguments. + :return: None. + """ + start = datetime.utcnow() + end = start + timedelta(seconds=1) + actual = await get_duration(start=start, end=end) + assert isinstance(actual, float) + assert actual == round(number=timedelta.total_seconds(end - start), + ndigits=3) + + +@pytest.mark.datetime_helper +@pytest.mark.asyncio +@pytest.mark.parametrize("start, end", + [(None, datetime.utcnow()), + (_DATETIME_STR, datetime.utcnow()), + (datetime.utcnow(), None), + (datetime.utcnow(), _DATETIME_STR), + (None, _DATETIME_STR), + (None, None), + (_DATETIME_STR, _DATETIME_STR), + (_DATETIME_STR, None)]) +async def test_get_duration_negative(start, end): + """ + Invoke get_duration function with invalid start and/or end arguments. + :param start: Event start datetime. + :param end: Event end datetime. + :return: None. + """ + with pytest.raises(TypeError): + await get_duration(start=start, end=end) + + +@pytest.mark.datetime_helper +@pytest.mark.asyncio +async def test_get_duration_swapped_arguments(): + """ + Invoke get_duration function with swapped start and end arguments. + :return: None. + """ + start = datetime.utcnow() + end = start + timedelta(seconds=1) + actual = await get_duration(start=end, end=start) + assert isinstance(actual, float) + assert actual == round(number=timedelta.total_seconds(start - end), + ndigits=3) diff --git a/histogramer/tests/test_log_helper.py b/histogramer/tests/test_log_helper.py new file mode 100644 index 0000000..9776a6d --- /dev/null +++ b/histogramer/tests/test_log_helper.py @@ -0,0 +1,96 @@ +""" +Tests for log_helper module. +""" +import os +import shutil + +import pytest + +from histogramer.src.helpers.log_helper import init_logger +from histogramer.src.helpers.random_helper import get_random_string + + +def __close_logger_handlers(logger): + """ + Close all logger handlers. + :param logger: Instance of logger. + :return: None. + """ + for handler in logger.handlers: + handler.close() + + +async def _remove_dirs_tree(logger, path): + """ + Remove directory and it's sub folders. + if a directory with such path exists. + :param logger: Instance of logger. + :param path: Root directory. + :return: None. + """ + {True: lambda: __close_logger_handlers(logger)}.get( + logger is not None, lambda: None)() + {True: lambda: shutil.rmtree(path, ignore_errors=True)}.get( + os.path.isdir(path), lambda: None)() + + +@pytest.mark.log_helper +@pytest.mark.asyncio +async def test_init_logger_positive(): + """ + Invoke init_logger function with valid arguments. + :return: None. + """ + folder_name = ".test_init_logger_positive" + root_path = os.path.join(os.getcwd(), await get_random_string()) + logger = None + + try: + logger = await init_logger(folder_name, root_path) + logger.info("test_init_logger_positive") + full_path = os.path.join(root_path, folder_name) + with open(os.path.join(full_path, ".histogramer")) as file: + lines = file.readlines() + assert len(lines) == 1 + assert "[INFO] test_init_logger_positive" in lines[0] + finally: + await _remove_dirs_tree(logger, root_path) + + +@pytest.mark.log_helper +@pytest.mark.asyncio +async def test_init_logger_no_file_logging(): + """ + Invoke init_logger function with valid arguments where path == "0". + :return: None. + """ + folder_name = ".test_init_logger_no_file_logging" + root_path = "0" + full_path = os.path.join(os.getcwd(), folder_name) + full_path_2 = os.path.join(root_path, folder_name) + logger = None + + try: + logger = await init_logger(folder_name, root_path) + logger.debug("test_init_logger_positive_no_file_logging") + assert not os.path.isdir(full_path) + assert not os.path.isdir(full_path_2) + finally: + await _remove_dirs_tree(logger, full_path) + await _remove_dirs_tree(logger, full_path_2) + + +@pytest.mark.log_helper +@pytest.mark.asyncio +@pytest.mark.parametrize("folder_name, root_path", + [(None, os.getcwd()), + (os.getcwd(), None), + (None, None)]) +async def test_init_logger_negative(folder_name, root_path): + """ + Invoke init_logger function with invalid arguments. + :return: None. + """ + with pytest.raises(TypeError): + logger = await init_logger(folder_name, root_path) + await _remove_dirs_tree(logger, os.path.join(root_path, folder_name)) diff --git a/histogramer/tests/test_random_helper.py b/histogramer/tests/test_random_helper.py new file mode 100644 index 0000000..1d09ee5 --- /dev/null +++ b/histogramer/tests/test_random_helper.py @@ -0,0 +1,31 @@ +""" +Tests for random_helper. +""" +import pytest + +from histogramer.src.helpers.random_helper import get_random_string + + +@pytest.mark.random_helper +@pytest.mark.asyncio +async def test_get_random_string_no_args(): + """ + Invoke get_random_string function without arguments. + :return: None. + """ + actual = await get_random_string() + assert isinstance(actual, str) + assert len(actual) == 10 + + +@pytest.mark.random_helper +@pytest.mark.asyncio +async def test_get_random_string_positive(): + """ + Invoke get_random_string function with length argument. + :return: None. + """ + length = 5 + actual = await get_random_string(length) + assert isinstance(actual, str) + assert len(actual) == length diff --git a/requirements_main.txt b/requirements_main.txt new file mode 100644 index 0000000..1e28f3c --- /dev/null +++ b/requirements_main.txt @@ -0,0 +1,17 @@ +colorama==0.4.3 +cursor==1.3.4 +cycler==0.10.0 +halo==0.0.29 +kiwisolver==1.1.0 +log-symbols==0.0.14 +matplotlib==3.1.3 +numpy==1.18.1 +pandas==1.0.1 +pyparsing==2.4.6 +python-dateutil==2.8.1 +pytz==2019.3 +scipy==1.4.1 +seaborn==0.10.0 +six==1.14.0 +spinners==0.0.24 +termcolor==1.1.0 diff --git a/requirements_tests.txt b/requirements_tests.txt new file mode 100644 index 0000000..f21fad6 --- /dev/null +++ b/requirements_tests.txt @@ -0,0 +1,15 @@ +apipkg==1.5 +attrs==19.3.0 +execnet==1.7.1 +importlib-metadata==1.5.0 +more-itertools==8.2.0 +packaging==20.1 +pluggy==0.13.1 +py==1.8.1 +pytest==5.3.5 +pytest-asyncio==0.10.0 +pytest-forked==1.1.3 +pytest-rerunfailures==8.0 +pytest-xdist==1.31.0 +wcwidth==0.1.8 +zipp==3.0.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2c1f5bd --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +""" +For package installation only. +""" +import setuptools + + +def read_file(file_name): + """ + Get content of the file. + :param file_name: Name of the file. + :return: Content of the file. + """ + with open(file_name) as file: + return file.read() + + +setuptools.setup( + name="histogramer", + version="1.0.6", + author="Petr Komissarov", + author_email="jim.molecule@gmail.com", + classifiers=[ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent"], + description="Tool for histogram building by words count in files", + install_requires=read_file("requirements_main.txt"), + long_description=read_file("README.md"), + long_description_content_type="text/markdown", + packages=setuptools.find_packages(exclude=["*tests"]), + python_requires=">=3.6, <3.8", + tests_require=read_file("requirements_tests.txt"), + url="https://github.com/jim-molecule/histogramer")