We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
datacube dataset index incorrect comparison of geometry
linked issue: https://github.com/opendatacube/odc-tools/issues/479 

### Expected behaviour
when performing `--update-if-exists` `geometry.coordinates` and `grid_spatial.projection.valid_data.coordinates` shouldn't mark `unsafe` when the values are identical but the type is a tuple

### Actual behaviour

```
root@546115d5f0ab:/# s3-to-dc --stac --update-if-exists ss3://sentinel-cogs/sentinel-s2-l2a-cogs/4/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/*.json s2_l2a
Usage: s3-to-dc [OPTIONS] URI PRODUCT
Try 's3-to-dc --help' for help.

Error: No such option: --update-if-exist (Possible options: --update, --update-if-exists)
root@546115d5f0ab:/# s3-to-dc --stac --update-if-exists s3://sentinel-cogs/sentinel-s2-l2a-cogs/42/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/*.json s2_l2a
Unsafe change in geometry.coordinates from [[[300001.0, 4590241.0], [300001.0, 4700039.0], [406734.0, 4700039.0], [383003.0, 4620099.0], [373985.0, 4590241.0], [300001.0, 4590241.0]]] to (((300001.0, 4590241.0), (300001.0, 4700039.0), (406734.0, 4700039.0), (383003.0, 4620099.0), (373985.0, 4590241.0), (300001.0, 4590241.0)),)
Unsafe change in grid_spatial.projection.valid_data.coordinates from [[[300001.0, 4590241.0], [300001.0, 4700039.0], [406734.0, 4700039.0], [383003.0, 4620099.0], [373985.0, 4590241.0], [300001.0, 4590241.0]]] to (((300001.0, 4590241.0), (300001.0, 4700039.0), (406734.0, 4700039.0), (383003.0, 4620099.0), (373985.0, 4590241.0), (300001.0, 4590241.0)),)
ERROR:root:Failed to index dataset s3://sentinel-cogs/sentinel-s2-l2a-cogs/42/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/S2A_42TUM_20220102_0_L2A.json with error Updating the dataset raised an exception: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/utils.py", line 163, in index_update_dataset
    dc.index.datasets.update(ds, updates_allowed=updates)
  File "/usr/local/lib/python3.8/dist-packages/datacube/index/_datasets.py", line 302, in update
    raise ValueError(f"Unsafe changes in {dataset.id}: " + (
ValueError: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/s3_to_dc.py", line 58, in dump_to_odc
    index_update_dataset(metadata, uri, dc, doc2ds, update, update_if_exists, allow_unsafe)
  File "/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/utils.py", line 165, in index_update_dataset
    raise IndexingException(
odc.apps.dc_tools.utils.IndexingException: Updating the dataset raised an exception: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates
Added 0 datasets and failed 1 datasets.
```

### Steps to reproduce the behaviour

1. add a product https://explorer.digitalearth.africa/products/s2_l2a.odc-product.yaml
2. bring up datacube-index to run `s3-to-dc --stac`, OR
3. create a yaml dataset with the same stac json content
4. index first
5. then run `--update-if-exists`

### Environment information

* Which ``datacube --version`` are you using?
```
root@546115d5f0ab:/# datacube --version
Open Data Cube core, version 1.8.6
```

* What datacube deployment/enviornment are you running against?
- datacube-index docker container

