We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
multi_index not accepting tuples
The [UDF apply docs](https://docs.tiledb.com/cloud/client-api/serverless-udfs#multi-index-usage) imply the tuples and slices are interchangeable.  The standard API throws an exception on tuples (for both tiledb: and s3:)


```
>>> import tiledb, tiledb.cloud
>>> A = tiledb.DenseArray("tiledb://TileDB-Inc/quickstart_dense", ctx=tiledb.cloud.Ctx())
>>> A.multi_index[[(1,2), 4], [slice(1,4)]]['a']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/ec2-user/venv/lib64/python3.7/site-packages/tiledb/multirange_indexing.py", line 121, in __getitem__
    ranges = self.getitem_ranges(idx)
  File "/home/ec2-user/venv/lib64/python3.7/site-packages/tiledb/multirange_indexing.py", line 109, in getitem_ranges
    subranges = sel_to_subranges(sel)
  File "/home/ec2-user/venv/lib64/python3.7/site-packages/tiledb/multirange_indexing.py", line 60, in sel_to_subranges
    raise TypeError("Unsupported selection ")
TypeError: Unsupported selection 
```

It would be great if multi_index and UDF apply accepted the same index types.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `tiledb/multirange_indexing.py`
Content:
```
1 import tiledb
2 from tiledb import Array, ArraySchema
3 import os, numpy as np
4 import sys, weakref
5 
6 try:
7     from tiledb.libtiledb import multi_index
8 except:
9     from tiledb.indexing import multi_index
10 
11 def _index_as_tuple(idx):
12     """Forces scalar index objects to a tuple representation"""
13     if isinstance(idx, tuple):
14         return idx
15     return (idx,)
16 
17 def mr_dense_result_shape(ranges, base_shape = None):
18     # assumptions: len(ranges) matches number of dims
19     if base_shape is not None:
20         assert len(ranges) == len(base_shape), "internal error: mismatched shapes"
21 
22     new_shape = list()
23     for i,rr in enumerate(ranges):
24         if rr != ():
25             m = list(map(lambda y: abs(y[1] - y[0]) + 1, rr))
26             new_shape.append(np.sum(m))
27         else:
28             if base_shape is None:
29                 raise ValueError("Missing required base_shape for whole-dimension slices")
30             # empty range covers dimension
31             new_shape.append(base_shape[i])
32 
33     return tuple(new_shape)
34 
35 def mr_dense_result_numel(ranges):
36     return np.prod(mr_dense_result_shape(ranges))
37 
38 def sel_to_subranges(dim_sel):
39     if isinstance(dim_sel, list):
40         dim_sel = tuple(dim_sel)
41     elif not isinstance(dim_sel, tuple):
42         dim_sel = (dim_sel,)
43 
44     subranges = list()
45     for range in dim_sel:
46         if np.isscalar(range):
47             subranges.append( (range, range) )
48         elif isinstance(range, slice):
49             if range.step is not None:
50                 raise ValueError("Stepped slice ranges are not supported")
51             elif range.start is None and range.stop is None:
52                 # ':' full slice
53                 pass
54             else:
55                 subranges.append( (range.start, range.stop) )
56         elif isinstance(range, list):
57             for el in range:
58                 subranges.append( (el, el) )
59         else:
60             raise TypeError("Unsupported selection ")
61 
62     return tuple(subranges)
63 
64 
65 class MultiRangeIndexer(object):
66     """
67     Implements multi-range / outer / orthogonal indexing.
68 
69     """
70     # for cython
71     # comment out for Python 2 :/
72     #array: Array
73     #schema: ArraySchema
74     #def __init__(self, array: Array, query = None):
75 
76     def __init__(self, array, query = None):
77         if not issubclass(type(array), tiledb.Array):
78             raise ValueError("Internal error: MultiRangeIndexer expected tiledb.Array")
79         self.array_ref = weakref.ref(array)
80         self.schema = array.schema
81         self.query = query
82 
83     @property
84     def array(self):
85         assert self.array_ref() is not None, \
86             "Internal error: invariant violation (indexing call w/ dead array_ref)"
87         return self.array_ref()
88 
89     @classmethod
90     def __test_init__(cls, array):
91         """
92         Internal helper method for testing getitem range calculation.
93         :param array:
94         :return:
95         """
96         m = cls.__new__(cls)
97         m.array_ref = weakref.ref(array)
98         m.schema = array.schema
99         m.query = None
100         return m
101 
102     def getitem_ranges(self, idx):
103         dom = self.schema.domain
104         ndim = dom.ndim
105         idx = _index_as_tuple(idx)
106 
107         ranges = list()
108         for i,sel in enumerate(idx):
109             subranges = sel_to_subranges(sel)
110             ranges.append(subranges)
111 
112         # extend the list to ndim
113         if len(ranges) < ndim:
114             ranges.extend([ tuple() for _ in range(ndim-len(ranges))])
115 
116         rval = tuple(ranges)
117         return rval
118 
119     def __getitem__(self, idx):
120         # implements multi-range / outer / orthogonal indexing
121         ranges = self.getitem_ranges(idx)
122 
123         dom = self.schema.domain
124         attr_names = tuple(self.schema.attr(i).name for i in range(self.schema.nattr))
125 
126         coords = None
127         if self.query is not None:
128             # if we are called via Query object, then we need to respect Query semantics
129             attr_names = tuple(self.query.attrs) if self.query.attrs else attr_names # query.attrs might be None -> all
130             coords = self.query.coords
131 
132         # TODO order
133         result_dict = multi_index(
134             self.array,
135             attr_names,
136             ranges,
137             coords=coords
138         )
139 
140         if self.schema.sparse:
141             return result_dict
142         else:
143             result_shape = mr_dense_result_shape(ranges, self.schema.shape)
144             for arr in result_dict.values():
145                 # TODO check/test layout
146                 arr.shape = result_shape
147             return result_dict
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/tiledb/multirange_indexing.py b/tiledb/multirange_indexing.py
--- a/tiledb/multirange_indexing.py
+++ b/tiledb/multirange_indexing.py
@@ -8,11 +8,6 @@
 except:
     from tiledb.indexing import multi_index
 
-def _index_as_tuple(idx):
-    """Forces scalar index objects to a tuple representation"""
-    if isinstance(idx, tuple):
-        return idx
-    return (idx,)
 
 def mr_dense_result_shape(ranges, base_shape = None):
     # assumptions: len(ranges) matches number of dims
@@ -36,11 +31,6 @@
     return np.prod(mr_dense_result_shape(ranges))
 
 def sel_to_subranges(dim_sel):
-    if isinstance(dim_sel, list):
-        dim_sel = tuple(dim_sel)
-    elif not isinstance(dim_sel, tuple):
-        dim_sel = (dim_sel,)
-
     subranges = list()
     for range in dim_sel:
         if np.isscalar(range):
@@ -53,6 +43,8 @@
                 pass
             else:
                 subranges.append( (range.start, range.stop) )
+        elif isinstance(range, tuple):
+            subranges.extend((range,))
         elif isinstance(range, list):
             for el in range:
                 subranges.append( (el, el) )
@@ -102,10 +94,16 @@
     def getitem_ranges(self, idx):
         dom = self.schema.domain
         ndim = dom.ndim
-        idx = _index_as_tuple(idx)
+
+        if isinstance(idx, tuple):
+            idx = list(idx)
+        else:
+            idx = [idx]
 
         ranges = list()
         for i,sel in enumerate(idx):
+            if not isinstance(sel, list):
+                sel = [sel]
             subranges = sel_to_subranges(sel)
             ranges.append(subranges)

{"golden_diff": "diff --git a/tiledb/multirange_indexing.py b/tiledb/multirange_indexing.py\n--- a/tiledb/multirange_indexing.py\n+++ b/tiledb/multirange_indexing.py\n@@ -8,11 +8,6 @@\n except:\n     from tiledb.indexing import multi_index\n \n-def _index_as_tuple(idx):\n-    \"\"\"Forces scalar index objects to a tuple representation\"\"\"\n-    if isinstance(idx, tuple):\n-        return idx\n-    return (idx,)\n \n def mr_dense_result_shape(ranges, base_shape = None):\n     # assumptions: len(ranges) matches number of dims\n@@ -36,11 +31,6 @@\n     return np.prod(mr_dense_result_shape(ranges))\n \n def sel_to_subranges(dim_sel):\n-    if isinstance(dim_sel, list):\n-        dim_sel = tuple(dim_sel)\n-    elif not isinstance(dim_sel, tuple):\n-        dim_sel = (dim_sel,)\n-\n     subranges = list()\n     for range in dim_sel:\n         if np.isscalar(range):\n@@ -53,6 +43,8 @@\n                 pass\n             else:\n                 subranges.append( (range.start, range.stop) )\n+        elif isinstance(range, tuple):\n+            subranges.extend((range,))\n         elif isinstance(range, list):\n             for el in range:\n                 subranges.append( (el, el) )\n@@ -102,10 +94,16 @@\n     def getitem_ranges(self, idx):\n         dom = self.schema.domain\n         ndim = dom.ndim\n-        idx = _index_as_tuple(idx)\n+\n+        if isinstance(idx, tuple):\n+            idx = list(idx)\n+        else:\n+            idx = [idx]\n \n         ranges = list()\n         for i,sel in enumerate(idx):\n+            if not isinstance(sel, list):\n+                sel = [sel]\n             subranges = sel_to_subranges(sel)\n             ranges.append(subranges)\n", "issue": "multi_index not accepting tuples\nThe [UDF apply docs](https://docs.tiledb.com/cloud/client-api/serverless-udfs#multi-index-usage) imply the tuples and slices are interchangeable.  The standard API throws an exception on tuples (for both tiledb: and s3:)\r\n\r\n\r\n```\r\n>>> import tiledb, tiledb.cloud\r\n>>> A = tiledb.DenseArray(\"tiledb://TileDB-Inc/quickstart_dense\", ctx=tiledb.cloud.Ctx())\r\n>>> A.multi_index[[(1,2), 4], [slice(1,4)]]['a']\r\nTraceback (most recent call last):\r\n  File \"<stdin>\", line 1, in <module>\r\n  File \"/home/ec2-user/venv/lib64/python3.7/site-packages/tiledb/multirange_indexing.py\", line 121, in __getitem__\r\n    ranges = self.getitem_ranges(idx)\r\n  File \"/home/ec2-user/venv/lib64/python3.7/site-packages/tiledb/multirange_indexing.py\", line 109, in getitem_ranges\r\n    subranges = sel_to_subranges(sel)\r\n  File \"/home/ec2-user/venv/lib64/python3.7/site-packages/tiledb/multirange_indexing.py\", line 60, in sel_to_subranges\r\n    raise TypeError(\"Unsupported selection \")\r\nTypeError: Unsupported selection \r\n```\r\n\r\nIt would be great if multi_index and UDF apply accepted the same index types.\n", "before_files": [{"content": "import tiledb\nfrom tiledb import Array, ArraySchema\nimport os, numpy as np\nimport sys, weakref\n\ntry:\n    from tiledb.libtiledb import multi_index\nexcept:\n    from tiledb.indexing import multi_index\n\ndef _index_as_tuple(idx):\n    \"\"\"Forces scalar index objects to a tuple representation\"\"\"\n    if isinstance(idx, tuple):\n        return idx\n    return (idx,)\n\ndef mr_dense_result_shape(ranges, base_shape = None):\n    # assumptions: len(ranges) matches number of dims\n    if base_shape is not None:\n        assert len(ranges) == len(base_shape), \"internal error: mismatched shapes\"\n\n    new_shape = list()\n    for i,rr in enumerate(ranges):\n        if rr != ():\n            m = list(map(lambda y: abs(y[1] - y[0]) + 1, rr))\n            new_shape.append(np.sum(m))\n        else:\n            if base_shape is None:\n                raise ValueError(\"Missing required base_shape for whole-dimension slices\")\n            # empty range covers dimension\n            new_shape.append(base_shape[i])\n\n    return tuple(new_shape)\n\ndef mr_dense_result_numel(ranges):\n    return np.prod(mr_dense_result_shape(ranges))\n\ndef sel_to_subranges(dim_sel):\n    if isinstance(dim_sel, list):\n        dim_sel = tuple(dim_sel)\n    elif not isinstance(dim_sel, tuple):\n        dim_sel = (dim_sel,)\n\n    subranges = list()\n    for range in dim_sel:\n        if np.isscalar(range):\n            subranges.append( (range, range) )\n        elif isinstance(range, slice):\n            if range.step is not None:\n                raise ValueError(\"Stepped slice ranges are not supported\")\n            elif range.start is None and range.stop is None:\n                # ':' full slice\n                pass\n            else:\n                subranges.append( (range.start, range.stop) )\n        elif isinstance(range, list):\n            for el in range:\n                subranges.append( (el, el) )\n        else:\n            raise TypeError(\"Unsupported selection \")\n\n    return tuple(subranges)\n\n\nclass MultiRangeIndexer(object):\n    \"\"\"\n    Implements multi-range / outer / orthogonal indexing.\n\n    \"\"\"\n    # for cython\n    # comment out for Python 2 :/\n    #array: Array\n    #schema: ArraySchema\n    #def __init__(self, array: Array, query = None):\n\n    def __init__(self, array, query = None):\n        if not issubclass(type(array), tiledb.Array):\n            raise ValueError(\"Internal error: MultiRangeIndexer expected tiledb.Array\")\n        self.array_ref = weakref.ref(array)\n        self.schema = array.schema\n        self.query = query\n\n    @property\n    def array(self):\n        assert self.array_ref() is not None, \\\n            \"Internal error: invariant violation (indexing call w/ dead array_ref)\"\n        return self.array_ref()\n\n    @classmethod\n    def __test_init__(cls, array):\n        \"\"\"\n        Internal helper method for testing getitem range calculation.\n        :param array:\n        :return:\n        \"\"\"\n        m = cls.__new__(cls)\n        m.array_ref = weakref.ref(array)\n        m.schema = array.schema\n        m.query = None\n        return m\n\n    def getitem_ranges(self, idx):\n        dom = self.schema.domain\n        ndim = dom.ndim\n        idx = _index_as_tuple(idx)\n\n        ranges = list()\n        for i,sel in enumerate(idx):\n            subranges = sel_to_subranges(sel)\n            ranges.append(subranges)\n\n        # extend the list to ndim\n        if len(ranges) < ndim:\n            ranges.extend([ tuple() for _ in range(ndim-len(ranges))])\n\n        rval = tuple(ranges)\n        return rval\n\n    def __getitem__(self, idx):\n        # implements multi-range / outer / orthogonal indexing\n        ranges = self.getitem_ranges(idx)\n\n        dom = self.schema.domain\n        attr_names = tuple(self.schema.attr(i).name for i in range(self.schema.nattr))\n\n        coords = None\n        if self.query is not None:\n            # if we are called via Query object, then we need to respect Query semantics\n            attr_names = tuple(self.query.attrs) if self.query.attrs else attr_names # query.attrs might be None -> all\n            coords = self.query.coords\n\n        # TODO order\n        result_dict = multi_index(\n            self.array,\n            attr_names,\n            ranges,\n            coords=coords\n        )\n\n        if self.schema.sparse:\n            return result_dict\n        else:\n            result_shape = mr_dense_result_shape(ranges, self.schema.shape)\n            for arr in result_dict.values():\n                # TODO check/test layout\n                arr.shape = result_shape\n            return result_dict", "path": "tiledb/multirange_indexing.py"}], "after_files": [{"content": "import tiledb\nfrom tiledb import Array, ArraySchema\nimport os, numpy as np\nimport sys, weakref\n\ntry:\n    from tiledb.libtiledb import multi_index\nexcept:\n    from tiledb.indexing import multi_index\n\n\ndef mr_dense_result_shape(ranges, base_shape = None):\n    # assumptions: len(ranges) matches number of dims\n    if base_shape is not None:\n        assert len(ranges) == len(base_shape), \"internal error: mismatched shapes\"\n\n    new_shape = list()\n    for i,rr in enumerate(ranges):\n        if rr != ():\n            m = list(map(lambda y: abs(y[1] - y[0]) + 1, rr))\n            new_shape.append(np.sum(m))\n        else:\n            if base_shape is None:\n                raise ValueError(\"Missing required base_shape for whole-dimension slices\")\n            # empty range covers dimension\n            new_shape.append(base_shape[i])\n\n    return tuple(new_shape)\n\ndef mr_dense_result_numel(ranges):\n    return np.prod(mr_dense_result_shape(ranges))\n\ndef sel_to_subranges(dim_sel):\n    subranges = list()\n    for range in dim_sel:\n        if np.isscalar(range):\n            subranges.append( (range, range) )\n        elif isinstance(range, slice):\n            if range.step is not None:\n                raise ValueError(\"Stepped slice ranges are not supported\")\n            elif range.start is None and range.stop is None:\n                # ':' full slice\n                pass\n            else:\n                subranges.append( (range.start, range.stop) )\n        elif isinstance(range, tuple):\n            subranges.extend((range,))\n        elif isinstance(range, list):\n            for el in range:\n                subranges.append( (el, el) )\n        else:\n            raise TypeError(\"Unsupported selection \")\n\n    return tuple(subranges)\n\n\nclass MultiRangeIndexer(object):\n    \"\"\"\n    Implements multi-range / outer / orthogonal indexing.\n\n    \"\"\"\n    # for cython\n    # comment out for Python 2 :/\n    #array: Array\n    #schema: ArraySchema\n    #def __init__(self, array: Array, query = None):\n\n    def __init__(self, array, query = None):\n        if not issubclass(type(array), tiledb.Array):\n            raise ValueError(\"Internal error: MultiRangeIndexer expected tiledb.Array\")\n        self.array_ref = weakref.ref(array)\n        self.schema = array.schema\n        self.query = query\n\n    @property\n    def array(self):\n        assert self.array_ref() is not None, \\\n            \"Internal error: invariant violation (indexing call w/ dead array_ref)\"\n        return self.array_ref()\n\n    @classmethod\n    def __test_init__(cls, array):\n        \"\"\"\n        Internal helper method for testing getitem range calculation.\n        :param array:\n        :return:\n        \"\"\"\n        m = cls.__new__(cls)\n        m.array_ref = weakref.ref(array)\n        m.schema = array.schema\n        m.query = None\n        return m\n\n    def getitem_ranges(self, idx):\n        dom = self.schema.domain\n        ndim = dom.ndim\n\n        if isinstance(idx, tuple):\n            idx = list(idx)\n        else:\n            idx = [idx]\n\n        ranges = list()\n        for i,sel in enumerate(idx):\n            if not isinstance(sel, list):\n                sel = [sel]\n            subranges = sel_to_subranges(sel)\n            ranges.append(subranges)\n\n        # extend the list to ndim\n        if len(ranges) < ndim:\n            ranges.extend([ tuple() for _ in range(ndim-len(ranges))])\n\n        rval = tuple(ranges)\n        return rval\n\n    def __getitem__(self, idx):\n        # implements multi-range / outer / orthogonal indexing\n        ranges = self.getitem_ranges(idx)\n\n        dom = self.schema.domain\n        attr_names = tuple(self.schema.attr(i).name for i in range(self.schema.nattr))\n\n        coords = None\n        if self.query is not None:\n            # if we are called via Query object, then we need to respect Query semantics\n            attr_names = tuple(self.query.attrs) if self.query.attrs else attr_names # query.attrs might be None -> all\n            coords = self.query.coords\n\n        # TODO order\n        result_dict = multi_index(\n            self.array,\n            attr_names,\n            ranges,\n            coords=coords\n        )\n\n        if self.schema.sparse:\n            return result_dict\n        else:\n            result_shape = mr_dense_result_shape(ranges, self.schema.shape)\n            for arr in result_dict.values():\n                # TODO check/test layout\n                arr.shape = result_shape\n            return result_dict", "path": "tiledb/multirange_indexing.py"}]}