Repository: modin-project/modin

Issue:
Add Spreadsheet API
Add the `modin.spreadsheet` API by integrating `modin-spreadsheet`.

Plan to expose 2 API:
`from_dataframe`: Creates spreadsheet from dataframe
`to_dataframe`:  Exports dataframe from spreadsheet

Conventional usage could look like:
`import modin.spreadsheet as mss`


Discussion:

Resolving patch:
diff --git a/modin/spreadsheet/__init__.py b/modin/spreadsheet/__init__.py
new file mode 100644
--- /dev/null
+++ b/modin/spreadsheet/__init__.py
@@ -0,0 +1,25 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team 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.
+
+try:
+    import modin_spreadsheet
+except ImportError:
+    raise ImportError(
+        "Please `pip install modin[spreadsheet]` to install the spreadsheet extension"
+    )
+
+from .general import from_dataframe, to_dataframe
+
+__all__ = ["from_dataframe", "to_dataframe"]
+
+del modin_spreadsheet
diff --git a/modin/spreadsheet/general.py b/modin/spreadsheet/general.py
new file mode 100644
--- /dev/null
+++ b/modin/spreadsheet/general.py
@@ -0,0 +1,201 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team 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 .. import pandas as pd
+from modin_spreadsheet import show_grid, SpreadsheetWidget
+
+
+def from_dataframe(
+    dataframe,
+    show_toolbar=None,
+    precision=None,
+    grid_options=None,
+    column_options=None,
+    column_definitions=None,
+    row_edit_callback=None,
+):
+    """
+    Renders a DataFrame or Series as an interactive spreadsheet, represented by
+    an instance of the ``SpreadsheetWidget`` class.  The ``SpreadsheetWidget`` instance
+    is constructed using the options passed in to this function.  The
+    ``dataframe`` argument to this function is used as the ``df`` kwarg in
+    call to the SpreadsheetWidget constructor, and the rest of the parameters
+    are passed through as is.
+
+    If the ``dataframe`` argument is a Series, it will be converted to a
+    DataFrame before being passed in to the SpreadsheetWidget constructor as the
+    ``df`` kwarg.
+
+    :rtype: SpreadsheetWidget
+
+    Parameters
+    ----------
+    dataframe : DataFrame
+        The DataFrame that will be displayed by this instance of
+        SpreadsheetWidget.
+    grid_options : dict
+        Options to use when creating the SlickGrid control (i.e. the
+        interactive grid).  See the Notes section below for more information
+        on the available options, as well as the default options that this
+        widget uses.
+    precision : integer
+        The number of digits of precision to display for floating-point
+        values.  If unset, we use the value of
+        `pandas.get_option('display.precision')`.
+    show_toolbar : bool
+        Whether to show a toolbar with options for adding/removing rows.
+        Adding/removing rows is an experimental feature which only works
+        with DataFrames that have an integer index.
+    column_options : dict
+        Column options that are to be applied to every column. See the
+        Notes section below for more information on the available options,
+        as well as the default options that this widget uses.
+    column_definitions : dict
+        Column options that are to be applied to individual
+        columns. The keys of the dict should be the column names, and each
+        value should be the column options for a particular column,
+        represented as a dict. The available options for each column are the
+        same options that are available to be set for all columns via the
+        ``column_options`` parameter. See the Notes section below for more
+        information on those options.
+    row_edit_callback : callable
+        A callable that is called to determine whether a particular row
+        should be editable or not. Its signature should be
+        ``callable(row)``, where ``row`` is a dictionary which contains a
+        particular row's values, keyed by column name. The callback should
+        return True if the provided row should be editable, and False
+        otherwise.
+
+
+    Notes
+    -----
+    The following dictionary is used for ``grid_options`` if none are
+    provided explicitly::
+
+        {
+            # SlickGrid options
+            'fullWidthRows': True,
+            'syncColumnCellResize': True,
+            'forceFitColumns': False,
+            'defaultColumnWidth': 150,
+            'rowHeight': 28,
+            'enableColumnReorder': False,
+            'enableTextSelectionOnCells': True,
+            'editable': True,
+            'autoEdit': False,
+            'explicitInitialization': True,
+
+            # Modin-spreadsheet options
+            'maxVisibleRows': 15,
+            'minVisibleRows': 8,
+            'sortable': True,
+            'filterable': True,
+            'highlightSelectedCell': False,
+            'highlightSelectedRow': True
+        }
+
+    The first group of options are SlickGrid "grid options" which are
+    described in the `SlickGrid documentation
+    <https://github.com/mleibman/SlickGrid/wiki/Grid-Options>`_.
+
+    The second group of option are options that were added specifically
+    for modin-spreadsheet and therefore are not documented in the SlickGrid documentation.
+    The following bullet points describe these options.
+
+    * **maxVisibleRows** The maximum number of rows that modin-spreadsheet will show.
+    * **minVisibleRows** The minimum number of rows that modin-spreadsheet will show
+    * **sortable** Whether the modin-spreadsheet instance will allow the user to sort
+      columns by clicking the column headers. When this is set to ``False``,
+      nothing will happen when users click the column headers.
+    * **filterable** Whether the modin-spreadsheet instance will allow the user to filter
+      the grid. When this is set to ``False`` the filter icons won't be shown
+      for any columns.
+    * **highlightSelectedCell** If you set this to True, the selected cell
+      will be given a light blue border.
+    * **highlightSelectedRow** If you set this to False, the light blue
+      background that's shown by default for selected rows will be hidden.
+
+    The following dictionary is used for ``column_options`` if none are
+    provided explicitly::
+
+        {
+            # SlickGrid column options
+            'defaultSortAsc': True,
+            'maxWidth': None,
+            'minWidth': 30,
+            'resizable': True,
+            'sortable': True,
+            'toolTip': "",
+            'width': None
+
+            # Modin-spreadsheet column options
+            'editable': True,
+        }
+
+    The first group of options are SlickGrid "column options" which are
+    described in the `SlickGrid documentation
+    <https://github.com/mleibman/SlickGrid/wiki/Column-Options>`_.
+
+    The ``editable`` option was added specifically for modin-spreadsheet and therefore is
+    not documented in the SlickGrid documentation.  This option specifies
+    whether a column should be editable or not.
+
+    See Also
+    --------
+    set_defaults : Permanently set global defaults for the parameters
+                   of ``show_grid``, with the exception of the ``dataframe``
+                   and ``column_definitions`` parameters, since those
+                   depend on the particular set of data being shown by an
+                   instance, and therefore aren't parameters we would want
+                   to set for all SpreadsheetWidget instances.
+    set_grid_option : Permanently set global defaults for individual
+                      grid options.  Does so by changing the defaults
+                      that the ``show_grid`` method uses for the
+                      ``grid_options`` parameter.
+    SpreadsheetWidget : The widget class that is instantiated and returned by this
+                  method.
+
+    """
+    if not isinstance(dataframe, pd.DataFrame):
+        raise TypeError("dataframe must be modin.DataFrame, not %s" % type(dataframe))
+    return show_grid(
+        dataframe,
+        show_toolbar,
+        precision,
+        grid_options,
+        column_options,
+        column_definitions,
+        row_edit_callback,
+    )
+
+
+def to_dataframe(spreadsheet):
+    """
+    Get a copy of the DataFrame that reflects the current state of the ``spreadsheet`` SpreadsheetWidget instance UI.
+    This includes any sorting or filtering changes, as well as edits
+    that have been made by double clicking cells.
+
+    :rtype: DataFrame
+
+    Parameters
+    ----------
+    spreadsheet : SpreadsheetWidget
+        The SpreadsheetWidget instance that DataFrame that will be displayed by this instance of
+        SpreadsheetWidget.
+    """
+    if not isinstance(spreadsheet, SpreadsheetWidget):
+        raise TypeError(
+            "spreadsheet must be modin_spreadsheet.SpreadsheetWidget, not %s"
+            % type(spreadsheet)
+        )
+    return spreadsheet.get_changed_df()
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -40,8 +40,9 @@ def is_pure(self):
 dask_deps = ["dask>=2.12.0,<=2.19.0", "distributed>=2.12.0,<=2.19.0"]
 ray_deps = ["ray>=1.0.0,<1.2.0", "pyarrow==1.0"]
 remote_deps = ["rpyc==4.1.5", "cloudpickle==1.4.1", "boto3==1.4.8"]
