diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ae11f80 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## 0.1.0 + +### Added +- A command for converting from JSON to YAML and a flag for the reverse action: from YAML to JSON. +- The command to convert an image to base64 format. \ No newline at end of file diff --git a/README.md b/README.md index 2a71803..78d01e4 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,27 @@ This is the tool's CLI for converting various formats. ## Using Format Fusion supports two commands for conversion: -- JSON to YAML - `format-fusion yaml` -- Image to Base64 - `format-fusion image` + + +| Name | Commands | +|-----------------|-----------------------| +| JSON to YAML | `format-fusion yaml` | +| Image to Base64 | `format-fusion image` | ### Usage example Command to generate from JSON to YAML: -`` +```shell format-fusion yaml D:\response_api.json -`` +``` + +Or you can perform the conversion in reverse order: **from YAML to JSON** + +```shell +format-fusion yaml D:\response_api.yaml --reverse +``` -The result of executing the command will be the generation of a YAML named `output.yaml` +The result of executing the command will be the creation of a YAML file named `output.yaml` or a JSON file named `output.json` Optionally, you can specify where to save the converted files: diff --git a/formatfusion/commands/cmd_image.py b/formatfusion/commands/cmd_image.py index 1719e9d..f49a009 100644 --- a/formatfusion/commands/cmd_image.py +++ b/formatfusion/commands/cmd_image.py @@ -11,39 +11,36 @@ --output Path to save file [default: output.txt] """ import logging -import os +import typing as t +from pathlib import Path -from formatfusion.converter import Converter +from ..core.image import ConverterImage logger = logging.getLogger(__name__) -def run(opts): +def run(opts: t.Dict[str, t.Any]): logger.info("Start converting..") return run_convert(opts) -def get_image_path(opts): +def get_image_path(opts: t.Dict[str, t.Any]) -> Path: opt_image_path = opts[""] - image_path = os.path.abspath(opt_image_path) - if not os.path.exists(image_path): + image_path = Path(opt_image_path).resolve() + if not image_path.exists(): raise FileNotFoundError(f"File not found: {image_path}.") return image_path -def get_output_path(opts): +def get_output_path(opts: t.Dict[str, t.Any]) -> Path: opt_file_path = opts["--output"] if opts["--output"] is not None else "output.txt" - file_path = os.path.abspath(opt_file_path) + file_path = Path(opt_file_path).resolve() return file_path -def run_convert(opts): +def run_convert(opts: t.Dict[str, t.Any]) -> None: image_file = get_image_path(opts) output_path = get_output_path(opts) - convert = Converter(input_file=image_file) - base64_image = convert.convert_image_to_base64() - - with open(output_path, "w") as file: - file.write(base64_image) - logger.info(f"The converted image was saved in {output_path}") + convert = ConverterImage(input_file=image_file, output_file=output_path) + convert.convert_image_to_base64() diff --git a/formatfusion/commands/cmd_yaml.py b/formatfusion/commands/cmd_yaml.py index 1e65b3c..15062e9 100644 --- a/formatfusion/commands/cmd_yaml.py +++ b/formatfusion/commands/cmd_yaml.py @@ -5,50 +5,59 @@ format-fusion [g-opts] yaml [options] Arguments: - Path to JSON file + Path to JSON or YAML file Options: - --output Path to save YAML file [default: output.yaml] + --output Path to save YAML or JSON file + --reverse A flag that allows you to convert from YAML to JSON """ import logging -import os +import typing as t +from pathlib import Path -from formatfusion.converter import Converter from formatfusion.helpers import validate_files +from ..core.json_and_yaml import ConverterYAMLandJSON + logger = logging.getLogger(__name__) -def run(opts): +def run(opts: t.Dict[str, t.Any]): logger.info("Start converting..") return run_convert(opts) -def get_json_path(opts): - opt_json_path = opts[""] - json_path = os.path.abspath(opt_json_path) - if not os.path.exists(json_path): - raise FileNotFoundError(f"File not found: {json_path}.") - return json_path +def get_input_file_path(opts: t.Dict[str, t.Any]) -> Path: + opt_input_path = opts[""] + input_path = Path(opt_input_path).resolve() + if not input_path.exists(): + raise FileNotFoundError(f"File not found: {input_path}.") + return input_path -def get_output_path(opts): - opt_yaml_path = opts["--output"] if opts["--output"] is not None else "output.yaml" - yaml_path = os.path.abspath(opt_yaml_path) - return yaml_path +def get_output_file_path(opts: t.Dict[str, t.Any], input_file: Path) -> Path: + if opts["--output"] is not None: + output_path = Path(opts["--output"]).resolve() + else: + if input_file.suffix == ".json": + default_output = input_file.parent / "output.yaml" + elif input_file.suffix == ".yaml": + default_output = input_file.parent / "output.json" + else: + raise ValueError(f"Unsupported input file extension: {input_file.suffix}.") + output_path = default_output.resolve() + return output_path -def run_convert(opts) -> None: - json_file = get_json_path(opts) - yaml_file = get_output_path(opts) - if not validate_files(json_file, yaml_file): - return - convert = Converter(input_file=json_file, output_file=yaml_file) - yaml_string = convert.convert_json_to_yaml() +def run_convert(opts: t.Dict[str, t.Any]) -> None: + input_file = get_input_file_path(opts) + output_file = get_output_file_path(opts, input_file) + if not validate_files(input_file, output_file): + return - with open(yaml_file, "w", encoding="utf-8") as file: - file.write(yaml_string) - logger.info( - f"The JSON from {json_file} was converted to YAML and saved in {yaml_file}" - ) + convert = ConverterYAMLandJSON(input_file=input_file, output_file=output_file) + if opts["--reverse"]: + convert.convert_yaml_to_json() + else: + convert.convert_json_to_yaml() diff --git a/formatfusion/converter.py b/formatfusion/converter.py deleted file mode 100644 index ce9a08d..0000000 --- a/formatfusion/converter.py +++ /dev/null @@ -1,22 +0,0 @@ -import base64 -import json -import logging - -import yaml - -logger = logging.getLogger(__name__) - - -class Converter: - def __init__(self, input_file: str, output_file: str | None = None): - self.input_file = input_file - self.output_file = output_file - - def convert_json_to_yaml(self) -> str: - with open(self.input_file, "r", encoding="utf-8") as file: - json_dict = json.load(file) - return yaml.dump(json_dict, sort_keys=False) - - def convert_image_to_base64(self) -> str: - with open(self.input_file, "rb") as image: - return base64.b64encode(image.read()).decode("utf-8") diff --git a/formatfusion/core/__init__.py b/formatfusion/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/formatfusion/core/base.py b/formatfusion/core/base.py new file mode 100644 index 0000000..6f5a54a --- /dev/null +++ b/formatfusion/core/base.py @@ -0,0 +1,20 @@ +import logging +import typing as t +from pathlib import Path + +logger = logging.getLogger(__name__) + + +class Base: + def __init__(self, input_file: Path, output_file: Path | None = None): + self.input_file = input_file + self.output_file = output_file + + def save_result(self, result: str, success_message: str) -> None: + if not self.output_file: + raise ValueError("Output file is not specified.") + + with open(self.output_file, "w", encoding="utf-8") as file: + file.write(result) + + logger.info(success_message) diff --git a/formatfusion/core/image.py b/formatfusion/core/image.py new file mode 100644 index 0000000..f41873c --- /dev/null +++ b/formatfusion/core/image.py @@ -0,0 +1,12 @@ +import base64 + +from .base import Base + + +class ConverterImage(Base): + def convert_image_to_base64(self) -> None: + with open(self.input_file, "rb") as image: + result = base64.b64encode(image.read()).decode("utf-8") + self.save_result( + result, f"The converted image was saved in {self.output_file}" + ) diff --git a/formatfusion/core/json_and_yaml.py b/formatfusion/core/json_and_yaml.py new file mode 100644 index 0000000..951e381 --- /dev/null +++ b/formatfusion/core/json_and_yaml.py @@ -0,0 +1,25 @@ +import json + +import yaml + +from .base import Base + + +class ConverterYAMLandJSON(Base): + def convert_json_to_yaml(self) -> None: + with open(self.input_file, "r", encoding="utf-8") as file: + json_dict = json.load(file) + result = yaml.dump(json_dict, sort_keys=False) + self.save_result( + result, + f"The JSON from {self.input_file} was converted to YAML and saved in {self.output_file}", + ) + + def convert_yaml_to_json(self) -> None: + with open(self.input_file, "r", encoding="utf-8") as file: + yaml_dict = yaml.safe_load(file) + result = json.dumps(yaml_dict, indent=4, ensure_ascii=False) + self.save_result( + result, + f"The YAML from {self.input_file} was converted to JSON and saved in {self.output_file}", + ) diff --git a/formatfusion/helpers.py b/formatfusion/helpers.py index eeb5e12..b25b571 100644 --- a/formatfusion/helpers.py +++ b/formatfusion/helpers.py @@ -3,7 +3,7 @@ logger = logging.getLogger(__name__) -VALID_EXTENSIONS = {"json", "png", "jpg"} +VALID_EXTENSIONS = {"json", "yaml", "png", "jpg"} def get_file_extension(filename: str): @@ -28,4 +28,9 @@ def validate_files(input_file, output_file) -> bool: ) return False + if input_extension == "yaml" and get_file_extension(output_file) != "json": + logger.error( + "For a YAML input file, the output file must have the extension '.json'." + ) + return False return True diff --git a/formatfusion/main.py b/formatfusion/main.py index 1a8e1d1..0f14ffb 100644 --- a/formatfusion/main.py +++ b/formatfusion/main.py @@ -18,7 +18,7 @@ from formatfusion import commands logging.basicConfig( - level=logging.DEBUG, # todo: поменять уровень на INFO после всех работ + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.StreamHandler()], ) diff --git a/pyproject.toml b/pyproject.toml index 0c9494d..f0e29e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cli-format-fusion" -version = "0.0.1" +version = "0.1.0" description = "A mini conversion tool" authors = ["R00kie "] packages = [ diff --git a/tests/test_cmd_image.py b/tests/test_cmd_image.py index cc017ea..dd9ebc1 100644 --- a/tests/test_cmd_image.py +++ b/tests/test_cmd_image.py @@ -1,6 +1,7 @@ import os import tempfile import unittest +from pathlib import Path from unittest.mock import patch from formatfusion.commands.cmd_image import get_image_path, get_output_path, run_convert @@ -9,7 +10,7 @@ class TestFormatFusionImage(unittest.TestCase): @patch("formatfusion.commands.cmd_image.get_image_path") @patch("formatfusion.commands.cmd_image.get_output_path") - @patch("formatfusion.converter.Converter.convert_image_to_base64") + @patch("formatfusion.core.image.ConverterImage.convert_image_to_base64") def test_run_convert_success( self, mock_convert_image_to_base64, mock_get_output_path, mock_get_image_path ): @@ -27,9 +28,6 @@ def test_run_convert_success( run_convert(opts) mock_convert_image_to_base64.assert_called_once() - with open(output_path, "r") as file: - content = file.read() - self.assertEqual(content, "base64_encoded_data") os.remove(image_path) os.remove(output_path) @@ -39,7 +37,7 @@ def test_get_image_path_valid(self): temp_image_path = temp_image.name opts = {"": temp_image_path} result = get_image_path(opts) - self.assertEqual(result, os.path.abspath(temp_image_path)) + self.assertEqual(result, Path(temp_image_path)) os.remove(temp_image_path) def test_get_image_path_invalid(self): @@ -47,14 +45,29 @@ def test_get_image_path_invalid(self): with self.assertRaises(FileNotFoundError): get_image_path(opts) - def test_get_output_path(self): - opts = {"--output": "custom_output.txt"} + def test_with_output_specified(self): + opts = {"--output": "/tmp/specified_output.txt"} result = get_output_path(opts) - self.assertEqual(result, os.path.abspath("custom_output.txt")) + expected = Path("/tmp/specified_output.txt").resolve() + self.assertEqual(result, expected) + def test_with_no_output_specified(self): opts = {"--output": None} result = get_output_path(opts) - self.assertEqual(result, os.path.abspath("output.txt")) + expected = Path("output.txt").resolve() + self.assertEqual(result, expected) + + def test_relative_path_output(self): + opts = {"--output": "relative_output.txt"} + result = get_output_path(opts) + expected = Path("relative_output.txt").resolve() + self.assertEqual(result, expected) + + def test_default_output(self): + opts = {"--output": "output.txt"} + result = get_output_path(opts) + expected = Path("output.txt").resolve() + self.assertEqual(result, expected) @patch( "formatfusion.commands.cmd_image.get_image_path", diff --git a/tests/test_cmd_yaml.py b/tests/test_cmd_yaml.py index e4d918c..ff3d3ad 100644 --- a/tests/test_cmd_yaml.py +++ b/tests/test_cmd_yaml.py @@ -1,65 +1,57 @@ -import os -import tempfile import unittest -from unittest.mock import patch +from pathlib import Path +from unittest.mock import MagicMock, patch -from formatfusion.commands.cmd_yaml import get_json_path, get_output_path, run_convert +from formatfusion.commands.cmd_yaml import ( + get_input_file_path, + get_output_file_path, + run_convert, +) class TestFormatFusionYaml(unittest.TestCase): - def test_run_convert(self): - with tempfile.NamedTemporaryFile( - suffix=".json", delete=False - ) as temp_json, tempfile.NamedTemporaryFile( - suffix=".yaml", delete=False - ) as temp_yaml: - json_content = '{"key": "value"}' - temp_json.write(json_content.encode()) - temp_json_path = temp_json.name - temp_yaml_path = temp_yaml.name + @patch("pathlib.Path.exists") + def test_get_input_file_path_file_exists(self, mock_exists): + mock_exists.return_value = True - opts = {"": temp_json_path, "--output": temp_yaml_path} + opts = {"": "/path/to/input/file.json"} + result = get_input_file_path(opts) + self.assertEqual(result, Path("/path/to/input/file.json").resolve()) - run_convert(opts) + @patch("pathlib.Path.exists") + def test_get_input_file_path_file_not_found(self, mock_exists): + mock_exists.return_value = False - with open(temp_yaml_path, "r", encoding="utf-8") as file: - yaml_content = file.read() - self.assertEqual(yaml_content, "key: value\n") - - os.remove(temp_json_path) - os.remove(temp_yaml_path) - - def test_get_json_path_valid(self): - with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_json: - temp_json_path = temp_json.name - opts = {"": temp_json_path} - result = get_json_path(opts) - self.assertEqual(result, os.path.abspath(temp_json_path)) - os.remove(temp_json_path) - - def test_get_json_path_invalid(self): - opts = {"": "nonexistent.json"} + opts = {"": "/path/to/input/file.json"} with self.assertRaises(FileNotFoundError): - get_json_path(opts) - - def test_get_output_path(self): - opts = {"--output": "custom_output.yaml"} - result = get_output_path(opts) - self.assertEqual(result, os.path.abspath("custom_output.yaml")) - - opts = {"--output": None} - result = get_output_path(opts) - self.assertEqual(result, os.path.abspath("output.yaml")) - - @patch( - "formatfusion.commands.cmd_yaml.get_json_path", - side_effect=FileNotFoundError("File not found: fake.json"), - ) - @patch("formatfusion.commands.cmd_yaml.get_output_path", return_value="fake.yaml") - def test_run_convert_file_not_found(self, mock_output, mock_json): - opts = {"": "fake.json", "--output": "fake.yaml"} - - with self.assertRaises(FileNotFoundError) as context: - run_convert(opts) - - self.assertIn("File not found: fake.json", str(context.exception)) + get_input_file_path(opts) + + @patch("pathlib.Path.exists") + def test_get_output_file_path_with_custom_output(self, mock_exists): + mock_exists.return_value = True + opts = { + "--output": "/path/to/output/file.yaml", + "": "/path/to/input/file.json", + } + input_file = Path("/path/to/input/file.json") + + result = get_output_file_path(opts, input_file) + self.assertEqual(result, Path("/path/to/output/file.yaml").resolve()) + + @patch("pathlib.Path.exists") + def test_get_output_file_path_with_default_output(self, mock_exists): + mock_exists.return_value = True + opts = {"--output": None, "": "/path/to/input/file.json"} + input_file = Path("/path/to/input/file.json") + + result = get_output_file_path(opts, input_file) + self.assertEqual(result, Path("/path/to/input/output.yaml").resolve()) + + @patch("pathlib.Path.exists") + def test_get_output_file_path_with_invalid_extension(self, mock_exists): + mock_exists.return_value = True + opts = {"--output": None, "": "/path/to/input/file.txt"} + input_file = Path("/path/to/input/file.txt") + + with self.assertRaises(ValueError): + get_output_file_path(opts, input_file) diff --git a/tests/test_converting.py b/tests/test_converting.py deleted file mode 100644 index d796f4c..0000000 --- a/tests/test_converting.py +++ /dev/null @@ -1,39 +0,0 @@ -import unittest -from unittest.mock import mock_open, patch - -from formatfusion.converter import Converter - - -class TestConvertJsonToYaml(unittest.TestCase): - @patch( - "builtins.open", - new_callable=mock_open, - read_data='{"key": "value", "nested": {"key2": "value2"}}', - ) - @patch("json.load") - @patch("yaml.dump") - def test_convert_json_to_yaml(self, mock_yaml_dump, mock_json_load, mock_open): - mock_json_load.return_value = {"key": "value", "nested": {"key2": "value2"}} - mock_yaml_dump.return_value = "key: value\nnested:\n key2: value2\n" - - converter = Converter("test.json") - result = converter.convert_json_to_yaml() - - mock_open.assert_called_once_with("test.json", "r", encoding="utf-8") - mock_json_load.assert_called_once() - mock_yaml_dump.assert_called_once_with( - {"key": "value", "nested": {"key2": "value2"}}, sort_keys=False - ) - self.assertEqual(result, "key: value\nnested:\n key2: value2\n") - - @patch("builtins.open", new_callable=mock_open, read_data=b"image_data") - @patch("base64.b64encode") - def test_convert_image_to_base64(self, mock_b64encode, mock_open): - mock_b64encode.return_value = b"encoded_image_data" - - converter = Converter("test_image.jpg") - result = converter.convert_image_to_base64() - - mock_open.assert_called_once_with("test_image.jpg", "rb") - mock_b64encode.assert_called_once_with(b"image_data") - self.assertEqual(result, "encoded_image_data") diff --git a/tests/test_core/test_image.py b/tests/test_core/test_image.py new file mode 100644 index 0000000..db799ba --- /dev/null +++ b/tests/test_core/test_image.py @@ -0,0 +1,23 @@ +import base64 +import unittest +from pathlib import Path +from unittest.mock import mock_open, patch + +from formatfusion.core.image import ConverterImage + + +class TestConvertImage(unittest.TestCase): + @patch("builtins.open", new_callable=mock_open, read_data=b"test_image_data") + @patch("formatfusion.core.base.Base.save_result") + def test_convert_image_to_base64(self, mock_save_result, mock_open): + input_file = Path("/path/to/input/image.jpg") + output_file = Path("/path/to/output/image_base64.txt") + converter = ConverterImage(input_file=input_file, output_file=output_file) + + expected_base64_result = base64.b64encode(b"test_image_data").decode("utf-8") + + converter.convert_image_to_base64() + + mock_save_result.assert_called_once_with( + expected_base64_result, f"The converted image was saved in {output_file}" + ) diff --git a/tests/test_core/test_json_and_yaml.py b/tests/test_core/test_json_and_yaml.py new file mode 100644 index 0000000..e2847f4 --- /dev/null +++ b/tests/test_core/test_json_and_yaml.py @@ -0,0 +1,39 @@ +import unittest +from pathlib import Path +from unittest.mock import mock_open, patch + +from formatfusion.core.json_and_yaml import ConverterYAMLandJSON + + +class TestConvertJsonToYaml(unittest.TestCase): + @patch("builtins.open", new_callable=mock_open, read_data='{"key": "value"}') + @patch("formatfusion.core.base.Base.save_result") + def test_convert_json_to_yaml(self, mock_save_result, mock_open): + input_file = Path("/path/to/input/file.json") + output_file = Path("/path/to/output/file.yaml") + converter = ConverterYAMLandJSON(input_file=input_file, output_file=output_file) + + converter.convert_json_to_yaml() + + mock_open.assert_called_once_with(input_file, "r", encoding="utf-8") + + mock_save_result.assert_called_once_with( + "key: value\n", + f"The JSON from {input_file} was converted to YAML and saved in {output_file}", + ) + + @patch("builtins.open", new_callable=mock_open, read_data="key: value\n") + @patch("formatfusion.core.base.Base.save_result") + def test_convert_yaml_to_json(self, mock_save_result, mock_open): + input_file = Path("/path/to/input/file.yaml") + output_file = Path("/path/to/output/file.json") + converter = ConverterYAMLandJSON(input_file=input_file, output_file=output_file) + + converter.convert_yaml_to_json() + + mock_open.assert_called_once_with(input_file, "r", encoding="utf-8") + + mock_save_result.assert_called_once_with( + '{\n "key": "value"\n}', + f"The YAML from {input_file} was converted to JSON and saved in {output_file}", + )