We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
write_geotiff helper function fails if CRS is string, not object
### Expected behaviour
The write_geotiff helper function assumes that you will pass the function a datacube crs object. If you are writing out a geotiff from data that was not loaded using `dc.load`, this crs object is not present. 
E.g. I read a Geotiff file produced by datacube-stats into a Notebook, ran some analysis on it, and wanted to write it back out to Geotiff. I have a crs string from the attributes of my original Geotiff, but no datacube crs object, so the write_geotiff function fails.

### Error
```
AttributeError                            Traceback (most recent call last)
<ipython-input-41-736bab55bae5> in <module>()
      3 Differenceds.attrs['crs'] = (GeotiffData.crs)
      4 
----> 5 write_geotiff(PercentileConfidence, Differenceds)

/g/data/v10/public/modules/dea/20180515/lib/python3.6/site-packages/datacube/helpers.py in write_geotiff(filename, dataset, profile_override, time_index)
     44     profile = DEFAULT_PROFILE.copy()
     45     profile.update({
---> 46         'width': dataset.dims[dataset.crs.dimensions[1]],
     47         'height': dataset.dims[dataset.crs.dimensions[0]],
     48         'transform': dataset.affine,

AttributeError: 'str' object has no attribute 'dimensions'
```

--- 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/helpers.py`
Content:
```
1 """
2 Useful functions for Datacube users
3 
4 Not used internally, those should go in `utils.py`
5 """
6 
7 import numpy as np
8 import rasterio
9 
10 DEFAULT_PROFILE = {
11     'blockxsize': 256,
12     'blockysize': 256,
13     'compress': 'lzw',
14     'driver': 'GTiff',
15     'interleave': 'band',
16     'nodata': 0.0,
17     'tiled': True}
18 
19 
20 def write_geotiff(filename, dataset, profile_override=None, time_index=None):
21     """
22     Write an ODC style xarray.Dataset to a GeoTIFF file.
23 
24     :param filename: Output filename
25     :param dataset: xarray dataset containing one or more bands to write to a file.
26     :param profile_override: option dict, overrides rasterio file creation options.
27     :param time_index: DEPRECATED
28     """
29     profile_override = profile_override or {}
30 
31     if time_index is not None:
32         raise ValueError('''The write_geotiff function no longer supports passing in `time_index`.
33         The same function can be achieved by calling `dataset.isel(time=<time_index>)` before passing
34         in your dataset. It was removed because it made the function much less useful for more advanced cases.''')
35 
36     try:
37         dtypes = {val.dtype for val in dataset.data_vars.values()}
38         assert len(dtypes) == 1  # Check for multiple dtypes
39     except AttributeError:
40         dtypes = [dataset.dtype]
41 
42     profile = DEFAULT_PROFILE.copy()
43     profile.update({
44         'width': dataset.dims[dataset.crs.dimensions[1]],
45         'height': dataset.dims[dataset.crs.dimensions[0]],
46         'transform': dataset.affine,
47         'crs': dataset.crs.crs_str,
48         'count': len(dataset.data_vars),
49         'dtype': str(dtypes.pop())
50     })
51     profile.update(profile_override)
52 
53     _calculate_blocksize(profile)
54 
55     with rasterio.open(str(filename), 'w', **profile) as dest:
56         if hasattr(dataset, 'data_vars'):
57             for bandnum, data in enumerate(dataset.data_vars.values(), start=1):
58                 dest.write(data.data, bandnum)
59 
60 
61 def _calculate_blocksize(profile):
62     # Block size must be smaller than the image size, and for geotiffs must be divisible by 16
63     # Fix for small images.
64     if profile['blockxsize'] > profile['width']:
65         if profile['width'] % 16 == 0 or profile['width'] < 16:
66             profile['blockxsize'] = profile['width']
67         else:
68             profile['blockxsize'] = 16
69 
70     if profile['blockysize'] > profile['height']:
71         if profile['height'] % 16 == 0 or profile['height'] < 16:
72             profile['blockysize'] = profile['height']
73         else:
74             profile['blockysize'] = 16
75 
76 
77 def ga_pq_fuser(dest, src):
78     """
79     Fuse two Geoscience Australia Pixel Quality ndarrays
80 
81     To be used as a `fuse_func` when loaded `grouped` data, for example when grouping
82     by solar day to avoid duplicate data from scene overlaps.
83     """
84     valid_bit = 8
85     valid_val = (1 << valid_bit)
86 
87     no_data_dest_mask = ~(dest & valid_val).astype(bool)
88     np.copyto(dest, src, where=no_data_dest_mask)
89 
90     both_data_mask = (valid_val & dest & src).astype(bool)
91     np.copyto(dest, src & dest, where=both_data_mask)
92
```

Path: `datacube/utils/xarray_geoextensions.py`
Content:
```
1 """
2 Add geometric extensions to :class:`xarray.Dataset` and :class:`xarray.DataArray` for use
3 with Data Cube by Monkey Patching those classes.
4 
5 This extension is reliant on an `xarray` object having a `.crs` property of type
6 :class:`datacube.utils.geometry.CRS`. This is used to inspect the spatial dimensions of the
7 :class:`Dataset` or :class:`DataArray`, and provide new attributes for accessing a
8 :class:`datacube.utils.geometry.GeoBox`, affine transform and extent for the dataset as
9 `.geobox`, `.affine` and `.extent` respectively.
10 
11 """
12 
13 import xarray
14 from affine import Affine
15 
16 from datacube.utils import data_resolution_and_offset, geometry
17 
18 
19 def _xarray_affine(obj):
20     dims = obj.crs.dimensions
21     xres, xoff = data_resolution_and_offset(obj[dims[1]].values)
22     yres, yoff = data_resolution_and_offset(obj[dims[0]].values)
23     return Affine.translation(xoff, yoff) * Affine.scale(xres, yres)
24 
25 
26 def _xarray_extent(obj):
27     return obj.geobox.extent
28 
29 
30 def _xarray_geobox(obj):
31     dims = obj.crs.dimensions
32     return geometry.GeoBox(obj[dims[1]].size, obj[dims[0]].size, obj.affine, obj.crs)
33 
34 
35 xarray.Dataset.geobox = property(_xarray_geobox)
36 xarray.Dataset.affine = property(_xarray_affine)
37 xarray.Dataset.extent = property(_xarray_extent)
38 xarray.DataArray.geobox = property(_xarray_geobox)
39 xarray.DataArray.affine = property(_xarray_affine)
40 xarray.DataArray.extent = property(_xarray_extent)
41
```
--- 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/helpers.py b/datacube/helpers.py
--- a/datacube/helpers.py
+++ b/datacube/helpers.py
@@ -26,12 +26,17 @@
     :param profile_override: option dict, overrides rasterio file creation options.
     :param time_index: DEPRECATED
     """
-    profile_override = profile_override or {}
-
     if time_index is not None:
         raise ValueError('''The write_geotiff function no longer supports passing in `time_index`.
         The same function can be achieved by calling `dataset.isel(time=<time_index>)` before passing
-        in your dataset. It was removed because it made the function much less useful for more advanced cases.''')
+ in your dataset. It was removed because it made the function much less useful for more advanced cases.''')
+
+    profile_override = profile_override or {}
+
+    geobox = getattr(dataset, 'geobox', None)
+
+    if geobox is None:
+        raise ValueError('Can only write datasets with specified `crs` attribute')
 
     try:
         dtypes = {val.dtype for val in dataset.data_vars.values()}
@@ -40,11 +45,13 @@
         dtypes = [dataset.dtype]
 
     profile = DEFAULT_PROFILE.copy()
+    height, width = geobox.shape
+
     profile.update({
-        'width': dataset.dims[dataset.crs.dimensions[1]],
-        'height': dataset.dims[dataset.crs.dimensions[0]],
-        'transform': dataset.affine,
-        'crs': dataset.crs.crs_str,
+        'width': width,
+        'height': height,
+        'transform': geobox.affine,
+        'crs': geobox.crs.crs_str,
         'count': len(dataset.data_vars),
         'dtype': str(dtypes.pop())
     })
diff --git a/datacube/utils/xarray_geoextensions.py b/datacube/utils/xarray_geoextensions.py
--- a/datacube/utils/xarray_geoextensions.py
+++ b/datacube/utils/xarray_geoextensions.py
@@ -16,20 +16,38 @@
 from datacube.utils import data_resolution_and_offset, geometry
 
 
+def _norm_crs(crs):
+    if crs is None or isinstance(crs, geometry.CRS):
+        return crs
+    elif isinstance(crs, str):
+        return geometry.CRS(crs)
+    else:
+        raise ValueError('Can not interpret {} as CRS'.format(type(crs)))
+
+
 def _xarray_affine(obj):
-    dims = obj.crs.dimensions
+    crs = _norm_crs(obj.crs)
+    if crs is None:
+        return None
+
+    dims = crs.dimensions
     xres, xoff = data_resolution_and_offset(obj[dims[1]].values)
     yres, yoff = data_resolution_and_offset(obj[dims[0]].values)
     return Affine.translation(xoff, yoff) * Affine.scale(xres, yres)
 
 
 def _xarray_extent(obj):
-    return obj.geobox.extent
+    geobox = obj.geobox
+    return None if geobox is None else geobox.extent
 
 
 def _xarray_geobox(obj):
-    dims = obj.crs.dimensions
-    return geometry.GeoBox(obj[dims[1]].size, obj[dims[0]].size, obj.affine, obj.crs)
+    crs = _norm_crs(obj.crs)
+    if crs is None:
+        return None
+
+    dims = crs.dimensions
+    return geometry.GeoBox(obj[dims[1]].size, obj[dims[0]].size, obj.affine, crs)
 
 
 xarray.Dataset.geobox = property(_xarray_geobox)

{"golden_diff": "diff --git a/datacube/helpers.py b/datacube/helpers.py\n--- a/datacube/helpers.py\n+++ b/datacube/helpers.py\n@@ -26,12 +26,17 @@\n     :param profile_override: option dict, overrides rasterio file creation options.\n     :param time_index: DEPRECATED\n     \"\"\"\n-    profile_override = profile_override or {}\n-\n     if time_index is not None:\n         raise ValueError('''The write_geotiff function no longer supports passing in `time_index`.\n         The same function can be achieved by calling `dataset.isel(time=<time_index>)` before passing\n-        in your dataset. It was removed because it made the function much less useful for more advanced cases.''')\n+ in your dataset. It was removed because it made the function much less useful for more advanced cases.''')\n+\n+    profile_override = profile_override or {}\n+\n+    geobox = getattr(dataset, 'geobox', None)\n+\n+    if geobox is None:\n+        raise ValueError('Can only write datasets with specified `crs` attribute')\n \n     try:\n         dtypes = {val.dtype for val in dataset.data_vars.values()}\n@@ -40,11 +45,13 @@\n         dtypes = [dataset.dtype]\n \n     profile = DEFAULT_PROFILE.copy()\n+    height, width = geobox.shape\n+\n     profile.update({\n-        'width': dataset.dims[dataset.crs.dimensions[1]],\n-        'height': dataset.dims[dataset.crs.dimensions[0]],\n-        'transform': dataset.affine,\n-        'crs': dataset.crs.crs_str,\n+        'width': width,\n+        'height': height,\n+        'transform': geobox.affine,\n+        'crs': geobox.crs.crs_str,\n         'count': len(dataset.data_vars),\n         'dtype': str(dtypes.pop())\n     })\ndiff --git a/datacube/utils/xarray_geoextensions.py b/datacube/utils/xarray_geoextensions.py\n--- a/datacube/utils/xarray_geoextensions.py\n+++ b/datacube/utils/xarray_geoextensions.py\n@@ -16,20 +16,38 @@\n from datacube.utils import data_resolution_and_offset, geometry\n \n \n+def _norm_crs(crs):\n+    if crs is None or isinstance(crs, geometry.CRS):\n+        return crs\n+    elif isinstance(crs, str):\n+        return geometry.CRS(crs)\n+    else:\n+        raise ValueError('Can not interpret {} as CRS'.format(type(crs)))\n+\n+\n def _xarray_affine(obj):\n-    dims = obj.crs.dimensions\n+    crs = _norm_crs(obj.crs)\n+    if crs is None:\n+        return None\n+\n+    dims = crs.dimensions\n     xres, xoff = data_resolution_and_offset(obj[dims[1]].values)\n     yres, yoff = data_resolution_and_offset(obj[dims[0]].values)\n     return Affine.translation(xoff, yoff) * Affine.scale(xres, yres)\n \n \n def _xarray_extent(obj):\n-    return obj.geobox.extent\n+    geobox = obj.geobox\n+    return None if geobox is None else geobox.extent\n \n \n def _xarray_geobox(obj):\n-    dims = obj.crs.dimensions\n-    return geometry.GeoBox(obj[dims[1]].size, obj[dims[0]].size, obj.affine, obj.crs)\n+    crs = _norm_crs(obj.crs)\n+    if crs is None:\n+        return None\n+\n+    dims = crs.dimensions\n+    return geometry.GeoBox(obj[dims[1]].size, obj[dims[0]].size, obj.affine, crs)\n \n \n xarray.Dataset.geobox = property(_xarray_geobox)\n", "issue": "write_geotiff helper function fails if CRS is string, not object\n### Expected behaviour\r\nThe write_geotiff helper function assumes that you will pass the function a datacube crs object. If you are writing out a geotiff from data that was not loaded using `dc.load`, this crs object is not present. \r\nE.g. I read a Geotiff file produced by datacube-stats into a Notebook, ran some analysis on it, and wanted to write it back out to Geotiff. I have a crs string from the attributes of my original Geotiff, but no datacube crs object, so the write_geotiff function fails.\r\n\r\n### Error\r\n```\r\nAttributeError                            Traceback (most recent call last)\r\n<ipython-input-41-736bab55bae5> in <module>()\r\n      3 Differenceds.attrs['crs'] = (GeotiffData.crs)\r\n      4 \r\n----> 5 write_geotiff(PercentileConfidence, Differenceds)\r\n\r\n/g/data/v10/public/modules/dea/20180515/lib/python3.6/site-packages/datacube/helpers.py in write_geotiff(filename, dataset, profile_override, time_index)\r\n     44     profile = DEFAULT_PROFILE.copy()\r\n     45     profile.update({\r\n---> 46         'width': dataset.dims[dataset.crs.dimensions[1]],\r\n     47         'height': dataset.dims[dataset.crs.dimensions[0]],\r\n     48         'transform': dataset.affine,\r\n\r\nAttributeError: 'str' object has no attribute 'dimensions'\r\n```\n", "before_files": [{"content": "\"\"\"\nUseful functions for Datacube users\n\nNot used internally, those should go in `utils.py`\n\"\"\"\n\nimport numpy as np\nimport rasterio\n\nDEFAULT_PROFILE = {\n    'blockxsize': 256,\n    'blockysize': 256,\n    'compress': 'lzw',\n    'driver': 'GTiff',\n    'interleave': 'band',\n    'nodata': 0.0,\n    'tiled': True}\n\n\ndef write_geotiff(filename, dataset, profile_override=None, time_index=None):\n    \"\"\"\n    Write an ODC style xarray.Dataset to a GeoTIFF file.\n\n    :param filename: Output filename\n    :param dataset: xarray dataset containing one or more bands to write to a file.\n    :param profile_override: option dict, overrides rasterio file creation options.\n    :param time_index: DEPRECATED\n    \"\"\"\n    profile_override = profile_override or {}\n\n    if time_index is not None:\n        raise ValueError('''The write_geotiff function no longer supports passing in `time_index`.\n        The same function can be achieved by calling `dataset.isel(time=<time_index>)` before passing\n        in your dataset. It was removed because it made the function much less useful for more advanced cases.''')\n\n    try:\n        dtypes = {val.dtype for val in dataset.data_vars.values()}\n        assert len(dtypes) == 1  # Check for multiple dtypes\n    except AttributeError:\n        dtypes = [dataset.dtype]\n\n    profile = DEFAULT_PROFILE.copy()\n    profile.update({\n        'width': dataset.dims[dataset.crs.dimensions[1]],\n        'height': dataset.dims[dataset.crs.dimensions[0]],\n        'transform': dataset.affine,\n        'crs': dataset.crs.crs_str,\n        'count': len(dataset.data_vars),\n        'dtype': str(dtypes.pop())\n    })\n    profile.update(profile_override)\n\n    _calculate_blocksize(profile)\n\n    with rasterio.open(str(filename), 'w', **profile) as dest:\n        if hasattr(dataset, 'data_vars'):\n            for bandnum, data in enumerate(dataset.data_vars.values(), start=1):\n                dest.write(data.data, bandnum)\n\n\ndef _calculate_blocksize(profile):\n    # Block size must be smaller than the image size, and for geotiffs must be divisible by 16\n    # Fix for small images.\n    if profile['blockxsize'] > profile['width']:\n        if profile['width'] % 16 == 0 or profile['width'] < 16:\n            profile['blockxsize'] = profile['width']\n        else:\n            profile['blockxsize'] = 16\n\n    if profile['blockysize'] > profile['height']:\n        if profile['height'] % 16 == 0 or profile['height'] < 16:\n            profile['blockysize'] = profile['height']\n        else:\n            profile['blockysize'] = 16\n\n\ndef ga_pq_fuser(dest, src):\n    \"\"\"\n    Fuse two Geoscience Australia Pixel Quality ndarrays\n\n    To be used as a `fuse_func` when loaded `grouped` data, for example when grouping\n    by solar day to avoid duplicate data from scene overlaps.\n    \"\"\"\n    valid_bit = 8\n    valid_val = (1 << valid_bit)\n\n    no_data_dest_mask = ~(dest & valid_val).astype(bool)\n    np.copyto(dest, src, where=no_data_dest_mask)\n\n    both_data_mask = (valid_val & dest & src).astype(bool)\n    np.copyto(dest, src & dest, where=both_data_mask)\n", "path": "datacube/helpers.py"}, {"content": "\"\"\"\nAdd geometric extensions to :class:`xarray.Dataset` and :class:`xarray.DataArray` for use\nwith Data Cube by Monkey Patching those classes.\n\nThis extension is reliant on an `xarray` object having a `.crs` property of type\n:class:`datacube.utils.geometry.CRS`. This is used to inspect the spatial dimensions of the\n:class:`Dataset` or :class:`DataArray`, and provide new attributes for accessing a\n:class:`datacube.utils.geometry.GeoBox`, affine transform and extent for the dataset as\n`.geobox`, `.affine` and `.extent` respectively.\n\n\"\"\"\n\nimport xarray\nfrom affine import Affine\n\nfrom datacube.utils import data_resolution_and_offset, geometry\n\n\ndef _xarray_affine(obj):\n    dims = obj.crs.dimensions\n    xres, xoff = data_resolution_and_offset(obj[dims[1]].values)\n    yres, yoff = data_resolution_and_offset(obj[dims[0]].values)\n    return Affine.translation(xoff, yoff) * Affine.scale(xres, yres)\n\n\ndef _xarray_extent(obj):\n    return obj.geobox.extent\n\n\ndef _xarray_geobox(obj):\n    dims = obj.crs.dimensions\n    return geometry.GeoBox(obj[dims[1]].size, obj[dims[0]].size, obj.affine, obj.crs)\n\n\nxarray.Dataset.geobox = property(_xarray_geobox)\nxarray.Dataset.affine = property(_xarray_affine)\nxarray.Dataset.extent = property(_xarray_extent)\nxarray.DataArray.geobox = property(_xarray_geobox)\nxarray.DataArray.affine = property(_xarray_affine)\nxarray.DataArray.extent = property(_xarray_extent)\n", "path": "datacube/utils/xarray_geoextensions.py"}], "after_files": [{"content": "\"\"\"\nUseful functions for Datacube users\n\nNot used internally, those should go in `utils.py`\n\"\"\"\n\nimport numpy as np\nimport rasterio\n\nDEFAULT_PROFILE = {\n    'blockxsize': 256,\n    'blockysize': 256,\n    'compress': 'lzw',\n    'driver': 'GTiff',\n    'interleave': 'band',\n    'nodata': 0.0,\n    'tiled': True}\n\n\ndef write_geotiff(filename, dataset, profile_override=None, time_index=None):\n    \"\"\"\n    Write an ODC style xarray.Dataset to a GeoTIFF file.\n\n    :param filename: Output filename\n    :param dataset: xarray dataset containing one or more bands to write to a file.\n    :param profile_override: option dict, overrides rasterio file creation options.\n    :param time_index: DEPRECATED\n    \"\"\"\n    if time_index is not None:\n        raise ValueError('''The write_geotiff function no longer supports passing in `time_index`.\n        The same function can be achieved by calling `dataset.isel(time=<time_index>)` before passing\n in your dataset. It was removed because it made the function much less useful for more advanced cases.''')\n\n    profile_override = profile_override or {}\n\n    geobox = getattr(dataset, 'geobox', None)\n\n    if geobox is None:\n        raise ValueError('Can only write datasets with specified `crs` attribute')\n\n    try:\n        dtypes = {val.dtype for val in dataset.data_vars.values()}\n        assert len(dtypes) == 1  # Check for multiple dtypes\n    except AttributeError:\n        dtypes = [dataset.dtype]\n\n    profile = DEFAULT_PROFILE.copy()\n    height, width = geobox.shape\n\n    profile.update({\n        'width': width,\n        'height': height,\n        'transform': geobox.affine,\n        'crs': geobox.crs.crs_str,\n        'count': len(dataset.data_vars),\n        'dtype': str(dtypes.pop())\n    })\n    profile.update(profile_override)\n\n    _calculate_blocksize(profile)\n\n    with rasterio.open(str(filename), 'w', **profile) as dest:\n        if hasattr(dataset, 'data_vars'):\n            for bandnum, data in enumerate(dataset.data_vars.values(), start=1):\n                dest.write(data.data, bandnum)\n\n\ndef _calculate_blocksize(profile):\n    # Block size must be smaller than the image size, and for geotiffs must be divisible by 16\n    # Fix for small images.\n    if profile['blockxsize'] > profile['width']:\n        if profile['width'] % 16 == 0 or profile['width'] < 16:\n            profile['blockxsize'] = profile['width']\n        else:\n            profile['blockxsize'] = 16\n\n    if profile['blockysize'] > profile['height']:\n        if profile['height'] % 16 == 0 or profile['height'] < 16:\n            profile['blockysize'] = profile['height']\n        else:\n            profile['blockysize'] = 16\n\n\ndef ga_pq_fuser(dest, src):\n    \"\"\"\n    Fuse two Geoscience Australia Pixel Quality ndarrays\n\n    To be used as a `fuse_func` when loaded `grouped` data, for example when grouping\n    by solar day to avoid duplicate data from scene overlaps.\n    \"\"\"\n    valid_bit = 8\n    valid_val = (1 << valid_bit)\n\n    no_data_dest_mask = ~(dest & valid_val).astype(bool)\n    np.copyto(dest, src, where=no_data_dest_mask)\n\n    both_data_mask = (valid_val & dest & src).astype(bool)\n    np.copyto(dest, src & dest, where=both_data_mask)\n", "path": "datacube/helpers.py"}, {"content": "\"\"\"\nAdd geometric extensions to :class:`xarray.Dataset` and :class:`xarray.DataArray` for use\nwith Data Cube by Monkey Patching those classes.\n\nThis extension is reliant on an `xarray` object having a `.crs` property of type\n:class:`datacube.utils.geometry.CRS`. This is used to inspect the spatial dimensions of the\n:class:`Dataset` or :class:`DataArray`, and provide new attributes for accessing a\n:class:`datacube.utils.geometry.GeoBox`, affine transform and extent for the dataset as\n`.geobox`, `.affine` and `.extent` respectively.\n\n\"\"\"\n\nimport xarray\nfrom affine import Affine\n\nfrom datacube.utils import data_resolution_and_offset, geometry\n\n\ndef _norm_crs(crs):\n    if crs is None or isinstance(crs, geometry.CRS):\n        return crs\n    elif isinstance(crs, str):\n        return geometry.CRS(crs)\n    else:\n        raise ValueError('Can not interpret {} as CRS'.format(type(crs)))\n\n\ndef _xarray_affine(obj):\n    crs = _norm_crs(obj.crs)\n    if crs is None:\n        return None\n\n    dims = crs.dimensions\n    xres, xoff = data_resolution_and_offset(obj[dims[1]].values)\n    yres, yoff = data_resolution_and_offset(obj[dims[0]].values)\n    return Affine.translation(xoff, yoff) * Affine.scale(xres, yres)\n\n\ndef _xarray_extent(obj):\n    geobox = obj.geobox\n    return None if geobox is None else geobox.extent\n\n\ndef _xarray_geobox(obj):\n    crs = _norm_crs(obj.crs)\n    if crs is None:\n        return None\n\n    dims = crs.dimensions\n    return geometry.GeoBox(obj[dims[1]].size, obj[dims[0]].size, obj.affine, crs)\n\n\nxarray.Dataset.geobox = property(_xarray_geobox)\nxarray.Dataset.affine = property(_xarray_affine)\nxarray.Dataset.extent = property(_xarray_extent)\nxarray.DataArray.geobox = property(_xarray_geobox)\nxarray.DataArray.affine = property(_xarray_affine)\nxarray.DataArray.extent = property(_xarray_extent)\n", "path": "datacube/utils/xarray_geoextensions.py"}]}