> **Note:** Stale issues will be automatically closed after a period of six months with no activity. 
> To ensure critical issues are not closed, tag them with the Github `pinned` tag.
> If you are a community member and not a maintainer please escalate this issue to maintainers via
> [GIS StackExchange](https://gis.stackexchange.com/questions/tagged/open-data-cube) or [Slack](http://slack.opendatacube.org).


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `datacube/utils/changes.py`
Content:
```
1 # This file is part of the Open Data Cube, see https://opendatacube.org for more information
2 #
3 # Copyright (c) 2015-2020 ODC Contributors
4 # SPDX-License-Identifier: Apache-2.0
5 """
6 Validation of document/dictionary changes.
7 """
8 from itertools import zip_longest
9 from typing import Any, Callable, List, Mapping, Sequence, Tuple, Union
10 
11 # Type that can be checked for changes.
12 # (MyPy approximation without recursive references)
13 Changable = Union[str, int, None, Sequence[Any], Mapping[str, Any]]
14 # More accurate recursive definition:
15 # Changable = Union[str, int, None, Sequence["Changable"], Mapping[str, "Changable"]]
16 
17 def contains(v1: Changable, v2: Changable, case_sensitive: bool = False) -> bool:
18     """
19     Check that v1 is a superset of v2.
20 
21     For dicts contains(v1[k], v2[k]) for all k in v2
22     For other types v1 == v2
23     v2 None is interpreted as {}
24 
25     """
26     if not case_sensitive:
27         if isinstance(v1, str):
28             return isinstance(v2, str) and v1.lower() == v2.lower()
29 
30     if isinstance(v1, dict):
31         return v2 is None or (isinstance(v2, dict) and
32                               all(contains(v1.get(k, object()), v, case_sensitive=case_sensitive)
33                                   for k, v in v2.items()))
34 
35     return v1 == v2
36 
37 
38 class MissingSentinel:
39     def __str__(self):
40         return "missing"
41 
42     def __repr__(self):
43         return "missing"
44 
45 
46 MISSING = MissingSentinel()
47 
48 # Representation of an offset in a dict structure
49 OffsetElem = Union[str, int]
50 Offset = Tuple[OffsetElem, ...]
51 
52 # Representation of a changed value
53 ChangedValue = Union[MissingSentinel, Changable]
54 
55 # Representation of a change
56 Change = Tuple[Offset, ChangedValue, ChangedValue]
57 
58 
59 def get_doc_changes(original: Changable,
60                     new: Changable,
61                     base_prefix: Offset = ()
62                    ) -> List[Change]:
63     """
64     Return a list of `changed fields` between two dict structures.
65 
66     A `changed field` is represented by a 3-tuple made up of:
67 
68     1. `offset` to the change - a tuple of `item` accessors on the document.
69     2. What is in `original` - Either a single value, a dict or list, or :data:`MISSING`.
70     3. What is in `new`
71 
72     If the documents are identical, an empty list is returned.
73 
74     :type original: Union[dict, list, int]
75     :rtype: list[(tuple, object, object)]
76 
77 
78     """
79     changed_fields: List[Change] = []
80     if original == new:
81         return changed_fields
82 
83     if isinstance(original, dict) and isinstance(new, dict):
84         all_keys = set(original.keys()).union(new.keys())
85         for key in all_keys:
86             changed_fields.extend(get_doc_changes(original.get(key, MISSING),
87                                                   new.get(key, MISSING),
88                                                   base_prefix + (key,)))
89     elif isinstance(original, list) and isinstance(new, list):
90         for idx, (orig_item, new_item) in enumerate(zip_longest(original, new)):
91             changed_fields.extend(get_doc_changes(orig_item, new_item, base_prefix + (idx, )))
92     else:
93         changed_fields.append((base_prefix, original, new))
94 
95     return sorted(changed_fields, key=lambda a: a[0])
96 
97 
98 class DocumentMismatchError(Exception):
99     pass
100 
101 
102 def check_doc_unchanged(original: Changable, new: Changable, doc_name: str) -> None:
103     """
104     Raise an error if any fields have been modified on a document.
105 
106     :param original: original document
107     :param new: new document to compare against the original
108     :param doc_name: Label used to name the document
109     """
110     changes = get_doc_changes(original, new)
111 
112     if changes:
113         raise DocumentMismatchError(
114             '{} differs from stored ({})'.format(
115                 doc_name,
116                 ', '.join(['{}: {!r}!={!r}'.format('.'.join(map(str, offset)), v1, v2) for offset, v1, v2 in changes])
117             )
118         )
119 
120 
121 AllowPolicy = Callable[[Offset, Offset, ChangedValue, ChangedValue], bool]
122 
123 def allow_truncation(key: Offset, offset: Offset,
124                      old_value: ChangedValue, new_value: ChangedValue) -> bool:
125     return bool(offset) and key == offset[:-1] and new_value == MISSING
126 
127 
128 def allow_extension(key: Offset, offset: Offset,
129                     old_value: ChangedValue, new_value: ChangedValue) -> bool:
130     return bool(offset) and key == offset[:-1] and old_value == MISSING
131 
132 
133 def allow_addition(key: Offset, offset: Offset,
134                    old_value: ChangedValue, new_value: ChangedValue) -> bool:
135     return key == offset and old_value == MISSING
136 
137 
138 def allow_removal(key: Offset, offset: Offset,
139                   old_value: ChangedValue, new_value: ChangedValue) -> bool:
140     return key == offset and new_value == MISSING
141 
142 
143 def allow_any(key: Offset, offset: Offset,
144               old: ChangedValue, new: ChangedValue) -> bool:
145     return True
146 
147 
148 def classify_changes(changes: List[Change], allowed_changes: Mapping[Offset, AllowPolicy]
149                     ) -> Tuple[List[Change], List[Change]]:
150     """
151     Classify list of changes into good(allowed) and bad(not allowed) based on allowed changes.
152 
153     :param list[(tuple,object,object)] changes: result of get_doc_changes
154     :param allowed_changes: mapping from key to change policy (subset, superset, any)
155     :return: good_changes, bad_chages
156     """
157     allowed_changes_index = dict(allowed_changes)
158 
159     good_changes: List[Change] = []
160     bad_changes: List[Change] = []
161 
162     for offset, old_val, new_val in changes:
163         allowance = allowed_changes_index.get(offset)
164         allowance_offset = offset
165         # If no allowance on this leaf, find if any parents have allowances.
166         while allowance is None:
167             if not allowance_offset:
168                 break
169 
170             allowance_offset = allowance_offset[:-1]
171             allowance = allowed_changes_index.get(allowance_offset)
172 
173         if allowance is None:
174             bad_changes.append((offset, old_val, new_val))
175         elif hasattr(allowance, '__call__'):
176             if allowance(allowance_offset, offset, old_val, new_val):
177                 good_changes.append((offset, old_val, new_val))
178             else:
179                 bad_changes.append((offset, old_val, new_val))
180         else:
181             raise RuntimeError('Unknown change type: expecting validation function at %r' % offset)
182 
183     return good_changes, bad_changes
184
```
--- 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/datacube/utils/changes.py b/datacube/utils/changes.py
--- a/datacube/utils/changes.py
+++ b/datacube/utils/changes.py
@@ -5,6 +5,8 @@
 """
 Validation of document/dictionary changes.
 """
+import numpy
+
 from itertools import zip_longest
 from typing import Any, Callable, List, Mapping, Sequence, Tuple, Union
 
@@ -14,6 +16,7 @@
 # More accurate recursive definition:
 # Changable = Union[str, int, None, Sequence["Changable"], Mapping[str, "Changable"]]
 
+
 def contains(v1: Changable, v2: Changable, case_sensitive: bool = False) -> bool:
     """
     Check that v1 is a superset of v2.
@@ -59,7 +62,7 @@
 def get_doc_changes(original: Changable,
                     new: Changable,
                     base_prefix: Offset = ()
-                   ) -> List[Change]:
+                    ) -> List[Change]:
     """
     Return a list of `changed fields` between two dict structures.
 
@@ -89,6 +92,9 @@
     elif isinstance(original, list) and isinstance(new, list):
         for idx, (orig_item, new_item) in enumerate(zip_longest(original, new)):
             changed_fields.extend(get_doc_changes(orig_item, new_item, base_prefix + (idx, )))
+    elif isinstance(original, tuple) or isinstance(new, tuple):
+        if not numpy.array_equal(original, new):
+            changed_fields.append((base_prefix, original, new))
     else:
         changed_fields.append((base_prefix, original, new))
 
@@ -120,6 +126,7 @@
 
 AllowPolicy = Callable[[Offset, Offset, ChangedValue, ChangedValue], bool]
 
+
 def allow_truncation(key: Offset, offset: Offset,
                      old_value: ChangedValue, new_value: ChangedValue) -> bool:
     return bool(offset) and key == offset[:-1] and new_value == MISSING
@@ -146,7 +153,7 @@
 
 
 def classify_changes(changes: List[Change], allowed_changes: Mapping[Offset, AllowPolicy]
-                    ) -> Tuple[List[Change], List[Change]]:
+                     ) -> Tuple[List[Change], List[Change]]:
     """
     Classify list of changes into good(allowed) and bad(not allowed) based on allowed changes.

{"golden_diff": "diff --git a/datacube/utils/changes.py b/datacube/utils/changes.py\n--- a/datacube/utils/changes.py\n+++ b/datacube/utils/changes.py\n@@ -5,6 +5,8 @@\n \"\"\"\n Validation of document/dictionary changes.\n \"\"\"\n+import numpy\n+\n from itertools import zip_longest\n from typing import Any, Callable, List, Mapping, Sequence, Tuple, Union\n \n@@ -14,6 +16,7 @@\n # More accurate recursive definition:\n # Changable = Union[str, int, None, Sequence[\"Changable\"], Mapping[str, \"Changable\"]]\n \n+\n def contains(v1: Changable, v2: Changable, case_sensitive: bool = False) -> bool:\n     \"\"\"\n     Check that v1 is a superset of v2.\n@@ -59,7 +62,7 @@\n def get_doc_changes(original: Changable,\n                     new: Changable,\n                     base_prefix: Offset = ()\n-                   ) -> List[Change]:\n+                    ) -> List[Change]:\n     \"\"\"\n     Return a list of `changed fields` between two dict structures.\n \n@@ -89,6 +92,9 @@\n     elif isinstance(original, list) and isinstance(new, list):\n         for idx, (orig_item, new_item) in enumerate(zip_longest(original, new)):\n             changed_fields.extend(get_doc_changes(orig_item, new_item, base_prefix + (idx, )))\n+    elif isinstance(original, tuple) or isinstance(new, tuple):\n+        if not numpy.array_equal(original, new):\n+            changed_fields.append((base_prefix, original, new))\n     else:\n         changed_fields.append((base_prefix, original, new))\n \n@@ -120,6 +126,7 @@\n \n AllowPolicy = Callable[[Offset, Offset, ChangedValue, ChangedValue], bool]\n \n+\n def allow_truncation(key: Offset, offset: Offset,\n                      old_value: ChangedValue, new_value: ChangedValue) -> bool:\n     return bool(offset) and key == offset[:-1] and new_value == MISSING\n@@ -146,7 +153,7 @@\n \n \n def classify_changes(changes: List[Change], allowed_changes: Mapping[Offset, AllowPolicy]\n-                    ) -> Tuple[List[Change], List[Change]]:\n+                     ) -> Tuple[List[Change], List[Change]]:\n     \"\"\"\n     Classify list of changes into good(allowed) and bad(not allowed) based on allowed changes.\n", "issue": "datacube dataset index incorrect comparison of geometry\nlinked issue: https://github.com/opendatacube/odc-tools/issues/479 \r\n\r\n### Expected behaviour\r\nwhen performing `--update-if-exists` `geometry.coordinates` and `grid_spatial.projection.valid_data.coordinates` shouldn't mark `unsafe` when the values are identical but the type is a tuple\r\n\r\n### Actual behaviour\r\n\r\n```\r\nroot@546115d5f0ab:/# s3-to-dc --stac --update-if-exists ss3://sentinel-cogs/sentinel-s2-l2a-cogs/4/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/*.json s2_l2a\r\nUsage: s3-to-dc [OPTIONS] URI PRODUCT\r\nTry 's3-to-dc --help' for help.\r\n\r\nError: No such option: --update-if-exist (Possible options: --update, --update-if-exists)\r\nroot@546115d5f0ab:/# s3-to-dc --stac --update-if-exists s3://sentinel-cogs/sentinel-s2-l2a-cogs/42/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/*.json s2_l2a\r\nUnsafe change in geometry.coordinates from [[[300001.0, 4590241.0], [300001.0, 4700039.0], [406734.0, 4700039.0], [383003.0, 4620099.0], [373985.0, 4590241.0], [300001.0, 4590241.0]]] to (((300001.0, 4590241.0), (300001.0, 4700039.0), (406734.0, 4700039.0), (383003.0, 4620099.0), (373985.0, 4590241.0), (300001.0, 4590241.0)),)\r\nUnsafe change in grid_spatial.projection.valid_data.coordinates from [[[300001.0, 4590241.0], [300001.0, 4700039.0], [406734.0, 4700039.0], [383003.0, 4620099.0], [373985.0, 4590241.0], [300001.0, 4590241.0]]] to (((300001.0, 4590241.0), (300001.0, 4700039.0), (406734.0, 4700039.0), (383003.0, 4620099.0), (373985.0, 4590241.0), (300001.0, 4590241.0)),)\r\nERROR:root:Failed to index dataset s3://sentinel-cogs/sentinel-s2-l2a-cogs/42/T/UM/2022/1/S2A_42TUM_20220102_0_L2A/S2A_42TUM_20220102_0_L2A.json with error Updating the dataset raised an exception: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates\r\nTraceback (most recent call last):\r\n  File \"/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/utils.py\", line 163, in index_update_dataset\r\n    dc.index.datasets.update(ds, updates_allowed=updates)\r\n  File \"/usr/local/lib/python3.8/dist-packages/datacube/index/_datasets.py\", line 302, in update\r\n    raise ValueError(f\"Unsafe changes in {dataset.id}: \" + (\r\nValueError: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates\r\n\r\nDuring handling of the above exception, another exception occurred:\r\n\r\nTraceback (most recent call last):\r\n  File \"/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/s3_to_dc.py\", line 58, in dump_to_odc\r\n    index_update_dataset(metadata, uri, dc, doc2ds, update, update_if_exists, allow_unsafe)\r\n  File \"/usr/local/lib/python3.8/dist-packages/odc/apps/dc_tools/utils.py\", line 165, in index_update_dataset\r\n    raise IndexingException(\r\nodc.apps.dc_tools.utils.IndexingException: Updating the dataset raised an exception: Unsafe changes in 22250846-0ba8-5ee5-8dbe-38c2adc1f4ab: geometry.coordinates, grid_spatial.projection.valid_data.coordinates\r\nAdded 0 datasets and failed 1 datasets.\r\n```\r\n\r\n### Steps to reproduce the behaviour\r\n\r\n1. add a product https://explorer.digitalearth.africa/products/s2_l2a.odc-product.yaml\r\n2. bring up datacube-index to run `s3-to-dc --stac`, OR\r\n3. create a yaml dataset with the same stac json content\r\n4. index first\r\n5. then run `--update-if-exists`\r\n\r\n### Environment information\r\n\r\n* Which ``datacube --version`` are you using?\r\n```\r\nroot@546115d5f0ab:/# datacube --version\r\nOpen Data Cube core, version 1.8.6\r\n```\r\n\r\n* What datacube deployment/enviornment are you running against?\r\n- datacube-index docker container\r\n\r\n> **Note:** Stale issues will be automatically closed after a period of six months with no activity. \r\n> To ensure critical issues are not closed, tag them with the Github `pinned` tag.\r\n> If you are a community member and not a maintainer please escalate this issue to maintainers via\r\n> [GIS StackExchange](https://gis.stackexchange.com/questions/tagged/open-data-cube) or [Slack](http://slack.opendatacube.org).\r\n\n", "before_files": [{"content": "# This file is part of the Open Data Cube, see https://opendatacube.org for more information\n#\n# Copyright (c) 2015-2020 ODC Contributors\n# SPDX-License-Identifier: Apache-2.0\n\"\"\"\nValidation of document/dictionary changes.\n\"\"\"\nfrom itertools import zip_longest\nfrom typing import Any, Callable, List, Mapping, Sequence, Tuple, Union\n\n# Type that can be checked for changes.\n# (MyPy approximation without recursive references)\nChangable = Union[str, int, None, Sequence[Any], Mapping[str, Any]]\n# More accurate recursive definition:\n# Changable = Union[str, int, None, Sequence[\"Changable\"], Mapping[str, \"Changable\"]]\n\ndef contains(v1: Changable, v2: Changable, case_sensitive: bool = False) -> bool:\n    \"\"\"\n    Check that v1 is a superset of v2.\n\n    For dicts contains(v1[k], v2[k]) for all k in v2\n    For other types v1 == v2\n    v2 None is interpreted as {}\n\n    \"\"\"\n    if not case_sensitive:\n        if isinstance(v1, str):\n            return isinstance(v2, str) and v1.lower() == v2.lower()\n\n    if isinstance(v1, dict):\n        return v2 is None or (isinstance(v2, dict) and\n                              all(contains(v1.get(k, object()), v, case_sensitive=case_sensitive)\n                                  for k, v in v2.items()))\n\n    return v1 == v2\n\n\nclass MissingSentinel:\n    def __str__(self):\n        return \"missing\"\n\n    def __repr__(self):\n        return \"missing\"\n\n\nMISSING = MissingSentinel()\n\n# Representation of an offset in a dict structure\nOffsetElem = Union[str, int]\nOffset = Tuple[OffsetElem, ...]\n\n# Representation of a changed value\nChangedValue = Union[MissingSentinel, Changable]\n\n# Representation of a change\nChange = Tuple[Offset, ChangedValue, ChangedValue]\n\n\ndef get_doc_changes(original: Changable,\n                    new: Changable,\n                    base_prefix: Offset = ()\n                   ) -> List[Change]:\n    \"\"\"\n    Return a list of `changed fields` between two dict structures.\n\n    A `changed field` is represented by a 3-tuple made up of:\n\n    1. `offset` to the change - a tuple of `item` accessors on the document.\n    2. What is in `original` - Either a single value, a dict or list, or :data:`MISSING`.\n    3. What is in `new`\n\n    If the documents are identical, an empty list is returned.\n\n    :type original: Union[dict, list, int]\n    :rtype: list[(tuple, object, object)]\n\n\n    \"\"\"\n    changed_fields: List[Change] = []\n    if original == new:\n        return changed_fields\n\n    if isinstance(original, dict) and isinstance(new, dict):\n        all_keys = set(original.keys()).union(new.keys())\n        for key in all_keys:\n            changed_fields.extend(get_doc_changes(original.get(key, MISSING),\n                                                  new.get(key, MISSING),\n                                                  base_prefix + (key,)))\n    elif isinstance(original, list) and isinstance(new, list):\n        for idx, (orig_item, new_item) in enumerate(zip_longest(original, new)):\n            changed_fields.extend(get_doc_changes(orig_item, new_item, base_prefix + (idx, )))\n    else:\n        changed_fields.append((base_prefix, original, new))\n\n    return sorted(changed_fields, key=lambda a: a[0])\n\n\nclass DocumentMismatchError(Exception):\n    pass\n\n\ndef check_doc_unchanged(original: Changable, new: Changable, doc_name: str) -> None:\n    \"\"\"\n    Raise an error if any fields have been modified on a document.\n\n    :param original: original document\n    :param new: new document to compare against the original\n    :param doc_name: Label used to name the document\n    \"\"\"\n    changes = get_doc_changes(original, new)\n\n    if changes:\n        raise DocumentMismatchError(\n            '{} differs from stored ({})'.format(\n                doc_name,\n                ', '.join(['{}: {!r}!={!r}'.format('.'.join(map(str, offset)), v1, v2) for offset, v1, v2 in changes])\n            )\n        )\n\n\nAllowPolicy = Callable[[Offset, Offset, ChangedValue, ChangedValue], bool]\n\ndef allow_truncation(key: Offset, offset: Offset,\n                     old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return bool(offset) and key == offset[:-1] and new_value == MISSING\n\n\ndef allow_extension(key: Offset, offset: Offset,\n                    old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return bool(offset) and key == offset[:-1] and old_value == MISSING\n\n\ndef allow_addition(key: Offset, offset: Offset,\n                   old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return key == offset and old_value == MISSING\n\n\ndef allow_removal(key: Offset, offset: Offset,\n                  old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return key == offset and new_value == MISSING\n\n\ndef allow_any(key: Offset, offset: Offset,\n              old: ChangedValue, new: ChangedValue) -> bool:\n    return True\n\n\ndef classify_changes(changes: List[Change], allowed_changes: Mapping[Offset, AllowPolicy]\n                    ) -> Tuple[List[Change], List[Change]]:\n    \"\"\"\n    Classify list of changes into good(allowed) and bad(not allowed) based on allowed changes.\n\n    :param list[(tuple,object,object)] changes: result of get_doc_changes\n    :param allowed_changes: mapping from key to change policy (subset, superset, any)\n    :return: good_changes, bad_chages\n    \"\"\"\n    allowed_changes_index = dict(allowed_changes)\n\n    good_changes: List[Change] = []\n    bad_changes: List[Change] = []\n\n    for offset, old_val, new_val in changes:\n        allowance = allowed_changes_index.get(offset)\n        allowance_offset = offset\n        # If no allowance on this leaf, find if any parents have allowances.\n        while allowance is None:\n            if not allowance_offset:\n                break\n\n            allowance_offset = allowance_offset[:-1]\n            allowance = allowed_changes_index.get(allowance_offset)\n\n        if allowance is None:\n            bad_changes.append((offset, old_val, new_val))\n        elif hasattr(allowance, '__call__'):\n            if allowance(allowance_offset, offset, old_val, new_val):\n                good_changes.append((offset, old_val, new_val))\n            else:\n                bad_changes.append((offset, old_val, new_val))\n        else:\n            raise RuntimeError('Unknown change type: expecting validation function at %r' % offset)\n\n    return good_changes, bad_changes\n", "path": "datacube/utils/changes.py"}], "after_files": [{"content": "# This file is part of the Open Data Cube, see https://opendatacube.org for more information\n#\n# Copyright (c) 2015-2020 ODC Contributors\n# SPDX-License-Identifier: Apache-2.0\n\"\"\"\nValidation of document/dictionary changes.\n\"\"\"\nimport numpy\n\nfrom itertools import zip_longest\nfrom typing import Any, Callable, List, Mapping, Sequence, Tuple, Union\n\n# Type that can be checked for changes.\n# (MyPy approximation without recursive references)\nChangable = Union[str, int, None, Sequence[Any], Mapping[str, Any]]\n# More accurate recursive definition:\n# Changable = Union[str, int, None, Sequence[\"Changable\"], Mapping[str, \"Changable\"]]\n\n\ndef contains(v1: Changable, v2: Changable, case_sensitive: bool = False) -> bool:\n    \"\"\"\n    Check that v1 is a superset of v2.\n\n    For dicts contains(v1[k], v2[k]) for all k in v2\n    For other types v1 == v2\n    v2 None is interpreted as {}\n\n    \"\"\"\n    if not case_sensitive:\n        if isinstance(v1, str):\n            return isinstance(v2, str) and v1.lower() == v2.lower()\n\n    if isinstance(v1, dict):\n        return v2 is None or (isinstance(v2, dict) and\n                              all(contains(v1.get(k, object()), v, case_sensitive=case_sensitive)\n                                  for k, v in v2.items()))\n\n    return v1 == v2\n\n\nclass MissingSentinel:\n    def __str__(self):\n        return \"missing\"\n\n    def __repr__(self):\n        return \"missing\"\n\n\nMISSING = MissingSentinel()\n\n# Representation of an offset in a dict structure\nOffsetElem = Union[str, int]\nOffset = Tuple[OffsetElem, ...]\n\n# Representation of a changed value\nChangedValue = Union[MissingSentinel, Changable]\n\n# Representation of a change\nChange = Tuple[Offset, ChangedValue, ChangedValue]\n\n\ndef get_doc_changes(original: Changable,\n                    new: Changable,\n                    base_prefix: Offset = ()\n                    ) -> List[Change]:\n    \"\"\"\n    Return a list of `changed fields` between two dict structures.\n\n    A `changed field` is represented by a 3-tuple made up of:\n\n    1. `offset` to the change - a tuple of `item` accessors on the document.\n    2. What is in `original` - Either a single value, a dict or list, or :data:`MISSING`.\n    3. What is in `new`\n\n    If the documents are identical, an empty list is returned.\n\n    :type original: Union[dict, list, int]\n    :rtype: list[(tuple, object, object)]\n\n\n    \"\"\"\n    changed_fields: List[Change] = []\n    if original == new:\n        return changed_fields\n\n    if isinstance(original, dict) and isinstance(new, dict):\n        all_keys = set(original.keys()).union(new.keys())\n        for key in all_keys:\n            changed_fields.extend(get_doc_changes(original.get(key, MISSING),\n                                                  new.get(key, MISSING),\n                                                  base_prefix + (key,)))\n    elif isinstance(original, list) and isinstance(new, list):\n        for idx, (orig_item, new_item) in enumerate(zip_longest(original, new)):\n            changed_fields.extend(get_doc_changes(orig_item, new_item, base_prefix + (idx, )))\n    elif isinstance(original, tuple) or isinstance(new, tuple):\n        if not numpy.array_equal(original, new):\n            changed_fields.append((base_prefix, original, new))\n    else:\n        changed_fields.append((base_prefix, original, new))\n\n    return sorted(changed_fields, key=lambda a: a[0])\n\n\nclass DocumentMismatchError(Exception):\n    pass\n\n\ndef check_doc_unchanged(original: Changable, new: Changable, doc_name: str) -> None:\n    \"\"\"\n    Raise an error if any fields have been modified on a document.\n\n    :param original: original document\n    :param new: new document to compare against the original\n    :param doc_name: Label used to name the document\n    \"\"\"\n    changes = get_doc_changes(original, new)\n\n    if changes:\n        raise DocumentMismatchError(\n            '{} differs from stored ({})'.format(\n                doc_name,\n                ', '.join(['{}: {!r}!={!r}'.format('.'.join(map(str, offset)), v1, v2) for offset, v1, v2 in changes])\n            )\n        )\n\n\nAllowPolicy = Callable[[Offset, Offset, ChangedValue, ChangedValue], bool]\n\n\ndef allow_truncation(key: Offset, offset: Offset,\n                     old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return bool(offset) and key == offset[:-1] and new_value == MISSING\n\n\ndef allow_extension(key: Offset, offset: Offset,\n                    old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return bool(offset) and key == offset[:-1] and old_value == MISSING\n\n\ndef allow_addition(key: Offset, offset: Offset,\n                   old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return key == offset and old_value == MISSING\n\n\ndef allow_removal(key: Offset, offset: Offset,\n                  old_value: ChangedValue, new_value: ChangedValue) -> bool:\n    return key == offset and new_value == MISSING\n\n\ndef allow_any(key: Offset, offset: Offset,\n              old: ChangedValue, new: ChangedValue) -> bool:\n    return True\n\n\ndef classify_changes(changes: List[Change], allowed_changes: Mapping[Offset, AllowPolicy]\n                     ) -> Tuple[List[Change], List[Change]]:\n    \"\"\"\n    Classify list of changes into good(allowed) and bad(not allowed) based on allowed changes.\n\n    :param list[(tuple,object,object)] changes: result of get_doc_changes\n    :param allowed_changes: mapping from key to change policy (subset, superset, any)\n    :return: good_changes, bad_chages\n    \"\"\"\n    allowed_changes_index = dict(allowed_changes)\n\n    good_changes: List[Change] = []\n    bad_changes: List[Change] = []\n\n    for offset, old_val, new_val in changes:\n        allowance = allowed_changes_index.get(offset)\n        allowance_offset = offset\n        # If no allowance on this leaf, find if any parents have allowances.\n        while allowance is None:\n            if not allowance_offset:\n                break\n\n            allowance_offset = allowance_offset[:-1]\n            allowance = allowed_changes_index.get(allowance_offset)\n\n        if allowance is None:\n            bad_changes.append((offset, old_val, new_val))\n        elif hasattr(allowance, '__call__'):\n            if allowance(allowance_offset, offset, old_val, new_val):\n                good_changes.append((offset, old_val, new_val))\n            else:\n                bad_changes.append((offset, old_val, new_val))\n        else:\n            raise RuntimeError('Unknown change type: expecting validation function at %r' % offset)\n\n    return good_changes, bad_changes\n", "path": "datacube/utils/changes.py"}]}