From f81298c8c0f5226aedba0d2e8f8bf6ed048c2a92 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Wed, 9 Dec 2020 14:50:57 +0100
Subject: [PATCH] add json-related routines

---
 CHANGELOG.md        |  4 +++
 docs/json.rst       |  8 ++++++
 satella/__init__.py |  2 +-
 satella/json.py     | 65 ++++++++++++++++++++++++++++++++++++++++++++-
 setup.py            |  2 +-
 tests/test_json.py  | 16 ++++++++++-
 6 files changed, 93 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdbf18fd..5715081c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1,5 @@
 # v2.14.23
+
+* added `write_json_to_file`
+* added `read_json_from_file`
+* added `write_json_to_file_if_different`
diff --git a/docs/json.rst b/docs/json.rst
index 904496bb..3ef1e485 100644
--- a/docs/json.rst
+++ b/docs/json.rst
@@ -21,3 +21,11 @@ This will serialize unknown objects in the following way.
 First, **__dict__** will be extracted out of this object. The dictionary
 will be constructed in such a way, that for each key in this **__dict__**,
 it's value's **repr** will be assigned.
+
+.. autofunction:: satella.json.read_json_from_file
+
+.. autofunction:: satella.json.write_json_to_file
+
+.. autofunction:: satella.json.write_json_to_file_if_different
+
+
diff --git a/satella/__init__.py b/satella/__init__.py
index 8bce6674..702d0b12 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.14.23_a1'
+__version__ = '2.14.23_a2'
diff --git a/satella/json.py b/satella/json.py
index 77efdbdd..b8704306 100644
--- a/satella/json.py
+++ b/satella/json.py
@@ -1,8 +1,13 @@
 import json
 import typing as tp
 from abc import ABCMeta, abstractmethod
+try:
+    import ujson
+except ImportError:
+    pass
 
-__all__ = ['JSONEncoder', 'JSONAble', 'json_encode']
+__all__ = ['JSONEncoder', 'JSONAble', 'json_encode', 'read_json_from_file',
+           'write_json_to_file', 'write_json_to_file_if_different']
 
 Jsonable = tp.TypeVar('Jsonable', list, dict, str, int, float, None)
 
@@ -39,3 +44,61 @@ def json_encode(x: tp.Any) -> str:
     :param x: object to convert
     """
     return JSONEncoder().encode(x)
+
+
+def write_json_to_file(path: str, value: JSONAble) -> None:
+    """
+    Write out a JSON to a file as UTF-8 encoded plain text.
+
+    :param path: path to the file
+    :param value: JSON-able content
+    """
+    if isinstance(value, JSONAble):
+        value = value.to_json()
+    with open(path, 'w') as f_out:
+        try:
+            ujson.dump(value, f_out)
+        except NameError:
+            json.dump(value, f_out)
+
+
+def write_json_to_file_if_different(path: str, value: JSONAble) -> bool:
+    """
+    Read JSON from a file. Write out a JSON to a file if it's value is different,
+    as UTF-8 encoded plain text.
+
+    :param path: path to the file
+    :param value: JSON-able content
+    :return: whether the write actually happened
+    """
+    if isinstance(value, JSONAble):
+        value = value.to_json()
+    try:
+        val = read_json_from_file(path)
+        if val != value:
+            write_json_to_file(path, value)
+            return True
+        return False
+    except (OSError, ValueError):
+        write_json_to_file(path, value)
+        return True
+
+
+def read_json_from_file(path: str) -> JSONAble:
+    """
+    Load a JSON from a provided file, as UTF-8 encoded plain text.
+
+    :param path: path to the file
+    :return: JSON content
+    :raises ValueError: the file contained an invalid JSON
+    :raises OSError: the file was not readable or did not exist
+    """
+    try:
+        with open(path, 'r') as f_in:
+            return ujson.load(f_in)
+    except NameError:
+        with open(path, 'r') as f_in:
+            try:
+                return json.load(f_in)
+            except json.decoder.JSONDecodeError as e:
+                raise ValueError(str(e))
diff --git a/setup.py b/setup.py
index 729eeac1..657a930f 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ setup(keywords=['ha', 'high availability', 'scalable', 'scalability', 'server',
             'HTTPJSONSource': ['requests'],
             'YAMLSource': ['pyyaml'],
             'TOMLSource': ['toml'],
-            'FasterJSONSource': ['ujson'],
+            'FasterJSON': ['ujson'],
             'cassandra': ['cassandra-driver'],
             'opentracing': ['opentracing']
       }
diff --git a/tests/test_json.py b/tests/test_json.py
index f1f120c0..1966d3c5 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -2,11 +2,25 @@ import json
 import typing as tp
 import unittest
 
-from satella.json import JSONAble, json_encode
+from satella.json import JSONAble, json_encode, read_json_from_file, write_json_to_file, \
+write_json_to_file_if_different
 
 
 class TestJson(unittest.TestCase):
 
+    def test_write_json_to_file_if_different(self):
+        d = {'test': 4}
+        self.assertTrue(write_json_to_file_if_different('test2.json', d))
+        self.assertFalse(write_json_to_file_if_different('test2.json', d))
+        d = {'test': 5}
+        self.assertTrue(write_json_to_file_if_different('test2.json', d))
+        self.assertFalse(write_json_to_file_if_different('test2.json', d))
+
+    def test_load_json_from_file(self):
+        d = {'test': 2}
+        write_json_to_file('test.json', d)
+        self.assertEqual(read_json_from_file('test.json'), d)
+
     def test_jsonable_objects(self):
         class MyClass(JSONAble):
             def to_json(self) -> tp.Union[list, dict, str, int, float, None]:
-- 
GitLab