+spreadsheet_deps = ["modin-spreadsheet>=0.1.0"]
 
-all_deps = dask_deps + ray_deps + remote_deps
+all_deps = dask_deps + ray_deps + remote_deps + spreadsheet_deps
 
 setup(
     name="modin",
@@ -61,6 +62,7 @@ def is_pure(self):
         "dask": dask_deps,
         "ray": ray_deps,
         "remote": remote_deps,
+        "spreadsheet": spreadsheet_deps,
         "all": all_deps,
     },
     python_requires=">=3.7.1",


Test patch:
diff --git a/modin/spreadsheet/test/__init__.py b/modin/spreadsheet/test/__init__.py
new file mode 100644
--- /dev/null
+++ b/modin/spreadsheet/test/__init__.py
@@ -0,0 +1,12 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team 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.
diff --git a/modin/spreadsheet/test/test_general.py b/modin/spreadsheet/test/test_general.py
new file mode 100644
--- /dev/null
+++ b/modin/spreadsheet/test/test_general.py
@@ -0,0 +1,58 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team 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 pandas
+import pytest
+import modin.pandas as pd
+import modin.spreadsheet as mss
+import numpy as np
+from modin_spreadsheet import SpreadsheetWidget
+
+
+def get_test_data():
+    return {
+        "A": 1.0,
+        "B": pd.Timestamp("20130102"),
+        "C": pd.Series(1, index=list(range(4)), dtype="float32"),
+        "D": np.array([3] * 4, dtype="int32"),
+        "E": pd.Categorical(["test", "train", "foo", "bar"]),
+        "F": ["foo", "bar", "buzz", "fox"],
+    }
+
+
+def test_from_dataframe():
+    data = get_test_data()
+    modin_df = pd.DataFrame(data)
+    pandas_df = pandas.DataFrame(data)
+
+    modin_result = mss.from_dataframe(modin_df)
+    assert isinstance(modin_result, SpreadsheetWidget)
+
+    with pytest.raises(TypeError):
+        mss.from_dataframe(pandas_df)
+
+
+def test_to_dataframe():
+    data = get_test_data()
+    modin_df = pd.DataFrame(data)
+    pandas_df = pandas.DataFrame(data)
+
+    spreadsheet = mss.from_dataframe(modin_df)
+    modin_result = mss.to_dataframe(spreadsheet)
+
+    assert modin_result.equals(modin_df)
+
+    with pytest.raises(TypeError):
+        mss.to_dataframe("Not a SpreadsheetWidget")
+    with pytest.raises(TypeError):
+        mss.to_dataframe(pandas_df)