Repository: rasterio/rasterio

Issue:
Warp wrapper API for warped VRT
I'm thinking that a MemoryFile-like API could be a Pythonic way to expose GDAL's `GDALAutoCreateWarpedVRT()`.

```python
with WarpWrapper('example.tif', dst_crs='EPSG:3857', resampling=Resampling.bilinear) as wrapper:
    with wrapper.open() as src:
        data = src.read()
```

Actual name of the class to be decided.

Thoughts?

/cc @vincentsarago






Discussion:
@sgillies that's seems good !

possible name:
- `VrtWrapper`
- `MemoryVRT`
- `WarpedVRT` <-- my favorite 


Resolving patch:
diff --git a/rasterio/io.py b/rasterio/io.py
--- a/rasterio/io.py
+++ b/rasterio/io.py
@@ -14,6 +14,7 @@
     DatasetReaderBase, DatasetWriterBase, BufferedDatasetWriterBase,
     MemoryFileBase)
 from rasterio import windows
+from rasterio.enums import Resampling
 from rasterio.env import ensure_env
 from rasterio.transform import guard_transform, xy, rowcol
 
diff --git a/rasterio/vrt.py b/rasterio/vrt.py
new file mode 100644
--- /dev/null
+++ b/rasterio/vrt.py
@@ -0,0 +1,64 @@
+"""rasterio.vrt: a module concerned with GDAL VRTs"""
+
+from rasterio._warp import WarpedVRTReaderBase
+from rasterio.enums import Resampling
+from rasterio.env import ensure_env
+from rasterio.io import WindowMethodsMixin, TransformMethodsMixin
+
+
+class WarpedVRT(WarpedVRTReaderBase, WindowMethodsMixin,
+                TransformMethodsMixin):
+    """Creates a virtual warped dataset.
+
+    Abstracts the details of raster warping and allows access to data
+    that is reprojected when read.
+
+    This class is backed by an in-memory GDAL VRTWarpedDataset VRT file.
+
+    Attributes
+    ----------
+
+    src_dataset : dataset
+        The dataset object to be virtually warped.
+    dst_crs : CRS or str
+        The warp operation's destination coordinate reference system.
+    resampling : int
+        One of the values from rasterio.enums.Resampling. The default is
+        `Resampling.nearest`.
+    tolerance : float
+        The maximum error tolerance in input pixels when approximating
+        the warp transformation. The default is 0.125.
+    src_nodata : float
+        A nodata value for the source data. It may be a value other
+        than the nodata value of src_dataset.
+    dst_nodata : float, int
+        The nodata value for the virtually warped dataset.
+    warp_extras : dict
+        GDAL extra warp options. See
+        http://www.gdal.org/structGDALWarpOptions.html.
+
+    Example
+    -------
+
+    >>> with rasterio.open('tests/data/RGB.byte.tif') as src:
+    ...     with WarpedVRT(src, dst_crs='EPSG:3857') as vrt:
+    ...         data = vrt.read()
+
+    """
+
+    def __repr__(self):
+        return "<{} WarpedVRT name='{}' mode='{}'>".format(
+            self.closed and 'closed' or 'open', self.name, self.mode)
+
+    def __enter__(self):
+        self.start()
+        return self
+
+    def __exit__(self, *args, **kwargs):
+        self.close()
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        self.stop()


Test patch:
diff --git a/tests/test_warpedvrt.py b/tests/test_warpedvrt.py
new file mode 100644
--- /dev/null
+++ b/tests/test_warpedvrt.py
@@ -0,0 +1,78 @@
+import boto3
+from packaging.version import parse
+import pytest
+
+import rasterio
+from rasterio.enums import Resampling
+from rasterio.vrt import WarpedVRT
+from rasterio.windows import Window
+
+
+# Custom markers.
+mingdalversion = pytest.mark.skipif(
+    parse(rasterio.__gdal_version__) < parse('2.1.0dev'),
+    reason="S3 raster access requires GDAL 2.1")
+
+credentials = pytest.mark.skipif(
+    not(boto3.Session()._session.get_credentials()),
+    reason="S3 raster access requires credentials")
+
+
+def test_warped_vrt(path_rgb_byte_tif):
+    """A VirtualVRT has the expected VRT properties."""
+    with rasterio.open(path_rgb_byte_tif) as src:
+        vrt = WarpedVRT(src, dst_crs='EPSG:3857')
+        assert vrt.dst_crs == 'EPSG:3857'
+        assert vrt.src_nodata is None
+        assert vrt.dst_nodata is None
+        assert vrt.tolerance == 0.125
+        assert vrt.resampling == Resampling.nearest
+        assert vrt.warp_extras == {'init_dest': 'NO_DATA'}
+
+
+def test_warped_vrt_source(path_rgb_byte_tif):
+    """A VirtualVRT has the expected source dataset."""
+    with rasterio.open(path_rgb_byte_tif) as src:
+        vrt = WarpedVRT(src, dst_crs='EPSG:3857')
+        assert vrt.src_dataset == src
+
+
+def test_wrap_file(path_rgb_byte_tif):
+    """A VirtualVRT has the expected dataset properties."""
+    with rasterio.open(path_rgb_byte_tif) as src:
+        vrt = WarpedVRT(src, dst_crs='EPSG:3857')
+        assert vrt.crs == 'EPSG:3857'
+        assert tuple(round(x, 1) for x in vrt.bounds) == (
+            -8789636.7, 2700460.0, -8524406.4, 2943560.2)
+        assert vrt.name.startswith('WarpedVRT(')
+        assert vrt.name.endswith('tests/data/RGB.byte.tif)')
+        assert vrt.indexes == (1, 2, 3)
+        assert vrt.nodatavals == (0, 0, 0)
+        assert vrt.dtypes == ('uint8', 'uint8', 'uint8')
+        assert vrt.read().shape == (3, 736, 803)
+
+
+def test_warp_extras(path_rgb_byte_tif):
+    """INIT_DEST warp extra is passed through."""
+    with rasterio.open(path_rgb_byte_tif) as src:
+        with WarpedVRT(src, dst_crs='EPSG:3857', init_dest=255) as vrt:
+            rgb = vrt.read()
+            assert (rgb[:, 0, 0] == 255).all()
+
+
+@mingdalversion
+@credentials
+@pytest.mark.network
+def test_wrap_s3():
+    """A warp wrapper's dataset has the expected properties"""
+    L8TIF = "s3://landsat-pds/L8/139/045/LC81390452014295LGN00/LC81390452014295LGN00_B1.TIF"
+    with rasterio.open(L8TIF) as src:
+        with WarpedVRT(src, dst_crs='EPSG:3857') as vrt:
+            assert vrt.crs == 'EPSG:3857'
+            assert tuple(round(x, 1) for x in vrt.bounds) == (
+                9556764.6, 2345109.3, 9804595.9, 2598509.1)
+            assert vrt.name == 'WarpedVRT(s3://landsat-pds/L8/139/045/LC81390452014295LGN00/LC81390452014295LGN00_B1.TIF)'
+            assert vrt.indexes == (1,)
+            assert vrt.nodatavals == (None,)
+            assert vrt.dtypes == ('uint16',)
+            assert vrt.shape == (7827, 7655)