[SYSTEMDS-2675+2676] Python Strings and printing

Add strings to python interface to enable calling SystemDS without
enforcing an transfer and parsing into python.
diff --git a/src/main/python/systemds/context/systemds_context.py b/src/main/python/systemds/context/systemds_context.py
index 017d56c..d80fdfa 100644
--- a/src/main/python/systemds/context/systemds_context.py
+++ b/src/main/python/systemds/context/systemds_context.py
@@ -37,7 +37,7 @@
 from systemds.utils.consts import VALID_INPUT_TYPES
 from systemds.utils.helpers import get_module_dir
 from systemds.operator import OperationNode
-
+from systemds.script_building import OutputType
 
 class SystemDSContext(object):
     """A context with a connection to a java instance with which SystemDS operations are executed. 
@@ -276,3 +276,6 @@
 
     def read(self, path: os.PathLike, **kwargs: Dict[str, VALID_INPUT_TYPES]):
         return OperationNode(self, 'read', [f'"{path}"'], named_input_nodes=kwargs)
+
+    def scalar(self, v: Dict[str, VALID_INPUT_TYPES]):
+        return OperationNode(self, v, output_type=OutputType.SCALAR)
\ No newline at end of file
diff --git a/src/main/python/systemds/operator/operation_node.py b/src/main/python/systemds/operator/operation_node.py
index aba4c63..ebe5804 100644
--- a/src/main/python/systemds/operator/operation_node.py
+++ b/src/main/python/systemds/operator/operation_node.py
@@ -161,6 +161,8 @@
             return f'{output}={self.operation}({inputs_comma_sep});'
         elif self.output_type == OutputType.NONE:
             return f'{self.operation}({inputs_comma_sep});'
+        elif self.output_type == OutputType.SCALAR:
+            return f'{var_name}={self.operation};'
         else:
             return f'{var_name}={self.operation}({inputs_comma_sep});'
 
@@ -339,12 +341,17 @@
         return OperationNode(self.sds_context, 'moment', unnamed_inputs, output_type=OutputType.DOUBLE)
 
     def write(self, destination: str, format:str = "binary", **kwargs: Dict[str, VALID_INPUT_TYPES]) -> 'OperationNode':
-        
         unnamed_inputs = [self, f'"{destination}"']
         named_parameters = {"format":f'"{format}"'}
         named_parameters.update(kwargs)
         return OperationNode(self.sds_context, 'write', unnamed_inputs, named_parameters, output_type= OutputType.NONE)
 
+    def to_string(self, **kwargs: Dict[str, VALID_INPUT_TYPES]) -> 'OperationNode':
+        return OperationNode(self.sds_context, 'toString', [self], kwargs, output_type= OutputType.DOUBLE)
+
+    def print(self, **kwargs: Dict[str, VALID_INPUT_TYPES]) -> 'OperationNode':
+        return OperationNode(self.sds_context, 'print', [self], kwargs, output_type= OutputType.NONE)
+
     def rev(self) -> 'OperationNode':
         """ Reverses the rows in a matrix
 
diff --git a/src/main/python/systemds/script_building/dag.py b/src/main/python/systemds/script_building/dag.py
index 69988f4..9e027e6 100644
--- a/src/main/python/systemds/script_building/dag.py
+++ b/src/main/python/systemds/script_building/dag.py
@@ -33,6 +33,7 @@
 class OutputType(Enum):
     MATRIX = auto()
     DOUBLE = auto()
+    SCALAR = auto()
     LIST = auto()
     NONE = auto()
 
diff --git a/src/main/python/tests/manual_tests/save_log_reg_mnist_sysds.py b/src/main/python/tests/manual_tests/save_log_reg_mnist_sysds.py
new file mode 100644
index 0000000..cbe22a0
--- /dev/null
+++ b/src/main/python/tests/manual_tests/save_log_reg_mnist_sysds.py
@@ -0,0 +1,39 @@
+# -------------------------------------------------------------
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# -------------------------------------------------------------
+
+from systemds.context import SystemDSContext
+from systemds.matrix import Matrix
+from systemds.examples.tutorials.mnist import DataManager
+
+d = DataManager()
+
+base_path = "systemds/examples/tutorials/mnist/"
+with SystemDSContext() as sds:
+    # Train Data
+    X = Matrix(sds, d.get_train_data().reshape((60000, 28*28)))
+    X.write(base_path + "train_data").compute()
+    Y = Matrix(sds, d.get_train_labels()) + 1.0
+    Y.write(base_path + "train_labels").compute()
+    Xt = Matrix(sds, d.get_test_data().reshape((10000, 28*28)))
+    Xt.write(base_path + "test_data").compute()
+    Yt = Matrix(sds, d.get_test_labels()) + 1.0
+    Yt.write(base_path + "test_labels").compute()
+    
diff --git a/src/main/python/tests/matrix/test_print.py b/src/main/python/tests/matrix/test_print.py
new file mode 100644
index 0000000..ee928df
--- /dev/null
+++ b/src/main/python/tests/matrix/test_print.py
@@ -0,0 +1,56 @@
+# -------------------------------------------------------------
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# -------------------------------------------------------------
+
+import math
+import os
+import random
+import shutil
+import sys
+import unittest
+
+import numpy as np
+import scipy.stats as st
+from systemds.context import SystemDSContext
+from systemds.matrix import Matrix
+
+
+class TestPrint(unittest.TestCase):
+
+    sds: SystemDSContext = None
+
+    @classmethod
+    def setUpClass(cls):
+        cls.sds = SystemDSContext()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.sds.close()
+
+    def test_print_01(self):
+        Matrix(self.sds, np.array([1])).to_string().print().compute()
+        self.assertEquals('1.000',self.sds.get_stdout()[0])
+
+    def test_print_02(self):
+        self.sds.scalar(1).print().compute()
+        self.assertEquals('1', self.sds.get_stdout()[0])
+
+if __name__ == "__main__":
+    unittest.main(exit=False)
diff --git a/src/main/python/tests/matrix/test_write.py b/src/main/python/tests/matrix/test_write.py
index 14b0b8f..a8f0d52 100644
--- a/src/main/python/tests/matrix/test_write.py
+++ b/src/main/python/tests/matrix/test_write.py
@@ -56,6 +56,13 @@
         res = NX.compute()
         self.assertTrue(np.allclose(original, res))
 
+    def test_write_02(self):
+        original = np.array([[1,2,3,4,5]])
+        X = Matrix(self.sds, original)
+        X.write(self.temp_dir + "02").compute()
+        NX = self.sds.read(self.temp_dir + "02")
+        res = NX.compute()
+        self.assertTrue(np.allclose(original, res))
 
 if __name__ == "__main__":
     unittest.main(exit=False)