[rasterio] 01/07: Imported Upstream version 0.33.0

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Sat Apr 2 00:29:32 UTC 2016


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository rasterio.

commit 54fe42cd60e9aeb25c495e162b9c738730078aa4
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Sat Apr 2 01:53:07 2016 +0200

    Imported Upstream version 0.33.0
---
 .gitignore                                     |   3 +
 .travis.yml                                    |  29 +-
 AUTHORS.txt                                    |  61 +--
 CHANGES.txt                                    |  32 ++
 CODE_OF_CONDUCT.md => CODE_OF_CONDUCT.txt      |   7 +-
 README.rst                                     |   4 +-
 docs/Makefile                                  | 223 ++++++++
 docs/api_docs.rst                              |   5 +
 docs/calc.rst                                  |   2 +-
 docs/cli.rst                                   |  29 +-
 docs/{colormaps.rst => color.rst}              |  41 +-
 docs/community.rst                             |  40 ++
 docs/concurrency.rst                           |   2 +-
 docs/conf.py                                   | 372 +++++++++++++
 docs/cookbook.rst                              | 163 ++++++
 docs/data_model.rst                            |  11 +
 docs/datasets.rst                              | 186 -------
 docs/developers.rst                            |  15 +
 docs/errors.rst                                |   6 +
 docs/features.rst                              |   4 +-
 docs/fillnodata.rst                            |   6 +
 docs/georeferencing.rst                        |  38 +-
 docs/{options.rst => image_options.rst}        |  19 +-
 docs/image_processing.rst                      |  27 +
 docs/img/RGB.byte.jpg                          | Bin 0 -> 198868 bytes
 docs/img/box_masked_rgb.png                    | Bin 0 -> 468885 bytes
 docs/img/box_rgb.png                           | Bin 0 -> 412194 bytes
 docs/img/filtered.jpg                          | Bin 0 -> 124568 bytes
 docs/img/reproject.jpg                         | Bin 0 -> 30470 bytes
 docs/img/saturation.jpg                        | Bin 0 -> 204255 bytes
 docs/img/world.jpg                             | Bin 0 -> 54297 bytes
 docs/index.rst                                 |  51 ++
 docs/installation.rst                          | 157 ++++++
 docs/masking-by-shapefile.rst                  |  38 ++
 docs/masks.rst                                 |  76 +--
 docs/modules.rst                               |   7 +
 docs/nodata.rst                                |   9 +
 docs/osgeo_gdal_migration.rst                  |  61 +++
 docs/overviews.rst                             |   7 +
 docs/plotting.rst                              |  23 +
 docs/python_manual.rst                         |  26 +
 docs/rasterio.aws.rst                          |   7 +
 docs/rasterio.coords.rst                       |   7 +
 docs/rasterio.crs.rst                          |   7 +
 docs/rasterio.dtypes.rst                       |   7 +
 docs/rasterio.enums.rst                        |   7 +
 docs/rasterio.errors.rst                       |   7 +
 docs/rasterio.features.rst                     |   7 +
 docs/rasterio.fill.rst                         |   7 +
 docs/rasterio.mask.rst                         |   7 +
 docs/rasterio.merge.rst                        |   7 +
 docs/rasterio.plot.rst                         |   7 +
 docs/rasterio.profiles.rst                     |   7 +
 docs/rasterio.rst                              |  32 ++
 docs/rasterio.sample.rst                       |   7 +
 docs/rasterio.tool.rst                         |   7 +
 docs/rasterio.tools.mask.rst                   |   7 +
 docs/rasterio.tools.merge.rst                  |   7 +
 docs/rasterio.tools.rst                        |  16 +
 docs/rasterio.transform.rst                    |   7 +
 docs/rasterio.vfs.rst                          |   7 +
 docs/rasterio.warnings.rst                     |   7 +
 docs/rasterio.warp.rst                         |   7 +
 docs/rasterio.windows.rst                      |   7 +
 docs/reading.rst                               | 133 +++++
 docs/recipies/band_summary_stats.py            |  17 +
 docs/recipies/filter.py                        |  16 +
 docs/recipies/mask_shp.py                      |  18 +
 docs/recipies/reproject.py                     |  49 ++
 docs/recipies/saturation.py                    |  55 ++
 docs/reproject.rst                             |   4 +-
 docs/resampling.rst                            |   7 +
 docs/tags.rst                                  |   6 +-
 docs/topics.rst                                |  23 +
 docs/vsi.rst                                   |  56 ++
 docs/watch.sh                                  |   3 +
 docs/windowed-rw.rst                           |  10 +-
 docs/working_with_datasets.rst                 |  57 ++
 docs/writing.rst                               |  48 ++
 rasterio/__init__.py                           | 257 +++++----
 rasterio/_base.pxd                             |   3 +-
 rasterio/_base.pyx                             | 133 +++--
 rasterio/_drivers.pyx                          |  98 +++-
 rasterio/_err.pyx                              | 196 +++++--
 rasterio/_fill.pyx                             |  40 +-
 rasterio/_io.pyx                               | 244 +++++----
 rasterio/_warp.pyx                             | 169 +++---
 rasterio/aws.py                                |  63 +++
 rasterio/coords.py                             |  37 +-
 rasterio/enums.py                              |  14 +
 rasterio/errors.py                             |  14 +-
 rasterio/five.py                               |   2 +
 rasterio/{tools => }/mask.py                   |   0
 rasterio/{tools => }/merge.py                  |   0
 rasterio/{tool.py => plot.py}                  |  48 +-
 rasterio/rio/bounds.py                         | 119 +++++
 rasterio/rio/{convert.py => clip.py}           |  97 ----
 rasterio/rio/convert.py                        |  90 ----
 rasterio/rio/edit_info.py                      | 155 ++++++
 rasterio/rio/env.py                            |  26 +
 rasterio/rio/features.py                       | 710 -------------------------
 rasterio/rio/helpers.py                        |   1 -
 rasterio/rio/info.py                           | 259 +--------
 rasterio/rio/insp.py                           |  93 ++++
 rasterio/rio/main.py                           |  31 +-
 rasterio/rio/mask.py                           | 131 +++++
 rasterio/rio/merge.py                          |   2 +-
 rasterio/rio/options.py                        |  18 +-
 rasterio/rio/rasterize.py                      | 271 ++++++++++
 rasterio/rio/sample.py                         |   3 +-
 rasterio/rio/shapes.py                         | 228 ++++++++
 rasterio/rio/{bands.py => stack.py}            |   1 +
 rasterio/rio/transform.py                      |  60 +++
 rasterio/rio/warp.py                           |  35 +-
 rasterio/tool.py                               | 181 +------
 rasterio/tools/mask.py                         |  96 +---
 rasterio/tools/merge.py                        | 170 +-----
 rasterio/vfs.py                                |  12 +-
 rasterio/warp.py                               |   1 +
 rasterio/windows.py                            |  79 +++
 requirements-dev.txt                           |   3 +
 scripts/travis_gdal_install.sh                 |  43 +-
 setup.py                                       |  23 +-
 tests/data/box.cpg                             |   1 +
 tests/data/box.dbf                             | Bin 0 -> 76 bytes
 tests/data/box.prj                             |   1 +
 tests/data/box.shp                             | Bin 0 -> 236 bytes
 tests/data/box.shx                             | Bin 0 -> 108 bytes
 tests/test_aws.py                              |  69 +++
 tests/test_colorinterp.py                      |  49 +-
 tests/test_deprecations.py                     |  63 +++
 tests/test_driver_management.py                |   3 +-
 tests/test_err.py                              |  28 +
 tests/test_indexing.py                         |  43 +-
 tests/test_rio_convert.py                      |   3 +-
 tests/test_rio_features.py                     | 295 +++++-----
 tests/test_rio_info.py                         | 101 ++--
 tests/test_rio_sample.py                       |  30 +-
 tests/{test_rio_bands.py => test_rio_stack.py} |  15 +-
 tests/test_rio_warp.py                         |  53 +-
 tests/test_tags.py                             |   4 +-
 tests/test_tool.py                             |   3 +-
 tests/test_tools_mask.py                       |   2 +-
 tests/test_update.py                           |   2 +-
 tests/test_vfs.py                              |   2 +-
 tests/test_warp_transform.py                   |  35 ++
 146 files changed, 4816 insertions(+), 2719 deletions(-)

diff --git a/.gitignore b/.gitignore
index bd98080..3be80b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,3 +75,6 @@ rasterio/_err.c
 rasterio/_example.c
 rasterio/_features.c
 rasterio/_io.c
+
+# vim
+.*.swp
diff --git a/.travis.yml b/.travis.yml
index 4d96006..d3c796e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,22 +10,28 @@ env:
     - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels
     - GDALINST=$HOME/gdalinstall
     - GDALBUILD=$HOME/gdalbuild
+    - secure: "Wp1x31bAV//2BF3yJeZKRtit5sAqmULgoTmTH825A+8Frgqb3k9tYf6OR/nP61q9Q84BI2ODqkSRd+EPTgz0hzQIHtDdnOEs4aFefE91dsFRml6LpyhJtoC1cAj45GySS/H8/qME2lby+OvBYceGdQAuaXWHjOT2cdhNqULxQOg="
+    - secure: "qAvCpDLYDiMdHhd8l6PZaukogRKFregFm2JG3aWatiOwNPA7qmp+O7Ac2Z0SsQdXxk7LNw6cVKyuRaiyXbWkAIGt2858a1eRJK++kssZ9NXnIGz5WvYeoVQutYMJU/CBCAgSxzpFo56JJg0CK4xizHU8AyDpi72UTDKAOdpCKd8="
   matrix:
-    - GDALVERSION = "1.9.2"
-    - GDALVERSION = "1.11.2"
-    - GDALVERSION = "2.0.1"
+    - GDALVERSION="1.9.2"
+    - GDALVERSION="1.10.0"  # doesn't exist but path/lib will fall back to system
+    - GDALVERSION="1.11.4"
+    - GDALVERSION="2.0.2"
+    - GDALVERSION="2.1.0"
 addons:
   apt:
     packages:
     - libgdal1h
     - gdal-bin
+    - libproj-dev
+    - libhdf5-serial-dev
+    - libpng-dev
     - libgdal-dev
     - libatlas-dev
     - libatlas-base-dev
     - gfortran
 python:
   - "2.7"
-  - "3.3"
   - "3.4"
 before_install:
   - pip install -U pip
@@ -34,24 +40,13 @@ before_install:
   - export PATH=$GDALINST/gdal-$GDALVERSION/bin:$PATH
   - export LD_LIBRARY_PATH=$GDALINST/gdal-$GDALVERSION/lib:$LD_LIBRARY_PATH
 install:
+  - "if [ $(gdal-config --version) == \"$GDALVERSION\" ]; then echo \"Using gdal $GDALVERSION\"; else echo \"NOT using gdal $GDALVERSION as expected; aborting\"; exit 1; fi"
   - "pip wheel -r requirements-dev.txt"
   - "pip install -r requirements-dev.txt"
   - "pip install --upgrade --force-reinstall --global-option=build_ext --global-option='-I$GDALINST/gdal-$GDALVERSION/include' --global-option='-L$GDALINST/gdal-$GDALVERSION/lib' --global-option='-R$GDALINST/gdal-$GDALVERSION/lib' -e ."
   - "pip install coveralls>=1.1"
-  - "pip install -e ."
+  - "pip install -e .[s3]"
 script:
   - py.test --cov rasterio --cov-report term-missing
 after_success:
   - coveralls
-before_deploy:
-  - pip wheel --wheel-dir=/tmp/wheelhouse -r requirements-dev.txt
-  - pip wheel --wheel-dir=/tmp/wheelhouse -r requirements.txt
-  - pip wheel --wheel-dir=/tmp/wheelhouse .
-  - tar -C /tmp -czvf rasterio-travis-wheels-$TRAVIS_PYTHON_VERSION.tar.gz wheelhouse
-deploy:
-  provider: releases
-  api_key:
-    secure: uP/hy8LRdDnN6XHSLChmKYdW9CdIy8pqvUyXFPgTDY/mlItMUdDNdP95bitzn/rNNXnOkCGsARqzRCLeGI3jB0nEGuAzY6fGWYt2igjfMOhpdDG6o3LcaoP4mITuFfe5/kCQeUb8WB3QK6c2cL7nEEPzoSniqZQ6MsxHIvUW7ts=
-  file: rasterio-travis-wheels-$TRAVIS_PYTHON_VERSION.tar.gz
-  on:
-    tags: true
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 3e35b11..6526ac6 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -1,34 +1,35 @@
 Authors
-=======
+-------
 
-Aldo Culquicondor <alculquicondor at gmail.com>
-Alessandro Amici <alexamici at gmail.com>
-Alexander <spatial.hast at gmail.com>
-Amit Kapadia <amit at planet.com>
-AsgerPetersen <asgerpetersen at gmail.com>
-Bas Couwenberg <sebastic at xs4all.nl>
-Brendan Ward <bcward at consbio.org>
-Etienne B. Racine <etiennebr at gmail.com>
-Even Rouault <even.rouault at spatialys.com>
-Jacques Tardie <hi at jacquestardie.org>
-James McBride <jmcbride at berkeley.edu>
-James Seppi <james.seppi at gmail.com>
-Jeffrey Gerard <jgerard at climate.com>
-Johan Van de Wauw <johan.vandewauw at gmail.com>
-Joshua Arnott <josh at snorfalorpagus.net>
-Kelsey Jordahl <kjordahl at alum.mit.edu>
-Kevin Wurster <wursterk at gmail.com>
-Martijn Visser <mgvisser at gmail.com>
-Matt Savoie <github at flamingbear.com>
-Matthew Perry <perrygeo at gmail.com>
-Maxim Dubinin <sim at gis-lab.info>
-Mike Toews <mwtoews at gmail.com>
-Nat Wilson <njwilson23 at gmail.com>
-Patrick Young <patrick.young at digitalglobe.com>
-Robin Wilson <robin at rtwilson.com>
-Ryan Grout <rgrout at continuum.io>
-Sean Gillies <sean at mapbox.com>
-Trevor R.H. Clarke <tclarke at ball.com>
-cgohlke <cgohlke at uci.edu>
+* Aldo Culquicondor <alculquicondor at gmail.com>
+* Alessandro Amici <alexamici at gmail.com>
+* Alexander <spatial.hast at gmail.com>
+* Amit Kapadia <amit at planet.com>
+* AsgerPetersen <asgerpetersen at gmail.com>
+* Bas Couwenberg <sebastic at xs4all.nl>
+* Brendan Ward <bcward at consbio.org>
+* Etienne B. Racine <etiennebr at gmail.com>
+* Even Rouault <even.rouault at spatialys.com>
+* Jacques Tardie <hi at jacquestardie.org>
+* James McBride <jmcbride at berkeley.edu>
+* James Seppi <james.seppi at gmail.com>
+* Jeffrey Gerard <jgerard at climate.com>
+* Johan Van de Wauw <johan.vandewauw at gmail.com>
+* Joshua Arnott <josh at snorfalorpagus.net>
+* Kelsey Jordahl <kjordahl at alum.mit.edu>
+* Kevin Wurster <wursterk at gmail.com>
+* Martijn Visser <mgvisser at gmail.com>
+* Matt Savoie <github at flamingbear.com>
+* Matthew Perry <perrygeo at gmail.com>
+* Maxim Dubinin <sim at gis-lab.info>
+* Mike Toews <mwtoews at gmail.com>
+* Nat Wilson <njwilson23 at gmail.com>
+* Patrick Young <patrick.young at digitalglobe.com>
+* Robin Wilson <robin at rtwilson.com>
+* Ryan Grout <rgrout at continuum.io>
+* Sean Gillies <sean at mapbox.com>
+* Trevor R.H. Clarke <tclarke at ball.com>
+* cgohlke <cgohlke at uci.edu>
+* Rob Emmanuele
 
 See also https://github.com/mapbox/rasterio/graphs/contributors.
diff --git a/CHANGES.txt b/CHANGES.txt
index 74411ee..6a97bb3 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,38 @@
 Changes
 =======
 
+0.33.0 (2016-04-01)
+-------------------
+
+I played a lot of One-on-One on the computers of the mid-1980s and dedicate
+this release to the best #33 of all time, Larry Bird.
+
+- Bug fix: YCbCr JPEG-in-TIFF files no longer break rio-info (#617, #618).
+- New feature: the ability to read dataset metadata and imagery from S3 objects
+  is an extra feature that can be installed like this `pip install -U
+  rasterio[s3]`. AWS Credentials are handled by boto3 (and botocore) and so can
+  be provided by environment variables, session arguments, `~/.aws/credentials`
+  file, or EC2 instance metadata. S3 access is enabled in the following CLI
+  commands: rio-clip, rio-info, rio-insp, rio-bounds, rio-shapes, rio-sample
+  and may be expanded in future versions. S3 objects are identified on the
+  command line and in API functions by URIs following the pattern
+  `s3://bucket/object`. Extra thanks to Rob Emanuele and Even Rouault for
+  helping on this one (#551, #610).
+- New feature: new and improved documentation coming soon to a website near
+  you (#588).
+- Refactoring: commands for the rio CLI have been moved to their own
+  modules so that they're easier to find (#594).
+- Refactoring: we've changed our primary pattern for checking errors set by
+  GDAL API functions (#600).
+
+0.32.0.post1 (2016-03-27)
+-------------------------
+- No changes to the library in this post-release version, but there is a
+  significant change to the distributions on PyPI: to help make Rasterio more
+  compatible with Shapely on OS X, the GDAL shared library included in the
+  macosx (only) binary wheels now statically links the GEOS library. See
+  https://github.com/sgillies/frs-wheel-builds/issues/5.
+
 0.32.0 (2016-03-22)
 -------------------
 - Bug fix: geometry factories and warp operations are properly deallocated
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.txt
similarity index 87%
rename from CODE_OF_CONDUCT.md
rename to CODE_OF_CONDUCT.txt
index 01b8644..ad8236f 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.txt
@@ -1,4 +1,5 @@
-# Contributor Code of Conduct
+Contributor Code of Conduct
+---------------------------
 
 As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
 
@@ -19,4 +20,6 @@ This code of conduct applies both within project spaces and in public spaces whe
 
 Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
 
-This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
+This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.2.0, available at http://contributor-covenant.org/version/1/2/0/
+
+.. _Contributor Covenant: http://contributor-covenant.org
diff --git a/README.rst b/README.rst
index 8cc6fc7..eaac96c 100644
--- a/README.rst
+++ b/README.rst
@@ -7,8 +7,8 @@ Rasterio reads and writes geospatial raster datasets.
 .. image:: https://travis-ci.org/mapbox/rasterio.png?branch=master
    :target: https://travis-ci.org/mapbox/rasterio
 
-.. image:: https://coveralls.io/repos/mapbox/rasterio/badge.png
-   :target: https://coveralls.io/r/mapbox/rasterio
+.. image:: https://coveralls.io/repos/github/mapbox/rasterio/badge.svg?branch=master
+   :target: https://coveralls.io/github/mapbox/rasterio?branch=master
 
 Rasterio employs GDAL under the hood for file I/O and raster formatting. Its
 functions typically accept and return Numpy ndarrays. Rasterio is designed to
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d209e6f
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,223 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  applehelp  to make an Apple Help Book"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  xml        to make Docutils-native XML files"
+	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  test    to run all doctests embedded in the documentation (if enabled)"
+	@echo "  apidocs    to autogenerate API docs"
+	@echo "  coverage   to run coverage check of the documentation (if enabled)"
+
+.PHONY: clean
+clean:
+	rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/rasterio.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/rasterio.qhc"
+
+.PHONY: applehelp
+applehelp:
+	$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+	@echo
+	@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+	@echo "N.B. You won't be able to view it unless you put it in" \
+	      "~/Library/Documentation/Help or install it in your application" \
+	      "bundle."
+
+.PHONY: devhelp
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/rasterio"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/rasterio"
+	@echo "# devhelp"
+
+.PHONY: epub
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: latex
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through platex and dvipdfmx..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: test
+test:
+	cd .. && py.test --doctest-glob="*.rst" docs/*.rst
+
+.PHONY: coverage
+coverage:
+	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+	@echo "Testing of coverage in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+	@echo
+	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+	@echo
+	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+
+.PHONY: apidocs
+apidocs:
+	sphinx-apidoc -f -e -T -M -o . ../rasterio ../rasterio/rio ../rasterio/five.py ../rasterio/tools ../rasterio/tool.py
+
+.PHONY: publish
+publish: html
+	aws s3 sync _build/html/ s3://mapbox/playground/perrygeo/rasterio-docs --delete --acl public-read
diff --git a/docs/api_docs.rst b/docs/api_docs.rst
new file mode 100644
index 0000000..ae661b3
--- /dev/null
+++ b/docs/api_docs.rst
@@ -0,0 +1,5 @@
+=================
+API Documentation
+=================
+
+.. include:: rasterio.rst
diff --git a/docs/calc.rst b/docs/calc.rst
index 7ee8c3b..d266981 100644
--- a/docs/calc.rst
+++ b/docs/calc.rst
@@ -13,7 +13,7 @@ Expressions
 
 Rio-calc expressions look like
 
-.. code-block::
+.. code-block:: console
 
     (func|operator arg [*args])
 
diff --git a/docs/cli.rst b/docs/cli.rst
index a1366ed..e5c3ace 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -1,5 +1,13 @@
-Command Line Interface
-======================
+==================================
+``rio`` Command Line User's Manual
+==================================
+
+.. todo:: 
+ 
+    Introduce the command line interface main concepts, 
+    when you should and should not use the command line,
+    Overview of the general design pholosophy,
+
 
 Rasterio's command line interface is a program named "rio".
 
@@ -37,9 +45,12 @@ Rasterio's command line interface is a program named "rio".
 
 It is developed using `Click <http://click.pocoo.org/>`__.
 
+Commands
+========
 Commands are shown below. See ``--help`` of individual commands for more
 details.
 
+
 creation options
 ----------------
 
@@ -155,8 +166,9 @@ The command above is also an example of a calculation that is far beyond the
 design of the calc command and something that could be done much more
 efficiently in Python.
 
-Please see `calc.rst <calc.rst>`__ for more details.
+.. toctree::
 
+    calc
 
 clip
 ----
@@ -371,12 +383,13 @@ The ``insp`` command opens a dataset and an interpreter.
 
 .. code-block:: console
 
-    $ rio insp tests/data/RGB.byte.tif
-    Rasterio 0.18 Interactive Inspector (Python 2.7.9)
+    $ rio insp --ipython tests/data/RGB.byte.tif
+    Rasterio 0.32.0 Interactive Inspector (Python 2.7.10)
     Type "src.meta", "src.read(1)", or "help(src)" for more information.
-    >>> print src.name
-    tests/data/RGB.byte.tif
-    >>> print src.bounds
+    In [1]: print(src.name)
+    /path/rasterio/tests/data/RGB.byte.tif
+
+    In [2]: print(src.bounds)
     BoundingBox(left=101985.0, bottom=2611485.0, right=339315.0, top=2826915.0)
 
 
diff --git a/docs/colormaps.rst b/docs/color.rst
similarity index 56%
rename from docs/colormaps.rst
rename to docs/color.rst
index a7e8cdd..9bcc827 100644
--- a/docs/colormaps.rst
+++ b/docs/color.rst
@@ -1,5 +1,44 @@
+Color
+*****
+
+Color interpretation
+^^^^^^^^^^^^^^^^^^^^^
+
+Color interpretation of raster bands can be read from the dataset
+
+
+.. code-block:: python
+
+    >>> import rasterio
+    >>> src = rasterio.open("tests/data/RGB.byte.tif")
+    >>> src.colorinterp(1)
+    <ColorInterp.red: 3>
+
+GDAL builds the color interpretation based on the driver and creation options.
+With the ``GTiff`` driver, rasters with exactly 3 bands of uint8 type will be RGB,
+4 bands of uint8 will be RGBA by default.
+
+You cannot set the color interpretation on existing data but you can
+specify a ``photometric`` string when writing a new raster.
+
+.. code:: python
+
+    >>> profile = src.profile
+    >>> profile['photometric'] = "RGB"
+    >>> with rasterio.open("/tmp/rgb.tif", 'w', **profile) as dst:
+    ...     dst.write(src.read())
+
+And the resulting raster will be interpretted as RGB.
+
+.. code:: python
+
+    >>> with rasterio.open("/tmp/rgb.tif") as src2:
+    ...     src2.colorinterp(2)
+    <ColorInterp.green: 4>
+
+
 Colormaps
-=========
+^^^^^^^^^
 
 Writing colormaps
 -----------------
diff --git a/docs/community.rst b/docs/community.rst
new file mode 100644
index 0000000..32a04ff
--- /dev/null
+++ b/docs/community.rst
@@ -0,0 +1,40 @@
+=========
+Community
+=========
+
+.. todo:: 
+
+    This is just a placeholder page to explain the use and development of rasterio.
+
+    If you have a project that uses rasterio, please contact us at https://github.com/mapbox/rasterio/issues     
+    
+Who is using rasterio?
+----------------------
+
+* The `Conservation Biology Institute`_ uses rasterio to provide `geoprocessing of NetCDF files`_.
+* `Mapbox`_ uses rasterio to process satellite imagery for it's global satellite base maps.
+* Mary Marek-Spartz's masters thesis compares the map algebra tools of the proprietary ESRI ArcPy library and the open source Rasterio library.  https://github.com/alfalimajuliett/masters-thesis
+
+Software built on rasterio
+--------------------------
+
+The following software projects use rasterio
+
+* pyimpute
+* rasterstats
+
+Rio plugins
+------------
+
+In addition to the core ``rio`` commands, visit the `Rio plugin registry`_ for a list of external plugins. 
+
+
+.. include:: ../AUTHORS.txt
+
+
+.. include:: ../CODE_OF_CONDUCT.txt
+
+.. _Mapbox: https://mapbox.com
+.. _Rio plugin registry: https://github.com/mapbox/rasterio/wiki/Rio-plugin-registry
+.. _Conservation Biology Institute: http://consbio.org/
+.. _geoprocessing of NetCDF files: https://github.com/consbio/clover
diff --git a/docs/concurrency.rst b/docs/concurrency.rst
index 14b4963..464b110 100644
--- a/docs/concurrency.rst
+++ b/docs/concurrency.rst
@@ -1,5 +1,5 @@
 Concurrent processing
-=====================
+*********************
 
 Rasterio affords concurrent processing of raster data. The Python GIL is
 released when calling GDAL's ``GDALRasterIO()`` function, which means that
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..ffdee8c
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+#
+# rasterio documentation build configuration file, created by
+# sphinx-quickstart on Thu Mar 17 07:05:00 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.doctest',
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.todo',
+    'sphinx.ext.coverage',
+    'sphinx.ext.mathjax',
+    'sphinx.ext.ifconfig',
+    'sphinx.ext.viewcode',
+    'numpydoc',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'rasterio'
+copyright = u'2016, Mapbox'
+author = u'Sean Gillies'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# Parse the version from the rasterio module.
+with open('../rasterio/__init__.py') as f:
+    for line in f:
+        if line.find("__version__") >= 0:
+            version = line.split("=")[1].strip()
+            version = version.strip('"')
+            version = version.strip("'")
+            continue
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = True
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
+#html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# Now only 'ja' uses this config value
+#html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'rasteriodoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+
+# Latex figure (float) alignment
+#'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'rasterio.tex', u'rasterio Documentation',
+     u'Sean Gillies', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'rasterio', u'rasterio Documentation',
+     [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'rasterio', u'rasterio Documentation',
+     author, 'rasterio', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+
+# -- Options for Epub output ----------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+epub_author = author
+epub_publisher = author
+epub_copyright = copyright
+
+# The basename for the epub file. It defaults to the project name.
+#epub_basename = project
+
+# The HTML theme for the epub output. Since the default themes are not
+# optimized for small screen space, using the same theme for HTML and epub
+# output is usually not wise. This defaults to 'epub', a theme designed to save
+# visual space.
+#epub_theme = 'epub'
+
+# The language of the text. It defaults to the language option
+# or 'en' if the language is not set.
+#epub_language = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+#epub_scheme = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#epub_identifier = ''
+
+# A unique identification for the text.
+#epub_uid = ''
+
+# A tuple containing the cover image and cover page html template filenames.
+#epub_cover = ()
+
+# A sequence of (type, uri, title) tuples for the guide element of content.opf.
+#epub_guide = ()
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_pre_files = []
+
+# HTML files that should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
+
+# The depth of the table of contents in toc.ncx.
+#epub_tocdepth = 3
+
+# Allow duplicate toc entries.
+#epub_tocdup = True
+
+# Choose between 'default' and 'includehidden'.
+#epub_tocscope = 'default'
+
+# Fix unsupported image types using the Pillow.
+#epub_fix_images = False
+
+# Scale large images.
+#epub_max_image_width = 0
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#epub_show_urls = 'inline'
+
+# If false, no index is generated.
+#epub_use_index = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/': None}
diff --git a/docs/cookbook.rst b/docs/cookbook.rst
new file mode 100644
index 0000000..4868f8e
--- /dev/null
+++ b/docs/cookbook.rst
@@ -0,0 +1,163 @@
+=================
+Rasterio Cookbook
+=================
+
+.. todo::
+
+    Fill out examples of using rasterio to handle tasks from typical
+    GIS and remote sensing workflows.
+
+The Rasterio cookbook is intended to provide in-depth examples of rasterio usage
+that are not covered by the basic usage in the User's Manual. Before using code
+from the cookbook, you should be familar with the basic usage of rasterio; see
+"Reading Datasets", "Working with Datasets" and "Writing Datasets" to brush up on
+the fundamentals.
+
+Generating summary statistics for each band
+-------------------------------------------
+
+.. literalinclude:: recipies/band_summary_stats.py
+    :language: python
+    :linenos:
+
+.. code::
+
+    $ python docs/recipies/band_summary_stats.py
+    [{'max': 255, 'mean': 29.94772668847656, 'median': 13.0, 'min': 0},
+     {'max': 255, 'mean': 44.516147889382289, 'median': 30.0, 'min': 0},
+     {'max': 255, 'mean': 48.113056354742945, 'median': 30.0, 'min': 0}]
+
+Raster algebra
+--------------
+
+Resampling rasters to a different cell size
+--------------------------------------------
+
+Reproject/warp a raster to a different CRS
+------------------------------------------
+
+Reproject to a Transverse Mercator projection, Hawaii zone 3 (ftUS),
+aka EPSG code 3759. 
+
+.. literalinclude:: recipies/reproject.py
+    :language: python
+    :linenos:
+
+.. code::
+
+    $ python docs/recipies/reproject.py
+
+
+The original image
+
+.. image:: img/world.jpg
+    :scale: 100 %
+
+Warped to ``EPSG:3759``. Notice that the bounds are contrainted to the new projection's
+valid region (``CHECK_WITH_INVERT_PROJ=True`` on line 13) and the new raster is wrapped seamlessly across the anti-meridian.
+
+.. image:: img/reproject.jpg
+    :scale: 100 %
+
+Raster to polygon features
+--------------------------
+
+Rasterizing GeoJSON features
+----------------------------
+
+Masking raster with a polygon feature
+-------------------------------------
+
+Using ``rasterio`` with ``fiona``, we can open a shapefile, read geometries, and
+mask out regions of a raster that are outside the polygons defined in the shapefile.
+
+This shapefile contains a single polygon, a box near the center of the raster,
+so in this case, our list of geometries is one element long.
+
+Applying the features in the shapefile as a mask on the raster sets all pixels outside
+of the features to be zero. Since ``crop=True`` in this example, the extent of the raster
+is also set to be the extent of the features in the shapefile.
+
+We can then use the updated spatial transform and raster height and width
+to write the masked raster to a new file.
+
+.. literalinclude:: recipies/mask_shp.py
+    :language: python
+    :linenos:
+
+.. code::
+
+    $ python docs/recipies/mask_shp.py
+
+
+The original image with the shapefile overlayed
+
+.. image:: img/box_rgb.png
+    :scale: 80 %
+
+Masked and cropped to the geometry
+
+.. image:: img/box_masked_rgb.png
+    :scale: 80 %
+
+Creating valid data bounding polygons
+-------------------------------------
+
+Raster to vector line feature
+-----------------------------
+
+Creating raster from numpy array
+--------------------------------
+
+Creating a least cost path
+--------------------------
+
+Using a scipy filter to smooth a raster
+---------------------------------------
+
+This recipie demonstrates the use of scipy's `signal processing filters <http://docs.scipy.org/doc/scipy/reference/signal.html#signal-processing-scipy-signal>`_ to manipulate multi-band raster imagery
+and save the results to a new GeoTIFF. Here we apply a median filter to smooth
+the image and remove small inclusions (at the expense of some sharpness and detail).
+
+.. literalinclude:: recipies/filter.py
+    :language: python
+    :linenos:
+
+.. code::
+
+    $ python docs/recipies/filter.py
+
+
+The original image
+
+.. image:: img/RGB.byte.jpg
+    :scale: 50 %
+
+With median filter applied
+
+.. image:: img/filtered.jpg
+    :scale: 50 %
+
+Using skimage to adjust the saturation of a RGB raster
+------------------------------------------------------
+
+This recipie demonstrates the use of manipulating color with the scikit image `color module <http://scikit-image.org/docs/stable/api/skimage.color.html>`_.
+
+.. literalinclude:: recipies/saturation.py
+    :language: python
+    :linenos:
+
+.. code::
+
+    $ python docs/recipies/saturation.py
+
+
+The original image
+
+.. image:: img/RGB.byte.jpg
+    :scale: 50 %
+
+With increased saturation
+
+.. image:: img/saturation.jpg
+    :scale: 50 %
diff --git a/docs/data_model.rst b/docs/data_model.rst
new file mode 100644
index 0000000..a124e05
--- /dev/null
+++ b/docs/data_model.rst
@@ -0,0 +1,11 @@
+Data Model
+==========
+
+.. todo:: 
+
+    Datasets, Bands, Ndarrays
+
+    Design Decisions
+       
+    Relationship to GDAL
+
diff --git a/docs/datasets.rst b/docs/datasets.rst
deleted file mode 100644
index 12a3870..0000000
--- a/docs/datasets.rst
+++ /dev/null
@@ -1,186 +0,0 @@
-Datasets and ndarrays
-=====================
-
-Dataset objects provide read, read-write, and write access to raster data files
-and are obtained by calling ``rasterio.open()``. That function mimics Python's
-built-in ``open()`` and the dataset objects it returns mimic Python ``file``
-objects.
-
-.. code-block:: pycon
-
-    >>> import rasterio
-    >>> dataset = rasterio.open('tests/data/RGB.byte.tif')
-    >>> dataset
-    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
-    >>> dataset.name
-    'tests/data/RGB.byte.tif'
-    >>> dataset.mode
-    r
-    >>> dataset.closed
-    False
-
-If you attempt to access a nonexistent path, ``rasterio.open()`` does the same
-thing as ``open()``, raising an exception immediately.
-
-.. code-block:: pycon
-
-    >>> open('/lol/wut.tif')
-    Traceback (most recent call last):
-      File "<stdin>", line 1, in <module>
-    IOError: [Errno 2] No such file or directory: '/lol/wut.tif'
-    >>> rasterio.open('/lol/wut.tif')
-    Traceback (most recent call last):
-      File "<stdin>", line 1, in <module>
-    IOError: no such file or directory: '/lol/wut.tif'
-
-Attributes
-----------
-
-In addition to the file-like attributes shown above, a dataset has a number
-of other read-only attributes that help explain its role in spatial information
-systems. The ``driver`` attribute gives you the name of the GDAL format
-driver used. The ``height`` and ``width`` are the number of rows and columns of
-the raster dataset and ``shape`` is a ``height, width`` tuple as used by
-Numpy. The ``count`` attribute tells you the number of bands in the dataset.
-
-.. code-block:: pycon
-
-    >>> dataset.driver
-    u'GTiff'
-    >>> dataset.height, dataset.width
-    (718, 791)
-    >>> dataset.shape
-    (718, 791)
-    >>> dataset.count
-    3
-
-What makes geospatial raster datasets different from other raster files is
-that their pixels map to regions of the Earth. A dataset has a coordinate
-reference system and an affine transformation matrix that maps pixel
-coordinates to coordinates in that reference system.
-
-.. code-block:: pycon
-
-    >>> dataset.crs
-    {u'units': u'm', u'no_defs': True, u'ellps': u'WGS84', u'proj': u'utm', u'zone': 18}
-    >>> dataset.affine
-    Affine(300.0379266750948, 0.0, 101985.0,
-           0.0, -300.041782729805, 2826915.0)
-
-To get the ``x, y`` world coordinates for the upper left corner of any pixel,
-take the product of the affine transformation matrix and the tuple ``(col,
-row)``.  
-
-.. code-block:: pycon
-
-    >>> col, row = 0, 0
-    >>> src.affine * (col, row)
-    (101985.0, 2826915.0)
-    >>> col, row = src.width, src.height
-    >>> src.affine * (col, row)
-    (339315.0, 2611485.0)
-
-Reading data
-------------
-
-Datasets generally have one or more bands (or layers). Following the GDAL
-convention, these are indexed starting with the number 1. The first band of
-a file can be read like this:
-
-.. code-block:: pycon
-
-    >>> dataset.read(1)
-    array([[0, 0, 0, ..., 0, 0, 0],
-           [0, 0, 0, ..., 0, 0, 0],
-           [0, 0, 0, ..., 0, 0, 0],
-           ...,
-           [0, 0, 0, ..., 0, 0, 0],
-           [0, 0, 0, ..., 0, 0, 0],
-           [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
-
-The returned object is a 2-dimensional Numpy ndarray. The representation of
-that array at the Python prompt is just a summary; the GeoTIFF file that
-Rasterio uses for testing has 0 values in the corners, but has nonzero values
-elsewhere.
-
-.. code-block::
-
-    >>> from matplotlib import pyplot
-    >>> pyplot.imshow(dataset.read(1), cmap='pink')
-    <matplotlib.image.AxesImage object at 0x111195c10>
-    >>> pyplot.show()
-
-.. image:: http://farm6.staticflickr.com/5032/13938576006_b99b23271b_o_d.png
-
-The indexes, Numpy data types, and nodata values of all a dataset's bands can
-be had from its ``indexes``, ``dtypes``, and ``nodatavals`` attributes.
-
-.. code-block:: pycon
-
-    >>> for i, dtype, ndval in zip(src.indexes, src.dtypes, src.nodatavals):
-    ...     print i, dtype, nodataval
-    ...
-    1 <type 'numpy.uint8'> 0.0
-    2 <type 'numpy.uint8'> 0.0
-    3 <type 'numpy.uint8'> 0.0
-
-To close a dataset, call its ``close()`` method.
-
-.. code-block:: pycon
-
-    >>> dataset.close()
-    >>> dataset
-    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
-
-After it's closed, data can no longer be read.
-
-.. code-block:: pycon
-
-    >>> dataset.read(1)
-    Traceback (most recent call last):
-      File "<stdin>", line 1, in <module>
-    ValueError: can't read closed raster file
-
-This is the same behavior as Python's ``file``.
-
-.. code-block:: pycon
-
-    >>> f = open('README.rst')
-    >>> f.close()
-    >>> f.read()
-    Traceback (most recent call last):
-      File "<stdin>", line 1, in <module>
-    ValueError: I/O operation on closed file
-
-As Python ``file`` objects can, Rasterio datasets can manage the entry into 
-and exit from runtime contexts created using a ``with`` statement. This 
-ensures that files are closed no matter what exceptions may be raised within
-the the block.
-
-.. code-block:: pycon
-
-    >>> with rasterio.open('tests/data/RGB.byte.tif', 'r') as one:
-    ...     with rasterio.open('tests/data/RGB.byte.tif', 'r') as two:
-                print two
-    ... print one
-    ... print two
-    >>> print one
-    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
-    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
-    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
-    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
-
-Writing data
-------------
-
-Opening a file in writing mode is a little more complicated than opening
-a text file in Python. The dimensions of the raster dataset, the 
-data types, and the specific format must be specified.
-
-.. code-block:: pycon
-
-   >>> with rasterio.open
-
-Writing data mostly works as with a Python file. There are a few format-
-specific differences. TODO: details.
-
diff --git a/docs/developers.rst b/docs/developers.rst
new file mode 100644
index 0000000..254d6b3
--- /dev/null
+++ b/docs/developers.rst
@@ -0,0 +1,15 @@
+Developers guide
+================
+
+.. todo::
+
+    Everything developers need to know to get a dev environment running,
+    run tests, modify code and submit a successful PR.
+
+    Currently most of this information lives on the wiki. 
+
+    * https://github.com/mapbox/rasterio/wiki/Development-Guide
+    * https://github.com/mapbox/rasterio/wiki/Exposing-GDAL-Functionality
+    * https://github.com/mapbox/rasterio/wiki/Cython-and-GDAL
+
+    The long term goal is to consolidate into this document.
diff --git a/docs/errors.rst b/docs/errors.rst
new file mode 100644
index 0000000..f821ce1
--- /dev/null
+++ b/docs/errors.rst
@@ -0,0 +1,6 @@
+Error Handling
+**************
+
+.. todo::
+
+    error enums, context managers, converting GDAL errors to python exceptions
diff --git a/docs/features.rst b/docs/features.rst
index 2bd51eb..a1adaf3 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -1,5 +1,5 @@
-Features
-========
+Vector Features
+***************
 
 Rasterio's ``features`` module provides functions to extract shapes of raster
 features and to create new features by "burning" shapes into rasters:
diff --git a/docs/fillnodata.rst b/docs/fillnodata.rst
new file mode 100644
index 0000000..8f1c8ea
--- /dev/null
+++ b/docs/fillnodata.rst
@@ -0,0 +1,6 @@
+Filling nodata areas
+********************
+
+.. todo::
+
+    fillnodata()
diff --git a/docs/georeferencing.rst b/docs/georeferencing.rst
index ae34617..d905b99 100644
--- a/docs/georeferencing.rst
+++ b/docs/georeferencing.rst
@@ -1,5 +1,5 @@
 Georeferencing
-==============
+**************
 
 There are two parts to the georeferencing of raster datasets: the definition
 of the local, regional, or global system in which a raster's pixels are
@@ -10,34 +10,32 @@ Coordinate Reference System
 ---------------------------
 
 The coordinate reference system of a dataset is accessed from its ``crs``
-attribute. Type ``rio insp tests/data/RGB.byte.tif`` from the 
-Rasterio distribution root to see.
+attribute. 
 
-.. code-block:: pycon
+.. code-block:: python
 
-    Rasterio 0.9 Interactive Inspector (Python 3.4.1)
-    Type "src.meta", "src.read(1)", or "help(src)" for more information.
-    >>> src
-    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    >>> import rasterio
+    >>> src = rasterio.open('tests/data/RGB.byte.tif')
     >>> src.crs
-    {'init': 'epsg:32618'}
+    {'init': u'epsg:32618'}
 
 Rasterio follows pyproj and uses PROJ.4 syntax in dict form as its native
 CRS syntax. If you want a WKT representation of the CRS, see the ``crs_wkt``
 attribute.
 
-.. code-block:: pycon
+.. code-block:: python
 
     >>> src.crs_wkt
-    'PROJCS["UTM Zone 18, Northern Hemisphere",GEOGCS["Unknown datum based upon the WGS 84 ellipsoid",DATUM["Not_specified_based_on_WGS_84_spheroid",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing [...]
+    u'PROJCS["UTM Zone 18, Northern Hemisphere",GEOGCS["Unknown datum based upon the WGS 84 ellipsoid",DATUM["Not_specified_based_on_WGS_84_spheroid",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northin [...]
 
 When opening a new file for writing, you may also use a CRS string as an
 argument.
 
-.. code-block:: pycon
+.. code-block:: python
 
-   >>> with rasterio.open('/tmp/foo.tif', 'w', crs='EPSG:3857', ...) as dst:
-   ...     # write data to this Web Mercator projection dataset.
+   >>> profile = {'driver': 'GTiff', 'height': 100, 'width': 100, 'count': 1, 'dtype': rasterio.uint8}
+   >>> with rasterio.open('/tmp/foo.tif', 'w', crs='EPSG:3857', **profile) as dst:
+   ...     pass # write data to this Web Mercator projection dataset.
 
 Coordinate Transformation
 -------------------------
@@ -66,16 +64,8 @@ a pixel's image coordinates are ``x, y`` and its world coordinates are
 The ``Affine`` class has a number of useful properties and methods
 described at https://github.com/sgillies/affine.
 
-The ``affine`` attribute is new. Previous versions of Rasterio had only a
-``transform`` attribute. As explained in the warning below, Rasterio is in
-a transitional phase.
-
-.. code-block:: pycon
-
-    >>> src.transform
-    /usr/local/Cellar/python3/3.4.1/Frameworks/Python.framework/Versions/3.4/lib/python3.4/code.py:90: FutureWarning: The value of this property will change in version 1.0. Please see https://github.com/mapbox/rasterio/issues/86 for details.
-    [101985.0, 300.0379266750948, 0.0, 2826915.0, 0.0, -300.041782729805]
-
+Previous versions of Rasterio had a ``transform`` attribute which was a 6-element
+tuple. This usage is deprecated, please see https://github.com/mapbox/rasterio/issues/86 for details. 
 In Rasterio 1.0, the value of a  ``transform`` attribute will be an instance
 of ``Affine`` and the ``affine`` attribute will remain as an alias.
 
diff --git a/docs/options.rst b/docs/image_options.rst
similarity index 78%
rename from docs/options.rst
rename to docs/image_options.rst
index 8d0c270..e1aa0bb 100644
--- a/docs/options.rst
+++ b/docs/image_options.rst
@@ -1,6 +1,8 @@
-Options
-=======
+Image Options
+*************
 
+Driver Options
+-----------------
 GDAL's format drivers have many [configuration
 options](https://trac.osgeo.org/gdal/wiki/ConfigOptions) The way to set options
 for rasterio is via ``rasterio.drivers()``. Options set on entering are deleted
@@ -20,3 +22,16 @@ on exit.
 Use native Python forms (``True`` and ``False``) for boolean options. Rasterio
 will convert them GDAL's internal forms.
 
+
+Creation options
+-----------------
+
+Blocksize
+tiled
+
+Compression
+-----------
+
+Interleaving
+------------
+
diff --git a/docs/image_processing.rst b/docs/image_processing.rst
new file mode 100644
index 0000000..74c98af
--- /dev/null
+++ b/docs/image_processing.rst
@@ -0,0 +1,27 @@
+Interoperability
+****************
+
+Image processing software
+-------------------------
+Some python image processing software packages
+organize arrays differently than rasterio. The interpretation of a
+3-dimension array read from ``rasterio`` is::
+
+    (bands, columns, rows)
+
+while image processing software like ``scikit-image`` is often::
+
+    (columns, rows, bands)
+
+Numpy provides a function to efficient swap the axis order:
+
+.. code:: python
+
+    # rasterio (bands, cols, rows) -> skimage (cols, rows, bands)
+    image_array = np.swapaxes(array, 0, 2)
+
+    # work in skimage
+
+    # skimage (cols, rows, bands) -> rasterio (bands, cols, rows)
+    array = np.swapaxes(image_array, 2, 0)
+
diff --git a/docs/img/RGB.byte.jpg b/docs/img/RGB.byte.jpg
new file mode 100644
index 0000000..5b31035
Binary files /dev/null and b/docs/img/RGB.byte.jpg differ
diff --git a/docs/img/box_masked_rgb.png b/docs/img/box_masked_rgb.png
new file mode 100644
index 0000000..3da81b3
Binary files /dev/null and b/docs/img/box_masked_rgb.png differ
diff --git a/docs/img/box_rgb.png b/docs/img/box_rgb.png
new file mode 100644
index 0000000..f04bd52
Binary files /dev/null and b/docs/img/box_rgb.png differ
diff --git a/docs/img/filtered.jpg b/docs/img/filtered.jpg
new file mode 100644
index 0000000..cec8ef3
Binary files /dev/null and b/docs/img/filtered.jpg differ
diff --git a/docs/img/reproject.jpg b/docs/img/reproject.jpg
new file mode 100644
index 0000000..1f0d71a
Binary files /dev/null and b/docs/img/reproject.jpg differ
diff --git a/docs/img/saturation.jpg b/docs/img/saturation.jpg
new file mode 100644
index 0000000..ceb0003
Binary files /dev/null and b/docs/img/saturation.jpg differ
diff --git a/docs/img/world.jpg b/docs/img/world.jpg
new file mode 100644
index 0000000..3fdfd49
Binary files /dev/null and b/docs/img/world.jpg differ
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..183ed81
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,51 @@
+Rasterio Documentation
+======================
+
+.. warning::
+    This is not the official documentation. Not yet.
+    This is a draft and everything here is subject to change.
+
+    For now, please refer to https://github.com/mapbox/rasterio
+    for documentation.
+
+Rasterio is for Python programmers and command line users
+who want to read, write and manipulate geospatial raster datasets.
+
+Rasterio employs GDAL_ under the hood for file I/O and raster formatting.
+Its functions typically accept and return Numpy ndarrays.
+Rasterio is designed to make working with geospatial raster data more productive and more fun.
+
+Install with pip (see the complete :doc:`installation docs </installation>` )
+
+.. code::
+
+    pip install rasterio
+
+And an example of use in python
+
+.. code:: python
+
+    import rasterio
+    with rasterio.open('data.tif') as src:
+        array = src.read()
+
+
+Contents:
+
+.. toctree::
+   :maxdepth: 2
+
+   python_manual
+   cli
+   api_docs
+   community
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+.. _GDAL: http://gdal.org/
diff --git a/docs/installation.rst b/docs/installation.rst
new file mode 100644
index 0000000..5a607c9
--- /dev/null
+++ b/docs/installation.rst
@@ -0,0 +1,157 @@
+Installation
+============
+
+Dependencies
+************************
+
+Rasterio has one C library dependency: ``GDAL >=1.9``. GDAL itself depends on a
+number of other libraries provided by most major operating systems and also
+depends on the non standard GEOS and PROJ4 libraries.
+
+Python package dependencies (see also requirements.txt): ``affine, cligj, click, enum34, numpy``.
+
+Development also requires (see requirements-dev.txt) Cython and other packages.
+
+Installing from binaries
+************************
+
+OS X
+----
+
+Binary wheels with the GDAL, GEOS, and PROJ4 libraries included are available
+for OS X versions 10.7+ starting with Rasterio version 0.17. To install, just
+run ``pip install rasterio``. These binary wheels are preferred by newer
+versions of pip. If you don't want these wheels and want to install from
+a source distribution, run ``pip install rasterio --no-use-wheel`` instead.
+
+The included GDAL library is fairly minimal, providing only the format drivers
+that ship with GDAL and are enabled by default. To get access to more formats,
+you must build from a source distribution (see below).
+
+Binary wheels for other operating systems will be available in a future
+release.
+
+Windows
+-------
+
+Binary wheels for rasterio and GDAL are created by Christoph Gohlke and are
+available from his website.
+
+To install rasterio, simply download both binaries for your system (`rasterio
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#rasterio>`__ and `GDAL
+<http://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal>`__) and run something like
+this from the downloads folder:
+
+.. code-block:: console
+
+    $ pip install -U pip 
+    $ pip install GDAL-1.11.2-cp27-none-win32.whl
+    $ pip install rasterio-0.24.0-cp27-none-win32.whl
+
+Installing from the source distribution
+***************************************
+
+Rasterio is a Python C extension and to build you'll need a working compiler
+(XCode on OS X etc). You'll also need Numpy preinstalled; the Numpy headers are
+required to run the rasterio setup script. Numpy has to be installed (via the
+indicated requirements file) before rasterio can be installed. See rasterio's
+Travis `configuration
+<https://github.com/mapbox/rasterio/blob/master/.travis.yml>`__ for more
+guidance.
+
+Linux
+-----
+
+The following commands are adapted from Rasterio's Travis-CI configuration.
+
+.. code-block:: console
+
+    $ sudo add-apt-repository ppa:ubuntugis/ppa
+    $ sudo apt-get update
+    $ sudo apt-get install python-numpy libgdal1h gdal-bin libgdal-dev
+    $ pip install rasterio
+
+Adapt them as necessary for your Linux system.
+
+OS X
+----
+
+For a Homebrew based Python environment, do the following.
+
+.. code-block:: console
+
+    $ brew install gdal
+    $ pip install rasterio
+
+Windows
+-------
+
+You can download a binary distribution of GDAL from `here
+<http://www.gisinternals.com/release.php>`__.  You will also need to download
+the compiled libraries and headers (include files).
+
+When building from source on Windows, it is important to know that setup.py
+cannot rely on gdal-config, which is only present on UNIX systems, to discover
+the locations of header files and libraries that rasterio needs to compile its
+C extensions. On Windows, these paths need to be provided by the user. You
+will need to find the include files and the library files for gdal and use
+setup.py as follows.
+
+.. code-block:: console
+
+    $ python setup.py build_ext -I<path to gdal include files> -lgdal_i -L<path to gdal library>
+    $ python setup.py install
+
+We have had success compiling code using the same version of Microsoft's
+Visual Studio used to compile the targeted version of Python (more info on
+versions used `here
+<https://docs.python.org/devguide/setup.html#windows>`__.).
+
+Note: The GDAL dll (gdal111.dll) and gdal-data directory need to be in your
+Windows PATH otherwise rasterio will fail to work.
+
+Testing
+***************************************
+
+From the repo directory, run py.test
+
+.. code-block:: console
+
+    $ py.test
+
+Note: some tests do not succeed on Windows (see
+`#66
+<https://github.com/mapbox/rasterio/issues/66>`__.).
+
+
+Downstream testing
+------------------
+
+If your project depends on Rasterio and uses Travis-CI, you can speed up your
+builds by fetching Rasterio and its dependencies as a set of wheels from 
+GitHub as done in `rio-plugin-example 
+<https://github.com/sgillies/rio-plugin-example/blob/master/.travis.yml>`__.
+
+.. code-block:: yaml
+
+    language: python
+    env:
+      - RASTERIO_VERSION=0.26
+    python:
+      - "2.7"
+      - "3.4"
+    cache:
+      directories:
+        - $HOME/.pip-cache/
+        - $HOME/wheelhouse
+    before_install:
+      - sudo add-apt-repository -y ppa:ubuntugis/ppa
+      - sudo apt-get update -qq
+      - sudo apt-get install -y libgdal1h gdal-bin
+      - curl -L https://github.com/mapbox/rasterio/releases/download/$RASTERIO_VERSION/rasterio-travis-wheels-$TRAVIS_PYTHON_VERSION.tar.gz > /tmp/wheelhouse.tar.gz
+      - tar -xzvf /tmp/wheelhouse.tar.gz -C $HOME
+    install:
+      - pip install --use-wheel --find-links=$HOME/wheelhouse -e .[test] --cache-dir $HOME/.pip-cache
+    script: 
+      - py.test
+
diff --git a/docs/masking-by-shapefile.rst b/docs/masking-by-shapefile.rst
new file mode 100644
index 0000000..f2a4ab5
--- /dev/null
+++ b/docs/masking-by-shapefile.rst
@@ -0,0 +1,38 @@
+Masking a raster using a shapefile
+==================================
+Using ``rasterio`` with ``fiona``, it is simple to open a shapefile, read geometries, and mask out regions of a raster that are outside the polygons defined in the shapefile.
+
+.. code-block:: python
+
+        import fiona
+        import rasterio
+        import rasterio.tools.mask
+
+        with fiona.open("tests/data/box.shp", "r") as shapefile:
+            features = [feature["geometry"] for feature in shapefile] 
+
+This shapefile contains a single polygon, a box near the center of the raster, so in this case, our list of features is one element long.
+
+.. code-block:: python
+
+        with rasterio.open("tests/data/RGB.byte.tif") as src:
+            out_image, out_transform = rasterio.tools.mask.mask(src, features,
+                                                                crop=True)
+            out_meta = src.meta.copy()
+
+Using ``plot`` and ``imshow`` from ``matplotlib``, we can see the region defined by the shapefile in red overlaid on the original raster.
+
+.. image:: img/box_rgb.png
+
+Applying the features in the shapefile as a mask on the raster sets all pixels outside of the features to be zero. Since ``crop=True`` in this example, the extent of the raster is also set to be the extent of the features in the shapefile. We can then use the updated spatial transform and raster height and width to write the masked raster to a new file.
+
+.. code-block:: python
+
+        out_meta.update({"driver": "GTiff",
+                         "height": out_image.shape[1],
+                         "width": out_image.shape[2],
+                         "transform": out_transform})
+        with rasterio.open("RGB.byte.masked.tif", "w", **out_meta) as dest:
+            dest.write(out_image) 
+
+.. image:: img/box_masked_rgb.png
diff --git a/docs/masks.rst b/docs/masks.rst
index 0cea876..e4ec648 100644
--- a/docs/masks.rst
+++ b/docs/masks.rst
@@ -1,5 +1,5 @@
 Masks
-=====
+*****
 
 In using Rasterio, you'll encounter two different kinds of masks. One is the
 the valid data mask from GDAL, an unsigned byte array with the same number of
@@ -22,14 +22,13 @@ invalid data or *nodata* pixels. In, e.g., merging the image with adjacent
 scenes, we'd like to ignore the nodata pixels and have only valid image data in
 our final mosaic.
 
-Let's use the rio-insp command to look at the two kinds of masks and their
+Let's look at the two kinds of masks and their
 inverse relationship in the context of RGB.byte.tif.
 
 .. code-block:: console
 
-    $ rio insp tests/data/RGB.byte.tif
-    Rasterio 0.19.0 Interactive Inspector (Python 2.7.9)
-    Type "src.meta", "src.read(1)", or "help(src)" for more information.
+    >>> import rasterio
+    >>> src = rasterio.open("tests/data/RGB.byte.tif")
     >>> src.shape
     (718, 791)
     >>> src.count
@@ -49,30 +48,30 @@ mask corresponding to the first dataset band.
 .. code-block:: python
 
     >>> msk = src.read_masks(1)
+    >>> msk.shape
+    (718, 791)
     >>> msk
     array([[0, 0, 0, ..., 0, 0, 0],
            [0, 0, 0, ..., 0, 0, 0],
            [0, 0, 0, ..., 0, 0, 0],
-           ...,
+           ...
            [0, 0, 0, ..., 0, 0, 0],
            [0, 0, 0, ..., 0, 0, 0],
            [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)
 
-This array is a valid data mask in the sense of `GDAL RFC 15
-<https://trac.osgeo.org/gdal/wiki/rfc15_nodatabitmask>`__. The 0 values in its
+This 2D array is a valid data mask in the sense of `GDAL RFC 15
+<https://trac.osgeo.org/gdal/wiki/rfc15_nodatabitmask>`__. The ``0`` values in its
 corners represent *nodata* regions. Zooming in on the interior of the mask
-array shows the ``255`` values that indicate valid data regions.
+array shows the ``255`` values that indicate *valid data* regions.
 
 .. code-block:: python
 
-    >>> m[200:250,200:250]
-    array([[255, 255, 255, ..., 255, 255, 255],
-           [255, 255, 255, ..., 255, 255, 255],
-           [255, 255, 255, ..., 255, 255, 255],
-           ...,
-           [255, 255, 255, ..., 255, 255, 255],
-           [255, 255, 255, ..., 255, 255, 255],
-           [255, 255, 255, ..., 255, 255, 255]], dtype=uint8)
+    >>> msk[200:205,200:205]
+    array([[255, 255, 255, 255, 255],
+           [255, 255, 255, 255, 255],
+           [255, 255, 255, 255, 255],
+           [255, 255, 255, 255, 255],
+           [255, 255, 255, 255, 255]], dtype=uint8)
 
 Displayed using Matplotlib's `imshow()`, the mask looks like this:
 
@@ -93,19 +92,21 @@ Writing masks
 Writing a mask that applies to all dataset bands is just as straightforward:
 pass an ndarray with ``True`` (or values that evaluate to ``True`` to indicate
 valid data and ``False`` to indicate no data to ``write_mask()``. Consider a
-copy of the test data opened using rio-insp in "r+" (update) mode.
+copy of the test data opened in "r+" (update) mode.
+
 
 .. code-block:: python
 
-    $ rio insp copy.tif --mode r+
-    Rasterio 0.19.0 Interactive Inspector (Python 2.7.9)
-    Type "src.meta", "src.read(1)", or "help(src)" for more information.
-    >>>
+    >>> import shutil
+    >>> import rasterio
+
+    >>> shutil.copy("tests/data/RGB.byte.tif", "/tmp/RGB.byte.tif")
+    >>> src = rasterio.open("/tmp/RGB.byte.tif", mode="r+")
 
 To mark that all pixels of all bands are valid (i.e., to override nodata
 metadata values that can't be unset), you'd do this.
 
-.. code-block::
+.. code-block:: python
 
     >>> src.write_mask(True)
     >>> src.read_masks(1).all()
@@ -124,17 +125,22 @@ return mask arrays based on the .msk file.
     -rw-r--r--  1 sean  staff      916 Mar 24 14:25 copy.tif.msk
 
 Can Rasterio help fix buggy nodata masks like the ones in RGB.byte.tif? It
-certainly can. Consider a fresh copy of that file. This time we'll read all
-3 band masks (based on the nodata values, not a .msk GeoTIFF) and show them
+certainly can. Consider a fresh copy of that file. 
+
+.. code-block:: python
+
+    >>> src.close()
+    >>> shutil.copy("tests/data/RGB.byte.tif", "/tmp/RGB.byte.tif")
+    >>> src = rasterio.open("/tmp/RGB.byte.tif", mode="r+")
+
+This time we'll read all 3 band masks 
+(based on the nodata values, not a .msk GeoTIFF) and show them
 as an RGB image (with the help of `numpy.dstack()`):
 
 .. code-block:: python
 
-    $rio insp copy.tif --mode r+
-    Rasterio 0.19.0 Interactive Inspector (Python 2.7.9)
-    Type "src.meta", "src.read(1)", or "help(src)" for more information.
     >>> msk = src.read_masks()
-    >>> show(np.dstack(msk))
+    >>> show(np.dstack(msk))  # doctest: +SKIP
 
 .. image:: img/mask_bands_rgb.png
 
@@ -146,7 +152,7 @@ masks we've read.
 .. code-block:: python
 
     >>> new_msk = (msk[0] & msk[1] & msk[2])
-    >>> show(new_msk)
+    >>> show(new_msk)  # doctest: +SKIP
 
 .. image:: img/mask_conj.png
 
@@ -157,7 +163,7 @@ found the right value for the ``size`` argument empirically.
 
     >>> from rasterio.features import sieve
     >>> sieved_msk = sieve(new_msk, size=800)
-    >>> show(sieved_msk)
+    >>> show(sieved_msk)  # doctest: +SKIP
 
 .. image:: img/mask_sieved.png
 
@@ -166,6 +172,7 @@ Last thing to do is write that sieved mask back to the dataset.
 .. code-block:: python
 
     >>> src.write_mask(sieved_msk)
+    >>> src.close()
 
 The result is a properly masked dataset that allows some 0 value pixels to be
 considered valid.
@@ -177,15 +184,13 @@ If you want, you can read dataset bands as numpy masked arrays.
 
 .. code-block:: python
 
-    $ rio insp tests/data/RGB.byte.tif
-    Rasterio 0.19.0 Interactive Inspector (Python 2.7.9)
-    Type "src.meta", "src.read(1)", or "help(src)" for more information.
+    >>> src = rasterio.open("tests/data/RGB.byte.tif")
     >>> blue = src.read(1, masked=True)
     >>> blue.mask
     array([[ True,  True,  True, ...,  True,  True,  True],
            [ True,  True,  True, ...,  True,  True,  True],
            [ True,  True,  True, ...,  True,  True,  True],
-           ...,
+           ...
            [ True,  True,  True, ...,  True,  True,  True],
            [ True,  True,  True, ...,  True,  True,  True],
            [ True,  True,  True, ...,  True,  True,  True]], dtype=bool)
@@ -201,5 +206,6 @@ You can rely on this Rasterio identity for any integer value ``N``.
 
 .. code-block:: python
 
+    >>> N = 1
     >>> (~src.read(N, masked=True).mask * 255 == src.read_masks(N)).all()
     True
diff --git a/docs/modules.rst b/docs/modules.rst
new file mode 100644
index 0000000..e7356de
--- /dev/null
+++ b/docs/modules.rst
@@ -0,0 +1,7 @@
+rasterio
+========
+
+.. toctree::
+   :maxdepth: 4
+
+   rasterio
diff --git a/docs/nodata.rst b/docs/nodata.rst
new file mode 100644
index 0000000..21b31df
--- /dev/null
+++ b/docs/nodata.rst
@@ -0,0 +1,9 @@
+Nodata
+******
+
+.. todo::
+
+    * nodata value
+    * alpha band
+
+.. image:: https://cloud.githubusercontent.com/assets/5084513/9670961/4f04da04-5244-11e5-93e5-86b69694f82f.jpg
diff --git a/docs/osgeo_gdal_migration.rst b/docs/osgeo_gdal_migration.rst
new file mode 100644
index 0000000..9a983db
--- /dev/null
+++ b/docs/osgeo_gdal_migration.rst
@@ -0,0 +1,61 @@
+Migration Guide for osgeo.gdal users
+====================================
+
+
+Differences between rasterio and osgeo.gdal
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Rasterio uses GDAL's shared library under the hood to provide a significant portion of its functionality.
+But GDAL also ships with its own python bindings, ``osgeo.gdal``.
+This section will discuss the differences between ``rasterio`` and ``osgeo.gdal`` and reasons why you might
+choose to use one over the other.
+
+``osgeo.gdal`` is automatically-generated using swig. As a result, the interface and method names are
+very similar to the native C++ API.  The ``rasterio`` library is built with Cython which allows
+us to create an interface that follows the style and conventions of familiar Python code.
+
+This is best illustrated by example.  Opening a raster file with ``osgeo.gdal`` involves using gdal constants and the programmer must provide their own error handling and memory management ::
+
+    from osgeo import gdal
+    from osgeo.gdalconst import *
+    dataset = gdal.Open( filename, GA_ReadOnly )
+    if dataset is None:
+        # ... handle a non-existant dataset
+    # ... work with dataset
+    del dataset
+
+Compared to the similar code in ``rasterio``::
+
+    import rasterio
+    with rasterio.drivers():
+        with rasterio.open(filename, 'r') as dataset:
+            # ... work with dataset
+
+The ``rasterio`` code:
+
+* Uses pep8 compliant module, method and property names
+* follows the conventions of python file handles
+* uses context managers to safely manage memory, environment variables and file resources
+* will raise proper exceptions (i.e. ``IOError`` if the file does not exist)
+
+Of course, readability is subjective but I think most Python programmers would agree that the
+``rasterio`` example is easier to understand and debug as well.
+
+.. todo::
+
+  * global state makes osgeo.gdal unsafe with other python modules
+  * hidden behavior with env vars vs explicit GDALEnv
+  * vsi vs URIs
+  * limited scope of rasterio, what does osgeo.gdal do that rasterio can't
+  * installation issues
+  * crs handling
+  * examples of unsafe memory situations
+  * rio and the relationship to gdal CLI tools
+  
+Migrating
+^^^^^^^^^
+
+.. todo::
+
+    Practical tips and examples of porting common use cases in both python and cli.
+    Some overlap with the cookbook here, so probably best to reference it when appropriate.
+
diff --git a/docs/overviews.rst b/docs/overviews.rst
new file mode 100644
index 0000000..414547e
--- /dev/null
+++ b/docs/overviews.rst
@@ -0,0 +1,7 @@
+Overviews
+*********
+
+.. todo::
+    
+    * "sidecar" files vs. internal
+    * supported formats
diff --git a/docs/plotting.rst b/docs/plotting.rst
new file mode 100644
index 0000000..dbc5e13
--- /dev/null
+++ b/docs/plotting.rst
@@ -0,0 +1,23 @@
+Plotting
+********
+
+.. todo::
+
+    * alt color ramps
+    * labeling axes with coordinates 
+    * multiplots
+    * RGB 
+   
+    
+.. code-block:: python
+
+    
+    >>> import rasterio
+    >>> from matplotlib import pyplot
+    >>> src = rasterio.open("tests/data/RGB.byte.tif")
+    >>> pyplot.imshow(src.read(1), cmap='pink')
+    <matplotlib.image.AxesImage object at 0x...>
+    >>> pyplot.show()  # doctest: +SKIP
+
+
+.. image:: http://farm6.staticflickr.com/5032/13938576006_b99b23271b_o_d.png
diff --git a/docs/python_manual.rst b/docs/python_manual.rst
new file mode 100644
index 0000000..cda7946
--- /dev/null
+++ b/docs/python_manual.rst
@@ -0,0 +1,26 @@
+=================================
+``rasterio`` Python User's Manual
+=================================
+
+This user's manual is for Python developers who want a clean API for accessing raster data.
+
+.. todo::
+
+    What does it do, narrative examples
+
+    What it does NOT do
+
+    For command line tools look to CLI users manual 
+
+.. toctree::
+    :maxdepth: 2
+
+    installation
+    data_model
+    reading
+    working_with_datasets
+    writing
+    topics
+    developers
+    cookbook
+    osgeo_gdal_migration
diff --git a/docs/rasterio.aws.rst b/docs/rasterio.aws.rst
new file mode 100644
index 0000000..d46aebb
--- /dev/null
+++ b/docs/rasterio.aws.rst
@@ -0,0 +1,7 @@
+rasterio.aws module
+===================
+
+.. automodule:: rasterio.aws
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.coords.rst b/docs/rasterio.coords.rst
new file mode 100644
index 0000000..d062b3a
--- /dev/null
+++ b/docs/rasterio.coords.rst
@@ -0,0 +1,7 @@
+rasterio.coords module
+======================
+
+.. automodule:: rasterio.coords
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.crs.rst b/docs/rasterio.crs.rst
new file mode 100644
index 0000000..229076d
--- /dev/null
+++ b/docs/rasterio.crs.rst
@@ -0,0 +1,7 @@
+rasterio.crs module
+===================
+
+.. automodule:: rasterio.crs
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.dtypes.rst b/docs/rasterio.dtypes.rst
new file mode 100644
index 0000000..bed24bc
--- /dev/null
+++ b/docs/rasterio.dtypes.rst
@@ -0,0 +1,7 @@
+rasterio.dtypes module
+======================
+
+.. automodule:: rasterio.dtypes
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.enums.rst b/docs/rasterio.enums.rst
new file mode 100644
index 0000000..ca3fb0f
--- /dev/null
+++ b/docs/rasterio.enums.rst
@@ -0,0 +1,7 @@
+rasterio.enums module
+=====================
+
+.. automodule:: rasterio.enums
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.errors.rst b/docs/rasterio.errors.rst
new file mode 100644
index 0000000..37a56cc
--- /dev/null
+++ b/docs/rasterio.errors.rst
@@ -0,0 +1,7 @@
+rasterio.errors module
+======================
+
+.. automodule:: rasterio.errors
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.features.rst b/docs/rasterio.features.rst
new file mode 100644
index 0000000..c12ec70
--- /dev/null
+++ b/docs/rasterio.features.rst
@@ -0,0 +1,7 @@
+rasterio.features module
+========================
+
+.. automodule:: rasterio.features
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.fill.rst b/docs/rasterio.fill.rst
new file mode 100644
index 0000000..8c932bf
--- /dev/null
+++ b/docs/rasterio.fill.rst
@@ -0,0 +1,7 @@
+rasterio.fill module
+====================
+
+.. automodule:: rasterio.fill
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.mask.rst b/docs/rasterio.mask.rst
new file mode 100644
index 0000000..4abb698
--- /dev/null
+++ b/docs/rasterio.mask.rst
@@ -0,0 +1,7 @@
+rasterio.mask module
+====================
+
+.. automodule:: rasterio.mask
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.merge.rst b/docs/rasterio.merge.rst
new file mode 100644
index 0000000..93d2fd6
--- /dev/null
+++ b/docs/rasterio.merge.rst
@@ -0,0 +1,7 @@
+rasterio.merge module
+=====================
+
+.. automodule:: rasterio.merge
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.plot.rst b/docs/rasterio.plot.rst
new file mode 100644
index 0000000..5ad61be
--- /dev/null
+++ b/docs/rasterio.plot.rst
@@ -0,0 +1,7 @@
+rasterio.plot module
+====================
+
+.. automodule:: rasterio.plot
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.profiles.rst b/docs/rasterio.profiles.rst
new file mode 100644
index 0000000..c412066
--- /dev/null
+++ b/docs/rasterio.profiles.rst
@@ -0,0 +1,7 @@
+rasterio.profiles module
+========================
+
+.. automodule:: rasterio.profiles
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.rst b/docs/rasterio.rst
new file mode 100644
index 0000000..5bd6c5d
--- /dev/null
+++ b/docs/rasterio.rst
@@ -0,0 +1,32 @@
+rasterio package
+================
+
+.. automodule:: rasterio
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+
+   rasterio.aws
+   rasterio.coords
+   rasterio.crs
+   rasterio.dtypes
+   rasterio.enums
+   rasterio.errors
+   rasterio.features
+   rasterio.fill
+   rasterio.mask
+   rasterio.merge
+   rasterio.plot
+   rasterio.profiles
+   rasterio.sample
+   rasterio.transform
+   rasterio.vfs
+   rasterio.warnings
+   rasterio.warp
+   rasterio.windows
+
diff --git a/docs/rasterio.sample.rst b/docs/rasterio.sample.rst
new file mode 100644
index 0000000..85ff45a
--- /dev/null
+++ b/docs/rasterio.sample.rst
@@ -0,0 +1,7 @@
+rasterio.sample module
+======================
+
+.. automodule:: rasterio.sample
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.tool.rst b/docs/rasterio.tool.rst
new file mode 100644
index 0000000..7741b32
--- /dev/null
+++ b/docs/rasterio.tool.rst
@@ -0,0 +1,7 @@
+rasterio.tool module
+====================
+
+.. automodule:: rasterio.tool
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.tools.mask.rst b/docs/rasterio.tools.mask.rst
new file mode 100644
index 0000000..96bbf33
--- /dev/null
+++ b/docs/rasterio.tools.mask.rst
@@ -0,0 +1,7 @@
+rasterio.tools.mask module
+==========================
+
+.. automodule:: rasterio.tools.mask
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.tools.merge.rst b/docs/rasterio.tools.merge.rst
new file mode 100644
index 0000000..04d5d23
--- /dev/null
+++ b/docs/rasterio.tools.merge.rst
@@ -0,0 +1,7 @@
+rasterio.tools.merge module
+===========================
+
+.. automodule:: rasterio.tools.merge
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.tools.rst b/docs/rasterio.tools.rst
new file mode 100644
index 0000000..7582796
--- /dev/null
+++ b/docs/rasterio.tools.rst
@@ -0,0 +1,16 @@
+rasterio.tools package
+======================
+
+.. automodule:: rasterio.tools
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+
+   rasterio.tools.mask
+   rasterio.tools.merge
+
diff --git a/docs/rasterio.transform.rst b/docs/rasterio.transform.rst
new file mode 100644
index 0000000..dae272a
--- /dev/null
+++ b/docs/rasterio.transform.rst
@@ -0,0 +1,7 @@
+rasterio.transform module
+=========================
+
+.. automodule:: rasterio.transform
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.vfs.rst b/docs/rasterio.vfs.rst
new file mode 100644
index 0000000..b593233
--- /dev/null
+++ b/docs/rasterio.vfs.rst
@@ -0,0 +1,7 @@
+rasterio.vfs module
+===================
+
+.. automodule:: rasterio.vfs
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.warnings.rst b/docs/rasterio.warnings.rst
new file mode 100644
index 0000000..348ba4b
--- /dev/null
+++ b/docs/rasterio.warnings.rst
@@ -0,0 +1,7 @@
+rasterio.warnings module
+========================
+
+.. automodule:: rasterio.warnings
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.warp.rst b/docs/rasterio.warp.rst
new file mode 100644
index 0000000..bf96a7f
--- /dev/null
+++ b/docs/rasterio.warp.rst
@@ -0,0 +1,7 @@
+rasterio.warp module
+====================
+
+.. automodule:: rasterio.warp
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/rasterio.windows.rst b/docs/rasterio.windows.rst
new file mode 100644
index 0000000..9352a29
--- /dev/null
+++ b/docs/rasterio.windows.rst
@@ -0,0 +1,7 @@
+rasterio.windows module
+=======================
+
+.. automodule:: rasterio.windows
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/reading.rst b/docs/reading.rst
new file mode 100644
index 0000000..2eab628
--- /dev/null
+++ b/docs/reading.rst
@@ -0,0 +1,133 @@
+Reading Datasets
+=====================
+
+.. todo::
+
+    * use of context manager
+    * ndarray shape is (band, cols, rows)
+    * Discuss and/or link to topics
+        - supported formats, drivers
+        - vsi
+        - tags
+        - profile
+        - crs
+        - transforms
+        - dtypes
+        - block windows
+
+
+Dataset objects provide read, read-write, and write access to raster data files
+and are obtained by calling ``rasterio.open()``. That function mimics Python's
+built-in ``open()`` and the dataset objects it returns mimic Python ``file``
+objects.
+
+.. code-block:: python
+
+    >>> import rasterio
+    >>> src = rasterio.open('tests/data/RGB.byte.tif')
+    >>> src
+    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    >>> src.name
+    'tests/data/RGB.byte.tif'
+    >>> src.mode
+    'r'
+    >>> src.closed
+    False
+
+If you attempt to access a nonexistent path, ``rasterio.open()`` does the same
+thing as ``open()``, raising an exception immediately.
+
+.. code-block:: python
+
+    >>> open('/lol/wut.tif')
+    Traceback (most recent call last):
+     ...
+    IOError: [Errno 2] No such file or directory: '/lol/wut.tif'
+    >>> rasterio.open('/lol/wut.tif')
+    Traceback (most recent call last):
+     ...
+    IOError: No such file or directory
+
+Datasets generally have one or more bands (or layers). Following the GDAL
+convention, these are indexed starting with the number 1. The first band of
+a file can be read like this:
+
+.. code-block:: python
+
+    >>> array = src.read(1)
+    >>> array.shape
+    (718, 791)
+
+The returned object is a 2-dimensional Numpy ndarray. The representation of
+that array at the Python prompt is just a summary; the GeoTIFF file that
+Rasterio uses for testing has 0 values in the corners, but has nonzero values
+elsewhere.
+
+.. code-block:: python
+
+    >>> from matplotlib import pyplot
+    >>> pyplot.imshow(array, cmap='pink')
+    <matplotlib.image.AxesImage object at 0x...>
+    >>> pyplot.show()  # doctest: +SKIP
+
+
+.. image:: http://farm6.staticflickr.com/5032/13938576006_b99b23271b_o_d.png
+
+The indexes, Numpy data types, and nodata values of all a dataset's bands can
+be had from its ``indexes``, ``dtypes``, and ``nodatavals`` attributes.
+
+.. code-block:: python
+
+    >>> for i, dtype, nodataval in zip(src.indexes, src.dtypes, src.nodatavals):
+    ...     print i, dtype, nodataval
+    ...
+    1 uint8 0.0
+    2 uint8 0.0
+    3 uint8 0.0
+
+To close a dataset, call its ``close()`` method.
+
+.. code-block:: python
+
+    >>> src.close()
+    >>> src
+    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+
+After it's closed, data can no longer be read.
+
+.. code-block:: python
+
+    >>> src.read(1)
+    Traceback (most recent call last):
+     ...
+    ValueError: can't read closed raster file
+
+This is the same behavior as Python's ``file``.
+
+.. code-block:: python
+
+    >>> f = open('README.rst')
+    >>> f.close()
+    >>> f.read()
+    Traceback (most recent call last):
+     ...
+    ValueError: I/O operation on closed file
+
+As Python ``file`` objects can, Rasterio datasets can manage the entry into 
+and exit from runtime contexts created using a ``with`` statement. This 
+ensures that files are closed no matter what exceptions may be raised within
+the the block.
+
+.. code-block:: python
+
+    >>> with rasterio.open('tests/data/RGB.byte.tif', 'r') as one:
+    ...     with rasterio.open('tests/data/RGB.byte.tif', 'r') as two:
+    ...        print two
+    ...     print one
+    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    <open RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+
+    >>> print two
+    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
+    >>> print one
+    <closed RasterReader name='tests/data/RGB.byte.tif' mode='r'>
diff --git a/docs/recipies/band_summary_stats.py b/docs/recipies/band_summary_stats.py
new file mode 100644
index 0000000..897d2c4
--- /dev/null
+++ b/docs/recipies/band_summary_stats.py
@@ -0,0 +1,17 @@
+from pprint import pprint
+import rasterio
+import numpy as np
+
+path = "tests/data/RGB.byte.tif"
+with rasterio.open(path) as src:
+    array = src.read()
+
+stats = []
+for band in array:
+    stats.append({
+        'min': band.min(),
+        'mean': band.mean(),
+        'median': np.median(band),
+        'max': band.max()})
+
+pprint(stats)
diff --git a/docs/recipies/filter.py b/docs/recipies/filter.py
new file mode 100644
index 0000000..3344f40
--- /dev/null
+++ b/docs/recipies/filter.py
@@ -0,0 +1,16 @@
+import rasterio
+from scipy.signal import medfilt
+
+path = "tests/data/RGB.byte.tif"
+output = "/tmp/filtered.tif"
+
+with rasterio.open(path) as src:
+    array = src.read()
+    profile = src.profile
+
+# apply a 5x5 median filter to each band
+filtered = medfilt(array, (1, 5, 5)).astype('uint8')
+
+# Write to tif, using the same profile as the source
+with rasterio.open(output, 'w', **profile) as dst:
+    dst.write(filtered)
diff --git a/docs/recipies/mask_shp.py b/docs/recipies/mask_shp.py
new file mode 100644
index 0000000..97566ab
--- /dev/null
+++ b/docs/recipies/mask_shp.py
@@ -0,0 +1,18 @@
+import fiona
+import rasterio
+from rasterio.tools.mask import mask
+
+with fiona.open("tests/data/box.shp", "r") as shapefile:
+    geoms = [feature["geometry"] for feature in shapefile]
+
+with rasterio.open("tests/data/RGB.byte.tif") as src:
+    out_image, out_transform = mask(src, geoms, crop=True)
+    out_meta = src.meta.copy()
+
+out_meta.update({"driver": "GTiff",
+                 "height": out_image.shape[1],
+                 "width": out_image.shape[2],
+                 "transform": out_transform})
+
+with rasterio.open("/tmp/masked.tif", "w", **out_meta) as dest:
+    dest.write(out_image)
diff --git a/docs/recipies/reproject.py b/docs/recipies/reproject.py
new file mode 100644
index 0000000..2ba260f
--- /dev/null
+++ b/docs/recipies/reproject.py
@@ -0,0 +1,49 @@
+import numpy as np
+import rasterio
+from rasterio.warp import calculate_default_transform, reproject, Resampling
+from rasterio import crs
+
+rgb = 'tests/data/world.tif'
+out = '/tmp/reproj.tif'
+
+# Reproject to NAD83(HARN) / Hawaii zone 3 (ftUS) - Transverse Mercator
+dst_crs = crs.from_string("EPSG:3759")
+
+
+with rasterio.drivers(CHECK_WITH_INVERT_PROJ=True):
+    with rasterio.open(rgb) as src:
+        profile = src.profile
+
+        # Calculate the ideal dimensions and transformation in the new crs
+        dst_affine, dst_width, dst_height = calculate_default_transform(
+            src.crs, dst_crs, src.width, src.height, *src.bounds)
+
+        # update the relevant parts of the profile
+        profile.update({
+            'crs': dst_crs,
+            'transform': dst_affine,
+            'affine': dst_affine,
+            'width': dst_width,
+            'height': dst_height
+        })
+
+        # Reproject and write each band
+        with rasterio.open(out, 'w', **profile) as dst:
+            for i in range(1, src.count + 1):
+                src_array = src.read(i)
+                dst_array = np.empty((dst_height, dst_width), dtype='uint8')
+
+                reproject(
+                    # Source parameters
+                    source=src_array,
+                    src_crs=src.crs,
+                    src_transform=src.affine,
+                    # Destination paramaters
+                    destination=dst_array,
+                    dst_transform=dst_affine,
+                    dst_crs=dst_crs,
+                    # Configuration
+                    resampling=Resampling.nearest,
+                    num_threads=2)
+
+                dst.write(dst_array, i)
diff --git a/docs/recipies/saturation.py b/docs/recipies/saturation.py
new file mode 100644
index 0000000..2bad260
--- /dev/null
+++ b/docs/recipies/saturation.py
@@ -0,0 +1,55 @@
+import rasterio
+import numpy as np
+from skimage.color import rgb2lab, lab2lch, lch2lab, lab2rgb
+
+path = "tests/data/RGB.byte.tif"
+output = "/tmp/saturation.tif"
+
+
+def saturation(arr, sat):
+    """Multiple saturation/chroma in LCH color space
+    Input and output are 3-band RGB scaled 0 to 255
+    """
+    # scale image 0 to 1
+    arr_norm = arr / 255.0
+    # Convert colorspace
+    lch = rgb2lch(arr_norm)
+    # Adjust chroma, band at index=1
+    lch[1] = lch[1] * sat
+    # Convert colorspace and rescale
+    return (lch2rgb(lch) * 255).astype('uint8')
+
+
+def rgb2lch(rgb):
+    """Convert RBG to LCH colorspace (via LAB)
+    Input and output are in (bands, cols, rows) order
+    """
+    # reshape for skimage (bands, cols, rows) -> (cols, rows, bands)
+    srgb = np.swapaxes(rgb, 0, 2)
+    # convert colorspace
+    lch = lab2lch(rgb2lab(srgb))
+    # return in (bands, cols, rows) order
+    return np.swapaxes(lch, 2, 0)
+
+
+def lch2rgb(lch):
+    """Convert LCH to RGB colorspace (via LAB)
+    Input and output are in (bands, cols, rows) order
+    """
+    # reshape for skimage (bands, cols, rows) -> (cols, rows, bands)
+    slch = np.swapaxes(lch, 0, 2)
+    # convert colorspace
+    rgb = lab2rgb(lch2lab(slch))
+    # return in (bands, cols, rows) order
+    return np.swapaxes(rgb, 2, 0)
+
+
+with rasterio.open(path) as src:
+    array = src.read()
+    profile = src.profile
+
+# Increase color saturation by 60%
+array_sat = saturation(array, 1.6)
+
+with rasterio.open(output, 'w', **profile) as dst:
+    dst.write(array_sat)
diff --git a/docs/reproject.rst b/docs/reproject.rst
index 906a60c..05507e3 100644
--- a/docs/reproject.rst
+++ b/docs/reproject.rst
@@ -1,5 +1,7 @@
 Reprojection
-============
+************
+
+TODO calc_default_transform
 
 Rasterio can map the pixels of a destination raster with an associated
 coordinate reference system and transform to the pixels of a source image with
diff --git a/docs/resampling.rst b/docs/resampling.rst
new file mode 100644
index 0000000..e86f6d9
--- /dev/null
+++ b/docs/resampling.rst
@@ -0,0 +1,7 @@
+Resampling Methods
+******************
+
+.. todo::
+
+    explain why you would use nearest vs bilinear/cubic/etc vs summary stats.
+    categorical, continuous data, imagery all have different concerns.
diff --git a/docs/tags.rst b/docs/tags.rst
index 541709d..a416d92 100644
--- a/docs/tags.rst
+++ b/docs/tags.rst
@@ -1,5 +1,5 @@
 Tagging datasets and bands
-==========================
+**************************
 
 GDAL's `data model <http://www.gdal.org/gdal_datamodel.html>`__ includes
 collections of key, value pairs for major classes. In that model, these are
@@ -23,7 +23,9 @@ namespace, just call ``tags()`` with no arguments.
 
 .. code-block:: pycon
 
-    >>>src.tags()
+    >>> import rasterio
+    >>> src = rasterio.open("tests/data/RGB.byte.tif")
+    >>> src.tags()
     {u'AREA_OR_POINT': u'Area'}
 
 A dataset's bands may have tags, too. Here are the tags from the default namespace
diff --git a/docs/topics.rst b/docs/topics.rst
new file mode 100644
index 0000000..3aaad39
--- /dev/null
+++ b/docs/topics.rst
@@ -0,0 +1,23 @@
+General Concepts
+================
+
+.. toctree::
+
+    reproject
+    errors
+    color
+    concurrency
+    image_options
+    fillnodata
+    overviews
+    plotting
+    features
+    masks
+    nodata
+    image_processing
+    resampling
+    tags
+    georeferencing
+    windowed-rw
+    vsi
+
diff --git a/docs/vsi.rst b/docs/vsi.rst
new file mode 100644
index 0000000..a7ef9c8
--- /dev/null
+++ b/docs/vsi.rst
@@ -0,0 +1,56 @@
+Virtual Files
+*************
+
+.. todo:: 
+
+    Support for URIs describing zip, s3, etc resources. Relationship to GDAL
+    vsicurl et al.
+
+AWS S3
+======
+
+After you have configured your AWS credentials as explained in the `boto3 guide
+<http://boto3.readthedocs.org/en/latest/guide/configuration.html>`__ you can
+read metadata and imagery from TIFFs stored as S3 objects with little change to
+your code.  Add a `rasterio.aws.Session` as shown below.
+
+.. code-block:: pycon
+
+    >>> import pprint
+    >>> from rasterio.aws import Session
+    >>> session = Session()
+    >>> with session.open('s3://landsat-pds/L8/139/045/LC81390452014295LGN00/LC81390452014295LGN00_B1.TIF') as src:
+    ...     pprint.pprint(src.profile)
+    ...
+    {'affine': Affine(30.0, 0.0, 381885.0,
+           0.0, -30.0, 2512815.0),
+     'blockxsize': 512,
+     'blockysize': 512,
+     'compress': 'deflate',
+     'count': 1,
+     'crs': {'init': u'epsg:32645'},
+     'driver': u'GTiff',
+     'dtype': 'uint16',
+     'height': 7791,
+     'interleave': 'band',
+     'nodata': None,
+     'tiled': True,
+     'transform': (381885.0, 30.0, 0.0, 2512815.0, 0.0, -30.0),
+     'width': 7621}
+
+If you provide no arguments when creating a session, your environment will be
+checked for credentials. Access keys may be explicitly provided when creating
+a session.
+
+.. code-block:: python
+
+    session = Session(aws_access_key_id='KEY',
+                      aws_secret_access_key='SECRET',
+                      aws_session_token='TOKEN')
+
+.. note:: AWS pricing concerns
+   While this feature can reduce latency by reading fewer bytes from S3
+   compared to downloading the entire TIFF and opening locally, it does
+   make at least 3 GET requests to fetch a TIFF's `profile` as shown above
+   and likely many more to fetch all the imagery from the TIFF. Consult the
+   AWS S3 pricing guidelines before deciding if `aws.Session` is for you.
diff --git a/docs/watch.sh b/docs/watch.sh
new file mode 100755
index 0000000..0be5e7a
--- /dev/null
+++ b/docs/watch.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+fswatch -o .. | xargs -n1 make html
diff --git a/docs/windowed-rw.rst b/docs/windowed-rw.rst
index 0393cef..8ff6290 100644
--- a/docs/windowed-rw.rst
+++ b/docs/windowed-rw.rst
@@ -1,5 +1,5 @@
 Windowed reading and writing
-============================
+****************************
 
 Beginning in rasterio 0.3, you can read and write "windows" of raster files.
 This feature allows you to operate on rasters that are larger than your
@@ -170,7 +170,7 @@ a dataset:
 
 .. code-block:: python
 
-    from rasterio import get_data_window
+    from rasterio.windows import get_data_window
 
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         window = get_data_window(src.read(1, masked=True))
@@ -197,16 +197,16 @@ with the same full extent.
 
 .. code-block:: python
 
-    from rasterio import window_union, window_intersection
+    from rasterio import windows
 
     # Full window is ((0, 1000), (0, 500))
     window1 = ((100, 500), (10, 500))
     window2 = ((10, 150), (50, 250))
 
-    outer = window_union([window1, window2])
+    outer = windows.union([window1, window2])
     # outer = ((10, 500), (10, 500))
 
-    inner = window_intersection([window1, window2])
+    inner = windows.intersection([window1, window2])
     # inner = ((100, 150), (50, 250))
 
 
diff --git a/docs/working_with_datasets.rst b/docs/working_with_datasets.rst
new file mode 100644
index 0000000..7dbc181
--- /dev/null
+++ b/docs/working_with_datasets.rst
@@ -0,0 +1,57 @@
+Working with Datasets
+======================
+
+.. todo::
+
+    * working with ndarrays
+    * src.profile
+
+Attributes
+----------
+
+In addition to the file-like attributes shown above, a dataset has a number
+of other read-only attributes that help explain its role in spatial information
+systems. The ``driver`` attribute gives you the name of the GDAL format
+driver used. The ``height`` and ``width`` are the number of rows and columns of
+the raster dataset and ``shape`` is a ``height, width`` tuple as used by
+Numpy. The ``count`` attribute tells you the number of bands in the dataset.
+
+.. code-block:: python
+
+    >>> import rasterio
+    >>> src = rasterio.open("tests/data/RGB.byte.tif")
+    >>> src.driver
+    u'GTiff'
+    >>> src.height, src.width
+    (718, 791)
+    >>> src.shape
+    (718, 791)
+    >>> src.count
+    3
+
+What makes geospatial raster datasets different from other raster files is
+that their pixels map to regions of the Earth. A dataset has a coordinate
+reference system and an affine transformation matrix that maps pixel
+coordinates to coordinates in that reference system.
+
+.. code-block:: python
+
+    >>> src.crs
+    {'init': u'epsg:32618'}
+    >>> src.affine
+    Affine(300.0379266750948, 0.0, 101985.0,
+           0.0, -300.041782729805, 2826915.0)
+
+To get the ``x, y`` world coordinates for the upper left corner of any pixel,
+take the product of the affine transformation matrix and the tuple ``(col,
+row)``.  
+
+.. code-block:: python
+
+    >>> col, row = 0, 0
+    >>> src.affine * (col, row)
+    (101985.0, 2826915.0)
+    >>> col, row = src.width, src.height
+    >>> src.affine * (col, row)
+    (339315.0, 2611485.0)
+
diff --git a/docs/writing.rst b/docs/writing.rst
new file mode 100644
index 0000000..4099c91
--- /dev/null
+++ b/docs/writing.rst
@@ -0,0 +1,48 @@
+Writing Datasets
+=================
+
+.. todo::
+
+    * supported drivers
+    * appending to existing data
+    * context manager
+    * write 3d vs write 2d
+    * profile.update
+    * document issues with writing compressed files (per #77)
+    * discuss and refer to topics
+        * creation options
+        * transforms
+        * dtypes
+        * block windows
+
+Opening a file in writing mode is a little more complicated than opening
+a text file in Python. The dimensions of the raster dataset, the 
+data types, and the specific format must be specified.
+
+Here's a simple example of the basic rasterio functionality. 
+An array is written to a new single band TIFF.
+
+.. code-block:: python
+
+    # Register GDAL format drivers and configuration options with a
+    # context manager.
+    with rasterio.drivers():
+
+        # Write the product as a raster band to a new 8-bit file. For
+        # the new file's profile, we start with the meta attributes of
+        # the source file, but then change the band count to 1, set the
+        # dtype to uint8, and specify LZW compression.
+        profile = src.profile
+        profile.update(
+            dtype=rasterio.uint8,
+            count=1,
+            compress='lzw')
+
+        with rasterio.open('example-total.tif', 'w', **profile) as dst:
+            dst.write(array.astype(rasterio.uint8), 1)
+
+    # At the end of the ``with rasterio.drivers()`` block, context
+    # manager exits and all drivers are de-registered.
+
+Writing data mostly works as with a Python file. There are a few format-
+specific differences.
diff --git a/rasterio/__init__.py b/rasterio/__init__.py
index 1ae488f..76eff68 100644
--- a/rasterio/__init__.py
+++ b/rasterio/__init__.py
@@ -1,9 +1,8 @@
 # rasterio
+from __future__ import absolute_import
 
 from collections import namedtuple
 import logging
-import os
-import warnings
 
 from rasterio._base import eval_window, window_shape, window_index
 from rasterio._drivers import driver_count, GDALEnv
@@ -14,6 +13,7 @@ from rasterio.dtypes import (
 from rasterio.five import string_types
 from rasterio.profiles import default_gtiff_profile
 from rasterio.transform import Affine, guard_transform
+from rasterio import windows
 
 # These modules are imported from the Cython extensions, but are also import
 # here to help tools like cx_Freeze find them automatically
@@ -23,7 +23,7 @@ from rasterio import _err, coords, enums, vfs
 
 __all__ = [
     'band', 'open', 'drivers', 'copy', 'pad']
-__version__ = "0.32.0"
+__version__ = "0.33.0"
 
 log = logging.getLogger('rasterio')
 class NullHandler(logging.Handler):
@@ -33,7 +33,7 @@ log.addHandler(NullHandler())
 
 
 def open(
-        path, mode='r', 
+        path, mode='r',
         driver=None,
         width=None, height=None,
         count=None,
@@ -43,7 +43,7 @@ def open(
         **kwargs):
     """Open file at ``path`` in ``mode`` "r" (read), "r+" (read/write),
     or "w" (write) and return a ``Reader`` or ``Updater`` object.
-    
+
     In write mode, a driver name such as "GTiff" or "JPEG" (see GDAL
     docs or ``gdal_translate --help`` on the command line), ``width``
     (number of pixels per line) and ``height`` (number of lines), the
@@ -52,41 +52,89 @@ def open(
     8-bit bands or ``rasterio.uint16`` for 16-bit bands must be
     specified using the ``dtype`` argument.
 
+    Parameters
+    ----------
+    mode: string
+        "r" (read), "r+" (read/write), or "w" (write)
+    driver: string
+        driver code specifying the format name (e.g. "GTiff" or "JPEG")
+        See GDAL docs at http://www.gdal.org/formats_list.html
+        (optional, required for write)
+    width: int
+        number of pixels per line
+        (optional, required for write)
+    height: int
+        number of lines
+        (optional, required for write)
+    count: int > 0
+        number of bands
+        (optional, required for write)
+    dtype: rasterio.dtype
+        the data type for bands such as ``rasterio.ubyte`` for
+        8-bit bands or ``rasterio.uint16`` for 16-bit bands
+        (optional, required for write)
+    crs: dict or string
+        Coordinate reference system
+        (optional, recommended for write)
+    transform: Affine instance
+        Affine transformation mapping the pixel space to geographic space
+        (optional, recommended for write)
+    nodata: number
+        Defines pixel value to be interpreted as null/nodata
+        (optional, recommended for write)
+
+    Returns
+    -------
+    A ``Reader`` or ``Updater`` object.
+
+    Notes
+    -----
+    In write mode, you must specify at least ``width``, ``height``, ``count``
+    and ``dtype``.
+
     A coordinate reference system for raster datasets in write mode can
     be defined by the ``crs`` argument. It takes Proj4 style mappings
     like
-    
-      {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84',
-       'no_defs': True}
+
+    .. code::
+
+      {'proj': 'longlat', 'ellps': 'WGS84', 'datum': 'WGS84', 'no_defs': True}
 
     An affine transformation that maps ``col,row`` pixel coordinates to
     ``x,y`` coordinates in the coordinate reference system can be
-    specified using the ``transform`` argument. The value may be either
-    an instance of ``affine.Affine`` or a 6-element sequence of the
-    affine transformation matrix coefficients ``a, b, c, d, e, f``.
+    specified using the ``transform`` argument. The value should be
+    an instance of ``affine.Affine``
+
+    .. code:: python
+
+        >>> from affine import Affine
+        >>> Affine(0.5, 0.0, -180.0, 0.0, -0.5, 90.0)
+
     These coefficients are shown in the figure below.
 
+    .. code::
+
       | x |   | a  b  c | | c |
       | y | = | d  e  f | | r |
       | 1 |   | 0  0  1 | | 1 |
 
-    a: rate of change of X with respect to increasing column, i.e.
-            pixel width
-    b: rotation, 0 if the raster is oriented "north up" 
-    c: X coordinate of the top left corner of the top left pixel 
-    f: Y coordinate of the top left corner of the top left pixel 
-    d: rotation, 0 if the raster is oriented "north up"
-    e: rate of change of Y with respect to increasing row, usually
-            a negative number i.e. -1 * pixel height
-    f: Y coordinate of the top left corner of the top left pixel 
+        a: rate of change of X with respect to increasing column, i.e.  pixel width
+        b: rotation, 0 if the raster is oriented "north up"
+        c: X coordinate of the top left corner of the top left pixel
+        d: rotation, 0 if the raster is oriented "north up"
+        e: rate of change of Y with respect to increasing row, usually
+                a negative number (i.e. -1 * pixel height) if north-up.
+        f: Y coordinate of the top left corner of the top left pixel
+
+    A 6-element sequence of the affine transformation
+    matrix coefficients in ``c, a, b, f, d, e`` order,
+    (i.e. GDAL geotransform order) will be accepted until 1.0 (deprecated).
 
     A virtual filesystem can be specified. The ``vfs`` parameter may be
     an Apache Commons VFS style string beginning with "zip://" or
     "tar://"". In this case, the ``path`` must be an absolute path
     within that container.
 
-    Finally, additional kwargs are passed to GDAL as driver-specific
-    dataset creation parameters.
     """
     if not isinstance(path, string_types):
         raise TypeError("invalid path: %r" % path)
@@ -125,14 +173,30 @@ def open(
 
 
 def copy(src, dst, **kw):
-    """Copy a source dataset to a new destination with driver specific
+    """Copy a source raster to a new destination with driver specific
     creation options.
 
-    ``src`` must be an existing file and ``dst`` a valid output file.
+    Parameters
+    ----------
+    src: string
+        an existing raster file
+    dst: string
+        valid path to output file.
+
+    Returns
+    -------
+    None
+
+    Raises
+    ------
+    ValueError:
+        If source path is not a valid Dataset
 
+    Notes
+    -----
     A ``driver`` keyword argument with value like 'GTiff' or 'JPEG' is
     used to control the output format.
-    
+
     This is the one way to create write-once files like JPEGs.
     """
     from rasterio._copy import RasterCopier
@@ -141,114 +205,101 @@ def copy(src, dst, **kw):
 
 
 def drivers(**kwargs):
-    """Returns a gdal environment with registered drivers."""
-    if driver_count() == 0:
-        log.debug("Creating a chief GDALEnv in drivers()")
-        return GDALEnv(True, **kwargs)
-    else:
-        log.debug("Creating a not-responsible GDALEnv in drivers()")
-        return GDALEnv(False, **kwargs)
-
-
-Band = namedtuple('Band', ['ds', 'bidx', 'dtype', 'shape'])
-
-def band(ds, bidx):
-    """Wraps a dataset and a band index up as a 'Band'"""
-    return Band(
-        ds, 
-        bidx, 
-        set(ds.dtypes).pop(),
-        ds.shape)
-
-
-def pad(array, transform, pad_width, mode=None, **kwargs):
-    """Returns a padded array and shifted affine transform matrix.
-    
-    Array is padded using `numpy.pad()`."""
-    import numpy
-    transform = guard_transform(transform)
-    padded_array = numpy.pad(array, pad_width, mode, **kwargs)
-    padded_trans = list(transform)
-    padded_trans[2] -= pad_width*padded_trans[0]
-    padded_trans[5] -= pad_width*padded_trans[4]
-    return padded_array, Affine(*padded_trans[:6])
-
-
-def get_data_window(arr, nodata=None):
-    """
-    Returns a window for the non-nodata pixels within the input array.
+    """Create a gdal environment with registered drivers and
+    creation options.
 
     Parameters
     ----------
-    arr: numpy ndarray, <= 3 dimensions
-    nodata: number
-        If None, will either return a full window if arr is not a masked
-        array, or will use the mask to determine non-nodata pixels.
-        If provided, it must be a number within the valid range of the dtype
-        of the input array.
+    **kwargs:: keyword arguments
+        Configuration options that define GDAL driver behavior
+
+        See https://trac.osgeo.org/gdal/wiki/ConfigOptions
 
     Returns
     -------
-    ((row_start, row_stop), (col_start, col_stop))
+    GDALEnv responsible for managing the environment.
 
+    Notes
+    -----
+    Use as a context manager, ``with rasterio.drivers(): ...``
     """
+    return GDALEnv(**kwargs)
 
-    from rasterio._io import get_data_window
-    return get_data_window(arr, nodata)
 
+Band = namedtuple('Band', ['ds', 'bidx', 'dtype', 'shape'])
 
-def window_union(windows):
-    """
-    Union windows and return the outermost extent they cover.
+def band(ds, bidx):
+    """Wraps a dataset and a band index up as a 'Band'
 
     Parameters
     ----------
-    windows: list-like of window objects
-        ((row_start, row_stop), (col_start, col_stop))
+    ds: rasterio.RasterReader
+        Open rasterio dataset
+    bidx: int
+        Band number, index starting at 1
 
     Returns
     -------
-    ((row_start, row_stop), (col_start, col_stop))
+    a rasterio.Band
     """
-
-    from rasterio._io import window_union
-    return window_union(windows)
+    return Band(
+        ds,
+        bidx,
+        set(ds.dtypes).pop(),
+        ds.shape)
 
 
-def window_intersection(windows):
-    """
-    Intersect windows and return the innermost extent they cover.
-
-    Will raise ValueError if windows do not intersect.
+def pad(array, transform, pad_width, mode=None, **kwargs):
+    """pad array and adjust affine transform matrix.
 
     Parameters
     ----------
-    windows: list-like of window objects
-        ((row_start, row_stop), (col_start, col_stop))
+    array: ndarray
+        Numpy ndarray, for best results a 2D array
+    transform: Affine transform
+        transform object mapping pixel space to coordinates
+    pad_width: int
+        number of pixels to pad array on all four
+    mode: str or function
+        define the method for determining padded values
 
     Returns
     -------
-    ((row_start, row_stop), (col_start, col_stop))
+    (array, transform): tuple
+        Tuple of new array and affine transform
+
+    Notes
+    -----
+    See numpy docs for details on mode and other kwargs:
+    http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.pad.html
     """
+    import numpy
+    transform = guard_transform(transform)
+    padded_array = numpy.pad(array, pad_width, mode, **kwargs)
+    padded_trans = list(transform)
+    padded_trans[2] -= pad_width*padded_trans[0]
+    padded_trans[5] -= pad_width*padded_trans[4]
+    return padded_array, Affine(*padded_trans[:6])
 
-    from rasterio._io import window_intersection
-    return window_intersection(windows)
 
+def get_data_window(arr, nodata=None):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.windows instead", DeprecationWarning)
+    return windows.get_data_window(arr, nodata)
 
-def windows_intersect(windows):
-    """
-    Test if windows intersect.
 
-    Parameters
-    ----------
-    windows: list-like of window objects
-        ((row_start, row_stop), (col_start, col_stop))
+def window_union(data):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.windows instead", DeprecationWarning)
+    return windows.union(data)
 
-    Returns
-    -------
-    boolean:
-        True if all windows intersect.
-    """
 
-    from rasterio._io import windows_intersect
-    return windows_intersect(windows)
+def window_intersection(data):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.windows instead", DeprecationWarning)
+    return windows.intersection(data)
+
+def windows_intersect(data):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.windows instead", DeprecationWarning)
+    return windows.intersect(data)
diff --git a/rasterio/_base.pxd b/rasterio/_base.pxd
index a193da9..cd838dc 100644
--- a/rasterio/_base.pxd
+++ b/rasterio/_base.pxd
@@ -7,6 +7,7 @@ cdef class DatasetReader:
 
     cdef readonly object name
     cdef readonly object mode
+    cdef readonly object options
     cdef readonly object width, height
     cdef readonly object shape
     cdef public object driver
@@ -21,7 +22,7 @@ cdef class DatasetReader:
     cdef public object _read
     cdef object env
 
-    cdef void *band(self, int bidx)
+    cdef void *band(self, int bidx) except NULL
 
 
 cdef void *_osr_from_crs(object crs)
diff --git a/rasterio/_base.pyx b/rasterio/_base.pyx
index d41b11e..1bafb72 100644
--- a/rasterio/_base.pyx
+++ b/rasterio/_base.pyx
@@ -14,11 +14,14 @@ from libc.stdlib cimport malloc, free
 
 from rasterio cimport _gdal, _ogr
 from rasterio._drivers import driver_count, GDALEnv
-from rasterio._err import cpl_errs, GDALError
+from rasterio._err import (
+    CPLErrors, GDALError, CPLE_IllegalArg, CPLE_OpenFailed, CPLE_NotSupported)
 from rasterio import dtypes
 from rasterio.coords import BoundingBox
+from rasterio.errors import RasterioIOError, CRSError
 from rasterio.transform import Affine
-from rasterio.enums import ColorInterp, Compression, Interleaving
+from rasterio.enums import (
+    ColorInterp, Compression, Interleaving, PhotometricInterp)
 from rasterio.vfs import parse_path, vsi_path
 
 
@@ -36,9 +39,10 @@ else:
 
 cdef class DatasetReader(object):
 
-    def __init__(self, path):
+    def __init__(self, path, options=None):
         self.name = path
         self.mode = 'r'
+        self.options = options or {}
         self._hds = NULL
         self._count = 0
         self._closed = True
@@ -57,13 +61,7 @@ cdef class DatasetReader(object):
             self.mode)
 
     def start(self):
-        # Is there not a driver manager already?
-        if driver_count() == 0 and not self.env:
-            # create a local manager and enter
-            self.env = GDALEnv(True)
-        else:
-            # create a local manager and enter
-            self.env = GDALEnv(False)
+        self.env = GDALEnv()
         self.env.start()
 
         path, archive, scheme = parse_path(self.name)
@@ -71,10 +69,14 @@ cdef class DatasetReader(object):
 
         name_b = path.encode('utf-8')
         cdef const char *fname = name_b
-        with cpl_errs:
-            self._hds = _gdal.GDALOpen(fname, 0)
-        if self._hds == NULL:
-            raise ValueError("Null dataset")
+
+        try:
+            with CPLErrors() as cple:
+                self._hds = _gdal.GDALOpen(fname, 0)
+                cple.check()
+        except CPLE_OpenFailed as err:
+            self.env.stop()
+            raise RasterioIOError(err.errmsg)
 
         cdef void *drv
         cdef const char *drv_name
@@ -96,14 +98,28 @@ cdef class DatasetReader(object):
 
         self._closed = False
 
-    cdef void *band(self, int bidx):
-        if self._hds == NULL:
-            raise ValueError("Null dataset")
-        cdef void *hband = _gdal.GDALGetRasterBand(self._hds, bidx)
-        if hband == NULL:
-            raise ValueError("Null band")
+    cdef void *band(self, int bidx) except NULL:
+        cdef void *hband = NULL
+        try:
+            with CPLErrors() as cple:
+                hband = _gdal.GDALGetRasterBand(self._hds, bidx)
+                cple.check()
+        except CPLE_IllegalArg as exc:
+            self.env.stop()
+            raise IndexError(str(exc))
         return hband
 
+    def _has_band(self, bidx):
+        cdef void *hband = NULL
+        try:
+            with CPLErrors() as cple:
+                hband = _gdal.GDALGetRasterBand(self._hds, bidx)
+                cple.check()
+        except CPLE_IllegalArg:
+            self.env.stop()
+            return False
+        return True
+
     def read_crs(self):
         cdef char *proj_c = NULL
         cdef const char * auth_key = NULL
@@ -467,6 +483,8 @@ cdef class DatasetReader(object):
     def compression(self):
         val = self.tags(ns='IMAGE_STRUCTURE').get('COMPRESSION')
         if val:
+            # 'YCbCr JPEG' will be normalized to 'JPEG'
+            val = val.split(' ')[-1]
             return Compression(val)
         else:
             return None
@@ -480,6 +498,14 @@ cdef class DatasetReader(object):
             return None
 
     @property
+    def photometric(self):
+        val = self.tags(ns='IMAGE_STRUCTURE').get('SOURCE_COLOR_SPACE')
+        if val:
+            return PhotometricInterp(val)
+        else:
+            return None
+
+    @property
     def is_tiled(self):
         if len(self.block_shapes) == 0:
             return False
@@ -493,7 +519,8 @@ cdef class DatasetReader(object):
         """
         def __get__(self):
             m = self.meta
-            m.update(self.tags(ns='rio_creation_kwds'))
+            m.update((k, v.lower()) for k, v in self.tags(
+                ns='rio_creation_kwds').items())
             if self.is_tiled:
                 m.update(
                     blockxsize=self.block_shapes[0][1],
@@ -505,6 +532,8 @@ cdef class DatasetReader(object):
                 m['compress'] = self.compression.name
             if self.interleaving:
                 m['interleave'] = self.interleaving.name
+            if self.photometric:
+                m['photometric'] = self.photometric.name
             return m
 
     def lnglat(self):
@@ -596,14 +625,9 @@ cdef class DatasetReader(object):
         cdef void *hobj
         cdef const char *domain_c
         cdef char **papszStrList
-        if self._hds == NULL:
-            raise ValueError("can't read closed raster file")
+
         if bidx > 0:
-            if bidx not in self.indexes:
-                raise ValueError("Invalid band index")
-            hobj = _gdal.GDALGetRasterBand(self._hds, bidx)
-            if hobj == NULL:
-                raise ValueError("NULL band")
+            hobj = self.band(bidx)
         else:
             hobj = self._hds
         if ns:
@@ -850,6 +874,8 @@ def tastes_like_gdal(t):
 
 
 cdef void *_osr_from_crs(object crs):
+    """Returns a reference to memory that must be deallocated
+    by the caller."""
     cdef char *proj_c = NULL
     cdef void *osr = _gdal.OSRNewSpatialReference(NULL)
     params = []
@@ -909,40 +935,32 @@ def _transform(src_crs, dst_crs, xs, ys, zs):
         for i in range(n):
             z[i] = zs[i]
 
-    transform = _gdal.OCTNewCoordinateTransformation(src, dst)
-    if transform == NULL:
+    try:
+        with CPLErrors() as cple:
+            transform = _gdal.OCTNewCoordinateTransformation(src, dst)
+            cple.check()
+            res = _gdal.OCTTransform(transform, n, x, y, z)
+            res_xs = [0]*n
+            res_ys = [0]*n
+            for i in range(n):
+                res_xs[i] = x[i]
+                res_ys[i] = y[i]
+            if zs is not None:
+                res_zs = [0]*n
+                for i in range(n):
+                    res_zs[i] = z[i]
+                retval = (res_xs, res_ys, res_zs)
+            else:
+                retval = (res_xs, res_ys)
+    except CPLE_NotSupported as err:
+        raise CRSError(err.errmsg)
+    finally:
         _gdal.CPLFree(x)
         _gdal.CPLFree(y)
         _gdal.CPLFree(z)
         _gdal.OSRDestroySpatialReference(src)
         _gdal.OSRDestroySpatialReference(dst)
-        raise ValueError("Cannot create coordinate transformer")
-    res = _gdal.OCTTransform(transform, n, x, y, z)
-    #if res:
-    #    raise ValueError("Failed coordinate transformation")
-
-    res_xs = [0]*n
-    res_ys = [0]*n
-
-    for i in range(n):
-        res_xs[i] = x[i]
-        res_ys[i] = y[i]
 
-    if zs is not None:
-        res_zs = [0]*n
-        for i in range(n):
-            res_zs[i] = z[i]
-        _gdal.CPLFree(z)
-
-        retval = (res_xs, res_ys, res_zs)
-    else:
-        retval = (res_xs, res_ys)
-
-    _gdal.CPLFree(x)
-    _gdal.CPLFree(y)
-    _gdal.OCTDestroyCoordinateTransformation(transform)
-    _gdal.OSRDestroySpatialReference(src)
-    _gdal.OSRDestroySpatialReference(dst)
     return retval
 
 
@@ -950,7 +968,6 @@ def is_geographic_crs(crs):
     cdef void *osr_crs = _osr_from_crs(crs)
     cdef int retval = _gdal.OSRIsGeographic(osr_crs)
     _gdal.OSRDestroySpatialReference(osr_crs)
-
     return retval == 1
 
 
@@ -958,7 +975,6 @@ def is_projected_crs(crs):
     cdef void *osr_crs = _osr_from_crs(crs)
     cdef int retval = _gdal.OSRIsProjected(osr_crs)
     _gdal.OSRDestroySpatialReference(osr_crs)
-
     return retval == 1
 
 
@@ -968,5 +984,4 @@ def is_same_crs(crs1, crs2):
     cdef int retval = _gdal.OSRIsSame(osr_crs1, osr_crs2)
     _gdal.OSRDestroySpatialReference(osr_crs1)
     _gdal.OSRDestroySpatialReference(osr_crs2)
-
     return retval == 1
diff --git a/rasterio/_drivers.pyx b/rasterio/_drivers.pyx
index 40b8271..ada694e 100644
--- a/rasterio/_drivers.pyx
+++ b/rasterio/_drivers.pyx
@@ -12,6 +12,7 @@ from rasterio.five import string_types
 cdef extern from "cpl_conv.h":
     void    CPLFree (void *ptr)
     void    CPLSetThreadLocalConfigOption (char *key, char *val)
+    void    CPLSetConfigOption (char *key, char *val)
     const char * CPLGetConfigOption ( const char *key, const char *default)
 
 
@@ -49,7 +50,6 @@ level_map = {
     4: logging.CRITICAL }
 
 code_map = {
-    0: 'CPLE_None',
     1: 'CPLE_AppDefined',
     2: 'CPLE_OutOfMemory',
     3: 'CPLE_FileIO',
@@ -59,30 +59,95 @@ code_map = {
     7: 'CPLE_AssertionFailed',
     8: 'CPLE_NoWriteAccess',
     9: 'CPLE_UserInterrupt',
-    10: 'CPLE_ObjectNull'
-}
+    10: 'ObjectNull',
+
+    # error numbers 11-16 are introduced in GDAL 2.1. See 
+    # https://github.com/OSGeo/gdal/pull/98.
+    11: 'CPLE_HttpResponse',
+    12: 'CPLE_AWSBucketNotFound',
+    13: 'CPLE_AWSObjectNotFound',
+    14: 'CPLE_AWSAccessDenied',
+    15: 'CPLE_AWSInvalidCredentials',
+    16: 'CPLE_AWSSignatureDoesNotMatch'}
+
 
 cdef void * errorHandler(int eErrClass, int err_no, char *msg):
-    log.log(level_map[eErrClass], "%s in %s", code_map[err_no], msg)
+    if err_no in code_map:
+        log.log(level_map[eErrClass], "%s in %s", code_map[err_no], msg)
+
 
 def driver_count():
     return GDALGetDriverCount() + OGRGetDriverCount()
 
 
-cdef class GDALEnv(object):
+cdef class ConfigEnv(object):
 
-    cdef object is_chef
     cdef public object options
+    cdef public object prev_options
+
+    def __init__(self, **options):
+        self.options = options.copy()
+        self.prev_options = {}
+
+    def enter_config_options(self):
+        """Set GDAL config options."""
+        cdef const char *key_c
+        cdef const char *val_c
+
+        for key, val in self.options.items():
+            key_b = key.upper().encode('utf-8')
+            key_c = key_b
+
+            # Save current value of that key.
+            val_c = CPLGetConfigOption(key_c, NULL)
+            if val_c != NULL:
+                val_b = val_c
+                self.prev_options[key_b] = val_b
+
+            if isinstance(val, string_types):
+                val_b = val.encode('utf-8')
+            else:
+                val_b = ('ON' if val else 'OFF').encode('utf-8')
+            val_c = val_b
+            CPLSetConfigOption(key_c, val_c)
+            log.debug("Option %s=%s", key, val)
+
+    def exit_config_options(self):
+        """Clear GDAL config options."""
+        cdef const char *key_c
+        cdef const char *val_c
+
+        for key in self.options:
+            key_b = key.upper().encode('utf-8')
+            key_c = key_b
+            if key_b in self.prev_options:
+                val_b = self.prev_options[key_b]
+                key_c = key_b; val_c = val_b
+                CPLSetConfigOption(key_c, val_c)
+            else:
+                CPLSetConfigOption(key_c, NULL)
+
+    def __enter__(self):
+        self.enter_config_options()
+        return self
 
-    def __init__(self, is_chef=True, **options):
-        self.is_chef = is_chef
+    def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
+        self.exit_config_options()
+
+
+cdef class GDALEnv(ConfigEnv):
+
+    def __init__(self, **options):
         self.options = options.copy()
+        self.prev_options = {}
 
     def __enter__(self):
         self.start()
+        self.enter_config_options()
         return self
 
     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
+        self.exit_config_options()
         self.stop()
 
     def start(self):
@@ -107,24 +172,7 @@ cdef class GDALEnv(object):
                 os.path.join(os.path.dirname(__file__), "proj_data"))
             os.environ['PROJ_LIB'] = whl_datadir
 
-        for key, val in self.options.items():
-            key_b = key.upper().encode('utf-8')
-            key_c = key_b
-            if isinstance(val, string_types):
-                val_b = val.encode('utf-8')
-            else:
-                val_b = ('ON' if val else 'OFF').encode('utf-8')
-            val_c = val_b
-            CPLSetThreadLocalConfigOption(key_c, val_c)
-            log.debug("Option %s=%s", key, CPLGetConfigOption(key_c, NULL))
-        return self
-
     def stop(self):
-        cdef const char *key_c
-        for key in self.options:
-            key_b = key.upper().encode('utf-8')
-            key_c = key_b
-            CPLSetThreadLocalConfigOption(key_c, NULL)
         CPLSetErrorHandler(NULL)
 
     def drivers(self):
diff --git a/rasterio/_err.pyx b/rasterio/_err.pyx
index 5d02c86..abe0d54 100644
--- a/rasterio/_err.pyx
+++ b/rasterio/_err.pyx
@@ -3,18 +3,18 @@
 Transformation of GDAL C API errors to Python exceptions using Python's
 ``with`` statement and an error-handling context manager class.
 
-The ``cpl_errs`` error-handling context manager is intended for use in
+The ``CPLErrors`` error-handling context manager is intended for use in
 Rasterio's Cython code. When entering the body of a ``with`` statement,
-the context manager clears GDAL's error stack. On exit, the context
-manager pops the last error off the stack and raises an appropriate
-Python exception. It's otherwise pretty difficult to do this kind of
-thing.  I couldn't make it work with a CPL error handler, Cython's
-C code swallows exceptions raised from C callbacks.
+the context manager clears GDAL's error stack. On exit, the stack is
+cleared again. Its ``check()`` method can be called after calling any
+GDAL function to determine if ``CPLError()`` was called, and raise an
+exception appropriately.
 
 When used to wrap a call to open a PNG in update mode
 
-    with cpl_errs:
+    with CPLErrors() as cple:
         cdef void *hds = GDALOpen('file.png', 1)
+        cple.check()
     if hds == NULL:
         raise ValueError("NULL dataset")
 
@@ -26,10 +26,12 @@ manager raises a more useful and informative error:
         with rasterio.open(args.src, args.mode) as src:
       File "/Users/sean/code/rasterio/rasterio/__init__.py", line 111, in open
         s.start()
-    ValueError: The PNG driver does not support update access to existing datasets.
+    CPLE_OpenFailed: The PNG driver does not support update access to existing datasets.
+
 """
 
 from enums import IntEnum
+import sys
 
 
 # CPL function declarations.
@@ -39,43 +41,157 @@ cdef extern from "cpl_error.h":
     int CPLGetLastErrorType()
     void CPLErrorReset()
 
-# Map GDAL error numbers to Python exceptions.
+
+# Python exceptions expressing the CPL error numbers.
+
+class CPLError(Exception):
+    """Base CPL error class
+    
+    Exceptions deriving from this class are intended for use only in
+    Rasterio's Cython code. Let's not expose API users to them.
+    """
+
+    def __init__(self, errtype, errno, errmsg):
+        self.errtype = errtype
+        self.errno = errno
+        self.errmsg = errmsg
+
+    @property
+    def args(self):
+        return self.errtype, self.errno, self.errmsg
+
+
+class CPLE_AppDefined(CPLError):
+    pass
+
+
+class CPLE_OutOfMemory(CPLError):
+    pass
+
+
+class CPLE_FileIO(CPLError):
+    pass
+
+
+class CPLE_OpenFailed(CPLError):
+    pass
+
+
+class CPLE_IllegalArg(CPLError):
+    pass
+
+
+class CPLE_NotSupported(CPLError):
+    pass
+
+
+class CPLE_AssertionFailed(CPLError):
+    pass
+
+
+class CPLE_NoWriteAccess(CPLError):
+    pass
+
+
+class CPLE_UserInterrupt(CPLError):
+    pass
+
+
+class ObjectNull(CPLError):
+    pass
+
+
+class CPLE_HttpResponse(CPLError):
+    pass
+
+
+class CPLE_AWSBucketNotFound(CPLError):
+    pass
+
+
+class CPLE_AWSObjectNotFound(CPLError):
+    pass
+
+
+class CPLE_AWSAccessDenied(CPLError):
+    pass
+
+
+class CPLE_AWSInvalidCredentials(CPLError):
+    pass
+
+
+class CPLE_AWSSignatureDoesNotMatch(CPLError):
+    pass
+
+
+# Map of GDAL error numbers to the Python exceptions.
 exception_map = {
-    1: RuntimeError,        # CPLE_AppDefined
-    2: MemoryError,         # CPLE_OutOfMemory
-    3: IOError,             # CPLE_FileIO
-    4: IOError,             # CPLE_OpenFailed
-    5: TypeError,           # CPLE_IllegalArg
-    6: ValueError,          # CPLE_NotSupported
-    7: AssertionError,      # CPLE_AssertionFailed
-    8: IOError,             # CPLE_NoWriteAccess
-    9: KeyboardInterrupt,   # CPLE_UserInterrupt
-    10: ValueError          # ObjectNull
-    }
-
-
-cdef class GDALErrCtxManager:
+    1: CPLE_AppDefined,
+    2: CPLE_OutOfMemory,
+    3: CPLE_FileIO,
+    4: CPLE_OpenFailed,
+    5: CPLE_IllegalArg,
+    6: CPLE_NotSupported,
+    7: CPLE_AssertionFailed,
+    8: CPLE_NoWriteAccess,
+    9: CPLE_UserInterrupt,
+    10: ObjectNull,
+
+    # error numbers 11-16 are introduced in GDAL 2.1. See 
+    # https://github.com/OSGeo/gdal/pull/98.
+    11: CPLE_HttpResponse,
+    12: CPLE_AWSBucketNotFound,
+    13: CPLE_AWSObjectNotFound,
+    14: CPLE_AWSAccessDenied,
+    15: CPLE_AWSInvalidCredentials,
+    16: CPLE_AWSSignatureDoesNotMatch}
+
+
+# CPL Error types as an enum.
+class GDALError(IntEnum):
+    none = 0    # CE_None
+    debug = 1   # CE_Debug
+    warning= 2  # CE_Warning
+    failure = 3 # CE_Failure
+    fatal = 4   # CE_Fatal
+
+
+cdef class CPLErrors:
     """A manager for GDAL error handling contexts."""
 
+    def check(self):
+        """Check the errror stack and raise or exit as appropriate."""
+        cdef const char *msg_c = NULL
+
+        err_type = CPLGetLastErrorType()
+        # Return True if there's no error.
+        # Debug and warnings are already picked up by the drivers()
+        # context manager.
+        if err_type < 3:
+            CPLErrorReset()
+            return
+
+        err_no = CPLGetLastErrorNo()
+        msg_c = CPLGetLastErrorMsg()
+        if msg_c == NULL:
+            msg = "No error message."
+        else:
+        # Reformat messages.
+            msg_b = msg_c
+            msg = msg_b.decode('utf-8')
+            msg = msg.replace("`", "'")
+            msg = msg.replace("\n", " ")
+
+        if err_type == 4:
+            sys.exit("Fatal error: {0}".format((err_type, err_no, msg)))
+        else:
+            CPLErrorReset()
+            raise exception_map.get(err_no, CPLError)(err_type, err_no, msg)
+
     def __enter__(self):
         CPLErrorReset()
         return self
 
     def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
-        cdef int err_type = CPLGetLastErrorType()
-        cdef int err_no = CPLGetLastErrorNo()
-        cdef const char *msg = CPLGetLastErrorMsg()
-        # TODO: warn for err_type 2?
-        if err_type >= 3:
-            raise exception_map[err_no](msg)
-
-
-cpl_errs = GDALErrCtxManager()
-
-
-class GDALError(IntEnum):
-    success = 0,  # CE_None
-    debug = 1,  # CE_Debug
-    warning= 2,  # CE_Warning
-    failure = 3,  # CE_Failure
-    fatal = 4  # CE_Fatal
+        CPLErrorReset()
diff --git a/rasterio/_fill.pyx b/rasterio/_fill.pyx
index 723af0a..4fb2c93 100644
--- a/rasterio/_fill.pyx
+++ b/rasterio/_fill.pyx
@@ -6,7 +6,7 @@ import numpy as np
 cimport numpy as np
 
 from rasterio import dtypes
-from rasterio._err import cpl_errs
+from rasterio._err import CPLErrors
 from rasterio cimport _gdal, _io
 
 from rasterio._io cimport InMemoryRaster
@@ -60,28 +60,22 @@ def _fillnodata(image, mask, double max_search_distance=100.0,
     else:
         raise ValueError("Invalid source image mask")
 
-    with cpl_errs:
-        alg_options = _gdal.CSLSetNameValue(
+    try:
+        with CPLErrors() as cple:
+            alg_options = _gdal.CSLSetNameValue(
                 alg_options, "TEMP_FILE_DRIVER", "MEM")
-
-        _gdal.GDALFillNodata(
-                image_band,
-                mask_band,
-                max_search_distance,
-                0,
-                smoothing_iterations,
-                alg_options,
-                NULL,
-                NULL)
-
-    # read the result into a numpy ndarray
-    result = np.empty(image.shape, dtype=image.dtype)
-    _io.io_auto(result, image_band, False)
-
-    if image_dataset != NULL:
-        _gdal.GDALClose(image_dataset)
-    if mask_dataset != NULL:
-        _gdal.GDALClose(mask_dataset)
-    _gdal.CSLDestroy(alg_options)
+            _gdal.GDALFillNodata(
+                image_band, mask_band, max_search_distance, 0,
+                    smoothing_iterations, alg_options, NULL, NULL)
+            cple.check()
+        # read the result into a numpy ndarray
+        result = np.empty(image.shape, dtype=image.dtype)
+        _io.io_auto(result, image_band, False)
+    finally:
+        if image_dataset != NULL:
+            _gdal.GDALClose(image_dataset)
+        if mask_dataset != NULL:
+            _gdal.GDALClose(mask_dataset)
+        _gdal.CSLDestroy(alg_options)
 
     return result
diff --git a/rasterio/_io.pyx b/rasterio/_io.pyx
index 1b5b2e7..5af4e43 100644
--- a/rasterio/_io.pyx
+++ b/rasterio/_io.pyx
@@ -17,10 +17,10 @@ from rasterio cimport _base, _gdal, _ogr, _io
 from rasterio._base import (
     crop_window, eval_window, window_shape, window_index, tastes_like_gdal)
 from rasterio._drivers import driver_count, GDALEnv
-from rasterio._err import cpl_errs, GDALError
+from rasterio._err import CPLErrors, GDALError, CPLE_OpenFailed
 from rasterio import dtypes
 from rasterio.coords import BoundingBox
-from rasterio.errors import DriverRegistrationError
+from rasterio.errors import DriverRegistrationError, RasterioIOError
 from rasterio.five import text_type, string_types
 from rasterio.transform import Affine
 from rasterio.enums import ColorInterp, MaskFlags, Resampling
@@ -1256,13 +1256,7 @@ cdef class RasterUpdater(RasterReader):
         cdef void *hband = NULL
         cdef int success
 
-
-        # Is there not a driver manager already?
-        if driver_count() == 0 and not self.env:
-            # create a local manager and enter
-            self.env = GDALEnv(True)
-        else:
-            self.env = GDALEnv(False)
+        self.env = GDALEnv()
         self.env.start()
 
         path, archive, scheme = parse_path(self.name)
@@ -1285,12 +1279,17 @@ cdef class RasterUpdater(RasterReader):
             # Delete existing file, create.
             if os.path.exists(path):
                 os.unlink(path)
-            
+
             driver_b = self.driver.encode('utf-8')
             drv_name = driver_b
-            drv = _gdal.GDALGetDriverByName(drv_name)
-            if drv == NULL:
-                raise ValueError("NULL driver for %s", self.driver)
+            
+            try:
+                with CPLErrors() as cple:
+                    drv = _gdal.GDALGetDriverByName(drv_name)
+                    cple.check()
+            except Exception as err:
+                self.env.stop()
+                raise DriverRegistrationError(str(err))
             
             # Find the equivalent GDAL data type or raise an exception
             # We've mapped numpy scalar types to GDAL types so see
@@ -1328,12 +1327,17 @@ cdef class RasterUpdater(RasterReader):
                     "Option: %r\n", 
                     (k, _gdal.CSLFetchNameValue(options, key_c)))
 
-            self._hds = _gdal.GDALCreate(
-                drv, fname, self.width, self.height, self._count,
-                gdal_dtype, options)
-
-            if self._hds == NULL:
-                raise ValueError("NULL dataset")
+            try:
+                with CPLErrors() as cple:
+                    self._hds = _gdal.GDALCreate(
+                        drv, fname, self.width, self.height, self._count,
+                        gdal_dtype, options)
+                    cple.check()
+            except Exception as err:
+                self.env.stop()
+                if options != NULL:
+                    _gdal.CSLDestroy(options)
+                raise
 
             if self._init_nodata is not None:
 
@@ -1354,10 +1358,13 @@ cdef class RasterUpdater(RasterReader):
                 self.set_crs(self._crs)
         
         elif self.mode == 'r+':
-            with cpl_errs:
-                self._hds = _gdal.GDALOpen(fname, 1)
-            if self._hds == NULL:
-                raise ValueError("NULL dataset")
+            try:
+                with CPLErrors() as cple:
+                    self._hds = _gdal.GDALOpen(fname, 1)
+                    cple.check()
+            except CPLE_OpenFailed as err:
+                self.env.stop()
+                raise RasterioIOError(str(err))
 
         drv = _gdal.GDALGetDatasetDriver(self._hds)
         drv_name = _gdal.GDALGetDriverShortName(drv)
@@ -1659,14 +1666,8 @@ cdef class RasterUpdater(RasterReader):
         cdef void *hobj = NULL
         cdef const char *domain_c = NULL
         cdef char **papszStrList = NULL
-        if self._hds == NULL:
-            raise ValueError("can't read closed raster file")
         if bidx > 0:
-            if bidx not in self.indexes:
-                raise ValueError("Invalid band index")
-            hobj = _gdal.GDALGetRasterBand(self._hds, bidx)
-            if hobj == NULL:
-                raise ValueError("NULL band")
+            hobj = self.band(bidx)
         else:
             hobj = self._hds
         if ns:
@@ -1700,21 +1701,15 @@ cdef class RasterUpdater(RasterReader):
         cdef void *hBand = NULL
         cdef void *hTable
         cdef _gdal.GDALColorEntry color
-        if self._hds == NULL:
-            raise ValueError("can't read closed raster file")
         if bidx > 0:
-            if bidx not in self.indexes:
-                raise ValueError("Invalid band index")
-            hBand = _gdal.GDALGetRasterBand(self._hds, bidx)
-            if hBand == NULL:
-                raise ValueError("NULL band")
+            hBand = self.band(bidx)
+
         # RGB only for now. TODO: the other types.
         # GPI_Gray=0,  GPI_RGB=1, GPI_CMYK=2,     GPI_HLS=3
         hTable = _gdal.GDALCreateColorTable(1)
         vals = range(256)
 
         for i, rgba in colormap.items():
-
             if len(rgba) == 4 and self.driver in ('GTiff'):
                 warnings.warn(
                     "This format doesn't support alpha in colormap entries. "
@@ -1745,19 +1740,20 @@ cdef class RasterUpdater(RasterReader):
 
         specifying a raster subset to write into.
         """
-        cdef void *hband
-        cdef void *hmask
-        if self._hds == NULL:
-            raise ValueError("can't write closed raster file")
-        hband = _gdal.GDALGetRasterBand(self._hds, 1)
-        if hband == NULL:
-            raise ValueError("NULL band mask")
-        if _gdal.GDALCreateMaskBand(hband, 0x02) != 0:
-            raise RuntimeError("Failed to create mask")
-        hmask = _gdal.GDALGetMaskBand(hband)
-        if hmask == NULL:
-            raise ValueError("NULL band mask")
-        log.debug("Created mask band")
+        cdef void *hband = NULL
+        cdef void *hmask = NULL
+
+        hband = self.band(1)
+
+        try:
+            with CPLErrors() as cple:
+                retval = _gdal.GDALCreateMaskBand(hband, 0x02)
+                cple.check()
+                hmask = _gdal.GDALGetMaskBand(hband)
+                cple.check()
+                log.debug("Created mask band")
+        except:
+            raise RasterioIOError("Failed to get mask.")
 
         if window:
             window = eval_window(window, self.height, self.width)
@@ -1788,23 +1784,21 @@ cdef class RasterUpdater(RasterReader):
         cdef int *factors_c = NULL
         cdef const char *resampling_c = NULL
 
-        if self._hds == NULL:
-            raise ValueError("can't write closed raster file")
-
         # Allocate arrays.
         if factors:
             factors_c = <int *>_gdal.CPLMalloc(len(factors)*sizeof(int))
             for i, factor in enumerate(factors):
                 factors_c[i] = factor
-
-            with cpl_errs:
-                resampling_b = resampling.value.encode('utf-8')
-                resampling_c = resampling_b
-                err = _gdal.GDALBuildOverviews(self._hds, resampling_c,
-                    len(factors), factors_c, 0, NULL, NULL, NULL)
-
-            if factors_c != NULL:
-                _gdal.CPLFree(factors_c)
+            try:
+                with CPLErrors() as cple:
+                    resampling_b = resampling.value.encode('utf-8')
+                    resampling_c = resampling_b
+                    err = _gdal.GDALBuildOverviews(self._hds, resampling_c,
+                        len(factors), factors_c, 0, NULL, NULL, NULL)
+                    cple.check()
+            finally:
+                if factors_c != NULL:
+                    _gdal.CPLFree(factors_c)
 
 
 cdef class InMemoryRaster:
@@ -1845,27 +1839,19 @@ cdef class InMemoryRaster:
         cdef int i = 0  # avoids Cython warning in for loop below
         cdef const char *srcwkt = NULL
         cdef void *osr = NULL
+        cdef void *memdriver = NULL
 
         # Several GDAL operations require the array of band IDs as input
         self.band_ids[0] = 1
 
-        cdef void *memdriver = _gdal.GDALGetDriverByName("MEM")
-        if memdriver == NULL:
-            raise DriverRegistrationError(
-                "MEM driver is not registered.")
-
-        self.dataset = _gdal.GDALCreate(
-            memdriver,
-            "output",
-            image.shape[1],
-            image.shape[0],
-            1,
-            <_gdal.GDALDataType>dtypes.dtype_rev[image.dtype.name],
-            NULL
-        )
-
-        if self.dataset == NULL:
-            raise ValueError("NULL output datasource")
+        with CPLErrors() as cple:
+            memdriver = _gdal.GDALGetDriverByName("MEM")
+            cple.check()
+            self.dataset = _gdal.GDALCreate(
+                memdriver, "output", image.shape[1], image.shape[0],
+                1, <_gdal.GDALDataType>dtypes.dtype_rev[image.dtype.name],
+                NULL)
+            cple.check()
 
         if transform is not None:
             for i in range(6):
@@ -1930,12 +1916,7 @@ cdef class IndirectRasterUpdater(RasterUpdater):
 
         memdrv = _gdal.GDALGetDriverByName("MEM")
 
-        # Is there not a driver manager already?
-        if driver_count() == 0 and not self.env:
-            # create a local manager and enter
-            self.env = GDALEnv(True)
-        else:
-            self.env = GDALEnv(False)
+        self.env = GDALEnv()
         self.env.start()
         
         if self.mode == 'w':
@@ -1951,14 +1932,20 @@ cdef class IndirectRasterUpdater(RasterUpdater):
                     gdal_dtype = dtypes.dtype_rev.get(tp)
             else:
                 gdal_dtype = dtypes.dtype_rev.get(self._init_dtype)
-            self._hds = _gdal.GDALCreate(
-                memdrv, "temp", self.width, self.height, self._count,
-                gdal_dtype, NULL)
-            if self._hds == NULL:
-                raise ValueError("NULL dataset")
+
+            try:
+                with CPLErrors() as cple:
+                    self._hds = _gdal.GDALCreate(
+                        memdrv, "temp", self.width, self.height, self._count,
+                        gdal_dtype, NULL)
+                    cple.check()
+            except:
+                self.env.close()
+                raise
+
             if self._init_nodata is not None:
                 for i in range(self._count):
-                    hband = _gdal.GDALGetRasterBand(self._hds, i+1)
+                    hband = self.band(i+1)
                     success = _gdal.GDALSetRasterNoDataValue(
                                     hband, self._init_nodata)
             if self._transform:
@@ -1967,14 +1954,21 @@ cdef class IndirectRasterUpdater(RasterUpdater):
                 self.set_crs(self._crs)
 
         elif self.mode == 'r+':
-            with cpl_errs:
-                temp = _gdal.GDALOpen(fname, 0)
-            if temp == NULL:
-                raise ValueError("Null dataset")
-            self._hds = _gdal.GDALCreateCopy(
-                                memdrv, "temp", temp, 1, NULL, NULL, NULL)
-            if self._hds == NULL:
-                raise ValueError("NULL dataset")
+            try:
+                with CPLErrors() as cple:
+                    temp = _gdal.GDALOpen(fname, 0)
+                    cple.check()
+            except Exception as exc:
+                raise RasterioIOError(str(exc))
+            
+            try:
+                with CPLErrors() as cple:
+                    self._hds = _gdal.GDALCreateCopy(
+                        memdrv, "temp", temp, 1, NULL, NULL, NULL)
+                    cple.check()
+            except:
+                raise
+
             drv = _gdal.GDALGetDatasetDriver(temp)
             drv_name = _gdal.GDALGetDriverShortName(drv)
             self.driver = drv_name.decode('utf-8')
@@ -2031,16 +2025,20 @@ cdef class IndirectRasterUpdater(RasterUpdater):
             log.debug(
                 "Option: %r\n", 
                 (k, _gdal.CSLFetchNameValue(options, key_c)))
-        
+
         #self.update_tags(ns='rio_creation_kwds', **kwds)
-        temp = _gdal.GDALCreateCopy(
+        try:
+            with CPLErrors() as cple:
+                temp = _gdal.GDALCreateCopy(
                     drv, fname, self._hds, 1, options, NULL, NULL)
-
-        if options != NULL:
-            _gdal.CSLDestroy(options)
-
-        if temp != NULL:
-            _gdal.GDALClose(temp)
+                cple.check()
+        except:
+            raise
+        finally:
+            if options != NULL:
+                _gdal.CSLDestroy(options)
+            if temp != NULL:
+                _gdal.GDALClose(temp)
 
 
 def writer(path, mode, **kwargs):
@@ -2067,15 +2065,18 @@ def writer(path, mode, **kwargs):
         # driver.
         name_b = path.encode('utf-8')
         fname = name_b
-        with cpl_errs:
-            hds = _gdal.GDALOpen(fname, 0)
-            if hds == NULL:
-                raise ValueError("NULL dataset")
-            drv = _gdal.GDALGetDatasetDriver(hds)
-            drv_name = _gdal.GDALGetDriverShortName(drv)
-            drv_name_b = drv_name
-            driver = drv_name_b.decode('utf-8')
-            _gdal.GDALClose(hds)
+        try:
+            with CPLErrors() as cple:
+                hds = _gdal.GDALOpen(fname, 0)
+                cple.check()
+        except CPLE_OpenFailed as exc:
+            raise RasterioIOError(str(exc))
+
+        drv = _gdal.GDALGetDatasetDriver(hds)
+        drv_name = _gdal.GDALGetDriverShortName(drv)
+        drv_name_b = drv_name
+        driver = drv_name_b.decode('utf-8')
+        _gdal.GDALClose(hds)
 
         if driver == 'GTiff':
             return RasterUpdater(path, mode)
@@ -2091,7 +2092,14 @@ def virtual_file_to_buffer(filename):
      
     filename_b = filename if not isinstance(filename, string_types) else filename.encode('utf-8')
     cfilename = filename_b
-    buff = _gdal.VSIGetMemFileBuffer(cfilename, &buff_len, 0)
+    
+    try:
+        with CPLErrors() as cple:
+            buff = _gdal.VSIGetMemFileBuffer(cfilename, &buff_len, 0)
+            cple.check()
+    except:
+        raise
+
     n = buff_len
     log.debug("Buffer length: %d bytes", n)
     cdef np.uint8_t[:] buff_view = <np.uint8_t[:n]>buff
diff --git a/rasterio/_warp.pyx b/rasterio/_warp.pyx
index e2ad940..fd8cb6d 100644
--- a/rasterio/_warp.pyx
+++ b/rasterio/_warp.pyx
@@ -8,9 +8,9 @@ cimport numpy as np
 
 from rasterio cimport _base, _gdal, _ogr, _io, _features
 from rasterio import dtypes
-from rasterio._err import cpl_errs, GDALError
+from rasterio._err import CPLErrors, GDALError, CPLE_NotSupported
 from rasterio._io cimport InMemoryRaster
-from rasterio.errors import DriverRegistrationError
+from rasterio.errors import DriverRegistrationError, CRSError
 from rasterio.transform import Affine, from_bounds
 
 
@@ -100,8 +100,15 @@ def _transform_geom(
 
     src = _base._osr_from_crs(src_crs)
     dst = _base._osr_from_crs(dst_crs)
-    transform = _gdal.OCTNewCoordinateTransformation(src, dst)
 
+    try:
+        with CPLErrors() as cple:
+            transform = _gdal.OCTNewCoordinateTransformation(src, dst)
+    except:
+        _gdal.OSRDestroySpatialReference(src)
+        _gdal.OSRDestroySpatialReference(dst)
+        raise
+        
     # Transform options.
     val_b = str(antimeridian_offset).encode('utf-8')
     val_c = val_b
@@ -110,22 +117,25 @@ def _transform_geom(
     if antimeridian_cutting:
         options = _gdal.CSLSetNameValue(options, "WRAPDATELINE", "YES")
 
-    factory = new OGRGeometryFactory()
-    src_ogr_geom = _features.OGRGeomBuilder().build(geom)
-    dst_ogr_geom = factory.transformWithOptions(
+    try:
+        factory = new OGRGeometryFactory()
+        src_ogr_geom = _features.OGRGeomBuilder().build(geom)
+        with CPLErrors() as cple:
+            dst_ogr_geom = factory.transformWithOptions(
                     <const OGRGeometry *>src_ogr_geom,
                     <OGRCoordinateTransformation *>transform,
                     options)
-    del factory
-    g = _features.GeomBuilder().build(dst_ogr_geom)
-
-    _ogr.OGR_G_DestroyGeometry(dst_ogr_geom)
-    _ogr.OGR_G_DestroyGeometry(src_ogr_geom)
-    _gdal.OCTDestroyCoordinateTransformation(transform)
-    if options != NULL:
-        _gdal.CSLDestroy(options)
-    _gdal.OSRDestroySpatialReference(src)
-    _gdal.OSRDestroySpatialReference(dst)
+            cple.check()
+        g = _features.GeomBuilder().build(dst_ogr_geom)
+    finally:
+        del factory
+        _ogr.OGR_G_DestroyGeometry(dst_ogr_geom)
+        _ogr.OGR_G_DestroyGeometry(src_ogr_geom)
+        _gdal.OCTDestroyCoordinateTransformation(transform)
+        if options != NULL:
+            _gdal.CSLDestroy(options)
+        _gdal.OSRDestroySpatialReference(src)
+        _gdal.OSRDestroySpatialReference(dst)
 
     if precision >= 0:
         if g['type'] == 'Point':
@@ -267,31 +277,41 @@ def _reproject(
             # source is a masked array
             src_nodata = source.fill_value
 
-        hrdriver = _gdal.GDALGetDriverByName("MEM")
-        if hrdriver == NULL:
+        try:
+            with CPLErrors() as cple:
+                hrdriver = _gdal.GDALGetDriverByName("MEM")
+                cple.check()
+        except:
             raise DriverRegistrationError(
                 "'MEM' driver not found. Check that this call is contained "
                 "in a `with rasterio.drivers()` or `with rasterio.open()` "
                 "block.")
 
-        hdsin = _gdal.GDALCreate(
+        try:
+            with CPLErrors() as cple:
+                hdsin = _gdal.GDALCreate(
                     hrdriver, "input", cols, rows, 
                     src_count, dtypes.dtype_rev[dtype], NULL)
-        if hdsin == NULL:
-            raise ValueError("NULL input datasource")
+                cple.check()
+        except:
+            raise
         _gdal.GDALSetDescription(
             hdsin, "Temporary source dataset for _reproject()")
         log.debug("Created temp source dataset")
+
         for i in range(6):
             gt[i] = src_transform[i]
         retval = _gdal.GDALSetGeoTransform(hdsin, gt)
         log.debug("Set transform on temp source dataset: %d", retval)
-        osr = _base._osr_from_crs(src_crs)
-        _gdal.OSRExportToWkt(osr, &srcwkt)
-        _gdal.GDALSetProjection(hdsin, srcwkt)
-        log.debug("Set CRS on temp source dataset: %s", srcwkt)
-        _gdal.CPLFree(srcwkt)
-        _gdal.OSRDestroySpatialReference(osr)
+
+        try:
+            osr = _base._osr_from_crs(src_crs)
+            _gdal.OSRExportToWkt(osr, &srcwkt)
+            _gdal.GDALSetProjection(hdsin, srcwkt)
+            log.debug("Set CRS on temp source dataset: %s", srcwkt)
+        finally:
+            _gdal.CPLFree(srcwkt)
+            _gdal.OSRDestroySpatialReference(osr)
         
         # Copy arrays to the dataset.
         retval = _io.io_auto(source, hdsin, 1)
@@ -315,19 +335,25 @@ def _reproject(
         if destination.shape[0] != src_count:
             raise ValueError("Destination's shape is invalid")
 
-        hrdriver = _gdal.GDALGetDriverByName("MEM")
-        if hrdriver == NULL:
+        try:
+            with CPLErrors() as cple:
+                hrdriver = _gdal.GDALGetDriverByName("MEM")
+                cple.check()
+        except:
             raise DriverRegistrationError(
                 "'MEM' driver not found. Check that this call is contained "
                 "in a `with rasterio.drivers()` or `with rasterio.open()` "
                 "block.")
 
         _, rows, cols = destination.shape
-        hdsout = _gdal.GDALCreate(
-                        hrdriver, "output", cols, rows, src_count, 
-                        dtypes.dtype_rev[np.dtype(destination.dtype).name], NULL)
-        if hdsout == NULL:
-            raise ValueError("Failed to create temp destination dataset.")
+        try:
+            with CPLErrors() as cple:
+                hdsout = _gdal.GDALCreate(
+                    hrdriver, "output", cols, rows, src_count, 
+                    dtypes.dtype_rev[np.dtype(destination.dtype).name], NULL)
+                cple.check()
+        except:
+            raise
         _gdal.GDALSetDescription(
             hdsout, "Temporary destination dataset for _reproject()")
         log.debug("Created temp destination dataset.")
@@ -335,20 +361,20 @@ def _reproject(
         for i in range(6):
             gt[i] = dst_transform[i]
 
-        if not GDALError.success == _gdal.GDALSetGeoTransform(hdsout, gt):
+        if not GDALError.none == _gdal.GDALSetGeoTransform(hdsout, gt):
             raise ValueError(
                 "Failed to set transform on temp destination dataset.")
 
-        osr = _base._osr_from_crs(dst_crs)
-        _gdal.OSRExportToWkt(osr, &dstwkt)
-        _gdal.OSRDestroySpatialReference(osr)
-        log.debug("CRS for temp destination dataset: %s.", dstwkt)
-
-        if not GDALError.success == _gdal.GDALSetProjection(hdsout, dstwkt):
-            raise ValueError(
-                "Failed to set projection on temp destination dataset.")
-
-        _gdal.CPLFree(dstwkt)
+        try:
+            osr = _base._osr_from_crs(dst_crs)
+            _gdal.OSRExportToWkt(osr, &dstwkt)
+            log.debug("CRS for temp destination dataset: %s.", dstwkt)
+            if not GDALError.none == _gdal.GDALSetProjection(
+                    hdsout, dstwkt):
+                raise ("Failed to set projection on temp destination dataset.")
+        finally:
+            _gdal.OSRDestroySpatialReference(osr)
+            _gdal.CPLFree(dstwkt)
 
         if dst_nodata is None and hasattr(destination, "fill_value"):
             # destination is a masked array
@@ -365,14 +391,19 @@ def _reproject(
     cdef void *hTransformArg = NULL
     cdef _gdal.GDALWarpOptions *psWOptions = NULL
 
-    hTransformArg = _gdal.GDALCreateGenImgProjTransformer(
-                                        hdsin, NULL, hdsout, NULL,
-                                        1, 1000.0, 0)
-    if hTransformArg == NULL:
-        raise ValueError("NULL transformer")
-    log.debug("Created transformer")
-
-    psWOptions = _gdal.GDALCreateWarpOptions()
+    try:
+        with CPLErrors() as cple:
+            hTransformArg = _gdal.GDALCreateGenImgProjTransformer(
+                                hdsin, NULL, hdsout, NULL,
+                                1, 1000.0, 0)
+            cple.check()
+            psWOptions = _gdal.GDALCreateWarpOptions()
+            cple.check()
+        log.debug("Created transformer and options.")
+    except:
+        _gdal.GDALDestroyGenImgProjTransformer(hTransformArg)
+        _gdal.GDALDestroyWarpOptions(psWOptions)
+        raise
 
     # Note: warp_extras is pointed to different memory locations on every
     # call to CSLSetNameValue call below, but needs to be set here to
@@ -476,20 +507,21 @@ def _reproject(
     # and run the warper.
     cdef GDALWarpOperation *oWarper = new GDALWarpOperation()
     try:
-        with cpl_errs:
+        with CPLErrors() as cple:
             oWarper.Initialize(psWOptions)
-
+            cple.check()
         rows, cols = destination.shape[-2:]
         log.debug(
             "Chunk and warp window: %d, %d, %d, %d.",
             0, 0, cols, rows)
 
-        with cpl_errs:
+        with CPLErrors() as cple:
             if num_threads > 1:
                 log.debug("Executing multi warp with num_threads: %d", num_threads)
                 oWarper.ChunkAndWarpMulti(0, 0, cols, rows)
             else:
                 oWarper.ChunkAndWarpImage(0, 0, cols, rows)
+            cple.check()
 
         if dtypes.is_ndarray(destination):
             retval = _io.io_auto(destination, hdsout, 0)
@@ -534,22 +566,19 @@ def _calculate_default_transform(
 
     with InMemoryRaster(
             img, transform=transform.to_gdal(), crs=src_crs) as temp:
-        hTransformArg = _gdal.GDALCreateGenImgProjTransformer(
-            temp.dataset, NULL, NULL, wkt, 1, 1000.0, 0)
-        if hTransformArg == NULL:
-            if wkt != NULL:
-                _gdal.CPLFree(wkt)
-            raise ValueError("NULL transformer")
-        log.debug("Created transformer")
-
-        # geotransform, npixels, and nlines are modified by the
-        # function called below.
         try:
-            if not GDALError.success == _gdal.GDALSuggestedWarpOutput2(
+            with CPLErrors() as cple:
+                hTransformArg = _gdal.GDALCreateGenImgProjTransformer(
+                                    temp.dataset, NULL, NULL, wkt,
+                                    1, 1000.0,0)
+                cple.check()
+                result = _gdal.GDALSuggestedWarpOutput2(
                     temp.dataset, _gdal.GDALGenImgProjTransform, hTransformArg,
-                    geotransform, &npixels, &nlines, extent, 0):
-                raise RuntimeError(
-                    "Failed to compute a suggested warp output.")
+                    geotransform, &npixels, &nlines, extent, 0)
+                cple.check()
+            log.debug("Created transformer and warp output.")
+        except CPLE_NotSupported as err:
+            raise CRSError(err.errmsg)
         finally:
             if wkt != NULL:
                 _gdal.CPLFree(wkt)
diff --git a/rasterio/aws.py b/rasterio/aws.py
new file mode 100644
index 0000000..e933101
--- /dev/null
+++ b/rasterio/aws.py
@@ -0,0 +1,63 @@
+"""Amazon Web Service sessions and S3 raster access.
+
+Reuses concepts from awscli and boto including environment variable
+names and the .aws/config and /.aws/credentials files.
+
+Raster datasets on S3 may be accessed using ``aws.Session.open()``
+
+    from rasterio.aws import Session
+
+    with Session().open('s3://bucket/foo.tif') as src:
+        ...
+
+or by calling ``rasterio.open()`` from within a session block
+
+    with Session() as sess:
+        with rasterio.open('s3://bucket/foo.tif') as src:
+            ...
+"""
+
+import os
+
+import boto3
+
+from rasterio._drivers import ConfigEnv
+from rasterio.five import configparser, string_types
+
+
+class Session(ConfigEnv):
+    """A credentialed AWS S3 raster access session."""
+
+    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
+                 aws_session_token=None, region_name=None, profile_name=None,
+                 **options):
+        self._session = boto3.Session(
+            aws_access_key_id=aws_access_key_id,
+            aws_secret_access_key=aws_secret_access_key,
+            aws_session_token=aws_session_token,
+            region_name=region_name,
+            profile_name=profile_name)
+        self._creds = self._session._session.get_credentials()
+        self.options = options.copy()
+        if self._creds.access_key:
+            self.options['AWS_ACCESS_KEY_ID'] = self._creds.access_key
+        if self._creds.secret_key:
+            self.options['AWS_SECRET_ACCESS_KEY'] = self._creds.secret_key
+        if self._creds.token:
+            self.options['AWS_SESSION_TOKEN'] = self._creds.token
+        if self._session.region_name:
+            self.options['AWS_REGION'] = self._session.region_name
+        self.prev_options = {}
+
+    def open(self, path, mode='r'):
+        """Read-only access to rasters on S3."""
+        if not isinstance(path, string_types):
+            raise TypeError("invalid path: %r" % path)
+        if mode == 'r-':
+            from rasterio._base import DatasetReader
+            s = DatasetReader(path, options=self.options)
+        else:
+            from rasterio._io import RasterReader
+            s = RasterReader(path, options=self.options)
+        s.start()
+        return s
diff --git a/rasterio/coords.py b/rasterio/coords.py
index 50eb59d..05beeba 100644
--- a/rasterio/coords.py
+++ b/rasterio/coords.py
@@ -1,17 +1,44 @@
 
 from collections import namedtuple
 
-BoundingBox = namedtuple('BoundingBox', ('left', 'bottom', 'right', 'top'))
+_BoundingBox = namedtuple('BoundingBox', ('left', 'bottom', 'right', 'top'))
 
+class BoundingBox(_BoundingBox):
+    """Bounding box named tuple, defining extent in cartesian coordinates
+
+    .. code::
+
+        BoundingBox(left, bottom, right, top)
+
+    Attributes
+    ----------
+    left :
+        Left coordinate
+    bottom :
+        Bottom coordinate
+    right :
+        Right coordinate
+    top :
+        Top coordinate
+    """
+    pass
 
 def disjoint_bounds(bounds1, bounds2):
-    """Returns True if bounds do not overlap
+    """Compare two bounds and determine if they are disjoint
 
     Parameters
     ----------
-    bounds1: rasterio bounds tuple (xmin, ymin, xmax, ymax)
-    bounds2: rasterio bounds tuple
+    bounds1: 4-tuple
+        rasterio bounds tuple (xmin, ymin, xmax, ymax)
+    bounds2: 4-tuple
+        rasterio bounds tuple
+
+    Returns
+    -------
+    boolean
+    ``True`` if bounds are disjoint,
+    ``False`` if bounds overlap
     """
 
     return (bounds1[0] > bounds2[2] or bounds1[2] < bounds2[0] or
-            bounds1[1] > bounds2[3] or bounds1[3] < bounds2[1])
\ No newline at end of file
+            bounds1[1] > bounds2[3] or bounds1[3] < bounds2[1])
diff --git a/rasterio/enums.py b/rasterio/enums.py
index 68563ee..f62ffaa 100644
--- a/rasterio/enums.py
+++ b/rasterio/enums.py
@@ -18,6 +18,9 @@ class ColorInterp(IntEnum):
     magenta=11
     yellow=12
     black=13
+    Y=14
+    Cb=15
+    Cr=16
 
 
 class Resampling(Enum):
@@ -53,3 +56,14 @@ class MaskFlags(IntEnum):
     per_dataset=2
     alpha=4
     nodata=8
+
+
+class PhotometricInterp(Enum):
+    black='MINISBLACK'
+    white='MINISWHITE'
+    rgb='RGB'
+    cmyk='CMYK'
+    ycbcr='YCbCr'
+    cielab='CIELAB'
+    icclab='ICCLAB'
+    itulab='ITULAB'
diff --git a/rasterio/errors.py b/rasterio/errors.py
index 3a4f34e..bb54e2c 100644
--- a/rasterio/errors.py
+++ b/rasterio/errors.py
@@ -3,16 +3,22 @@
 from click import FileError
 
 
-class RasterioIOError(IOError):
-    """A failure to open a dataset using the presently registered drivers."""
+class CRSError(ValueError):
+    """Raised when a CRS string or mapping is invalid or cannot serve
+    to define a coordinate transformation."""
 
 
 class DriverRegistrationError(ValueError):
-    """To be raised when, eg, _gdal.GDALGetDriverByName("MEM") returns NULL."""
+    """Raised when a format driver is requested but is not registered."""
 
 
 class FileOverwriteError(FileError):
-    """Rasterio's CLI refuses to implicitly clobber output files."""
+    """Raised when Rasterio's CLI refuses to clobber output files."""
 
     def __init__(self, message):
         super(FileOverwriteError, self).__init__('', hint=message)
+
+
+class RasterioIOError(IOError):
+    """Raised when a dataset cannot be opened using one of the
+    registered format drivers."""
diff --git a/rasterio/five.py b/rasterio/five.py
index ec38a12..aa6268e 100644
--- a/rasterio/five.py
+++ b/rasterio/five.py
@@ -8,8 +8,10 @@ if sys.version_info[0] >= 3:
     text_type = str
     integer_types = int,
     zip_longest = itertools.zip_longest
+    import configparser
 else:
     string_types = basestring,
     text_type = unicode
     integer_types = int, long
     zip_longest = itertools.izip_longest
+    import ConfigParser as configparser
diff --git a/rasterio/tools/mask.py b/rasterio/mask.py
similarity index 100%
copy from rasterio/tools/mask.py
copy to rasterio/mask.py
diff --git a/rasterio/tools/merge.py b/rasterio/merge.py
similarity index 100%
copy from rasterio/tools/merge.py
copy to rasterio/merge.py
diff --git a/rasterio/tool.py b/rasterio/plot.py
similarity index 77%
copy from rasterio/tool.py
copy to rasterio/plot.py
index ebd3a04..f6f0606 100644
--- a/rasterio/tool.py
+++ b/rasterio/plot.py
@@ -1,17 +1,15 @@
+"""Implementations of various common operations,
+like `show()` for displaying an array or with matplotlib.
+Most can handle a numpy array or `rasterio.Band()`.
+Primarily supports `$ rio insp`.
 """
-Implementations of various common operations, like `show()` for displaying an
-array or with matplotlib, and `stats()` for computing min/max/avg.  Most can
-handle a numpy array or `rasterio.Band()`.  Primarily supports `$ rio insp`.
-"""
-
-
 from __future__ import absolute_import
 
-import code
-import collections
 import logging
 import warnings
 
+import rasterio
+
 try:
     import matplotlib.pyplot as plt
 except ImportError:
@@ -25,20 +23,11 @@ except RuntimeError as e:
     warnings.warn(str(e), RuntimeWarning, stacklevel=2)
     plt = None
 
-import numpy
 
-import rasterio
 from rasterio.five import zip_longest
 
-
 logger = logging.getLogger('rasterio')
 
-Stats = collections.namedtuple('Stats', ['min', 'max', 'mean'])
-
-# Collect dictionary of functions for use in the interpreter in main()
-funcs = locals()
-
-
 def show(source, cmap='gray', with_bounds=True):
     """
     Display a raster or raster band using matplotlib.
@@ -78,16 +67,6 @@ def show(source, cmap='gray', with_bounds=True):
         raise ImportError("matplotlib could not be imported")
 
 
-def stats(source):
-    """Return a tuple with raster min, max, and mean.
-    """
-    if isinstance(source, tuple):
-        arr = source[0].read(source[1])
-    else:
-        arr = source
-    return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
-
-
 def show_hist(source, bins=10, masked=True, title='Histogram'):
 
     """
@@ -157,18 +136,3 @@ def show_hist(source, bins=10, masked=True, title='Histogram'):
     plt.ylabel('Frequency')
     fig = plt.gcf()
     fig.show()
-
-
-def main(banner, dataset, alt_interpreter=None):
-    """ Main entry point for use with python interpreter """
-    local = dict(funcs, src=dataset, np=numpy, rio=rasterio, plt=plt)
-    if not alt_interpreter:
-        code.interact(banner, local=local)
-    elif alt_interpreter == 'ipython':
-        import IPython
-        IPython.InteractiveShell.banner1 = banner
-        IPython.start_ipython(argv=[], user_ns=local)
-    else:
-        raise ValueError("Unsupported interpreter '%s'" % alt_interpreter)
-
-    return 0
diff --git a/rasterio/rio/bounds.py b/rasterio/rio/bounds.py
new file mode 100644
index 0000000..4b5466d
--- /dev/null
+++ b/rasterio/rio/bounds.py
@@ -0,0 +1,119 @@
+import logging
+import os
+
+import click
+from cligj import (
+    precision_opt, indent_opt, compact_opt, projection_geographic_opt,
+    projection_mercator_opt, projection_projected_opt, sequence_opt,
+    use_rs_opt, geojson_type_feature_opt, geojson_type_bbox_opt,
+    geojson_type_collection_opt)
+
+from .helpers import write_features, to_lower
+import rasterio
+from rasterio.warp import transform_bounds
+
+logger = logging.getLogger('rio')
+
+
+# Bounds command.
+ at click.command(short_help="Write bounding boxes to stdout as GeoJSON.")
+# One or more files, the bounds of each are a feature in the collection
+# object or feature sequence.
+ at click.argument('INPUT', nargs=-1, type=click.Path(exists=True), required=True)
+ at precision_opt
+ at indent_opt
+ at compact_opt
+ at projection_geographic_opt
+ at projection_projected_opt
+ at projection_mercator_opt
+ at click.option(
+    '--dst-crs', default='', metavar="EPSG:NNNN", callback=to_lower,
+    help="Output in specified coordinates.")
+ at sequence_opt
+ at use_rs_opt
+ at geojson_type_collection_opt(True)
+ at geojson_type_feature_opt(False)
+ at geojson_type_bbox_opt(False)
+ at click.pass_context
+def bounds(ctx, input, precision, indent, compact, projection, dst_crs,
+           sequence, use_rs, geojson_type):
+    """Write bounding boxes to stdout as GeoJSON for use with, e.g.,
+    geojsonio
+
+      $ rio bounds *.tif | geojsonio
+
+    If a destination crs is passed via dst_crs, it takes precedence over
+    the projection parameter.
+    """
+    import rasterio.warp
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+    dump_kwds = {'sort_keys': True}
+    if indent:
+        dump_kwds['indent'] = indent
+    if compact:
+        dump_kwds['separators'] = (',', ':')
+    stdout = click.get_text_stream('stdout')
+
+    # This is the generator for (feature, bbox) pairs.
+    class Collection(object):
+
+        def __init__(self):
+            self._xs = []
+            self._ys = []
+
+        @property
+        def bbox(self):
+            return min(self._xs), min(self._ys), max(self._xs), max(self._ys)
+
+        def __call__(self):
+            for i, path in enumerate(input):
+                with rasterio.open(path) as src:
+                    bounds = src.bounds
+                    if dst_crs:
+                        bbox = transform_bounds(src.crs,
+                                                dst_crs, *bounds)
+                    elif projection == 'mercator':
+                        bbox = transform_bounds(src.crs,
+                                                {'init': 'epsg:3857'}, *bounds)
+                    elif projection == 'geographic':
+                        bbox = transform_bounds(src.crs,
+                                                {'init': 'epsg:4326'}, *bounds)
+                    else:
+                        bbox = bounds
+
+                if precision >= 0:
+                    bbox = [round(b, precision) for b in bbox]
+
+                yield {
+                    'type': 'Feature',
+                    'bbox': bbox,
+                    'geometry': {
+                        'type': 'Polygon',
+                        'coordinates': [[
+                            [bbox[0], bbox[1]],
+                            [bbox[2], bbox[1]],
+                            [bbox[2], bbox[3]],
+                            [bbox[0], bbox[3]],
+                            [bbox[0], bbox[1]]]]},
+                    'properties': {
+                        'id': str(i),
+                        'title': path,
+                        'filename': os.path.basename(path)}}
+
+                self._xs.extend(bbox[::2])
+                self._ys.extend(bbox[1::2])
+
+    col = Collection()
+    # Use the generator defined above as input to the generic output
+    # writing function.
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+            write_features(
+                stdout, col, sequence=sequence,
+                geojson_type=geojson_type, use_rs=use_rs,
+                **dump_kwds)
+
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
diff --git a/rasterio/rio/convert.py b/rasterio/rio/clip.py
similarity index 50%
copy from rasterio/rio/convert.py
copy to rasterio/rio/clip.py
index 017b2c9..843609f 100644
--- a/rasterio/rio/convert.py
+++ b/rasterio/rio/clip.py
@@ -1,10 +1,7 @@
 """File translation command"""
 
-import logging
-
 import click
 from cligj import format_opt
-import numpy as np
 
 from .helpers import resolve_inout
 from . import options
@@ -99,97 +96,3 @@ def clip(
 
             with rasterio.open(output, 'w', **out_kwargs) as out:
                 out.write(src.read(window=window))
-
-
- at click.command(short_help="Copy and convert raster dataset.")
- at click.argument(
-    'files',
-    nargs=-1,
-    type=click.Path(resolve_path=True),
-    required=True,
-    metavar="INPUT OUTPUT")
- at options.output_opt
- at format_opt
- at options.dtype_opt
- at click.option('--scale-ratio', type=float, default=None,
-              help="Source to destination scaling ratio.")
- at click.option('--scale-offset', type=float, default=None,
-              help="Source to destination scaling offset.")
- at options.rgb_opt
- at options.creation_options
- at click.pass_context
-def convert(
-        ctx, files, output, driver, dtype, scale_ratio, scale_offset,
-        photometric, creation_options):
-    """Copy and convert raster datasets to other data types and formats.
-
-    Data values may be linearly scaled when copying by using the
-    --scale-ratio and --scale-offset options. Destination raster values
-    are calculated as
-
-      dst = scale_ratio * src + scale_offset
-
-    For example, to scale uint16 data with an actual range of 0-4095 to
-    0-255 as uint8:
-
-      $ rio convert in16.tif out8.tif --dtype uint8 --scale-ratio 0.0625
-
-    Format specific creation options may also be passed using --co. To
-    tile a new GeoTIFF output file, do the following.
-
-      --co tiled=true --co blockxsize=256 --co blockysize=256
-
-    To compress it using the LZW method, add
-
-      --co compress=LZW
-
-    """
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-
-    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
-
-        outputfile, files = resolve_inout(files=files, output=output)
-        inputfile = files[0]
-
-        with rasterio.open(inputfile) as src:
-
-            # Use the input file's profile, updated by CLI
-            # options, as the profile for the output file.
-            profile = src.profile
-
-            if 'affine' in profile:
-                profile['transform'] = profile.pop('affine')
-
-            if driver:
-                profile['driver'] = driver
-
-            if dtype:
-                profile['dtype'] = dtype
-            dst_dtype = profile['dtype']
-
-            if photometric:
-                creation_options['photometric'] = photometric
-
-            profile.update(**creation_options)
-
-            with rasterio.open(outputfile, 'w', **profile) as dst:
-
-                data = src.read()
-
-                if scale_ratio:
-                    # Cast to float64 before multiplying.
-                    data = data.astype('float64', casting='unsafe', copy=False)
-                    np.multiply(
-                        data, scale_ratio, out=data, casting='unsafe')
-
-                if scale_offset:
-                    # My understanding of copy=False is that this is a
-                    # no-op if the array was cast for multiplication.
-                    data = data.astype('float64', casting='unsafe', copy=False)
-                    np.add(
-                        data, scale_offset, out=data, casting='unsafe')
-
-                # Cast to the output dtype and write.
-                result = data.astype(dst_dtype, casting='unsafe', copy=False)
-                dst.write(result)
diff --git a/rasterio/rio/convert.py b/rasterio/rio/convert.py
index 017b2c9..7937b4c 100644
--- a/rasterio/rio/convert.py
+++ b/rasterio/rio/convert.py
@@ -9,96 +9,6 @@ import numpy as np
 from .helpers import resolve_inout
 from . import options
 import rasterio
-from rasterio.coords import disjoint_bounds
-
-
-# Clip command
- at click.command(short_help='Clip a raster to given bounds.')
- at click.argument(
-    'files',
-    nargs=-1,
-    type=click.Path(resolve_path=True),
-    required=True,
-    metavar="INPUT OUTPUT")
- at options.output_opt
- at options.bounds_opt
- at click.option(
-    '--like',
-    type=click.Path(exists=True),
-    help='Raster dataset to use as a template for bounds')
- at format_opt
- at options.creation_options
- at click.pass_context
-def clip(
-        ctx,
-        files,
-        output,
-        bounds,
-        like,
-        driver,
-        creation_options):
-    """Clips a raster using bounds input directly or from a template raster.
-
-    \b
-      $ rio clip input.tif output.tif --bounds xmin ymin xmax ymax
-      $ rio clip input.tif output.tif --like template.tif
-
-    If using --bounds, values must be in coordinate reference system of input.
-    If using --like, bounds will automatically be transformed to match the
-    coordinate reference system of the input.
-
-    It can also be combined to read bounds of a feature dataset using Fiona:
-
-    \b
-      $ rio clip input.tif output.tif --bounds $(fio info features.shp --bounds)
-
-    """
-
-    from rasterio.warp import transform_bounds
-
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-
-    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
-
-        output, files = resolve_inout(files=files, output=output)
-        input = files[0]
-
-        with rasterio.open(input) as src:
-            if bounds:
-                if disjoint_bounds(bounds, src.bounds):
-                    raise click.BadParameter('must overlap the extent of '
-                                             'the input raster',
-                                             param='--bounds',
-                                             param_hint='--bounds')
-            elif like:
-                with rasterio.open(like) as template_ds:
-                    bounds = template_ds.bounds
-                    if template_ds.crs != src.crs:
-                        bounds = transform_bounds(template_ds.crs, src.crs,
-                                                  *bounds)
-
-                    if disjoint_bounds(bounds, src.bounds):
-                        raise click.BadParameter('must overlap the extent of '
-                                                 'the input raster',
-                                                 param='--like',
-                                                 param_hint='--like')
-
-            else:
-                raise click.UsageError('--bounds or --like required')
-
-            window = src.window(*bounds)
-
-            out_kwargs = src.meta.copy()
-            out_kwargs.update({
-                'driver': driver,
-                'height': window[0][1] - window[0][0],
-                'width': window[1][1] - window[1][0],
-                'transform': src.window_transform(window)
-            })
-            out_kwargs.update(**creation_options)
-
-            with rasterio.open(output, 'w', **out_kwargs) as out:
-                out.write(src.read(window=window))
 
 
 @click.command(short_help="Copy and convert raster dataset.")
diff --git a/rasterio/rio/edit_info.py b/rasterio/rio/edit_info.py
new file mode 100644
index 0000000..e62bf22
--- /dev/null
+++ b/rasterio/rio/edit_info.py
@@ -0,0 +1,155 @@
+"""Fetch and edit raster dataset metadata from the command line."""
+
+import json
+import logging
+
+import click
+
+from . import options
+import rasterio
+import rasterio.crs
+from rasterio.transform import guard_transform
+
+
+# Handlers for info module options.
+
+def all_handler(ctx, param, value):
+    """Get tags from a template file or command line."""
+    if ctx.obj and ctx.obj.get('like') and value is not None:
+        ctx.obj['all_like'] = value
+        value = ctx.obj.get('like')
+    return value
+
+
+def crs_handler(ctx, param, value):
+    """Get crs value from a template file or command line."""
+    retval = options.from_like_context(ctx, param, value)
+    if retval is None and value:
+        try:
+            retval = json.loads(value)
+        except ValueError:
+            retval = value
+        if not rasterio.crs.is_valid_crs(retval):
+            raise click.BadParameter(
+                "'%s' is not a recognized CRS." % retval,
+                param=param, param_hint='crs')
+    return retval
+
+
+def tags_handler(ctx, param, value):
+    """Get tags from a template file or command line."""
+    retval = options.from_like_context(ctx, param, value)
+    if retval is None and value:
+        try:
+            retval = dict(p.split('=') for p in value)
+        except:
+            raise click.BadParameter(
+                "'%s' contains a malformed tag." % value,
+                param=param, param_hint='transform')
+    return retval
+
+
+def transform_handler(ctx, param, value):
+    """Get transform value from a template file or command line."""
+    retval = options.from_like_context(ctx, param, value)
+    if retval is None and value:
+        try:
+            value = json.loads(value)
+        except ValueError:
+            pass
+        try:
+            retval = guard_transform(value)
+        except:
+            raise click.BadParameter(
+                "'%s' is not recognized as an Affine or GDAL "
+                "geotransform array." % value,
+                param=param, param_hint='transform')
+    return retval
+
+
+# The edit-info command.
+
+ at click.command('edit-info', short_help="Edit dataset metadata.")
+ at options.file_in_arg
+ at options.nodata_opt
+ at click.option('--crs', callback=crs_handler, default=None,
+              help="New coordinate reference system")
+ at click.option('--transform', callback=transform_handler,
+              help="New affine transform matrix")
+ at click.option('--tag', 'tags', callback=tags_handler, multiple=True,
+              metavar='KEY=VAL', help="New tag.")
+ at click.option('--all', 'allmd', callback=all_handler, flag_value='like',
+              is_eager=True, default=False,
+              help="Copy all metadata items from the template file.")
+ at options.like_opt
+ at click.pass_context
+def edit(ctx, input, nodata, crs, transform, tags, allmd, like):
+    """Edit a dataset's metadata: coordinate reference system, affine
+    transformation matrix, nodata value, and tags.
+
+    The coordinate reference system may be either a PROJ.4 or EPSG:nnnn
+    string,
+
+      --crs 'EPSG:4326'
+
+    or a JSON text-encoded PROJ.4 object.
+
+      --crs '{"proj": "utm", "zone": 18, ...}'
+
+    Transforms are either JSON-encoded Affine objects (preferred),
+
+      --transform '[300.038, 0.0, 101985.0, 0.0, -300.042, 2826915.0]'
+
+    or JSON text-encoded GDAL geotransform arrays.
+
+      --transform '[101985.0, 300.038, 0.0, 2826915.0, 0.0, -300.042]'
+
+    Metadata items may also be read from an existing dataset using a
+    combination of the --like option with at least one of --all,
+    `--crs like`, `--nodata like`, and `--transform like`.
+
+      rio edit-info example.tif --like template.tif --all
+
+    To get just the transform from the template:
+
+      rio edit-info example.tif --like template.tif --transform like
+
+    """
+    import numpy as np
+
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+
+    def in_dtype_range(value, dtype):
+        infos = {'c': np.finfo, 'f': np.finfo, 'i': np.iinfo,
+                 'u': np.iinfo}
+        rng = infos[np.dtype(dtype).kind](dtype)
+        return rng.min <= value <= rng.max
+
+    with rasterio.drivers(CPL_DEBUG=(verbosity > 2)) as env:
+
+        with rasterio.open(input, 'r+') as dst:
+
+            if allmd:
+                nodata = allmd['nodata']
+                crs = allmd['crs']
+                transform = allmd['transform']
+                tags = allmd['tags']
+
+            if nodata is not None:
+                dtype = dst.dtypes[0]
+                if not in_dtype_range(nodata, dtype):
+                    raise click.BadParameter(
+                        "outside the range of the file's "
+                        "data type (%s)." % dtype,
+                        param=nodata, param_hint='nodata')
+                dst.nodata = nodata
+
+            if crs:
+                dst.crs = crs
+
+            if transform:
+                dst.transform = transform
+
+            if tags:
+                dst.update_tags(**tags)
diff --git a/rasterio/rio/env.py b/rasterio/rio/env.py
new file mode 100644
index 0000000..79535ac
--- /dev/null
+++ b/rasterio/rio/env.py
@@ -0,0 +1,26 @@
+"""Fetch and edit raster dataset metadata from the command line."""
+
+import logging
+
+import click
+
+import rasterio
+import rasterio.crs
+
+
+ at click.command(short_help="Print information about the rio environment.")
+ at click.option('--formats', 'key', flag_value='formats', default=True,
+              help="Enumerate the available formats.")
+ at click.pass_context
+def env(ctx, key):
+    """Print information about the Rasterio environment: available
+    formats, etc.
+    """
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+    stdout = click.get_text_stream('stdout')
+    with rasterio.drivers(CPL_DEBUG=(verbosity > 2)) as env:
+        if key == 'formats':
+            for k, v in sorted(env.drivers().items()):
+                stdout.write("%s: %s\n" % (k, v))
+            stdout.write('\n')
diff --git a/rasterio/rio/features.py b/rasterio/rio/features.py
deleted file mode 100644
index e35cd66..0000000
--- a/rasterio/rio/features.py
+++ /dev/null
@@ -1,710 +0,0 @@
-import json
-import logging
-from math import ceil
-import os
-import re
-import shutil
-
-import click
-import cligj
-from cligj import (
-    precision_opt, indent_opt, compact_opt, projection_geographic_opt,
-    projection_mercator_opt, projection_projected_opt, sequence_opt,
-    use_rs_opt, geojson_type_feature_opt, geojson_type_bbox_opt,
-    format_opt, geojson_type_collection_opt)
-
-from .helpers import coords, resolve_inout, write_features, to_lower
-from . import options
-import rasterio
-from rasterio.transform import Affine
-from rasterio.coords import disjoint_bounds
-from rasterio.warp import transform_bounds
-
-logger = logging.getLogger('rio')
-
-
-# Common options used below
-
-# Unlike the version in cligj, this one doesn't require values.
-files_inout_arg = click.argument(
-    'files',
-    nargs=-1,
-    type=click.Path(resolve_path=True),
-    metavar="INPUTS... OUTPUT")
-
-all_touched_opt = click.option(
-    '-a', '--all', '--all_touched', 'all_touched',
-    is_flag=True,
-    default=False,
-    help='Use all pixels touched by features, otherwise (default) use only '
-         'pixels whose center is within the polygon or that are selected by '
-         'Bresenhams line algorithm')
-
-
-# Mask command
- at click.command(short_help='Mask in raster using features.')
- at cligj.files_inout_arg
- at options.output_opt
- at click.option('-j', '--geojson-mask', 'geojson_mask',
-              type=click.Path(), default=None,
-              help='GeoJSON file to use for masking raster.  Use "-" to read '
-                   'from stdin.  If not provided, original raster will be '
-                   'returned')
- at format_opt
- at all_touched_opt
- at click.option('--crop', is_flag=True, default=False,
-              help='Crop output raster to the extent of the geometries. '
-                   'GeoJSON must overlap input raster to use --crop')
- at click.option('-i', '--invert', is_flag=True, default=False,
-              help='Inverts the mask, so that areas covered by features are'
-                   'masked out and areas not covered are retained.  Ignored '
-                   'if using --crop')
- at options.force_overwrite_opt
- at options.creation_options
- at click.pass_context
-def mask(
-        ctx,
-        files,
-        output,
-        geojson_mask,
-        driver,
-        all_touched,
-        crop,
-        invert,
-        force_overwrite,
-        creation_options):
-
-    """Masks in raster using GeoJSON features (masks out all areas not covered
-    by features), and optionally crops the output raster to the extent of the
-    features.  Features are assumed to be in the same coordinate reference
-    system as the input raster.
-
-    GeoJSON must be the first input file or provided from stdin:
-
-    > rio mask input.tif output.tif --geojson-mask features.json
-
-    > rio mask input.tif output.tif --geojson-mask - < features.json
-
-    If the output raster exists, it will be completely overwritten with the
-    results of this operation.
-
-    The result is always equal to or within the bounds of the input raster.
-
-    --crop and --invert options are mutually exclusive.
-
-    --crop option is not valid if features are completely outside extent of
-    input raster.
-    """
-
-    from rasterio.tools.mask import mask as mask_tool
-    from rasterio.features import bounds as calculate_bounds
-
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-
-    output, files = resolve_inout(
-        files=files, output=output, force_overwrite=force_overwrite)
-    input = files[0]
-
-    if geojson_mask is None:
-        click.echo('No GeoJSON provided, INPUT will be copied to OUTPUT',
-                   err=True)
-        shutil.copy(input, output)
-        return
-
-    if crop and invert:
-        click.echo('Invert option ignored when using --crop', err=True)
-        invert = False
-
-    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
-        try:
-            with click.open_file(geojson_mask) as f:
-                geojson = json.loads(f.read())
-        except ValueError:
-            raise click.BadParameter('GeoJSON could not be read from '
-                                     '--geojson-mask or stdin',
-                                     param_hint='--geojson-mask')
-
-        if 'features' in geojson:
-            geometries = [f['geometry'] for f in geojson['features']]
-        elif 'geometry' in geojson:
-            geometries = (geojson['geometry'], )
-        else:
-            raise click.BadParameter('Invalid GeoJSON', param=input,
-                                     param_hint='input')
-        bounds = geojson.get('bbox', calculate_bounds(geojson))
-
-        with rasterio.open(input) as src:
-            try:
-                out_image, out_transform = mask_tool(src, geometries,
-                                                     crop=crop, invert=invert,
-                                                     all_touched=all_touched)
-            except ValueError as e:
-                if e.args[0] == 'Input shapes do not overlap raster.':
-                    if crop:
-                        raise click.BadParameter('not allowed for GeoJSON '
-                                                 'outside the extent of the '
-                                                 'input raster',                                                                 param=crop,
-                                                 param_hint='--crop')
-
-            meta = src.meta.copy()
-            meta.update(**creation_options)
-            meta.update({
-                'driver': driver,
-                'height': out_image.shape[1],
-                'width': out_image.shape[2],
-                'transform': out_transform
-            })
-
-            with rasterio.open(output, 'w', **meta) as out:
-                out.write(out_image)
-
-
-# Shapes command.
- at click.command(short_help="Write shapes extracted from bands or masks.")
- at options.file_in_arg
- at options.output_opt
- at precision_opt
- at indent_opt
- at compact_opt
- at projection_geographic_opt
- at projection_projected_opt
- at sequence_opt
- at use_rs_opt
- at geojson_type_feature_opt(True)
- at geojson_type_bbox_opt(False)
- at click.option('--band/--mask', default=True,
-              help="Choose to extract from a band (the default) or a mask.")
- at click.option('--bidx', 'bandidx', type=int, default=None,
-              help="Index of the band or mask that is the source of shapes.")
- at click.option('--sampling', type=int, default=1,
-              help="Inverse of the sampling fraction; "
-                   "a value of 10 decimates.")
- at click.option('--with-nodata/--without-nodata', default=False,
-              help="Include or do not include (the default) nodata regions.")
- at click.option('--as-mask/--not-as-mask', default=False,
-              help="Interpret a band as a mask and output only one class of "
-                   "valid data shapes.")
- at click.pass_context
-def shapes(
-        ctx, input, output, precision, indent, compact, projection, sequence,
-        use_rs, geojson_type, band, bandidx, sampling, with_nodata, as_mask):
-    """Extracts shapes from one band or mask of a dataset and writes
-    them out as GeoJSON. Unless otherwise specified, the shapes will be
-    transformed to WGS 84 coordinates.
-
-    The default action of this command is to extract shapes from the
-    first band of the input dataset. The shapes are polygons bounding
-    contiguous regions (or features) of the same raster value. This
-    command performs poorly for int16 or float type datasets.
-
-    Bands other than the first can be specified using the `--bidx`
-    option:
-
-      $ rio shapes --bidx 3 tests/data/RGB.byte.tif
-
-    The valid data footprint of a dataset's i-th band can be extracted
-    by using the `--mask` and `--bidx` options:
-
-      $ rio shapes --mask --bidx 1 tests/data/RGB.byte.tif
-
-    Omitting the `--bidx` option results in a footprint extracted from
-    the conjunction of all band masks. This is generally smaller than
-    any individual band's footprint.
-
-    A dataset band may be analyzed as though it were a binary mask with
-    the `--as-mask` option:
-
-      $ rio shapes --as-mask --bidx 1 tests/data/RGB.byte.tif
-    """
-    # These import numpy, which we don't want to do unless it's needed.
-    import numpy
-    import rasterio.features
-    import rasterio.warp
-
-    verbosity = ctx.obj['verbosity'] if ctx.obj else 1
-    logger = logging.getLogger('rio')
-    dump_kwds = {'sort_keys': True}
-    if indent:
-        dump_kwds['indent'] = indent
-    if compact:
-        dump_kwds['separators'] = (',', ':')
-
-    stdout = click.open_file(
-        output, 'w') if output else click.get_text_stream('stdout')
-
-    bidx = 1 if bandidx is None and band else bandidx
-
-    # This is the generator for (feature, bbox) pairs.
-    class Collection(object):
-
-        def __init__(self):
-            self._xs = []
-            self._ys = []
-
-        @property
-        def bbox(self):
-            return min(self._xs), min(self._ys), max(self._xs), max(self._ys)
-
-        def __call__(self):
-            with rasterio.open(input) as src:
-                if bidx is not None and bidx > src.count:
-                    raise ValueError('bidx is out of range for raster')
-
-                img = None
-                msk = None
-
-                # Adjust transforms.
-                transform = src.affine
-                if sampling > 1:
-                    # Decimation of the raster produces a georeferencing
-                    # shift that we correct with a translation.
-                    transform *= Affine.translation(
-                                    src.width%sampling, src.height%sampling)
-                    # And follow by scaling.
-                    transform *= Affine.scale(float(sampling))
-
-                # Most of the time, we'll use the valid data mask.
-                # We skip reading it if we're extracting every possible
-                # feature (even invalid data features) from a band.
-                if not band or (band and not as_mask and not with_nodata):
-                    if sampling == 1:
-                        msk = src.read_masks(bidx)
-                    else:
-                        msk_shape = (
-                            src.height//sampling, src.width//sampling)
-                        if bidx is None:
-                            msk = numpy.zeros(
-                                (src.count,) + msk_shape, 'uint8')
-                        else:
-                            msk = numpy.zeros(msk_shape, 'uint8')
-                        msk = src.read_masks(bidx, msk)
-
-                    if bidx is None:
-                        msk = numpy.logical_or.reduce(msk).astype('uint8')
-
-                    # Possibly overridden below.
-                    img = msk
-
-                # Read the band data unless the --mask option is given.
-                if band:
-                    if sampling == 1:
-                        img = src.read(bidx, masked=False)
-                    else:
-                        img = numpy.zeros(
-                            (src.height//sampling, src.width//sampling),
-                            dtype=src.dtypes[src.indexes.index(bidx)])
-                        img = src.read(bidx, img, masked=False)
-
-                # If --as-mask option was given, convert the image
-                # to a binary image. This reduces the number of shape
-                # categories to 2 and likely reduces the number of
-                # shapes.
-                if as_mask:
-                    tmp = numpy.ones_like(img, 'uint8') * 255
-                    tmp[img == 0] = 0
-                    img = tmp
-                    if not with_nodata:
-                        msk = tmp
-
-                # Transform the raster bounds.
-                bounds = src.bounds
-                xs = [bounds[0], bounds[2]]
-                ys = [bounds[1], bounds[3]]
-                if projection == 'geographic':
-                    xs, ys = rasterio.warp.transform(
-                        src.crs, {'init': 'epsg:4326'}, xs, ys)
-                if precision >= 0:
-                    xs = [round(v, precision) for v in xs]
-                    ys = [round(v, precision) for v in ys]
-                self._xs = xs
-                self._ys = ys
-
-                # Prepare keyword arguments for shapes().
-                kwargs = {'transform': transform}
-                if not with_nodata:
-                    kwargs['mask'] = msk
-
-                src_basename = os.path.basename(src.name)
-
-                # Yield GeoJSON features.
-                for i, (g, val) in enumerate(
-                        rasterio.features.shapes(img, **kwargs)):
-                    if projection == 'geographic':
-                        g = rasterio.warp.transform_geom(
-                            src.crs, 'EPSG:4326', g,
-                            antimeridian_cutting=True, precision=precision)
-                    xs, ys = zip(*coords(g))
-                    yield {
-                        'type': 'Feature',
-                        'id': "{0}:{1}".format(src_basename, i),
-                        'properties': {
-                            'val': val, 'filename': src_basename
-                        },
-                        'bbox': [min(xs), min(ys), max(xs), max(ys)],
-                        'geometry': g
-                    }
-
-    if not sequence:
-        geojson_type = 'collection'
-
-    try:
-        with rasterio.drivers(CPL_DEBUG=(verbosity > 2)):
-            write_features(
-                stdout, Collection(), sequence=sequence,
-                geojson_type=geojson_type, use_rs=use_rs,
-                **dump_kwds)
-    except Exception:
-        logger.exception("Exception caught during processing")
-        raise click.Abort()
-
-
-# Rasterize command.
- at click.command(short_help='Rasterize features.')
- at files_inout_arg
- at options.output_opt
- at format_opt
- at options.like_file_opt
- at options.bounds_opt
- at options.dimensions_opt
- at options.resolution_opt
- at click.option('--src-crs', '--src_crs', 'src_crs', default=None,
-              help='Source coordinate reference system.  Limited to EPSG '
-              'codes for now.  Used as output coordinate system if output '
-              'does not exist or --like option is not used. '
-              'Default: EPSG:4326')
- at all_touched_opt
- at click.option('--default-value', '--default_value', 'default_value',
-              type=float, default=1, help='Default value for rasterized pixels')
- at click.option('--fill', type=float, default=0,
-              help='Fill value for all pixels not overlapping features.  Will '
-              'be evaluated as NoData pixels for output.  Default: 0')
- at click.option('--property', 'prop', type=str, default=None, help='Property in '
-              'GeoJSON features to use for rasterized values.  Any features '
-              'that lack this property will be given --default_value instead.')
- at options.force_overwrite_opt
- at options.creation_options
- at click.pass_context
-def rasterize(
-        ctx,
-        files,
-        output,
-        driver,
-        like,
-        bounds,
-        dimensions,
-        res,
-        src_crs,
-        all_touched,
-        default_value,
-        fill,
-        prop,
-        force_overwrite,
-        creation_options):
-
-    """Rasterize GeoJSON into a new or existing raster.
-
-    If the output raster exists, rio-rasterize will rasterize feature values
-    into all bands of that raster.  The GeoJSON is assumed to be in the same
-    coordinate reference system as the output unless --src-crs is provided.
-
-    --default_value or property values when using --property must be using a
-    data type valid for the data type of that raster.
-
-
-    If a template raster is provided using the --like option, the affine
-    transform and data type from that raster will be used to create the output.
-    Only a single band will be output.
-
-    The GeoJSON is assumed to be in the same coordinate reference system unless
-    --src-crs is provided.
-
-    --default_value or property values when using --property must be using a
-    data type valid for the data type of that raster.
-
-    --driver, --bounds, --dimensions, and --res are ignored when output exists
-    or --like raster is provided
-
-
-    If the output does not exist and --like raster is not provided, the input
-    GeoJSON will be used to determine the bounds of the output unless
-    provided using --bounds.
-
-    --dimensions or --res are required in this case.
-
-    If --res is provided, the bottom and right coordinates of bounds are
-    ignored.
-
-
-    Note:
-    The GeoJSON is not projected to match the coordinate reference system
-    of the output or --like rasters at this time.  This functionality may be
-    added in the future.
-    """
-
-    from rasterio._base import is_geographic_crs, is_same_crs
-    from rasterio.features import rasterize
-    from rasterio.features import bounds as calculate_bounds
-
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-
-    output, files = resolve_inout(
-        files=files, output=output, force_overwrite=force_overwrite)
-
-    has_src_crs = src_crs is not None
-    src_crs = src_crs or 'EPSG:4326'
-
-    # If values are actually meant to be integers, we need to cast them
-    # as such or rasterize creates floating point outputs
-    if default_value == int(default_value):
-        default_value = int(default_value)
-    if fill == int(fill):
-        fill = int(fill)
-
-    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
-
-        def feature_value(feature):
-            if prop and 'properties' in feature:
-                return feature['properties'].get(prop, default_value)
-            return default_value
-
-        with click.open_file(files.pop(0) if files else '-') as gj_f:
-            geojson = json.loads(gj_f.read())
-        if 'features' in geojson:
-            geometries = []
-            for f in geojson['features']:
-                geometries.append((f['geometry'], feature_value(f)))
-        elif 'geometry' in geojson:
-            geometries = ((geojson['geometry'], feature_value(geojson)), )
-        else:
-            raise click.BadParameter('Invalid GeoJSON', param=input,
-                                     param_hint='input')
-
-        geojson_bounds = geojson.get('bbox', calculate_bounds(geojson))
-
-        if os.path.exists(output):
-            with rasterio.open(output, 'r+') as out:
-                if has_src_crs and not is_same_crs(src_crs, out.crs):
-                    raise click.BadParameter('GeoJSON does not match crs of '
-                                             'existing output raster',
-                                             param='input', param_hint='input')
-
-                if disjoint_bounds(geojson_bounds, out.bounds):
-                    click.echo("GeoJSON outside bounds of existing output "
-                               "raster. Are they in different coordinate "
-                               "reference systems?",
-                               err=True)
-
-                meta = out.meta.copy()
-
-                result = rasterize(
-                    geometries,
-                    out_shape=(meta['height'], meta['width']),
-                    transform=meta.get('affine', meta['transform']),
-                    all_touched=all_touched,
-                    dtype=meta.get('dtype', None),
-                    default_value=default_value,
-                    fill = fill)
-
-                for bidx in range(1, meta['count'] + 1):
-                    data = out.read(bidx, masked=True)
-                    # Burn in any non-fill pixels, and update mask accordingly
-                    ne = result != fill
-                    data[ne] = result[ne]
-                    data.mask[ne] = False
-                    out.write_band(bidx, data)
-
-        else:
-            if like is not None:
-                template_ds = rasterio.open(like)
-
-                if has_src_crs and not is_same_crs(src_crs, template_ds.crs):
-                    raise click.BadParameter('GeoJSON does not match crs of '
-                                             '--like raster',
-                                             param='input', param_hint='input')
-
-                if disjoint_bounds(geojson_bounds, template_ds.bounds):
-                    click.echo("GeoJSON outside bounds of --like raster. "
-                               "Are they in different coordinate reference "
-                               "systems?",
-                               err=True)
-
-                kwargs = template_ds.meta.copy()
-                kwargs['count'] = 1
-
-                # DEPRECATED
-                # upgrade transform to affine object or we may get an invalid
-                # transform set on output
-                kwargs['transform'] = template_ds.affine
-
-                template_ds.close()
-
-            else:
-                bounds = bounds or geojson_bounds
-
-                if is_geographic_crs(src_crs):
-                    if (bounds[0] < -180 or bounds[2] > 180 or
-                            bounds[1] < -80 or bounds[3] > 80):
-                        raise click.BadParameter(
-                            "Bounds are beyond the valid extent for "
-                            "EPSG:4326.",
-                            ctx, param=bounds, param_hint='--bounds')
-
-                if dimensions:
-                    width, height = dimensions
-                    res = (
-                        (bounds[2] - bounds[0]) / float(width),
-                        (bounds[3] - bounds[1]) / float(height)
-                    )
-
-                else:
-                    if not res:
-                        raise click.BadParameter(
-                            'pixel dimensions are required',
-                            ctx, param=res, param_hint='--res')
-
-                    elif len(res) == 1:
-                        res = (res[0], res[0])
-
-                    width = max(int(ceil((bounds[2] - bounds[0]) /
-                                float(res[0]))), 1)
-                    height = max(int(ceil((bounds[3] - bounds[1]) /
-                                 float(res[1]))), 1)
-
-                src_crs = src_crs.upper()
-                if not src_crs.count('EPSG:'):
-                    raise click.BadParameter(
-                        'invalid CRS.  Must be an EPSG code.',
-                        ctx, param=src_crs, param_hint='--src_crs')
-
-                kwargs = {
-                    'count': 1,
-                    'crs': src_crs,
-                    'width': width,
-                    'height': height,
-                    'transform': Affine(res[0], 0, bounds[0], 0, -res[1],
-                                        bounds[3]),
-                    'driver': driver
-                }
-                kwargs.update(**creation_options)
-
-            result = rasterize(
-                geometries,
-                out_shape=(kwargs['height'], kwargs['width']),
-                transform=kwargs.get('affine', kwargs['transform']),
-                all_touched=all_touched,
-                dtype=kwargs.get('dtype', None),
-                default_value=default_value,
-                fill = fill)
-
-            if 'dtype' not in kwargs:
-                kwargs['dtype'] = result.dtype
-
-            kwargs['nodata'] = fill
-
-            with rasterio.open(output, 'w', **kwargs) as out:
-                out.write_band(1, result)
-
-
-# Bounds command.
- at click.command(short_help="Write bounding boxes to stdout as GeoJSON.")
-# One or more files, the bounds of each are a feature in the collection
-# object or feature sequence.
- at click.argument('INPUT', nargs=-1, type=click.Path(exists=True), required=True)
- at precision_opt
- at indent_opt
- at compact_opt
- at projection_geographic_opt
- at projection_projected_opt
- at projection_mercator_opt
- at click.option(
-    '--dst-crs', default='', metavar="EPSG:NNNN", callback=to_lower,
-    help="Output in specified coordinates.")
- at sequence_opt
- at use_rs_opt
- at geojson_type_collection_opt(True)
- at geojson_type_feature_opt(False)
- at geojson_type_bbox_opt(False)
- at click.pass_context
-def bounds(ctx, input, precision, indent, compact, projection, dst_crs,
-           sequence, use_rs, geojson_type):
-    """Write bounding boxes to stdout as GeoJSON for use with, e.g.,
-    geojsonio
-
-      $ rio bounds *.tif | geojsonio
-    
-    If a destination crs is passed via dst_crs, it takes precedence over
-    the projection parameter.
-    """
-    import rasterio.warp
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-    dump_kwds = {'sort_keys': True}
-    if indent:
-        dump_kwds['indent'] = indent
-    if compact:
-        dump_kwds['separators'] = (',', ':')
-    stdout = click.get_text_stream('stdout')
-
-    # This is the generator for (feature, bbox) pairs.
-    class Collection(object):
-
-        def __init__(self):
-            self._xs = []
-            self._ys = []
-
-        @property
-        def bbox(self):
-            return min(self._xs), min(self._ys), max(self._xs), max(self._ys)
-
-        def __call__(self):
-            for i, path in enumerate(input):
-                with rasterio.open(path) as src:
-                    bounds = src.bounds
-                    if dst_crs:
-                        bbox = transform_bounds(src.crs,
-                                                dst_crs, *bounds)
-                    elif projection == 'mercator':
-                        bbox = transform_bounds(src.crs,
-                                                {'init': 'epsg:3857'}, *bounds)
-                    elif projection == 'geographic':
-                        bbox = transform_bounds(src.crs,
-                                                {'init': 'epsg:4326'}, *bounds)
-                    else:
-                        bbox = bounds
-
-                if precision >= 0:
-                    bbox = [round(b, precision) for b in bbox]
-
-                yield {
-                    'type': 'Feature',
-                    'bbox': bbox,
-                    'geometry': {
-                        'type': 'Polygon',
-                        'coordinates': [[
-                            [bbox[0], bbox[1]],
-                            [bbox[2], bbox[1]],
-                            [bbox[2], bbox[3]],
-                            [bbox[0], bbox[3]],
-                            [bbox[0], bbox[1]]]]},
-                    'properties': {
-                        'id': str(i),
-                        'title': path,
-                        'filename': os.path.basename(path)} }
-
-                self._xs.extend(bbox[::2])
-                self._ys.extend(bbox[1::2])
-
-    col = Collection()
-    # Use the generator defined above as input to the generic output
-    # writing function.
-    try:
-        with rasterio.drivers(CPL_DEBUG=verbosity>2):
-            write_features(
-                stdout, col, sequence=sequence,
-                geojson_type=geojson_type, use_rs=use_rs,
-                **dump_kwds)
-
-    except Exception:
-        logger.exception("Exception caught during processing")
-        raise click.Abort()
diff --git a/rasterio/rio/helpers.py b/rasterio/rio/helpers.py
index b9c9b02..6d0700c 100644
--- a/rasterio/rio/helpers.py
+++ b/rasterio/rio/helpers.py
@@ -2,7 +2,6 @@
 Helper objects used by multiple CLI commands.
 """
 
-
 import json
 import os
 
diff --git a/rasterio/rio/info.py b/rasterio/rio/info.py
index e7acea9..551b48e 100644
--- a/rasterio/rio/info.py
+++ b/rasterio/rio/info.py
@@ -1,181 +1,13 @@
 """Fetch and edit raster dataset metadata from the command line."""
 
-
 import json
 import logging
-import os
-import sys
 
 import click
-from cligj import precision_opt
 
 from . import options
 import rasterio
 import rasterio.crs
-from rasterio.transform import guard_transform
-
-
-# Handlers for info module options.
-
-
-def all_handler(ctx, param, value):
-    """Get tags from a template file or command line."""
-    if ctx.obj and ctx.obj.get('like') and value is not None:
-        ctx.obj['all_like'] = value
-        value = ctx.obj.get('like')
-    return value
-
-
-def crs_handler(ctx, param, value):
-    """Get crs value from a template file or command line."""
-    retval = options.from_like_context(ctx, param, value)
-    if retval is None and value:
-        try:
-            retval = json.loads(value)
-        except ValueError:
-            retval = value
-        if not rasterio.crs.is_valid_crs(retval):
-            raise click.BadParameter(
-                "'%s' is not a recognized CRS." % retval,
-                param=param, param_hint='crs')
-    return retval
-
-
-def tags_handler(ctx, param, value):
-    """Get tags from a template file or command line."""
-    retval = options.from_like_context(ctx, param, value)
-    if retval is None and value:
-        try:
-            retval = dict(p.split('=') for p in value)
-        except:
-            raise click.BadParameter(
-                "'%s' contains a malformed tag." % value,
-                param=param, param_hint='transform')
-    return retval
-
-
-def transform_handler(ctx, param, value):
-    """Get transform value from a template file or command line."""
-    retval = options.from_like_context(ctx, param, value)
-    if retval is None and value:
-        try:
-            value = json.loads(value)
-        except ValueError:
-            pass
-        try:
-            retval = guard_transform(value)
-        except:
-            raise click.BadParameter(
-                "'%s' is not recognized as an Affine or GDAL "
-                "geotransform array." % value,
-                param=param, param_hint='transform')
-    return retval
-
-
-# The edit-info command.
-
- at click.command('edit-info', short_help="Edit dataset metadata.")
- at options.file_in_arg
- at options.nodata_opt
- at click.option('--crs', callback=crs_handler, default=None,
-              help="New coordinate reference system")
- at click.option('--transform', callback=transform_handler,
-              help="New affine transform matrix")
- at click.option('--tag', 'tags', callback=tags_handler, multiple=True,
-              metavar='KEY=VAL', help="New tag.")
- at click.option('--all', 'allmd', callback=all_handler, flag_value='like',
-              is_eager=True, default=False,
-              help="Copy all metadata items from the template file.")
- at options.like_opt
- at click.pass_context
-def edit(ctx, input, nodata, crs, transform, tags, allmd, like):
-    """Edit a dataset's metadata: coordinate reference system, affine
-    transformation matrix, nodata value, and tags.
-
-    The coordinate reference system may be either a PROJ.4 or EPSG:nnnn
-    string,
-    
-      --crs 'EPSG:4326'
-    
-    or a JSON text-encoded PROJ.4 object.
-
-      --crs '{"proj": "utm", "zone": 18, ...}'
-
-    Transforms are either JSON-encoded Affine objects (preferred),
-
-      --transform '[300.038, 0.0, 101985.0, 0.0, -300.042, 2826915.0]'
-
-    or JSON text-encoded GDAL geotransform arrays.
-
-      --transform '[101985.0, 300.038, 0.0, 2826915.0, 0.0, -300.042]'
-
-    Metadata items may also be read from an existing dataset using a
-    combination of the --like option with at least one of --all,
-    `--crs like`, `--nodata like`, and `--transform like`.
-
-      rio edit-info example.tif --like template.tif --all
-
-    To get just the transform from the template:
-
-      rio edit-info example.tif --like template.tif --transform like
-
-    """
-    import numpy as np
-
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-
-    def in_dtype_range(value, dtype):
-        infos = {'c': np.finfo, 'f': np.finfo, 'i': np.iinfo,
-                 'u': np.iinfo}
-        rng = infos[np.dtype(dtype).kind](dtype)
-        return rng.min <= value <= rng.max
-
-    with rasterio.drivers(CPL_DEBUG=(verbosity > 2)) as env:
-
-        with rasterio.open(input, 'r+') as dst:
-
-            if allmd:
-                nodata = allmd['nodata']
-                crs = allmd['crs']
-                transform = allmd['transform']
-                tags = allmd['tags']
-
-            if nodata is not None:
-                dtype = dst.dtypes[0]
-                if not in_dtype_range(nodata, dtype):
-                    raise click.BadParameter(
-                        "outside the range of the file's "
-                        "data type (%s)." % dtype,
-                        param=nodata, param_hint='nodata')
-                dst.nodata = nodata
-
-            if crs:
-                dst.crs = crs
-
-            if transform:
-                dst.transform = transform
-
-            if tags:
-                dst.update_tags(**tags)
-
-
- at click.command(short_help="Print information about the rio environment.")
- at click.option('--formats', 'key', flag_value='formats', default=True,
-              help="Enumerate the available formats.")
- at click.pass_context
-def env(ctx, key):
-    """Print information about the Rasterio environment: available
-    formats, etc.
-    """
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-    stdout = click.get_text_stream('stdout')
-    with rasterio.drivers(CPL_DEBUG=(verbosity > 2)) as env:
-        if key == 'formats':
-            for k, v in sorted(env.drivers().items()):
-                stdout.write("%s: %s\n" % (k, v))
-            stdout.write('\n')
 
 
 @click.command(short_help="Print information about a data file.")
@@ -229,11 +61,14 @@ def info(ctx, input, aspect, indent, namespace, meta_member, verbose, bidx,
 
     Optionally print a single metadata item as a string.
     """
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    verbosity = ctx.obj.get('verbosity')
+    aws_session = ctx.obj.get('aws_session')
+
     logger = logging.getLogger('rio')
     mode = 'r' if (verbose or meta_member == 'stats') else 'r-'
     try:
-        with rasterio.drivers(CPL_DEBUG=(verbosity > 2)):
+        with rasterio.drivers(
+                CPL_DEBUG=(verbosity > 2)), aws_session:
             with rasterio.open(input, mode) as src:
                 info = src.profile
                 info['transform'] = info['affine'][:6]
@@ -276,87 +111,3 @@ def info(ctx, input, aspect, indent, namespace, meta_member, verbose, bidx,
     except Exception:
         logger.exception("Exception caught during processing")
         raise click.Abort()
-
-
-# Insp command.
- at click.command(short_help="Open a data file and start an interpreter.")
- at options.file_in_arg
- at click.option('--ipython', 'interpreter', flag_value='ipython',
-              help="Use IPython as interpreter.")
- at click.option(
-    '-m',
-    '--mode',
-    type=click.Choice(['r', 'r+']),
-    default='r',
-    help="File mode (default 'r').")
- at click.pass_context
-def insp(ctx, input, mode, interpreter):
-    """ Open the input file in a Python interpreter.
-    """
-    import rasterio.tool
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-    try:
-        with rasterio.drivers(CPL_DEBUG=verbosity > 2):
-            with rasterio.open(input, mode) as src:
-                rasterio.tool.main(
-                    'Rasterio %s Interactive Inspector (Python %s)\n'
-                    'Type "src.meta", "src.read(1)", or "help(src)" '
-                    'for more information.' % (
-                        rasterio.__version__,
-                        '.'.join(map(str, sys.version_info[:3]))),
-                    src, interpreter)
-    except Exception:
-        logger.exception("Exception caught during processing")
-        raise click.Abort()
-
-
-# Transform command.
- at click.command(short_help="Transform coordinates.")
- at click.argument('INPUT', default='-', required=False)
- at click.option('--src-crs', '--src_crs', default='EPSG:4326',
-              help="Source CRS.")
- at click.option('--dst-crs', '--dst_crs', default='EPSG:4326',
-              help="Destination CRS.")
- at precision_opt
- at click.pass_context
-def transform(ctx, input, src_crs, dst_crs, precision):
-    import rasterio.warp
-
-    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
-    logger = logging.getLogger('rio')
-
-    # Handle the case of file, stream, or string input.
-    try:
-        src = click.open_file(input).readlines()
-    except IOError:
-        src = [input]
-
-    try:
-        with rasterio.drivers(CPL_DEBUG=verbosity > 2):
-            if src_crs.startswith('EPSG'):
-                src_crs = {'init': src_crs}
-            elif os.path.exists(src_crs):
-                with rasterio.open(src_crs) as f:
-                    src_crs = f.crs
-            if dst_crs.startswith('EPSG'):
-                dst_crs = {'init': dst_crs}
-            elif os.path.exists(dst_crs):
-                with rasterio.open(dst_crs) as f:
-                    dst_crs = f.crs
-            for line in src:
-                coords = json.loads(line)
-                xs = coords[::2]
-                ys = coords[1::2]
-                xs, ys = rasterio.warp.transform(src_crs, dst_crs, xs, ys)
-                if precision >= 0:
-                    xs = [round(v, precision) for v in xs]
-                    ys = [round(v, precision) for v in ys]
-                result = [0]*len(coords)
-                result[::2] = xs
-                result[1::2] = ys
-                print(json.dumps(result))
-
-    except Exception:
-        logger.exception("Exception caught during processing")
-        raise click.Abort()
diff --git a/rasterio/rio/insp.py b/rasterio/rio/insp.py
new file mode 100644
index 0000000..16c40c6
--- /dev/null
+++ b/rasterio/rio/insp.py
@@ -0,0 +1,93 @@
+"""Fetch and edit raster dataset metadata from the command line.
+"""
+from __future__ import absolute_import
+
+import code
+import logging
+import sys
+import collections
+import warnings
+
+import numpy
+import click
+
+from . import options
+from rasterio.plot import show, show_hist
+import rasterio
+
+try:
+    import matplotlib.pyplot as plt
+except ImportError:
+    plt = None
+except RuntimeError as e:
+    # Certain environment configurations can trigger a RuntimeError like:
+
+    # Trying to import matplotlibRuntimeError: Python is not installed as a
+    # framework. The Mac OS X backend will not be able to function correctly
+    # if Python is not installed as a framework. See the Python ...
+    warnings.warn(str(e), RuntimeWarning, stacklevel=2)
+    plt = None
+
+
+logger = logging.getLogger('rasterio')
+
+Stats = collections.namedtuple('Stats', ['min', 'max', 'mean'])
+
+# Collect dictionary of functions for use in the interpreter in main()
+funcs = locals()
+
+
+def stats(source):
+    """Return a tuple with raster min, max, and mean.
+    """
+    if isinstance(source, tuple):
+        arr = source[0].read(source[1])
+    else:
+        arr = source
+    return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
+
+
+def main(banner, dataset, alt_interpreter=None):
+    """ Main entry point for use with python interpreter """
+    local = dict(funcs, src=dataset, np=numpy, rio=rasterio, plt=plt)
+    if not alt_interpreter:
+        code.interact(banner, local=local)
+    elif alt_interpreter == 'ipython':
+        import IPython
+        IPython.InteractiveShell.banner1 = banner
+        IPython.start_ipython(argv=[], user_ns=local)
+    else:
+        raise ValueError("Unsupported interpreter '%s'" % alt_interpreter)
+
+    return 0
+
+
+ at click.command(short_help="Open a data file and start an interpreter.")
+ at options.file_in_arg
+ at click.option('--ipython', 'interpreter', flag_value='ipython',
+              help="Use IPython as interpreter.")
+ at click.option(
+    '-m',
+    '--mode',
+    type=click.Choice(['r', 'r+']),
+    default='r',
+    help="File mode (default 'r').")
+ at click.pass_context
+def insp(ctx, input, mode, interpreter):
+    """ Open the input file in a Python interpreter.
+    """
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+            with rasterio.open(input, mode) as src:
+                main(
+                    'Rasterio %s Interactive Inspector (Python %s)\n'
+                    'Type "src.meta", "src.read(1)", or "help(src)" '
+                    'for more information.' % (
+                        rasterio.__version__,
+                        '.'.join(map(str, sys.version_info[:3]))),
+                    src, interpreter)
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
diff --git a/rasterio/rio/main.py b/rasterio/rio/main.py
index 28bfb60..5fc4cfe 100644
--- a/rasterio/rio/main.py
+++ b/rasterio/rio/main.py
@@ -1,5 +1,5 @@
 """
-Main click group for CLI
+Main command group for Rasterio's CLI.
 """
 
 
@@ -7,8 +7,8 @@ import logging
 from pkg_resources import iter_entry_points
 import sys
 
-import click
 from click_plugins import with_plugins
+import click
 import cligj
 
 from . import options
@@ -20,14 +20,38 @@ def configure_logging(verbosity):
     logging.basicConfig(stream=sys.stderr, level=log_level)
 
 
+class FakeSession(object):
+    """Fake AWS Session."""
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, *args):
+        pass
+
+    def open(self, path, mode='r'):
+        return rasterio.open(path, mode)
+
+
+def get_aws_session(profile_name):
+    """Return a credentialed AWS session or a fake, depending on 
+    whether boto3 could be imported."""
+    try:
+        import rasterio.aws
+        return rasterio.aws.Session(profile_name=profile_name)
+    except ImportError:
+        return FakeSession()
+
+
 @with_plugins(ep for ep in list(iter_entry_points('rasterio.rio_commands')) +
               list(iter_entry_points('rasterio.rio_plugins')))
 @click.group()
 @cligj.verbose_opt
 @cligj.quiet_opt
+ at click.option('--aws-profile', help="Use a specific profile from your shared AWS credentials file")
 @click.version_option(version=rasterio.__version__, message='%(version)s')
 @click.pass_context
-def main_group(ctx, verbose, quiet):
+def main_group(ctx, verbose, quiet, aws_profile):
 
     """
     Rasterio command line interface.
@@ -37,3 +61,4 @@ def main_group(ctx, verbose, quiet):
     configure_logging(verbosity)
     ctx.obj = {}
     ctx.obj['verbosity'] = verbosity
+    ctx.obj['aws_session'] = get_aws_session(aws_profile)
diff --git a/rasterio/rio/mask.py b/rasterio/rio/mask.py
new file mode 100644
index 0000000..fd58c27
--- /dev/null
+++ b/rasterio/rio/mask.py
@@ -0,0 +1,131 @@
+import json
+import logging
+import shutil
+
+import click
+import cligj
+
+from .helpers import resolve_inout
+from . import options
+import rasterio
+
+logger = logging.getLogger('rio')
+
+
+# Mask command
+ at click.command(short_help='Mask in raster using features.')
+ at cligj.files_inout_arg
+ at options.output_opt
+ at click.option('-j', '--geojson-mask', 'geojson_mask',
+              type=click.Path(), default=None,
+              help='GeoJSON file to use for masking raster.  Use "-" to read '
+                   'from stdin.  If not provided, original raster will be '
+                   'returned')
+ at cligj.format_opt
+ at options.all_touched_opt
+ at click.option('--crop', is_flag=True, default=False,
+              help='Crop output raster to the extent of the geometries. '
+                   'GeoJSON must overlap input raster to use --crop')
+ at click.option('-i', '--invert', is_flag=True, default=False,
+              help='Inverts the mask, so that areas covered by features are'
+                   'masked out and areas not covered are retained.  Ignored '
+                   'if using --crop')
+ at options.force_overwrite_opt
+ at options.creation_options
+ at click.pass_context
+def mask(
+        ctx,
+        files,
+        output,
+        geojson_mask,
+        driver,
+        all_touched,
+        crop,
+        invert,
+        force_overwrite,
+        creation_options):
+
+    """Masks in raster using GeoJSON features (masks out all areas not covered
+    by features), and optionally crops the output raster to the extent of the
+    features.  Features are assumed to be in the same coordinate reference
+    system as the input raster.
+
+    GeoJSON must be the first input file or provided from stdin:
+
+    > rio mask input.tif output.tif --geojson-mask features.json
+
+    > rio mask input.tif output.tif --geojson-mask - < features.json
+
+    If the output raster exists, it will be completely overwritten with the
+    results of this operation.
+
+    The result is always equal to or within the bounds of the input raster.
+
+    --crop and --invert options are mutually exclusive.
+
+    --crop option is not valid if features are completely outside extent of
+    input raster.
+    """
+
+    from rasterio.mask import mask as mask_tool
+    from rasterio.features import bounds as calculate_bounds
+
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+
+    output, files = resolve_inout(
+        files=files, output=output, force_overwrite=force_overwrite)
+    input = files[0]
+
+    if geojson_mask is None:
+        click.echo('No GeoJSON provided, INPUT will be copied to OUTPUT',
+                   err=True)
+        shutil.copy(input, output)
+        return
+
+    if crop and invert:
+        click.echo('Invert option ignored when using --crop', err=True)
+        invert = False
+
+    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+        try:
+            with click.open_file(geojson_mask) as fh:
+                geojson = json.loads(fh.read())
+        except ValueError:
+            raise click.BadParameter('GeoJSON could not be read from '
+                                     '--geojson-mask or stdin',
+                                     param_hint='--geojson-mask')
+
+        if 'features' in geojson:
+            geometries = [f['geometry'] for f in geojson['features']]
+        elif 'geometry' in geojson:
+            geometries = (geojson['geometry'], )
+        else:
+            raise click.BadParameter('Invalid GeoJSON', param=input,
+                                     param_hint='input')
+        bounds = geojson.get('bbox', calculate_bounds(geojson))
+
+        with rasterio.open(input) as src:
+            try:
+                out_image, out_transform = mask_tool(src, geometries,
+                                                     crop=crop, invert=invert,
+                                                     all_touched=all_touched)
+            except ValueError as e:
+                if e.args[0] == 'Input shapes do not overlap raster.':
+                    if crop:
+                        raise click.BadParameter('not allowed for GeoJSON '
+                                                 'outside the extent of the '
+                                                 'input raster',                                                                 param=crop,
+                                                 param_hint='--crop')
+
+            meta = src.meta.copy()
+            meta.update(**creation_options)
+            meta.update({
+                'driver': driver,
+                'height': out_image.shape[1],
+                'width': out_image.shape[2],
+                'transform': out_transform
+            })
+
+            with rasterio.open(output, 'w', **meta) as out:
+                out.write(out_image)
diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py
index ee81dec..5f167c0 100644
--- a/rasterio/rio/merge.py
+++ b/rasterio/rio/merge.py
@@ -48,7 +48,7 @@ def merge(ctx, files, output, driver, bounds, res, nodata, force_overwrite,
       --res 0.1 0.2  => --res 0.1 --res 0.2  (rectangular)
     """
 
-    from rasterio.tools.merge import merge as merge_tool
+    from rasterio.merge import merge as merge_tool
 
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
     logger = logging.getLogger('rio')
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
index 1911c2d..90250d4 100644
--- a/rasterio/rio/options.py
+++ b/rasterio/rio/options.py
@@ -91,19 +91,23 @@ def file_in_handler(ctx, param, value):
     except ValueError as exc:
         raise click.BadParameter(str(exc))
     path_to_check = archive or path
-    if not os.path.exists(path_to_check):
+    if not scheme in ['http', 'https', 's3'] and not os.path.exists(path_to_check):
         raise click.BadParameter(
             "Input file {0} does not exist".format(path_to_check))
     if archive and scheme:
         archive = os.path.abspath(archive)
         path = "{0}://{1}!{2}".format(scheme, archive, path)
+    elif scheme and scheme.startswith('http'):
+        path = "{0}://{1}".format(scheme, path)
+    elif scheme == 's3':
+        path = "{0}://{1}".format(scheme, path)
     else:
         path = os.path.abspath(path)
     return path
 
 
 def from_like_context(ctx, param, value):
-    """Return the value for an option from the context if the option 
+    """Return the value for an option from the context if the option
     or `--all` is given, else return None."""
     if ctx.obj and ctx.obj.get('like') and (
             value == 'like' or ctx.obj.get('all_like')):
@@ -215,7 +219,7 @@ creation_options = click.option(
          "more information.")
 
 rgb_opt = click.option(
-    '--rgb', 'photometric', 
+    '--rgb', 'photometric',
     flag_value='rgb',
     default=False,
     help="Set RGB photometric interpretation.")
@@ -236,3 +240,11 @@ like_opt = click.option(
     is_eager=True,
     help="Raster dataset to use as a template for obtaining affine "
          "transform (bounds and resolution), crs, and nodata values.")
+
+all_touched_opt = click.option(
+    '-a', '--all', '--all_touched', 'all_touched',
+    is_flag=True,
+    default=False,
+    help='Use all pixels touched by features, otherwise (default) use only '
+         'pixels whose center is within the polygon or that are selected by '
+         'Bresenhams line algorithm')
diff --git a/rasterio/rio/rasterize.py b/rasterio/rio/rasterize.py
new file mode 100644
index 0000000..3807f20
--- /dev/null
+++ b/rasterio/rio/rasterize.py
@@ -0,0 +1,271 @@
+import json
+import logging
+from math import ceil
+import os
+
+import click
+import cligj
+
+from .helpers import resolve_inout
+from . import options
+import rasterio
+from rasterio.transform import Affine
+from rasterio.coords import disjoint_bounds
+
+logger = logging.getLogger('rio')
+
+
+# Common options used below
+
+# Unlike the version in cligj, this one doesn't require values.
+files_inout_arg = click.argument(
+    'files',
+    nargs=-1,
+    type=click.Path(resolve_path=True),
+    metavar="INPUTS... OUTPUT")
+
+
+ at click.command(short_help='Rasterize features.')
+ at files_inout_arg
+ at options.output_opt
+ at cligj.format_opt
+ at options.like_file_opt
+ at options.bounds_opt
+ at options.dimensions_opt
+ at options.resolution_opt
+ at click.option('--src-crs', '--src_crs', 'src_crs', default=None,
+              help='Source coordinate reference system.  Limited to EPSG '
+              'codes for now.  Used as output coordinate system if output '
+              'does not exist or --like option is not used. '
+              'Default: EPSG:4326')
+ at options.all_touched_opt
+ at click.option('--default-value', '--default_value', 'default_value',
+              type=float, default=1, help='Default value for rasterized pixels')
+ at click.option('--fill', type=float, default=0,
+              help='Fill value for all pixels not overlapping features.  Will '
+              'be evaluated as NoData pixels for output.  Default: 0')
+ at click.option('--property', 'prop', type=str, default=None, help='Property in '
+              'GeoJSON features to use for rasterized values.  Any features '
+              'that lack this property will be given --default_value instead.')
+ at options.force_overwrite_opt
+ at options.creation_options
+ at click.pass_context
+def rasterize(
+        ctx,
+        files,
+        output,
+        driver,
+        like,
+        bounds,
+        dimensions,
+        res,
+        src_crs,
+        all_touched,
+        default_value,
+        fill,
+        prop,
+        force_overwrite,
+        creation_options):
+
+    """Rasterize GeoJSON into a new or existing raster.
+
+    If the output raster exists, rio-rasterize will rasterize feature values
+    into all bands of that raster.  The GeoJSON is assumed to be in the same
+    coordinate reference system as the output unless --src-crs is provided.
+
+    --default_value or property values when using --property must be using a
+    data type valid for the data type of that raster.
+
+
+    If a template raster is provided using the --like option, the affine
+    transform and data type from that raster will be used to create the output.
+    Only a single band will be output.
+
+    The GeoJSON is assumed to be in the same coordinate reference system unless
+    --src-crs is provided.
+
+    --default_value or property values when using --property must be using a
+    data type valid for the data type of that raster.
+
+    --driver, --bounds, --dimensions, and --res are ignored when output exists
+    or --like raster is provided
+
+
+    If the output does not exist and --like raster is not provided, the input
+    GeoJSON will be used to determine the bounds of the output unless
+    provided using --bounds.
+
+    --dimensions or --res are required in this case.
+
+    If --res is provided, the bottom and right coordinates of bounds are
+    ignored.
+
+
+    Note:
+    The GeoJSON is not projected to match the coordinate reference system
+    of the output or --like rasters at this time.  This functionality may be
+    added in the future.
+    """
+
+    from rasterio._base import is_geographic_crs, is_same_crs
+    from rasterio.features import rasterize
+    from rasterio.features import bounds as calculate_bounds
+
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+
+    output, files = resolve_inout(
+        files=files, output=output, force_overwrite=force_overwrite)
+
+    has_src_crs = src_crs is not None
+    src_crs = src_crs or 'EPSG:4326'
+
+    # If values are actually meant to be integers, we need to cast them
+    # as such or rasterize creates floating point outputs
+    if default_value == int(default_value):
+        default_value = int(default_value)
+    if fill == int(fill):
+        fill = int(fill)
+
+    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+
+        def feature_value(feature):
+            if prop and 'properties' in feature:
+                return feature['properties'].get(prop, default_value)
+            return default_value
+
+        with click.open_file(files.pop(0) if files else '-') as gj_f:
+            geojson = json.loads(gj_f.read())
+        if 'features' in geojson:
+            geometries = []
+            for f in geojson['features']:
+                geometries.append((f['geometry'], feature_value(f)))
+        elif 'geometry' in geojson:
+            geometries = ((geojson['geometry'], feature_value(geojson)), )
+        else:
+            raise click.BadParameter('Invalid GeoJSON', param=input,
+                                     param_hint='input')
+
+        geojson_bounds = geojson.get('bbox', calculate_bounds(geojson))
+
+        if os.path.exists(output):
+            with rasterio.open(output, 'r+') as out:
+                if has_src_crs and not is_same_crs(src_crs, out.crs):
+                    raise click.BadParameter('GeoJSON does not match crs of '
+                                             'existing output raster',
+                                             param='input', param_hint='input')
+
+                if disjoint_bounds(geojson_bounds, out.bounds):
+                    click.echo("GeoJSON outside bounds of existing output "
+                               "raster. Are they in different coordinate "
+                               "reference systems?",
+                               err=True)
+
+                meta = out.meta.copy()
+
+                result = rasterize(
+                    geometries,
+                    out_shape=(meta['height'], meta['width']),
+                    transform=meta.get('affine', meta['transform']),
+                    all_touched=all_touched,
+                    dtype=meta.get('dtype', None),
+                    default_value=default_value,
+                    fill = fill)
+
+                for bidx in range(1, meta['count'] + 1):
+                    data = out.read(bidx, masked=True)
+                    # Burn in any non-fill pixels, and update mask accordingly
+                    ne = result != fill
+                    data[ne] = result[ne]
+                    data.mask[ne] = False
+                    out.write_band(bidx, data)
+
+        else:
+            if like is not None:
+                template_ds = rasterio.open(like)
+
+                if has_src_crs and not is_same_crs(src_crs, template_ds.crs):
+                    raise click.BadParameter('GeoJSON does not match crs of '
+                                             '--like raster',
+                                             param='input', param_hint='input')
+
+                if disjoint_bounds(geojson_bounds, template_ds.bounds):
+                    click.echo("GeoJSON outside bounds of --like raster. "
+                               "Are they in different coordinate reference "
+                               "systems?",
+                               err=True)
+
+                kwargs = template_ds.meta.copy()
+                kwargs['count'] = 1
+
+                # DEPRECATED
+                # upgrade transform to affine object or we may get an invalid
+                # transform set on output
+                kwargs['transform'] = template_ds.affine
+
+                template_ds.close()
+
+            else:
+                bounds = bounds or geojson_bounds
+
+                if is_geographic_crs(src_crs):
+                    if (bounds[0] < -180 or bounds[2] > 180 or
+                            bounds[1] < -80 or bounds[3] > 80):
+                        raise click.BadParameter(
+                            "Bounds are beyond the valid extent for "
+                            "EPSG:4326.",
+                            ctx, param=bounds, param_hint='--bounds')
+
+                if dimensions:
+                    width, height = dimensions
+                    res = (
+                        (bounds[2] - bounds[0]) / float(width),
+                        (bounds[3] - bounds[1]) / float(height)
+                    )
+
+                else:
+                    if not res:
+                        raise click.BadParameter(
+                            'pixel dimensions are required',
+                            ctx, param=res, param_hint='--res')
+
+                    elif len(res) == 1:
+                        res = (res[0], res[0])
+
+                    width = max(int(ceil((bounds[2] - bounds[0]) /
+                                float(res[0]))), 1)
+                    height = max(int(ceil((bounds[3] - bounds[1]) /
+                                 float(res[1]))), 1)
+
+                src_crs = src_crs.upper()
+                if not src_crs.count('EPSG:'):
+                    raise click.BadParameter(
+                        'invalid CRS.  Must be an EPSG code.',
+                        ctx, param=src_crs, param_hint='--src_crs')
+
+                kwargs = {
+                    'count': 1,
+                    'crs': src_crs,
+                    'width': width,
+                    'height': height,
+                    'transform': Affine(res[0], 0, bounds[0], 0, -res[1],
+                                        bounds[3]),
+                    'driver': driver
+                }
+                kwargs.update(**creation_options)
+
+            result = rasterize(
+                geometries,
+                out_shape=(kwargs['height'], kwargs['width']),
+                transform=kwargs.get('affine', kwargs['transform']),
+                all_touched=all_touched,
+                dtype=kwargs.get('dtype', None),
+                default_value=default_value,
+                fill = fill)
+
+            if 'dtype' not in kwargs:
+                kwargs['dtype'] = result.dtype
+
+            kwargs['nodata'] = fill
+
+            with rasterio.open(output, 'w', **kwargs) as out:
+                out.write_band(1, result)
diff --git a/rasterio/rio/sample.py b/rasterio/rio/sample.py
index 16cc0c6..8a96b15 100644
--- a/rasterio/rio/sample.py
+++ b/rasterio/rio/sample.py
@@ -53,6 +53,7 @@ def sample(ctx, files, bidx):
 
     """
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    aws_session = ctx.obj.get('aws_session')
     logger = logging.getLogger('rio')
 
     files = list(files)
@@ -66,7 +67,7 @@ def sample(ctx, files, bidx):
         points = [input]
 
     try:
-        with rasterio.drivers(CPL_DEBUG=verbosity>2):
+        with rasterio.drivers(CPL_DEBUG=verbosity>2), aws_session:
             with rasterio.open(source) as src:
                 if bidx is None:
                     indexes = src.indexes
diff --git a/rasterio/rio/shapes.py b/rasterio/rio/shapes.py
new file mode 100644
index 0000000..45827b3
--- /dev/null
+++ b/rasterio/rio/shapes.py
@@ -0,0 +1,228 @@
+import logging
+import os
+
+import click
+import cligj
+
+from .helpers import coords, write_features
+from . import options
+import rasterio
+from rasterio.transform import Affine
+
+logger = logging.getLogger('rio')
+
+
+# Common options used below
+
+# Unlike the version in cligj, this one doesn't require values.
+files_inout_arg = click.argument(
+    'files',
+    nargs=-1,
+    type=click.Path(resolve_path=True),
+    metavar="INPUTS... OUTPUT")
+
+all_touched_opt = click.option(
+    '-a', '--all', '--all_touched', 'all_touched',
+    is_flag=True,
+    default=False,
+    help='Use all pixels touched by features, otherwise (default) use only '
+         'pixels whose center is within the polygon or that are selected by '
+         'Bresenhams line algorithm')
+
+
+ at click.command(short_help="Write shapes extracted from bands or masks.")
+ at options.file_in_arg
+ at options.output_opt
+ at cligj.precision_opt
+ at cligj.indent_opt
+ at cligj.compact_opt
+ at cligj.projection_geographic_opt
+ at cligj.projection_projected_opt
+ at cligj.sequence_opt
+ at cligj.use_rs_opt
+ at cligj.geojson_type_feature_opt(True)
+ at cligj.geojson_type_bbox_opt(False)
+ at click.option('--band/--mask', default=True,
+              help="Choose to extract from a band (the default) or a mask.")
+ at click.option('--bidx', 'bandidx', type=int, default=None,
+              help="Index of the band or mask that is the source of shapes.")
+ at click.option('--sampling', type=int, default=1,
+              help="Inverse of the sampling fraction; "
+                   "a value of 10 decimates.")
+ at click.option('--with-nodata/--without-nodata', default=False,
+              help="Include or do not include (the default) nodata regions.")
+ at click.option('--as-mask/--not-as-mask', default=False,
+              help="Interpret a band as a mask and output only one class of "
+                   "valid data shapes.")
+ at click.pass_context
+def shapes(
+        ctx, input, output, precision, indent, compact, projection, sequence,
+        use_rs, geojson_type, band, bandidx, sampling, with_nodata, as_mask):
+    """Extracts shapes from one band or mask of a dataset and writes
+    them out as GeoJSON. Unless otherwise specified, the shapes will be
+    transformed to WGS 84 coordinates.
+
+    The default action of this command is to extract shapes from the
+    first band of the input dataset. The shapes are polygons bounding
+    contiguous regions (or features) of the same raster value. This
+    command performs poorly for int16 or float type datasets.
+
+    Bands other than the first can be specified using the `--bidx`
+    option:
+
+      $ rio shapes --bidx 3 tests/data/RGB.byte.tif
+
+    The valid data footprint of a dataset's i-th band can be extracted
+    by using the `--mask` and `--bidx` options:
+
+      $ rio shapes --mask --bidx 1 tests/data/RGB.byte.tif
+
+    Omitting the `--bidx` option results in a footprint extracted from
+    the conjunction of all band masks. This is generally smaller than
+    any individual band's footprint.
+
+    A dataset band may be analyzed as though it were a binary mask with
+    the `--as-mask` option:
+
+      $ rio shapes --as-mask --bidx 1 tests/data/RGB.byte.tif
+    """
+    # These import numpy, which we don't want to do unless it's needed.
+    import numpy
+    import rasterio.features
+    import rasterio.warp
+
+    verbosity = ctx.obj['verbosity'] if ctx.obj else 1
+    logger = logging.getLogger('rio')
+    dump_kwds = {'sort_keys': True}
+    if indent:
+        dump_kwds['indent'] = indent
+    if compact:
+        dump_kwds['separators'] = (',', ':')
+
+    stdout = click.open_file(
+        output, 'w') if output else click.get_text_stream('stdout')
+
+    bidx = 1 if bandidx is None and band else bandidx
+
+    # This is the generator for (feature, bbox) pairs.
+    class Collection(object):
+
+        def __init__(self):
+            self._xs = []
+            self._ys = []
+
+        @property
+        def bbox(self):
+            return min(self._xs), min(self._ys), max(self._xs), max(self._ys)
+
+        def __call__(self):
+            with rasterio.open(input) as src:
+                if bidx is not None and bidx > src.count:
+                    raise ValueError('bidx is out of range for raster')
+
+                img = None
+                msk = None
+
+                # Adjust transforms.
+                transform = src.affine
+                if sampling > 1:
+                    # Decimation of the raster produces a georeferencing
+                    # shift that we correct with a translation.
+                    transform *= Affine.translation(
+                        src.width % sampling, src.height % sampling)
+                    # And follow by scaling.
+                    transform *= Affine.scale(float(sampling))
+
+                # Most of the time, we'll use the valid data mask.
+                # We skip reading it if we're extracting every possible
+                # feature (even invalid data features) from a band.
+                if not band or (band and not as_mask and not with_nodata):
+                    if sampling == 1:
+                        msk = src.read_masks(bidx)
+                    else:
+                        msk_shape = (
+                            src.height//sampling, src.width//sampling)
+                        if bidx is None:
+                            msk = numpy.zeros(
+                                (src.count,) + msk_shape, 'uint8')
+                        else:
+                            msk = numpy.zeros(msk_shape, 'uint8')
+                        msk = src.read_masks(bidx, msk)
+
+                    if bidx is None:
+                        msk = numpy.logical_or.reduce(msk).astype('uint8')
+
+                    # Possibly overridden below.
+                    img = msk
+
+                # Read the band data unless the --mask option is given.
+                if band:
+                    if sampling == 1:
+                        img = src.read(bidx, masked=False)
+                    else:
+                        img = numpy.zeros(
+                            (src.height//sampling, src.width//sampling),
+                            dtype=src.dtypes[src.indexes.index(bidx)])
+                        img = src.read(bidx, img, masked=False)
+
+                # If --as-mask option was given, convert the image
+                # to a binary image. This reduces the number of shape
+                # categories to 2 and likely reduces the number of
+                # shapes.
+                if as_mask:
+                    tmp = numpy.ones_like(img, 'uint8') * 255
+                    tmp[img == 0] = 0
+                    img = tmp
+                    if not with_nodata:
+                        msk = tmp
+
+                # Transform the raster bounds.
+                bounds = src.bounds
+                xs = [bounds[0], bounds[2]]
+                ys = [bounds[1], bounds[3]]
+                if projection == 'geographic':
+                    xs, ys = rasterio.warp.transform(
+                        src.crs, {'init': 'epsg:4326'}, xs, ys)
+                if precision >= 0:
+                    xs = [round(v, precision) for v in xs]
+                    ys = [round(v, precision) for v in ys]
+                self._xs = xs
+                self._ys = ys
+
+                # Prepare keyword arguments for shapes().
+                kwargs = {'transform': transform}
+                if not with_nodata:
+                    kwargs['mask'] = msk
+
+                src_basename = os.path.basename(src.name)
+
+                # Yield GeoJSON features.
+                for i, (g, val) in enumerate(
+                        rasterio.features.shapes(img, **kwargs)):
+                    if projection == 'geographic':
+                        g = rasterio.warp.transform_geom(
+                            src.crs, 'EPSG:4326', g,
+                            antimeridian_cutting=True, precision=precision)
+                    xs, ys = zip(*coords(g))
+                    yield {
+                        'type': 'Feature',
+                        'id': "{0}:{1}".format(src_basename, i),
+                        'properties': {
+                            'val': val, 'filename': src_basename
+                        },
+                        'bbox': [min(xs), min(ys), max(xs), max(ys)],
+                        'geometry': g
+                    }
+
+    if not sequence:
+        geojson_type = 'collection'
+
+    try:
+        with rasterio.drivers(CPL_DEBUG=(verbosity > 2)):
+            write_features(
+                stdout, Collection(), sequence=sequence,
+                geojson_type=geojson_type, use_rs=use_rs,
+                **dump_kwds)
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
diff --git a/rasterio/rio/bands.py b/rasterio/rio/stack.py
similarity index 98%
rename from rasterio/rio/bands.py
rename to rasterio/rio/stack.py
index 53c3dae..f30d576 100644
--- a/rasterio/rio/bands.py
+++ b/rasterio/rio/stack.py
@@ -1,3 +1,4 @@
+"""Commands for operating on bands of datasets."""
 import logging
 
 import click
diff --git a/rasterio/rio/transform.py b/rasterio/rio/transform.py
new file mode 100644
index 0000000..bae7e5f
--- /dev/null
+++ b/rasterio/rio/transform.py
@@ -0,0 +1,60 @@
+"""Fetch and edit raster dataset metadata from the command line."""
+
+import json
+import logging
+import os
+
+import click
+from cligj import precision_opt
+
+import rasterio
+
+
+ at click.command(short_help="Transform coordinates.")
+ at click.argument('INPUT', default='-', required=False)
+ at click.option('--src-crs', '--src_crs', default='EPSG:4326',
+              help="Source CRS.")
+ at click.option('--dst-crs', '--dst_crs', default='EPSG:4326',
+              help="Destination CRS.")
+ at precision_opt
+ at click.pass_context
+def transform(ctx, input, src_crs, dst_crs, precision):
+    import rasterio.warp
+
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
+    logger = logging.getLogger('rio')
+
+    # Handle the case of file, stream, or string input.
+    try:
+        src = click.open_file(input).readlines()
+    except IOError:
+        src = [input]
+
+    try:
+        with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+            if src_crs.startswith('EPSG'):
+                src_crs = {'init': src_crs}
+            elif os.path.exists(src_crs):
+                with rasterio.open(src_crs) as f:
+                    src_crs = f.crs
+            if dst_crs.startswith('EPSG'):
+                dst_crs = {'init': dst_crs}
+            elif os.path.exists(dst_crs):
+                with rasterio.open(dst_crs) as f:
+                    dst_crs = f.crs
+            for line in src:
+                coords = json.loads(line)
+                xs = coords[::2]
+                ys = coords[1::2]
+                xs, ys = rasterio.warp.transform(src_crs, dst_crs, xs, ys)
+                if precision >= 0:
+                    xs = [round(v, precision) for v in xs]
+                    ys = [round(v, precision) for v in ys]
+                result = [0]*len(coords)
+                result[::2] = xs
+                result[1::2] = ys
+                print(json.dumps(result))
+
+    except Exception:
+        logger.exception("Exception caught during processing")
+        raise click.Abort()
diff --git a/rasterio/rio/warp.py b/rasterio/rio/warp.py
index b65f8e8..8e13ec0 100644
--- a/rasterio/rio/warp.py
+++ b/rasterio/rio/warp.py
@@ -9,6 +9,7 @@ from .helpers import resolve_inout
 from . import options
 import rasterio
 from rasterio import crs
+from rasterio.errors import CRSError
 from rasterio.transform import Affine
 from rasterio.warp import (reproject, Resampling, calculate_default_transform,
    transform_bounds)
@@ -159,16 +160,20 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
             elif dst_crs:
                 try:
                     dst_crs = crs.from_string(dst_crs)
-                except ValueError:
-                    raise click.BadParameter("invalid crs format.",
-                                             param=dst_crs, param_hint=dst_crs)
+                except ValueError as err:
+                    raise click.BadParameter(
+                        str(err), param='dst_crs', param_hint='dst_crs')
 
                 if dimensions:
                     # Calculate resolution appropriate for dimensions
                     # in target.
                     dst_width, dst_height = dimensions
-                    xmin, ymin, xmax, ymax = transform_bounds(src.crs, dst_crs,
-                                                              *src.bounds)
+                    try:
+                        xmin, ymin, xmax, ymax = transform_bounds(
+                            src.crs, dst_crs, *src.bounds)
+                    except CRSError as err:
+                        raise click.BadParameter(
+                            str(err), param='dst_crs', param_hint='dst_crs')
                     dst_transform = Affine(
                         (xmax - xmin) / float(dst_width),
                         0, xmin, 0,
@@ -183,8 +188,13 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
                             param='res', param_hint='res')
 
                     if src_bounds:
-                        xmin, ymin, xmax, ymax = transform_bounds(
-                            src.crs, dst_crs, *src_bounds)
+                        try:
+                            xmin, ymin, xmax, ymax = transform_bounds(
+                                src.crs, dst_crs, *src_bounds)
+                        except CRSError as err:
+                            raise click.BadParameter(
+                                str(err), param='dst_crs',
+                                param_hint='dst_crs')
                     else:
                         xmin, ymin, xmax, ymax = dst_bounds
 
@@ -193,10 +203,13 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
                     dst_height = max(int(ceil((ymax - ymin) / res[1])), 1)
 
                 else:
-                    dst_transform, dst_width, dst_height = calculate_default_transform(
-                        src.crs, dst_crs, src.width, src.height, *src.bounds,
-                        resolution=res)
-
+                    try:
+                        dst_transform, dst_width, dst_height = calculate_default_transform(
+                            src.crs, dst_crs, src.width, src.height,
+                            *src.bounds, resolution=res)
+                    except CRSError as err:
+                        raise click.BadParameter(
+                            str(err), param='dst_crs', param_hint='dst_crs')
             elif dimensions:
                 # Same projection, different dimensions, calculate resolution.
                 dst_crs = src.crs
diff --git a/rasterio/tool.py b/rasterio/tool.py
index ebd3a04..fdf8639 100644
--- a/rasterio/tool.py
+++ b/rasterio/tool.py
@@ -1,174 +1,29 @@
 """
-Implementations of various common operations, like `show()` for displaying an
-array or with matplotlib, and `stats()` for computing min/max/avg.  Most can
-handle a numpy array or `rasterio.Band()`.  Primarily supports `$ rio insp`.
+DEPRECATED; To be removed in 1.0
 """
-
-
 from __future__ import absolute_import
+import rasterio.rio.insp
 
-import code
-import collections
-import logging
-import warnings
-
-try:
-    import matplotlib.pyplot as plt
-except ImportError:
-    plt = None
-except RuntimeError as e:
-    # Certain environment configurations can trigger a RuntimeError like:
-
-    # Trying to import matplotlibRuntimeError: Python is not installed as a
-    # framework. The Mac OS X backend will not be able to function correctly
-    # if Python is not installed as a framework. See the Python ...
-    warnings.warn(str(e), RuntimeWarning, stacklevel=2)
-    plt = None
-
-import numpy
-
-import rasterio
-from rasterio.five import zip_longest
-
-
-logger = logging.getLogger('rasterio')
-
-Stats = collections.namedtuple('Stats', ['min', 'max', 'mean'])
-
-# Collect dictionary of functions for use in the interpreter in main()
-funcs = locals()
-
-
-def show(source, cmap='gray', with_bounds=True):
-    """
-    Display a raster or raster band using matplotlib.
-
-    Parameters
-    ----------
-    source : array-like or (raster dataset, bidx)
-        If array-like, should be of format compatible with
-        matplotlib.pyplot.imshow. If the tuple (raster dataset, bidx),
-        selects band `bidx` from raster.
-    cmap : str (opt)
-        Specifies the colormap to use in plotting. See
-        matplotlib.Colors.Colormap. Default is 'gray'.
-    with_bounds : bool (opt)
-        Whether to change the image extent to the spatial bounds of the image,
-        rather than pixel coordinates. Only works when source is
-        (raster dataset, bidx).
-    """
-
-    if isinstance(source, tuple):
-        arr = source[0].read(source[1])
-        xs = source[0].res[0] / 2.
-        ys = source[0].res[1] / 2.
-        if with_bounds:
-            extent = (source[0].bounds.left - xs, source[0].bounds.right - xs,
-                      source[0].bounds.bottom - ys, source[0].bounds.top - ys)
-        else:
-            extent = None
-    else:
-        arr = source
-        extent = None
-    if plt is not None:
-        imax = plt.imshow(arr, cmap=cmap, extent=extent)
-        fig = plt.gcf()
-        fig.show()
-    else:
-        raise ImportError("matplotlib could not be imported")
-
-
-def stats(source):
-    """Return a tuple with raster min, max, and mean.
-    """
-    if isinstance(source, tuple):
-        arr = source[0].read(source[1])
-    else:
-        arr = source
-    return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
-
-
-def show_hist(source, bins=10, masked=True, title='Histogram'):
-
-    """
-    Easily display a histogram with matplotlib.
-
-    Parameters
-    ----------
-    bins : int, optional
-        Compute histogram across N bins.
-    data : np.array or rasterio.Band or tuple(dataset, bidx)
-        Input data to display.  The first three arrays in multi-dimensional
-        arrays are plotted as red, green, and blue.
-    masked : bool, optional
-        When working with a `rasterio.Band()` object, specifies if the data
-        should be masked on read.
-    title : str, optional
-        Title for the figure.
-    """
-
-    if plt is None:
-        raise ImportError("Could not import matplotlib")
-
-    if isinstance(source, (tuple, rasterio.Band)):
-        arr = source[0].read(source[1], masked=masked)
-    else:
-        arr = source
-
-    # The histogram is computed individually for each 'band' in the array
-    # so we need the overall min/max to constrain the plot
-    rng = arr.min(), arr.max()
-
-    if len(arr.shape) is 2:
-        arr = [arr]
-        colors = ['gold']
-    else:
-        colors = ('red', 'green', 'blue', 'violet', 'gold', 'saddlebrown')
 
-    # If a rasterio.Band() is given make sure the proper index is displayed
-    # in the legend.
-    if isinstance(source, (tuple, rasterio.Band)):
-        labels = [str(source[1])]
-    else:
-        labels = (str(i + 1) for i in range(len(arr)))
+def show(*args, **kwargs):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.rio.insp instead", DeprecationWarning)
+    return rasterio.rio.insp.show(*args, **kwargs)
 
-    # This loop should add a single plot each band in the input array,
-    # regardless of if the number of bands exceeds the number of colors.
-    # The colors slicing ensures that the number of iterations always
-    # matches the number of bands.
-    # The goal is to provide a curated set of colors for working with
-    # smaller datasets and let matplotlib define additional colors when
-    # working with larger datasets.
-    for bnd, color, label in zip_longest(arr, colors[:len(arr)], labels):
 
-        plt.hist(
-            bnd.flatten(),
-            bins=bins,
-            alpha=0.5,
-            color=color,
-            label=label,
-            range=rng
-        )
+def stats(*args, **kwargs):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.rio.insp instead", DeprecationWarning)
+    return rasterio.rio.insp.stats(*args, **kwargs)
 
-    plt.legend(loc="upper right")
-    plt.title(title, fontweight='bold')
-    plt.grid(True)
-    plt.xlabel('DN')
-    plt.ylabel('Frequency')
-    fig = plt.gcf()
-    fig.show()
 
+def show_hist(*args, **kwargs):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.rio.insp instead", DeprecationWarning)
+    return rasterio.rio.insp.show_hist(*args, **kwargs)
 
-def main(banner, dataset, alt_interpreter=None):
-    """ Main entry point for use with python interpreter """
-    local = dict(funcs, src=dataset, np=numpy, rio=rasterio, plt=plt)
-    if not alt_interpreter:
-        code.interact(banner, local=local)
-    elif alt_interpreter == 'ipython':
-        import IPython
-        IPython.InteractiveShell.banner1 = banner
-        IPython.start_ipython(argv=[], user_ns=local)
-    else:
-        raise ValueError("Unsupported interpreter '%s'" % alt_interpreter)
 
-    return 0
+def main(*args, **kwargs):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.rio.insp instead", DeprecationWarning)
+    return rasterio.rio.insp.main(*args, **kwargs)
diff --git a/rasterio/tools/mask.py b/rasterio/tools/mask.py
index ec5423c..d8a05a4 100644
--- a/rasterio/tools/mask.py
+++ b/rasterio/tools/mask.py
@@ -1,92 +1,12 @@
+"""
+DEPRECATED; To be removed in 1.0
+"""
 from __future__ import absolute_import
 
-import warnings
+import rasterio.mask
 
-import rasterio
-from rasterio.features import geometry_mask
 
-
-def mask(raster, shapes, nodata=None, crop=False, all_touched=False,
-         invert=False):
-    """
-    For all regions in the input raster outside of the regions defined by
-    `shapes`, sets any data present to nodata.
-
-    Parameters
-    ----------
-    raster: rasterio RasterReader object
-        Raster to which the mask will be applied.
-    shapes: list of polygons
-        Polygons are GeoJSON-like dicts specifying the boundaries of features
-        in the raster to be kept. All data outside of specified polygons
-        will be set to nodata.
-    nodata: int or float (opt)
-        Value representing nodata within each raster band. If not set,
-        defaults to the nodata value for the input raster. If there is no
-        set nodata value for the raster, it defaults to 0.
-    crop: bool (opt)
-        Whether to crop the raster to the extent of the data. Defaults to
-        False.
-    all_touched: bool (opt)
-        Use all pixels touched by features. If False (default), use only
-        pixels whose center is within the polygon or that are selected by
-        Bresenhams line algorithm.
-    invert: bool (opt)
-        If True, mask will be True for pixels that overlap shapes.
-        False by default.
-
-    Returns
-    -------
-    masked: numpy ndarray
-        Data contained in raster after applying the mask.
-    out_transform: affine object
-        Information for mapping pixel coordinates in `masked` to another
-        coordinate system.
-    """
-
-    if crop and invert:
-        raise ValueError("crop and invert cannot both be True.")
-    if nodata is None:
-        if raster.nodata is not None:
-            nodata = raster.nodata
-        else:
-            nodata = 0
-
-    all_bounds = [rasterio.features.bounds(shape) for shape in shapes]
-    minxs, minys, maxxs, maxys = zip(*all_bounds)
-    mask_bounds = (min(minxs), min(minys), max(maxxs), max(maxys))
-
-    invert_y = raster.affine.e > 0
-    source_bounds = raster.bounds
-    if invert_y:
-        source_bounds = [source_bounds[0], source_bounds[3],
-                         source_bounds[2], source_bounds[1]]
-    if rasterio.coords.disjoint_bounds(source_bounds, mask_bounds):
-        if crop:
-            raise ValueError("Input shapes do not overlap raster.")
-        else:
-            warnings.warn("GeoJSON outside bounds of existing output " +
-                          "raster. Are they in different coordinate " +
-                          "reference systems?")
-    if invert_y:
-        mask_bounds = [mask_bounds[0], mask_bounds[3],
-                       mask_bounds[2], mask_bounds[1]]
-    if crop:
-        window = raster.window(*mask_bounds)
-        out_transform = raster.window_transform(window)
-    else:
-        window = None
-        out_transform = raster.affine
-
-    out_image = raster.read(window=window, masked=True)
-    out_shape = out_image.shape[1:]
-
-    shape_mask = geometry_mask(shapes, transform=out_transform, invert=invert,
-                               out_shape=out_shape, all_touched=all_touched)
-    out_image.mask = out_image.mask | shape_mask
-    out_image.fill_value = nodata
-
-    for i in range(raster.count):
-        out_image[i] = out_image[i].filled(nodata)
-
-    return out_image, out_transform
+def mask(*args, **kwargs):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.mask instead", DeprecationWarning)
+    return rasterio.mask.mask(*args, **kwargs)
diff --git a/rasterio/tools/merge.py b/rasterio/tools/merge.py
index b073183..21d28da 100644
--- a/rasterio/tools/merge.py
+++ b/rasterio/tools/merge.py
@@ -1,166 +1,12 @@
+"""
+DEPRECATED; To be removed in 1.0
+"""
 from __future__ import absolute_import
 
-import logging
-import math
-import warnings
+import rasterio.merge
 
-import numpy as np
 
-import rasterio
-from rasterio._base import get_index, get_window
-from rasterio.transform import Affine
-
-
-logger = logging.getLogger('rasterio')
-
-
-def merge(sources, bounds=None, res=None, nodata=None, precision=7):
-    """Copy valid pixels from input files to an output file.
-
-    All files must have the same number of bands, data type, and
-    coordinate reference system.
-
-    Input files are merged in their listed order using the reverse
-    painter's algorithm. If the output file exists, its values will be
-    overwritten by input values.
-
-    Geospatial bounds and resolution of a new output file in the
-    units of the input file coordinate reference system may be provided
-    and are otherwise taken from the first input file.
-
-    Parameters
-    ----------
-    sources: list of source datasets 
-        Open rasterio RasterReader objects to be merged.
-    bounds: tuple, optional 
-        Bounds of the output image (left, bottom, right, top).
-        If not set, bounds are determined from bounds of input rasters. 
-    res: tuple, optional 
-        Output resolution in units of coordinate reference system. If not set,
-        the resolution of the first raster is used. If a single value is passed,
-        output pixels will be square.
-    nodata: float, optional
-        nodata value to use in output file. If not set, uses the nodata value
-        in the first input raster. 
-
-    Returns
-    -------
-    dest: numpy ndarray
-        Contents of all input rasters in single array.
-    out_transform: affine object
-        Information for mapping pixel coordinates in `dest` to another
-        coordinate system
-    """
-
-    first = sources[0]
-    first_res = first.res
-    nodataval = first.nodatavals[0]
-    dtype = first.dtypes[0]
-
-    # Extent from option or extent of all inputs.
-    if bounds:
-        dst_w, dst_s, dst_e, dst_n = bounds
-    else:
-        # scan input files.
-        xs = []
-        ys = []
-        for src in sources:
-           left, bottom, right, top = src.bounds
-           xs.extend([left, right])
-           ys.extend([bottom, top])
-        dst_w, dst_s, dst_e, dst_n = min(xs), min(ys), max(xs), max(ys)
-    
-    logger.debug("Output bounds: %r", (dst_w, dst_s, dst_e, dst_n))
-    output_transform = Affine.translation(dst_w, dst_n)
-    logger.debug("Output transform, before scaling: %r", output_transform)
-
-    # Resolution/pixel size.
-    if not res:
-        res = first_res
-    elif not np.iterable(res):
-        res = (res, res)
-    elif len(res) == 1:
-        res = (res[0], res[0])
-    output_transform *= Affine.scale(res[0], -res[1])
-    logger.debug("Output transform, after scaling: %r", output_transform)
-
-    # Compute output array shape. We guarantee it will cover the output
-    # bounds completely.
-    output_width = int(math.ceil((dst_e - dst_w) / res[0]))
-    output_height = int(math.ceil((dst_n - dst_s) / res[1]))
-
-    # Adjust bounds to fit.
-    dst_e, dst_s = output_transform * (output_width, output_height)
-    logger.debug("Output width: %d, height: %d", output_width, output_height)
-    logger.debug("Adjusted bounds: %r", (dst_w, dst_s, dst_e, dst_n))
-
-    # create destination array
-    dest = np.zeros((first.count, output_height, output_width), dtype=dtype)
-
-    if nodata is not None:
-        nodataval = nodata
-        logger.debug("Set nodataval: %r", nodataval)
-
-    if nodataval is not None:
-        # Only fill if the nodataval is within dtype's range.
-        inrange = False
-        if np.dtype(dtype).kind in ('i', 'u'):
-            info = np.iinfo(dtype)
-            inrange = (info.min <= nodataval <= info.max)
-        elif np.dtype(dtype).kind == 'f':
-            info = np.finfo(dtype)
-            inrange = (info.min <= nodataval <= info.max)
-        if inrange:
-            dest.fill(nodataval)
-        else:
-            warnings.warn(
-                "Input file's nodata value, %s, is beyond the valid "
-                "range of its data type, %s. Consider overriding it "
-                "using the --nodata option for better results." % (
-                    nodataval, dtype))
-    else:
-        nodataval = 0
-
-    for src in sources:
-        # Real World (tm) use of boundless reads.
-        # This approach uses the maximum amount of memory to solve the problem.
-        # Making it more efficient is a TODO.
-
-        # 1. Compute spatial intersection of destination and source.
-        src_w, src_s, src_e, src_n = src.bounds
-
-        int_w = src_w if src_w > dst_w else dst_w
-        int_s = src_s if src_s > dst_s else dst_s
-        int_e = src_e if src_e < dst_e else dst_e
-        int_n = src_n if src_n < dst_n else dst_n
-
-        # 2. Compute the source window.
-        src_window = get_window(
-            int_w, int_s, int_e, int_n, src.affine, precision=precision)
-        logger.debug("Src %s window: %r", src.name, src_window)
-
-        # 3. Compute the destination window.
-        dst_window = get_window(
-            int_w, int_s, int_e, int_n, output_transform, precision=precision)
-        logger.debug("Dst window: %r", dst_window)
-
-        # 4. Initialize temp array.
-        tcount = first.count
-        trows, tcols = tuple(b - a for a, b in dst_window)
-
-        temp_shape = (tcount, trows, tcols)
-        logger.debug("Temp shape: %r", temp_shape)
-
-        temp = np.zeros(temp_shape, dtype=dtype)
-        temp = src.read(out=temp, window=src_window, boundless=False,
-                        masked=True)
-
-        # 5. Copy elements of temp into dest.
-        roff, coff = dst_window[0][0], dst_window[1][0]
-
-        region = dest[:, roff:roff + trows, coff:coff + tcols]
-        np.copyto(
-            region, temp,
-            where=np.logical_and(region==nodataval, temp.mask==False))
-
-    return dest, output_transform
+def merge(*args, **kwargs):
+    import warnings
+    warnings.warn("Deprecated; Use rasterio.merge instead", DeprecationWarning)
+    return rasterio.merge.merge(*args, **kwargs)
diff --git a/rasterio/vfs.py b/rasterio/vfs.py
index 4af1faf..acaf555 100644
--- a/rasterio/vfs.py
+++ b/rasterio/vfs.py
@@ -6,7 +6,8 @@ import os
 # NB: As not to propagate fallacies of distributed computing, Rasterio
 # does not support HTTP or FTP URLs via GDAL's vsicurl handler. Only
 # the following local filesystem schemes are supported.
-SCHEMES = ['gzip', 'zip', 'tar']
+SCHEMES = {'gzip': 'gzip', 'zip': 'zip', 'tar': 'tar', 'https': 'curl',
+        'http': 'curl', 's3': 's3'}
 
 
 def parse_path(path, vfs=None):
@@ -35,9 +36,14 @@ def vsi_path(path, archive=None, scheme=None):
     """Convert a parsed path to a GDAL VSI path."""
     # If a VSF and archive file are specified, we convert the path to
     # a GDAL VSI path (see cpl_vsi.h).
-    if scheme and scheme != 'file':
+    if scheme and scheme.startswith('http'):
+        result = "/vsicurl/{0}://{1}".format(scheme, path)
+    elif scheme and scheme == 's3':
+        result = "/vsis3/{0}".format(path)
+    elif scheme and scheme != 'file':
         path = path.strip(os.path.sep)
-        result = os.path.sep.join(['/vsi{0}'.format(scheme), archive, path])
+        result = os.path.sep.join(
+            ['/vsi{0}'.format(scheme), archive, path])
     else:
         result = path
     return result
diff --git a/rasterio/warp.py b/rasterio/warp.py
index ed9bdc7..81bfc9a 100644
--- a/rasterio/warp.py
+++ b/rasterio/warp.py
@@ -80,6 +80,7 @@ def transform_geom(
 
     Returns
     ---------
+
     out: GeoJSON like dict object
         Transformed geometry in GeoJSON dict format
     """
diff --git a/rasterio/windows.py b/rasterio/windows.py
new file mode 100644
index 0000000..0950452
--- /dev/null
+++ b/rasterio/windows.py
@@ -0,0 +1,79 @@
+
+def get_data_window(arr, nodata=None):
+    """
+    Returns a window for the non-nodata pixels within the input array.
+
+    Parameters
+    ----------
+    arr: numpy ndarray, <= 3 dimensions
+    nodata: number
+        If None, will either return a full window if arr is not a masked
+        array, or will use the mask to determine non-nodata pixels.
+        If provided, it must be a number within the valid range of the dtype
+        of the input array.
+
+    Returns
+    -------
+    ((row_start, row_stop), (col_start, col_stop))
+
+    """
+
+    from rasterio._io import get_data_window
+    return get_data_window(arr, nodata)
+
+
+def union(windows):
+    """
+    Union windows and return the outermost extent they cover.
+
+    Parameters
+    ----------
+    windows: list-like of window objects
+        ((row_start, row_stop), (col_start, col_stop))
+
+    Returns
+    -------
+    ((row_start, row_stop), (col_start, col_stop))
+    """
+
+    from rasterio._io import window_union
+    return window_union(windows)
+
+
+def intersection(windows):
+    """
+    Intersect windows and return the innermost extent they cover.
+
+    Will raise ValueError if windows do not intersect.
+
+    Parameters
+    ----------
+    windows: list-like of window objects
+        ((row_start, row_stop), (col_start, col_stop))
+
+    Returns
+    -------
+    ((row_start, row_stop), (col_start, col_stop))
+    """
+
+    from rasterio._io import window_intersection
+    return window_intersection(windows)
+
+
+def intersect(windows):
+    """
+    Test if windows intersect.
+
+    Parameters
+    ----------
+    windows: list-like of window objects
+        ((row_start, row_stop), (col_start, col_stop))
+
+    Returns
+    -------
+    boolean:
+        True if all windows intersect.
+    """
+
+    from rasterio._io import windows_intersect
+    return windows_intersect(windows)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 925b579..6a13aa3 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -9,3 +9,6 @@ pytest>=2.8.2
 pytest-cov>=2.2.0
 setuptools>=0.9.8
 wheel
+sphinx
+sphinx-rtd-theme
+numpydoc
diff --git a/scripts/travis_gdal_install.sh b/scripts/travis_gdal_install.sh
index 022dfd8..a7791e6 100644
--- a/scripts/travis_gdal_install.sh
+++ b/scripts/travis_gdal_install.sh
@@ -17,13 +17,13 @@ GDALOPTS="  --with-ogr \
             --without-cfitsio \
             --without-pcraster \
             --without-netcdf \
-            --without-png \
+            --with-png \
             --with-jpeg=internal \
             --without-gif \
             --without-ogdi \
             --without-fme \
             --without-hdf4 \
-            --without-hdf5 \
+            --with-hdf5 \
             --without-jasper \
             --without-ecw \
             --without-kakadu \
@@ -35,7 +35,7 @@ GDALOPTS="  --with-ogr \
             --without-ingres \
             --without-xerces \
             --without-odbc \
-            --without-curl \
+            --with-curl \
             --without-sqlite3 \
             --without-dwgdirect \
             --without-idb \
@@ -43,7 +43,8 @@ GDALOPTS="  --with-ogr \
             --without-perl \
             --without-php \
             --without-ruby \
-            --without-python"
+            --without-python \
+            --with-static-proj4=/usr/lib"
 
 # Create build dir if not exists
 if [ ! -d "$GDALBUILD" ]; then
@@ -67,22 +68,36 @@ if [ ! -d $GDALINST/gdal-1.9.2 ]; then
   make install
 fi
 
-if [ ! -d $GDALINST/gdal-1.11.2 ]; then
+if [ ! -d $GDALINST/gdal-1.11.4 ]; then
   cd $GDALBUILD
-  wget http://download.osgeo.org/gdal/1.11.2/gdal-1.11.2.tar.gz
-  tar -xzf gdal-1.11.2.tar.gz
-  cd gdal-1.11.2
-  ./configure --prefix=$GDALINST/gdal-1.11.2 $GDALOPTS
+  wget http://download.osgeo.org/gdal/1.11.4/gdal-1.11.4.tar.gz
+  tar -xzf gdal-1.11.4.tar.gz
+  cd gdal-1.11.4
+  ./configure --prefix=$GDALINST/gdal-1.11.4 $GDALOPTS
   make -s -j 2
   make install
 fi
 
-if [ ! -d $GDALINST/gdal-2.0.1 ]; then
+if [ ! -d $GDALINST/gdal-2.0.2 ]; then
   cd $GDALBUILD
-  wget http://download.osgeo.org/gdal/2.0.1/gdal-2.0.1.tar.gz
-  tar -xzf gdal-2.0.1.tar.gz
-  cd gdal-2.0.1
-  ./configure --prefix=$GDALINST/gdal-2.0.1 $GDALOPTS
+  wget http://download.osgeo.org/gdal/2.0.2/gdal-2.0.2.tar.gz
+  tar -xzf gdal-2.0.2.tar.gz
+  cd gdal-2.0.2
+  ./configure --prefix=$GDALINST/gdal-2.0.2 $GDALOPTS
+  make -s -j 2
+  make install
+fi
+
+
+if [ ! -d $GDALINST/gdal-2.1.0 ]; then
+  cd $GDALBUILD
+  #
+  # TODO Use official release, for now use a copy of GDAL daily from mar 28
+  #
+  wget http://download.osgeo.org/gdal/2.1.0beta1/gdal-2.1.0beta1.tar.gz
+  tar -xzf gdal-2.1.0beta1.tar.gz
+  cd gdal-2.1.0beta1
+  ./configure --prefix=$GDALINST/gdal-2.1.0 $GDALOPTS
   make -s -j 2
   make install
 fi
diff --git a/setup.py b/setup.py
index 60789fd..9427592 100755
--- a/setup.py
+++ b/setup.py
@@ -239,30 +239,31 @@ setup_args = dict(
         rio=rasterio.rio.main:main_group
 
         [rasterio.rio_commands]
-        bounds=rasterio.rio.features:bounds
+        bounds=rasterio.rio.bounds:bounds
         calc=rasterio.rio.calc:calc
-        clip=rasterio.rio.convert:clip
+        clip=rasterio.rio.clip:clip
         convert=rasterio.rio.convert:convert
-        edit-info=rasterio.rio.info:edit
-        env=rasterio.rio.info:env
+        edit-info=rasterio.rio.edit_info:edit
+        env=rasterio.rio.env:env
         info=rasterio.rio.info:info
-        insp=rasterio.rio.info:insp
-        mask=rasterio.rio.features:mask
+        insp=rasterio.rio.insp:insp
+        mask=rasterio.rio.mask:mask
         merge=rasterio.rio.merge:merge
         overview=rasterio.rio.overview:overview
-        rasterize=rasterio.rio.features:rasterize
+        rasterize=rasterio.rio.rasterize:rasterize
         sample=rasterio.rio.sample:sample
-        shapes=rasterio.rio.features:shapes
-        stack=rasterio.rio.bands:stack
+        shapes=rasterio.rio.shapes:shapes
+        stack=rasterio.rio.stack:stack
         warp=rasterio.rio.warp:warp
-        transform=rasterio.rio.info:transform
+        transform=rasterio.rio.transform:transform
     ''',
     include_package_data=True,
     ext_modules=ext_modules,
     zip_safe=False,
     install_requires=inst_reqs,
     extras_require={
-        'ipython': ['ipython>=2.0']})
+        'ipython': ['ipython>=2.0'],
+        's3': ['boto3']})
 
 if os.environ.get('PACKAGE_DATA'):
     setup_args['package_data'] = {'rasterio': ['gdal_data/*', 'proj_data/*']}
diff --git a/tests/data/box.cpg b/tests/data/box.cpg
new file mode 100644
index 0000000..cd89cb9
--- /dev/null
+++ b/tests/data/box.cpg
@@ -0,0 +1 @@
+ISO-8859-1
\ No newline at end of file
diff --git a/tests/data/box.dbf b/tests/data/box.dbf
new file mode 100644
index 0000000..54a92a7
Binary files /dev/null and b/tests/data/box.dbf differ
diff --git a/tests/data/box.prj b/tests/data/box.prj
new file mode 100644
index 0000000..1f84195
--- /dev/null
+++ b/tests/data/box.prj
@@ -0,0 +1 @@
+PROJCS["WGS_1984_UTM_Zone_18N",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]]
\ No newline at end of file
diff --git a/tests/data/box.shp b/tests/data/box.shp
new file mode 100644
index 0000000..5961520
Binary files /dev/null and b/tests/data/box.shp differ
diff --git a/tests/data/box.shx b/tests/data/box.shx
new file mode 100644
index 0000000..a33a6c6
Binary files /dev/null and b/tests/data/box.shx differ
diff --git a/tests/test_aws.py b/tests/test_aws.py
new file mode 100644
index 0000000..6dbed62
--- /dev/null
+++ b/tests/test_aws.py
@@ -0,0 +1,69 @@
+import logging
+import os
+import sys
+
+import pytest
+
+import rasterio
+from rasterio.aws import Session
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_session():
+    """Create a session with arguments."""
+    s = Session(aws_access_key_id='id', aws_secret_access_key='key',
+                 aws_session_token='token', region_name='null-island-1')
+    assert s._creds.access_key == 'id'
+    assert s._creds.secret_key == 'key'
+    assert s._creds.token == 'token'
+    assert s._session.region_name == 'null-island-1'
+
+
+def test_session_env(monkeypatch):
+    """Create a session with env vars."""
+    monkeypatch.setenv('AWS_ACCESS_KEY_ID', 'id')
+    monkeypatch.setenv('AWS_SECRET_ACCESS_KEY', 'key')
+    monkeypatch.setenv('AWS_SESSION_TOKEN', 'token')
+    s = Session()
+    assert s._creds.access_key == 'id'
+    assert s._creds.secret_key == 'key'
+    assert s._creds.token == 'token'
+    monkeypatch.undo()
+
+
+ at pytest.mark.xfail(
+    (not(os.environ.get('GDALVERSION', '2.1').startswith('2.1')) or
+        'AWS_ACCESS_KEY_ID' not in os.environ or
+        'AWS_SECRET_ACCESS_KEY' not in os.environ),
+    reason="S3 raster access requires GDAL 2.1")
+def test_with_session():
+    """Enter and exit a session."""
+    with Session() as s:
+        with rasterio.open("s3://landsat-pds/L8/139/045/LC81390452014295LGN00/LC81390452014295LGN00_B1.TIF") as f:
+            assert f.count == 1
+
+
+ at pytest.mark.xfail(
+    (not(os.environ.get('GDALVERSION', '2.1').startswith('2.1')) or
+        'AWS_ACCESS_KEY_ID' not in os.environ or
+        'AWS_SECRET_ACCESS_KEY' not in os.environ),
+    reason="S3 raster access requires GDAL 2.1")
+def test_open_with_session():
+    """Enter and exit a session."""
+    s = Session()
+    with s.open("s3://landsat-pds/L8/139/045/LC81390452014295LGN00/LC81390452014295LGN00_B1.TIF") as f:
+        assert f.count == 1
+
+
+ at pytest.mark.xfail(
+    (not(os.environ.get('GDALVERSION', '2.1').startswith('2.1')) or
+        'AWS_ACCESS_KEY_ID' not in os.environ or
+        'AWS_SECRET_ACCESS_KEY' not in os.environ),
+    reason="S3 raster access requires GDAL 2.1")
+def test_open_with_session_minus_mode():
+    """Enter and exit a session, reading in 'r-' mode"""
+    s = Session()
+    with s.open("s3://landsat-pds/L8/139/045/LC81390452014295LGN00/LC81390452014295LGN00_B1.TIF", 'r-') as f:
+        assert f.count == 1
diff --git a/tests/test_colorinterp.py b/tests/test_colorinterp.py
index d05e8a2..84d843f 100644
--- a/tests/test_colorinterp.py
+++ b/tests/test_colorinterp.py
@@ -1,25 +1,52 @@
+import pytest
 
 import rasterio
-from rasterio.enums import ColorInterp
+from rasterio.enums import ColorInterp, PhotometricInterp
 
 
-def test_colorinterp(tmpdir):
-    
+def test_cmyk_interp(tmpdir):
+    """A CMYK TIFF has cyan, magenta, yellow, black bands."""
     with rasterio.drivers():
-
         with rasterio.open('tests/data/RGB.byte.tif') as src:
-            assert src.colorinterp(1) == ColorInterp.red
-            assert src.colorinterp(2) == ColorInterp.green
-            assert src.colorinterp(3) == ColorInterp.blue
-            
-        tiffname = str(tmpdir.join('foo.tif'))
-        
-        meta = src.meta
+            meta = src.meta
         meta['photometric'] = 'CMYK'
         meta['count'] = 4
+        tiffname = str(tmpdir.join('foo.tif'))
         with rasterio.open(tiffname, 'w', **meta) as dst:
+            assert dst.profile['photometric'] == 'cmyk'
             assert dst.colorinterp(1) == ColorInterp.cyan
             assert dst.colorinterp(2) == ColorInterp.magenta
             assert dst.colorinterp(3) == ColorInterp.yellow
             assert dst.colorinterp(4) == ColorInterp.black
 
+
+def test_ycbcr_interp(tmpdir):
+    """A YCbCr TIFF has red, green, blue bands."""
+    with rasterio.drivers():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            meta = src.meta
+        meta['photometric'] = 'ycbcr'
+        meta['compress'] = 'jpeg'
+        meta['count'] = 3
+        tiffname = str(tmpdir.join('foo.tif'))
+        with rasterio.open(tiffname, 'w', **meta) as dst:
+            assert dst.colorinterp(1) == ColorInterp.red
+            assert dst.colorinterp(2) == ColorInterp.green
+            assert dst.colorinterp(3) == ColorInterp.blue
+
+
+ at pytest.mark.xfail()
+def test_ycbcr_no_convert(tmpdir):
+    """An unconverted YCbCr TIFF has Y, Cb, Cr bands."""
+    with rasterio.drivers(GDAL_JPEG_TO_RGB=False):
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            meta = src.meta
+        meta['photometric'] = 'ycbcr'
+        meta['compress'] = 'jpeg'
+        meta['count'] = 3
+        tiffname = str(tmpdir.join('foo.tif'))
+        with rasterio.open(tiffname, 'w', **meta) as dst:
+            assert dst.profile['photometric'] == 'ycbcr'
+            assert dst.colorinterp(1) == ColorInterp.Y
+            assert dst.colorinterp(2) == ColorInterp.Cb
+            assert dst.colorinterp(3) == ColorInterp.Cr
diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py
new file mode 100644
index 0000000..be9b6a6
--- /dev/null
+++ b/tests/test_deprecations.py
@@ -0,0 +1,63 @@
+# TODO delete this in 1.0
+# This ensures that deprecation warnings are given but behavior is maintained
+# on the way to stabilizing the API for 1.0
+import pytest
+import numpy
+
+# New modules
+from rasterio import windows
+
+# Deprecated modules
+from rasterio import (
+    get_data_window, window_intersection, window_union, windows_intersect
+)
+
+
+DATA_WINDOW = ((3, 5), (2, 6))
+
+
+ at pytest.fixture
+def data():
+    data = numpy.zeros((10, 10), dtype='uint8')
+    data[slice(*DATA_WINDOW[0]), slice(*DATA_WINDOW[1])] = 1
+    return data
+
+
+def test_data_window_unmasked(data):
+    with pytest.warns(DeprecationWarning):
+        old = get_data_window(data)
+    new = windows.get_data_window(data)
+    assert old == new
+
+
+def test_windows_intersect_disjunct():
+    data = [
+        ((0, 6), (3, 6)),
+        ((2, 4), (1, 5))]
+
+    with pytest.warns(DeprecationWarning):
+        old = windows_intersect(data)
+    new = windows.intersect(data)
+    assert old == new
+
+
+def test_window_intersection():
+    data = [
+        ((0, 6), (3, 6)),
+        ((2, 4), (1, 5))]
+
+    with pytest.warns(DeprecationWarning):
+        old = window_intersection(data)
+    new = windows.intersection(data)
+    assert old == new
+
+
+def test_window_union():
+    data = [
+        ((0, 6), (3, 6)),
+        ((2, 4), (1, 5))]
+
+    with pytest.warns(DeprecationWarning):
+        old = window_union(data)
+    new = windows.union(data)
+    assert old == new
diff --git a/tests/test_driver_management.py b/tests/test_driver_management.py
index 7368ed3..e31c007 100644
--- a/tests/test_driver_management.py
+++ b/tests/test_driver_management.py
@@ -31,7 +31,7 @@ def test_options(tmpdir):
             pass
 
     log = open(logfile1).read()
-    assert "GDAL: GDALOpen(tests/data/RGB.byte.tif" in log
+    assert "Option CPL_DEBUG=True" in log
     
     # The GDAL env above having exited, CPL_DEBUG should be OFF.
     logfile2 = str(tmpdir.join('test_options2.log'))
@@ -44,4 +44,3 @@ def test_options(tmpdir):
     # Expect no debug messages from GDAL.
     log = open(logfile2).read()
     assert "GDAL: GDALOpen(tests/data/RGB.byte.tif" not in log
-
diff --git a/tests/test_err.py b/tests/test_err.py
new file mode 100644
index 0000000..3c182d6
--- /dev/null
+++ b/tests/test_err.py
@@ -0,0 +1,28 @@
+# Testing use of cpl_errs
+
+import pytest
+
+import rasterio
+from rasterio.errors import RasterioIOError
+
+
+def test_io_error(tmpdir):
+    """RasterioIOError is raised when a disk file can't be opened.
+    Newlines are removed from GDAL error messages."""
+    with pytest.raises(RasterioIOError) as exc_info:
+        rasterio.open(str(tmpdir.join('foo.tif')))
+    msg, = exc_info.value.args
+    assert "\n" not in msg
+
+
+def test_io_error_env(tmpdir):
+    with rasterio.drivers() as env:
+        drivers_start = env.drivers()
+        with pytest.raises(RasterioIOError):
+            rasterio.open(str(tmpdir.join('foo.tif')))
+    assert env.drivers() == drivers_start
+
+
+def test_bogus_band_error():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        assert src._has_band(4) is False
diff --git a/tests/test_indexing.py b/tests/test_indexing.py
index d42ca16..f8d6c90 100644
--- a/tests/test_indexing.py
+++ b/tests/test_indexing.py
@@ -2,10 +2,7 @@ import numpy
 import pytest
 
 import rasterio
-from rasterio import (
-    get_data_window, window_intersection, window_union, windows_intersect
-)
-
+from rasterio import windows
 
 DATA_WINDOW = ((3, 5), (2, 6))
 
@@ -86,21 +83,21 @@ def data():
 
 
 def test_data_window_unmasked(data):
-    window = get_data_window(data)
+    window = windows.get_data_window(data)
     assert window == ((0, data.shape[0]), (0, data.shape[1]))
 
 
 def test_data_window_masked(data):
     data = numpy.ma.masked_array(data, data == 0)
-    window = get_data_window(data)
+    window = windows.get_data_window(data)
     assert window == DATA_WINDOW
 
 
 def test_data_window_nodata(data):
-    window = get_data_window(data, nodata=0)
+    window = windows.get_data_window(data, nodata=0)
     assert window == DATA_WINDOW
 
-    window = get_data_window(numpy.ones_like(data), nodata=0)
+    window = windows.get_data_window(numpy.ones_like(data), nodata=0)
     assert window == ((0, data.shape[0]), (0, data.shape[1]))
 
 
@@ -109,44 +106,44 @@ def test_data_window_nodata_disjunct():
     data[0, :4, 1:4] = 1
     data[1, 2:5, 2:8] = 1
     data[2, 1:6, 1:6] = 1
-    window = get_data_window(data, nodata=0)
+    window = windows.get_data_window(data, nodata=0)
     assert window == ((0, 6), (1, 8))
 
 
 def test_data_window_empty_result():
     data = numpy.zeros((3, 10, 10), dtype='uint8')
-    window = get_data_window(data, nodata=0)
+    window = windows.get_data_window(data, nodata=0)
     assert window == ((0, 0), (0, 0))
 
 
 def test_data_window_masked_file():
     with rasterio.open('tests/data/RGB.byte.tif') as src:
-        window = get_data_window(src.read(1, masked=True))
+        window = windows.get_data_window(src.read(1, masked=True))
         assert window == ((3, 714), (13, 770))
 
-        window = get_data_window(src.read(masked=True))
+        window = windows.get_data_window(src.read(masked=True))
         assert window == ((3, 714), (13, 770))
 
 
 def test_window_union():
-    assert window_union([
+    assert windows.union([
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5))
     ]) == ((0, 6), (1, 6))
 
 
 def test_window_intersection():
-    assert window_intersection([
+    assert windows.intersection([
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5))
     ]) == ((2, 4), (3, 5))
 
-    assert window_intersection([
+    assert windows.intersection([
         ((0, 6), (3, 6)),
         ((6, 10), (1, 5))
     ]) == ((6, 6), (3, 5))
 
-    assert window_intersection([
+    assert windows.intersection([
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5)),
         ((3, 6), (0, 6))
@@ -155,7 +152,7 @@ def test_window_intersection():
 
 def test_window_intersection_disjunct():
     with pytest.raises(ValueError):
-        window_intersection([
+        windows.intersection([
             ((0, 6), (3, 6)),
             ((100, 200), (0, 12)),
             ((7, 12), (7, 12))
@@ -163,12 +160,12 @@ def test_window_intersection_disjunct():
 
 
 def test_windows_intersect():
-    assert windows_intersect([
+    assert windows.intersect([
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5))
     ]) == True
 
-    assert windows_intersect([
+    assert windows.intersect([
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5)),
         ((3, 6), (0, 6))
@@ -176,19 +173,19 @@ def test_windows_intersect():
 
 
 def test_windows_intersect_disjunct():
-    assert windows_intersect([
+    assert windows.intersect([
         ((0, 6), (3, 6)),
         ((10, 20), (0, 6))
     ]) == False
 
-    assert windows_intersect([
+    assert windows.intersect([
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5)),
         ((5, 6), (0, 6))
     ]) == False
 
-    assert windows_intersect([
+    assert windows.intersect([
         ((0, 6), (3, 6)),
         ((2, 4), (1, 3)),
         ((3, 6), (4, 6))
-    ]) == False
\ No newline at end of file
+    ]) == False
diff --git a/tests/test_rio_convert.py b/tests/test_rio_convert.py
index e905180..001dcee 100644
--- a/tests/test_rio_convert.py
+++ b/tests/test_rio_convert.py
@@ -6,7 +6,8 @@ from click.testing import CliRunner
 
 import rasterio
 from rasterio.rio.main import main_group
-from rasterio.rio.convert import convert, clip
+from rasterio.rio.clip import clip
+from rasterio.rio.convert import convert
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
diff --git a/tests/test_rio_features.py b/tests/test_rio_features.py
index 93c43b5..6a3a9eb 100644
--- a/tests/test_rio_features.py
+++ b/tests/test_rio_features.py
@@ -7,7 +7,10 @@ import json
 from affine import Affine
 
 import rasterio
-from rasterio.rio import features
+from rasterio.rio.mask import mask
+from rasterio.rio.shapes import shapes
+from rasterio.rio.rasterize import rasterize
+from rasterio.rio.main import main_group
 
 
 DEFAULT_SHAPE = (10, 10)
@@ -21,8 +24,8 @@ def test_mask(runner, tmpdir, basic_feature, basic_image_2x2,
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--geojson-mask', '-'],
+        main_group,
+        ['mask', pixelated_image_file, output, '--geojson-mask', '-'],
         input=json.dumps(basic_feature)
     )
 
@@ -42,8 +45,9 @@ def test_mask_all_touched(runner, tmpdir, basic_feature, basic_image,
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--all', '--geojson-mask', '-'],
+        main_group, [
+            'mask', pixelated_image_file, output, '--all', '--geojson-mask',
+            '-'],
         input=json.dumps(basic_feature)
     )
     assert result.exit_code == 0
@@ -65,38 +69,35 @@ def test_mask_invert(runner, tmpdir, basic_feature, pixelated_image,
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--invert', '--geojson-mask', '-'],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'mask', pixelated_image_file, output, '--invert', '--geojson-mask',
+            '-'],
+        input=json.dumps(basic_feature))
     assert result.exit_code == 0
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
         assert numpy.array_equal(
             truth,
-            out.read(1, masked=True).filled(0)
-        )
+            out.read(1, masked=True).filled(0))
 
 
 def test_mask_featurecollection(runner, tmpdir, basic_featurecollection,
-                                 basic_image_2x2, pixelated_image_file):
+                                basic_image_2x2, pixelated_image_file):
 
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--geojson-mask', '-'],
-        input=json.dumps(basic_featurecollection)
-    )
+        main_group,
+        ['mask', pixelated_image_file, output, '--geojson-mask', '-'],
+        input=json.dumps(basic_featurecollection))
     assert result.exit_code == 0
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
         assert numpy.array_equal(
             basic_image_2x2,
-            out.read(1, masked=True).filled(0)
-        )
+            out.read(1, masked=True).filled(0))
 
 
 def test_mask_out_of_bounds(runner, tmpdir, basic_feature,
@@ -112,10 +113,9 @@ def test_mask_out_of_bounds(runner, tmpdir, basic_feature,
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--geojson-mask', '-'],
-        input=json.dumps(basic_feature)
-    )
+        main_group,
+        ['mask', pixelated_image_file, output, '--geojson-mask', '-'],
+        input=json.dumps(basic_feature))
     assert result.exit_code == 0
     assert 'outside bounds' in result.output
     assert os.path.exists(output)
@@ -129,15 +129,16 @@ def test_mask_no_geojson(runner, tmpdir, pixelated_image, pixelated_image_file):
 
     output = str(tmpdir.join('test.tif'))
 
-    result = runner.invoke(features.mask, [pixelated_image_file, output])
+    result = runner.invoke(
+        main_group,
+        ['mask', pixelated_image_file, output])
     assert result.exit_code == 0
     assert os.path.exists(output)
 
     with rasterio.open(output) as out:
         assert numpy.array_equal(
             pixelated_image,
-            out.read(1, masked=True).filled(0)
-        )
+            out.read(1, masked=True).filled(0))
 
 
 def test_mask_invalid_geojson(runner, tmpdir, pixelated_image_file):
@@ -147,19 +148,17 @@ def test_mask_invalid_geojson(runner, tmpdir, pixelated_image_file):
 
     # Using invalid JSON
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--geojson-mask', '-'],
-        input='{bogus: value}'
-    )
+        main_group,
+        ['mask', pixelated_image_file, output, '--geojson-mask', '-'],
+        input='{bogus: value}')
     assert result.exit_code == 2
     assert 'GeoJSON could not be read' in result.output
 
     # Using invalid GeoJSON
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--geojson-mask', '-'],
-        input='{"bogus": "value"}'
-    )
+        main_group,
+        ['mask', pixelated_image_file, output, '--geojson-mask', '-'],
+        input='{"bogus": "value"}')
     assert result.exit_code == 2
     assert 'Invalid GeoJSON' in result.output
 
@@ -180,31 +179,26 @@ def test_mask_crop(runner, tmpdir, basic_feature, pixelated_image):
         "driver": "GTiff",
         "width": image.shape[1],
         "height": image.shape[0],
-        "nodata": 255
-    }
+        "nodata": 255}
     with rasterio.drivers():
         with rasterio.open(outfilename, 'w', **kwargs) as out:
             out.write_band(1, image)
 
-
     output = str(tmpdir.join('test.tif'))
 
     truth = numpy.zeros((4, 3))
     truth[1:3, 0:2] = 1
 
     result = runner.invoke(
-        features.mask,
-        [outfilename, output, '--crop', '--geojson-mask', '-'],
-        input=json.dumps(basic_feature)
-    )
-
+        main_group,
+        ['mask', outfilename, output, '--crop', '--geojson-mask', '-'],
+        input=json.dumps(basic_feature))
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
         assert numpy.array_equal(
             truth,
-            out.read(1, masked=True).filled(0)
-        )
+            out.read(1, masked=True).filled(0))
 
 
 def test_mask_crop_inverted_y(runner, tmpdir, basic_feature, pixelated_image_file):
@@ -219,18 +213,17 @@ def test_mask_crop_inverted_y(runner, tmpdir, basic_feature, pixelated_image_fil
     truth[1:3, 0:2] = 1
 
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--crop', '--geojson-mask', '-'],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'mask', pixelated_image_file, output, '--crop',
+            '--geojson-mask', '-'],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
     with rasterio.open(output) as out:
         assert numpy.array_equal(
             truth,
-            out.read(1, masked=True).filled(0)
-        )
+            out.read(1, masked=True).filled(0))
 
 
 def test_mask_crop_out_of_bounds(runner, tmpdir, basic_feature,
@@ -246,10 +239,10 @@ def test_mask_crop_out_of_bounds(runner, tmpdir, basic_feature,
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.mask,
-        [pixelated_image_file, output, '--crop', '--geojson-mask', '-'],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'mask', pixelated_image_file, output, '--crop',
+            '--geojson-mask', '-'],
+        input=json.dumps(basic_feature))
     assert result.exit_code == 2
     assert 'not allowed' in result.output
 
@@ -261,33 +254,28 @@ def test_mask_crop_and_invert(runner, tmpdir, basic_feature, pixelated_image,
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.mask,
-        [
-            pixelated_image_file, output,
-            '--crop',
-            '--invert',
-            '--geojson-mask', '-'
-        ],
-        input=json.dumps(basic_feature)
-    )
+        main_group,
+        ['mask', pixelated_image_file, output, '--crop', '--invert',
+         '--geojson-mask', '-'],
+        input=json.dumps(basic_feature))
     assert result.exit_code == 0
     assert 'Invert option ignored' in result.output
 
 
 def test_shapes(runner, pixelated_image_file):
-    result = runner.invoke(features.shapes, [pixelated_image_file])
+    result = runner.invoke(main_group, ['shapes', pixelated_image_file])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 4
     assert numpy.allclose(
         json.loads(result.output)['features'][0]['geometry']['coordinates'],
-        [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]
-    )
+        [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]])
 
 
 def test_shapes_invalid_bidx(runner, pixelated_image_file):
-    result = runner.invoke(features.shapes, [pixelated_image_file, '--bidx', 4])
+    result = runner.invoke(
+        main_group, ['shapes', pixelated_image_file, '--bidx', 4])
 
     assert result.exit_code == 1
     # Underlying exception message trapped by shapes
@@ -299,7 +287,8 @@ def test_shapes_sequence(runner, pixelated_image_file):
     inside a feature collection.
     """
 
-    result = runner.invoke(features.shapes, [pixelated_image_file, '--sequence'])
+    result = runner.invoke(
+        main_group, ['shapes', pixelated_image_file, '--sequence'])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 0
@@ -311,8 +300,7 @@ def test_shapes_sequence_rs(runner, pixelated_image_file):
     """ --rs option should use the feature separator character. """
 
     result = runner.invoke(
-        features.shapes, [pixelated_image_file, '--sequence', '--rs']
-    )
+        main_group, ['shapes', pixelated_image_file, '--sequence', '--rs'])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 0
@@ -320,7 +308,6 @@ def test_shapes_sequence_rs(runner, pixelated_image_file):
     assert result.output.count(u'\u001e') == 4
 
 
-
 def test_shapes_with_nodata(runner, pixelated_image, pixelated_image_file):
     """
     An area of nodata should also be represented with a shape when using
@@ -333,8 +320,7 @@ def test_shapes_with_nodata(runner, pixelated_image, pixelated_image_file):
         out.write_band(1, pixelated_image)
 
     result = runner.invoke(
-        features.shapes, [pixelated_image_file, '--with-nodata']
-    )
+        main_group, ['shapes', pixelated_image_file, '--with-nodata'])
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 5
@@ -346,8 +332,7 @@ def test_shapes_indent(runner, pixelated_image_file):
     """
 
     result = runner.invoke(
-        features.shapes, [pixelated_image_file, '--indent', 2]
-    )
+        main_group, ['shapes', pixelated_image_file, '--indent', 2])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
@@ -357,7 +342,8 @@ def test_shapes_indent(runner, pixelated_image_file):
 
 
 def test_shapes_compact(runner, pixelated_image_file):
-    result = runner.invoke(features.shapes, [pixelated_image_file, '--compact'])
+    result = runner.invoke(
+        main_group, ['shapes', pixelated_image_file, '--compact'])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
@@ -369,8 +355,7 @@ def test_shapes_compact(runner, pixelated_image_file):
 def test_shapes_sampling(runner, pixelated_image_file):
     """ --sampling option should remove the single pixel features """
     result = runner.invoke(
-        features.shapes, [pixelated_image_file, '--sampling', 2]
-    )
+        main_group, ['shapes', pixelated_image_file, '--sampling', 2])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
@@ -381,8 +366,7 @@ def test_shapes_precision(runner, pixelated_image_file):
     """ Output numbers should have no more than 1 decimal place """
 
     result = runner.invoke(
-        features.shapes, [pixelated_image_file, '--precision', 1]
-    )
+        main_group, ['shapes', pixelated_image_file, '--precision', 1])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
@@ -400,7 +384,8 @@ def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
     with rasterio.open(pixelated_image_file, 'r+') as out:
         out.write_band(1, pixelated_image)
 
-    result = runner.invoke(features.shapes, [pixelated_image_file, '--mask'])
+    result = runner.invoke(
+        main_group, ['shapes', pixelated_image_file, '--mask'])
 
     print(result.output)
     print(result.exception)
@@ -411,8 +396,7 @@ def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
 
     assert numpy.allclose(
         json.loads(result.output)['features'][0]['geometry']['coordinates'],
-        [[[3, 5], [3, 10], [8, 10], [8, 8], [9, 8], [10, 8], [10, 5], [3, 5]]]
-    )
+        [[[3, 5], [3, 10], [8, 10], [8, 8], [9, 8], [10, 8], [10, 5], [3, 5]]])
 
 
 def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
@@ -428,8 +412,8 @@ def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
         out.write_band(1, pixelated_image)
 
     result = runner.invoke(
-        features.shapes, [pixelated_image_file, '--mask', '--sampling', 5]
-    )
+        main_group,
+        ['shapes', pixelated_image_file, '--mask', '--sampling', 5])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
@@ -437,8 +421,7 @@ def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
 
     assert numpy.allclose(
         json.loads(result.output)['features'][0]['geometry']['coordinates'],
-        [[[5, 5], [5, 10], [10, 10], [10, 5], [5, 5]]]
-    )
+        [[[5, 5], [5, 10], [10, 10], [10, 5], [5, 5]]])
 
 
 def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
@@ -453,26 +436,24 @@ def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
         out.write_band(1, pixelated_image)
 
     result = runner.invoke(
-        features.shapes,
-        [pixelated_image_file, '--band', '--bidx', '1', '--as-mask']
-    )
+        main_group,
+        ['shapes', pixelated_image_file, '--band', '--bidx', '1', '--as-mask'])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 3
     assert numpy.allclose(
         json.loads(result.output)['features'][1]['geometry']['coordinates'],
-        [[[2, 2], [2, 5], [5, 5], [5, 2], [2, 2]]]
-    )
+        [[[2, 2], [2, 5], [5, 5], [5, 2], [2, 2]]])
 
 
 def test_rasterize(tmpdir, runner, basic_feature):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [output, '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1]],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'rasterize', output, '--dimensions', DEFAULT_SHAPE[0],
+            DEFAULT_SHAPE[1]],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
@@ -486,14 +467,10 @@ def test_rasterize(tmpdir, runner, basic_feature):
 def test_rasterize_bounds(tmpdir, runner, basic_feature, basic_image_2x2):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [
-            output,
-            '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1],
-            '--bounds', 0, 10, 10, 0
-        ],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'rasterize', output, '--dimensions', DEFAULT_SHAPE[0],
+            DEFAULT_SHAPE[1], '--bounds', 0, 10, 10, 0],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
@@ -507,10 +484,9 @@ def test_rasterize_bounds(tmpdir, runner, basic_feature, basic_image_2x2):
 def test_rasterize_resolution(tmpdir, runner, basic_feature):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [output, '--res', 0.15],
-        input=json.dumps(basic_feature)
-    )
+        main_group,
+        ['rasterize', output, '--res', 0.15],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
@@ -524,14 +500,10 @@ def test_rasterize_resolution(tmpdir, runner, basic_feature):
 def test_rasterize_src_crs(tmpdir, runner, basic_feature):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [
-            output,
-            '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1],
-            '--src-crs', 'EPSG:3857'
-        ],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'rasterize', output, '--dimensions', DEFAULT_SHAPE[0],
+            DEFAULT_SHAPE[1], '--src-crs', 'EPSG:3857'],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
@@ -550,14 +522,10 @@ def test_rasterize_mismatched_src_crs(tmpdir, runner, basic_feature):
 
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [
-            output,
-            '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1],
-            '--src-crs', 'EPSG:4326'
-        ],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'rasterize', output, '--dimensions', DEFAULT_SHAPE[0],
+            DEFAULT_SHAPE[1], '--src-crs', 'EPSG:4326'],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 2
     assert 'Bounds are beyond the valid extent for EPSG:4326' in result.output
@@ -566,14 +534,10 @@ def test_rasterize_mismatched_src_crs(tmpdir, runner, basic_feature):
 def test_rasterize_invalid_src_crs(tmpdir, runner, basic_feature):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [
-            output,
-            '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1],
-            '--src-crs', 'foo:bar'
-        ],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'rasterize', output, '--dimensions', DEFAULT_SHAPE[0],
+            DEFAULT_SHAPE[1], '--src-crs', 'foo:bar'],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 2
     assert 'invalid CRS.  Must be an EPSG code.' in result.output
@@ -582,7 +546,7 @@ def test_rasterize_invalid_src_crs(tmpdir, runner, basic_feature):
 def test_rasterize_existing_output(tmpdir, runner, basic_feature):
     """
     Create a rasterized output, then rasterize additional pixels into it.
-    The final result should include rasterized pixels from both features.
+    The final result should include rasterized pixels from both
     """
 
     truth = numpy.zeros(DEFAULT_SHAPE)
@@ -591,14 +555,11 @@ def test_rasterize_existing_output(tmpdir, runner, basic_feature):
 
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [
-            output,
+        main_group, [
+            'rasterize', output,
             '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1],
-            '--bounds', 0, 10, 10, 0
-        ],
-        input=json.dumps(basic_feature)
-    )
+            '--bounds', 0, 10, 10, 0],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
@@ -607,10 +568,10 @@ def test_rasterize_existing_output(tmpdir, runner, basic_feature):
     basic_feature['geometry']['coordinates'] = coords.tolist()
 
     result = runner.invoke(
-        features.rasterize,
-        ['-o', output, '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1]],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'rasterize', '-o', output, '--dimensions', DEFAULT_SHAPE[0],
+            DEFAULT_SHAPE[1]],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
 
@@ -624,10 +585,9 @@ def test_rasterize_like_raster(tmpdir, runner, basic_feature, basic_image_2x2,
     output = str(tmpdir.join('test.tif'))
 
     result = runner.invoke(
-        features.rasterize,
-        [output, '--like', pixelated_image_file],
-        input=json.dumps(basic_feature)
-    )
+        main_group,
+        ['rasterize', output, '--like', pixelated_image_file],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
@@ -643,10 +603,9 @@ def test_rasterize_like_raster(tmpdir, runner, basic_feature, basic_image_2x2,
 def test_rasterize_invalid_like_raster(tmpdir, runner, basic_feature):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [output, '--like', str(tmpdir.join('foo.tif'))],
-        input=json.dumps(basic_feature)
-    )
+        main_group,
+        ['rasterize', output, '--like', str(tmpdir.join('foo.tif'))],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 2
     assert 'Invalid value for "--like":' in result.output
@@ -656,10 +615,9 @@ def test_rasterize_like_raster_src_crs_mismatch(tmpdir, runner, basic_feature,
                                                 pixelated_image_file):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [output, '--like', pixelated_image_file, '--src-crs', 'EPSG:3857'],
-        input=json.dumps(basic_feature)
-    )
+        main_group,
+        ['rasterize', output, '--like', pixelated_image_file, '--src-crs', 'EPSG:3857'],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 2
     assert 'GeoJSON does not match crs of --like raster' in result.output
@@ -668,14 +626,10 @@ def test_rasterize_like_raster_src_crs_mismatch(tmpdir, runner, basic_feature,
 def test_rasterize_property_value(tmpdir, runner, basic_feature):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [
-            output,
-            '--dimensions', DEFAULT_SHAPE[0], DEFAULT_SHAPE[1],
-            '--property', 'val'
-        ],
-        input=json.dumps(basic_feature)
-    )
+        main_group, [
+            'rasterize', output, '--dimensions', DEFAULT_SHAPE[0],
+            DEFAULT_SHAPE[1], '--property', 'val'],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 0
     assert os.path.exists(output)
@@ -698,8 +652,8 @@ def test_rasterize_like_raster_outside_bounds(tmpdir, runner, basic_feature,
 
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize,
-        [output, '--like', pixelated_image_file],
+        main_group,
+        ['rasterize', output, '--like', pixelated_image_file],
         input=json.dumps(basic_feature)
     )
 
@@ -714,7 +668,8 @@ def test_rasterize_invalid_stdin(tmpdir, runner):
     """ Invalid value for stdin should fail with exception """
 
     output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(features.rasterize, [output], input='BOGUS')
+    result = runner.invoke(
+        main_group, ['rasterize', output], input='BOGUS')
 
     assert result.exit_code == -1
 
@@ -722,7 +677,8 @@ def test_rasterize_invalid_stdin(tmpdir, runner):
 def test_rasterize_invalid_geojson(tmpdir, runner):
     """ Invalid GeoJSON should fail with error  """
     output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(features.rasterize, [output], input='{"A": "B"}')
+    result = runner.invoke(
+        main_group, ['rasterize', output], input='{"A": "B"}')
 
     assert result.exit_code == 2
     assert 'Invalid GeoJSON' in result.output
@@ -733,8 +689,9 @@ def test_rasterize_missing_parameters(tmpdir, runner, basic_feature):
 
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        features.rasterize, ['-o', output], input=json.dumps(basic_feature)
-    )
+        main_group,
+        ['rasterize', '-o', output],
+        input=json.dumps(basic_feature))
 
     assert result.exit_code == 2
     assert 'pixel dimensions are required' in result.output
diff --git a/tests/test_rio_info.py b/tests/test_rio_info.py
index f8efb44..14bf64e 100644
--- a/tests/test_rio_info.py
+++ b/tests/test_rio_info.py
@@ -9,6 +9,8 @@ import pytest
 
 import rasterio
 from rasterio.rio import info
+from rasterio.rio.edit_info import (edit, all_handler, crs_handler,
+                                    tags_handler, transform_handler)
 from rasterio.rio.main import main_group
 
 
@@ -18,14 +20,14 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 def test_edit_nodata_err(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [inputfile, '--nodata', '-1'])
+    result = runner.invoke(edit, [inputfile, '--nodata', '-1'])
     assert result.exit_code == 2
 
 
 def test_edit_nodata(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [inputfile, '--nodata', '255'])
+    result = runner.invoke(edit, [inputfile, '--nodata', '255'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         assert src.nodata == 255.0
@@ -34,14 +36,14 @@ def test_edit_nodata(data):
 def test_edit_crs_err(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [inputfile, '--crs', 'LOL:WUT'])
+    result = runner.invoke(edit, [inputfile, '--crs', 'LOL:WUT'])
     assert result.exit_code == 2
 
 
 def test_edit_crs_epsg(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [inputfile, '--crs', 'EPSG:32618'])
+    result = runner.invoke(edit, [inputfile, '--crs', 'EPSG:32618'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         assert src.crs == {'init': 'epsg:32618'}
@@ -50,7 +52,7 @@ def test_edit_crs_epsg(data):
 def test_edit_crs_proj4(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [inputfile, '--crs', '+init=epsg:32618'])
+    result = runner.invoke(edit, [inputfile, '--crs', '+init=epsg:32618'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         assert src.crs == {'init': 'epsg:32618'}
@@ -60,7 +62,7 @@ def test_edit_crs_obj(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
     result = runner.invoke(
-        info.edit, [inputfile, '--crs', '{"init": "epsg:32618"}'])
+        edit, [inputfile, '--crs', '{"init": "epsg:32618"}'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         assert src.crs == {'init': 'epsg:32618'}
@@ -69,14 +71,14 @@ def test_edit_crs_obj(data):
 def test_edit_transform_err_not_json(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [inputfile, '--transform', 'LOL'])
+    result = runner.invoke(edit, [inputfile, '--transform', 'LOL'])
     assert result.exit_code == 2
 
 
 def test_edit_transform_err_bad_array(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [inputfile, '--transform', '[1,2]'])
+    result = runner.invoke(edit, [inputfile, '--transform', '[1,2]'])
     assert result.exit_code == 2
 
 
@@ -84,7 +86,7 @@ def test_edit_transform_affine(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
     input_t = '[300.038, 0.0, 101985.0, 0.0, -300.042, 2826915.0]'
-    result = runner.invoke(info.edit, [inputfile, '--transform', input_t])
+    result = runner.invoke(edit, [inputfile, '--transform', input_t])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         for a, b in zip(src.affine, json.loads(input_t)):
@@ -95,7 +97,7 @@ def test_edit_transform_gdal(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
     input_t = '[300.038, 0.0, 101985.0, 0.0, -300.042, 2826915.0]'
-    result = runner.invoke(info.edit, [
+    result = runner.invoke(edit, [
         inputfile,
         '--transform', '[101985.0, 300.038, 0.0, 2826915.0, 0.0, -300.042]'])
     assert result.exit_code == 0
@@ -107,7 +109,7 @@ def test_edit_transform_gdal(data):
 def test_edit_tags(data):
     runner = CliRunner()
     inputfile = str(data.join('RGB.byte.tif'))
-    result = runner.invoke(info.edit, [
+    result = runner.invoke(edit, [
         inputfile, '--tag', 'lol=1', '--tag', 'wut=2'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
@@ -130,64 +132,64 @@ class MockOption:
 def test_all_callback_pass(data):
     ctx = MockContext()
     ctx.obj['like'] = {'transform': 'foo'}
-    assert info.all_handler(ctx, None, None) == None
+    assert all_handler(ctx, None, None) == None
 
 
 def test_all_callback(data):
     ctx = MockContext()
     ctx.obj['like'] = {'transform': 'foo'}
-    assert info.all_handler(ctx, None, True) == {'transform': 'foo'}
+    assert all_handler(ctx, None, True) == {'transform': 'foo'}
 
 
 def test_all_callback_None(data):
     ctx = MockContext()
-    assert info.all_handler(ctx, None, None) is None
+    assert all_handler(ctx, None, None) is None
 
 
 def test_transform_callback_pass(data):
     """Always return None if the value is None"""
     ctx = MockContext()
     ctx.obj['like'] = {'transform': 'foo'}
-    assert info.transform_handler(ctx, MockOption('transform'), None) is None
+    assert transform_handler(ctx, MockOption('transform'), None) is None
 
 
 def test_transform_callback_err(data):
     ctx = MockContext()
     ctx.obj['like'] = {'transform': 'foo'}
     with pytest.raises(click.BadParameter):
-        info.transform_handler(ctx, MockOption('transform'), '?')
+        transform_handler(ctx, MockOption('transform'), '?')
 
 
 def test_transform_callback(data):
     ctx = MockContext()
     ctx.obj['like'] = {'transform': 'foo'}
-    assert info.transform_handler(ctx, MockOption('transform'), 'like') == 'foo'
+    assert transform_handler(ctx, MockOption('transform'), 'like') == 'foo'
 
 
 def test_crs_callback_pass(data):
     """Always return None if the value is None"""
     ctx = MockContext()
     ctx.obj['like'] = {'crs': 'foo'}
-    assert info.crs_handler(ctx, MockOption('crs'), None) is None
+    assert crs_handler(ctx, MockOption('crs'), None) is None
 
 
 def test_crs_callback(data):
     ctx = MockContext()
     ctx.obj['like'] = {'crs': 'foo'}
-    assert info.crs_handler(ctx, MockOption('crs'), 'like') == 'foo'
+    assert crs_handler(ctx, MockOption('crs'), 'like') == 'foo'
 
 
 def test_tags_callback_err(data):
     ctx = MockContext()
     ctx.obj['like'] = {'tags': {'foo': 'bar'}}
     with pytest.raises(click.BadParameter):
-        info.tags_handler(ctx, MockOption('tags'), '?') == {'foo': 'bar'}
+        tags_handler(ctx, MockOption('tags'), '?') == {'foo': 'bar'}
 
 
 def test_tags_callback(data):
     ctx = MockContext()
     ctx.obj['like'] = {'tags': {'foo': 'bar'}}
-    assert info.tags_handler(ctx, MockOption('tags'), 'like') == {'foo': 'bar'}
+    assert tags_handler(ctx, MockOption('tags'), 'like') == {'foo': 'bar'}
 
 
 def test_edit_crs_like(data):
@@ -206,8 +208,9 @@ def test_edit_crs_like(data):
 
     # The test.
     templatefile = 'tests/data/RGB.byte.tif'
-    result = runner.invoke(info.edit, [
-        inputfile, '--like', templatefile, '--crs', 'like'])
+    result = runner.invoke(
+        main_group,
+        ['edit-info', inputfile, '--like', templatefile, '--crs', 'like'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         assert src.crs == {'init': 'epsg:32618'}
@@ -230,8 +233,9 @@ def test_edit_nodata_like(data):
 
     # The test.
     templatefile = 'tests/data/RGB.byte.tif'
-    result = runner.invoke(info.edit, [
-        inputfile, '--like', templatefile, '--nodata', 'like'])
+    result = runner.invoke(
+        main_group,
+        ['edit-info', inputfile, '--like', templatefile, '--nodata', 'like'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         assert src.crs == {'init': 'epsg:32617'}
@@ -252,8 +256,8 @@ def test_edit_all_like(data):
         assert src.nodata == 1.0
 
     templatefile = 'tests/data/RGB.byte.tif'
-    result = runner.invoke(info.edit, [
-        inputfile, '--like', templatefile, '--all'])
+    result = runner.invoke(
+        main_group, ['edit-info', inputfile, '--like', templatefile, '--all'])
     assert result.exit_code == 0
     with rasterio.open(inputfile) as src:
         assert src.crs == {'init': 'epsg:32618'}
@@ -273,16 +277,14 @@ def test_env():
 def test_info_err():
     runner = CliRunner()
     result = runner.invoke(
-        info.info,
-        ['tests'])
+        main_group, ['info', 'tests'])
     assert result.exit_code == 1
 
 
 def test_info():
     runner = CliRunner()
     result = runner.invoke(
-        info.info,
-        ['tests/data/RGB.byte.tif'])
+        main_group, ['info', 'tests/data/RGB.byte.tif'])
     assert result.exit_code == 0
     assert '"count": 3' in result.output
 
@@ -310,8 +312,7 @@ def test_info_quiet():
 def test_info_count():
     runner = CliRunner()
     result = runner.invoke(
-        info.info,
-        ['tests/data/RGB.byte.tif', '--count'])
+        main_group, ['info', 'tests/data/RGB.byte.tif', '--count'])
     assert result.exit_code == 0
     assert result.output == '3\n'
 
@@ -319,8 +320,7 @@ def test_info_count():
 def test_info_nodatavals():
     runner = CliRunner()
     result = runner.invoke(
-        info.info,
-        ['tests/data/RGB.byte.tif', '--bounds'])
+        main_group, ['info', 'tests/data/RGB.byte.tif', '--bounds'])
     assert result.exit_code == 0
     assert result.output == '101985.0 2611485.0 339315.0 2826915.0\n'
 
@@ -328,8 +328,7 @@ def test_info_nodatavals():
 def test_info_tags():
     runner = CliRunner()
     result = runner.invoke(
-        info.info,
-        ['tests/data/RGB.byte.tif', '--tags'])
+        main_group, ['info', 'tests/data/RGB.byte.tif', '--tags'])
     assert result.exit_code == 0
     assert result.output == '{"AREA_OR_POINT": "Area"}\n'
 
@@ -337,8 +336,7 @@ def test_info_tags():
 def test_info_res():
     runner = CliRunner()
     result = runner.invoke(
-        info.info,
-        ['tests/data/RGB.byte.tif', '--res'])
+        main_group, ['info', 'tests/data/RGB.byte.tif', '--res'])
     assert result.exit_code == 0
     assert result.output.startswith('300.037')
 
@@ -346,15 +344,14 @@ def test_info_res():
 def test_info_lnglat():
     runner = CliRunner()
     result = runner.invoke(
-        info.info,
-        ['tests/data/RGB.byte.tif', '--lnglat'])
+        main_group, ['info', 'tests/data/RGB.byte.tif', '--lnglat'])
     assert result.exit_code == 0
     assert result.output.startswith('-77.757')
 
 
 def test_mo_info():
     runner = CliRunner()
-    result = runner.invoke(info.info, ['tests/data/RGB.byte.tif'])
+    result = runner.invoke(main_group, ['info', 'tests/data/RGB.byte.tif'])
     assert result.exit_code == 0
     assert '"res": [300.037' in result.output
     assert '"lnglat": [-77.757' in result.output
@@ -363,7 +360,7 @@ def test_mo_info():
 def test_info_stats():
     runner = CliRunner()
     result = runner.invoke(
-        info.info, ['tests/data/RGB.byte.tif', '--tell-me-more'])
+        main_group, ['info', 'tests/data/RGB.byte.tif', '--tell-me-more'])
     assert result.exit_code == 0
     assert '"max": 255.0' in result.output
     assert '"min": 1.0' in result.output
@@ -373,7 +370,8 @@ def test_info_stats():
 def test_info_stats_only():
     runner = CliRunner()
     result = runner.invoke(
-        info.info, ['tests/data/RGB.byte.tif', '--stats', '--bidx', '2'])
+        main_group,
+        ['info', 'tests/data/RGB.byte.tif', '--stats', '--bidx', '2'])
     assert result.exit_code == 0
     assert result.output.startswith('1.000000 255.000000 66.02')
 
@@ -595,26 +593,20 @@ def test_bounds_seq_rs():
 
 def test_insp():
     runner = CliRunner()
-    result = runner.invoke(main_group, [
-        'insp',
-        'tests/data/RGB.byte.tif'
-    ])
+    result = runner.invoke(main_group, ['insp', 'tests/data/RGB.byte.tif'])
     assert result.exit_code == 0
 
 
 def test_insp_err():
     runner = CliRunner()
-    result = runner.invoke(main_group, [
-        'insp',
-        'tests'
-    ])
+    result = runner.invoke(main_group, ['insp', 'tests'])
     assert result.exit_code == 1
 
 
 def test_info_checksums():
     runner = CliRunner()
     result = runner.invoke(
-        info.info, ['tests/data/RGB.byte.tif', '--tell-me-more'])
+        main_group, ['info', 'tests/data/RGB.byte.tif', '--tell-me-more'])
     assert result.exit_code == 0
     assert '"checksum": [25420, 29131, 37860]' in result.output
 
@@ -622,6 +614,7 @@ def test_info_checksums():
 def test_info_checksums_only():
     runner = CliRunner()
     result = runner.invoke(
-        info.info, ['tests/data/RGB.byte.tif', '--checksum', '--bidx', '2'])
+        main_group,
+        ['info', 'tests/data/RGB.byte.tif', '--checksum', '--bidx', '2'])
     assert result.exit_code == 0
     assert result.output.strip() == '29131'
diff --git a/tests/test_rio_sample.py b/tests/test_rio_sample.py
index deae77a..7627a26 100644
--- a/tests/test_rio_sample.py
+++ b/tests/test_rio_sample.py
@@ -5,7 +5,7 @@ import click
 from click.testing import CliRunner
 
 import rasterio
-from rasterio.rio import sample
+from rasterio.rio.main import main_group
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
@@ -14,8 +14,8 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 def test_sample_err():
     runner = CliRunner()
     result = runner.invoke(
-        sample.sample,
-        ['bogus.tif'],
+        main_group,
+        ['sample', 'bogus.tif'],
         "[220650.0, 2719200.0]")
     assert result.exit_code == 1
 
@@ -23,8 +23,8 @@ def test_sample_err():
 def test_sample_stdin():
     runner = CliRunner()
     result = runner.invoke(
-        sample.sample,
-        ['tests/data/RGB.byte.tif'],
+        main_group,
+        ['sample', 'tests/data/RGB.byte.tif'],
         "[220650.0, 2719200.0]\n[220650.0, 2719200.0]",
         catch_exceptions=False)
     assert result.exit_code == 0
@@ -34,8 +34,8 @@ def test_sample_stdin():
 def test_sample_arg():
     runner = CliRunner()
     result = runner.invoke(
-        sample.sample,
-        ['tests/data/RGB.byte.tif', "[220650.0, 2719200.0]"],
+        main_group,
+        ['sample', 'tests/data/RGB.byte.tif', "[220650.0, 2719200.0]"],
         catch_exceptions=False)
     assert result.exit_code == 0
     assert result.output.strip() == '[18, 25, 14]'
@@ -44,8 +44,8 @@ def test_sample_arg():
 def test_sample_bidx():
     runner = CliRunner()
     result = runner.invoke(
-        sample.sample,
-        ['tests/data/RGB.byte.tif', '--bidx', '1,2', "[220650.0, 2719200.0]"],
+        main_group,
+        ['sample', 'tests/data/RGB.byte.tif', '--bidx', '1,2', "[220650.0, 2719200.0]"],
         catch_exceptions=False)
     assert result.exit_code == 0
     assert result.output.strip() == '[18, 25]'
@@ -54,8 +54,8 @@ def test_sample_bidx():
 def test_sample_bidx2():
     runner = CliRunner()
     result = runner.invoke(
-        sample.sample,
-        ['tests/data/RGB.byte.tif', '--bidx', '1..2', "[220650.0, 2719200.0]"],
+        main_group,
+        ['sample', 'tests/data/RGB.byte.tif', '--bidx', '1..2', "[220650.0, 2719200.0]"],
         catch_exceptions=False)
     assert result.exit_code == 0
     assert result.output.strip() == '[18, 25]'
@@ -64,8 +64,8 @@ def test_sample_bidx2():
 def test_sample_bidx3():
     runner = CliRunner()
     result = runner.invoke(
-        sample.sample,
-        ['tests/data/RGB.byte.tif', '--bidx', '..2', "[220650.0, 2719200.0]"],
+        main_group,
+        ['sample', 'tests/data/RGB.byte.tif', '--bidx', '..2', "[220650.0, 2719200.0]"],
         catch_exceptions=False)
     assert result.exit_code == 0
     assert result.output.strip() == '[18, 25]'
@@ -74,8 +74,8 @@ def test_sample_bidx3():
 def test_sample_bidx4():
     runner = CliRunner()
     result = runner.invoke(
-        sample.sample,
-        ['tests/data/RGB.byte.tif', '--bidx', '3', "[220650.0, 2719200.0]"],
+        main_group,
+        ['sample', 'tests/data/RGB.byte.tif', '--bidx', '3', "[220650.0, 2719200.0]"],
         catch_exceptions=False)
     assert result.exit_code == 0
     assert result.output.strip() == '[14]'
diff --git a/tests/test_rio_bands.py b/tests/test_rio_stack.py
similarity index 91%
rename from tests/test_rio_bands.py
rename to tests/test_rio_stack.py
index d6d9dd0..951a4b3 100644
--- a/tests/test_rio_bands.py
+++ b/tests/test_rio_stack.py
@@ -1,15 +1,14 @@
-import click
 from click.testing import CliRunner
 
 import rasterio
-from rasterio.rio import bands
+from rasterio.rio.stack import stack
 
 
 def test_stack(tmpdir):
     outputname = str(tmpdir.join('stacked.tif'))
     runner = CliRunner()
     result = runner.invoke(
-        bands.stack,
+        stack,
         ['tests/data/RGB.byte.tif', outputname],
         catch_exceptions=False)
     assert result.exit_code == 0
@@ -21,7 +20,7 @@ def test_stack_list(tmpdir):
     outputname = str(tmpdir.join('stacked.tif'))
     runner = CliRunner()
     result = runner.invoke(
-        bands.stack,
+        stack,
         ['tests/data/RGB.byte.tif', '--bidx', '1,2,3', outputname])
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
@@ -32,7 +31,7 @@ def test_stack_slice(tmpdir):
     outputname = str(tmpdir.join('stacked.tif'))
     runner = CliRunner()
     result = runner.invoke(
-        bands.stack, 
+        stack,
         [
             'tests/data/RGB.byte.tif', '--bidx', '..2',
             'tests/data/RGB.byte.tif', '--bidx', '3..',
@@ -46,7 +45,7 @@ def test_stack_single_slice(tmpdir):
     outputname = str(tmpdir.join('stacked.tif'))
     runner = CliRunner()
     result = runner.invoke(
-        bands.stack, 
+        stack,
         [
             'tests/data/RGB.byte.tif', '--bidx', '1',
             'tests/data/RGB.byte.tif', '--bidx', '2..',
@@ -61,7 +60,7 @@ def test_format_jpeg(tmpdir):
     outputname = str(tmpdir.join('stacked.jpg'))
     runner = CliRunner()
     result = runner.invoke(
-        bands.stack,
+        stack,
         ['tests/data/RGB.byte.tif', outputname, '--format', 'JPEG'])
     assert result.exit_code == 0
 
@@ -70,6 +69,6 @@ def test_error(tmpdir):
     outputname = str(tmpdir.join('stacked.tif'))
     runner = CliRunner()
     result = runner.invoke(
-        bands.stack,
+        stack,
         ['tests/data/RGB.byte.tif', outputname, '--driver', 'BOGUS'])
     assert result.exit_code == 1
diff --git a/tests/test_rio_warp.py b/tests/test_rio_warp.py
index 8aa779b..93e90cc 100644
--- a/tests/test_rio_warp.py
+++ b/tests/test_rio_warp.py
@@ -8,11 +8,55 @@ import pytest
 
 import rasterio
 from rasterio.rio import warp
+from rasterio.rio.main import main_group
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
 
 
+def test_dst_crs_error(runner, tmpdir):
+    """Invalid JSON is a bad parameter."""
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--dst-crs', '{foo: bar}'])
+    assert result.exit_code == 2
+    assert 'for dst_crs: crs appears to be JSON but is not' in result.output
+
+
+ at pytest.mark.xfail(
+    os.environ.get('GDALVERSION', 'a.b.c').startswith('1.9'),
+                   reason="GDAL 1.9 doesn't catch this error")
+def test_dst_crs_error_2(runner, tmpdir):
+    """Invalid PROJ.4 is a bad parameter."""
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--dst-crs', '{"proj": "foobar"}'])
+    assert result.exit_code == 2
+    assert 'for dst_crs: Failed to initialize PROJ.4' in result.output
+
+
+def test_dst_crs_error_epsg(runner, tmpdir):
+    """Malformed EPSG string is a bad parameter."""
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--dst-crs', 'EPSG:'])
+    assert result.exit_code == 2
+    assert 'for dst_crs: invalid literal for int()' in result.output
+
+
+def test_dst_crs_error_epsg_2(runner, tmpdir):
+    """Invalid EPSG code is a bad parameter."""
+    srcname = 'tests/data/RGB.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(main_group, [
+        'warp', srcname, outputname, '--dst-crs', 'EPSG:0'])
+    assert result.exit_code == 2
+    assert 'for dst_crs: EPSG codes are positive integers' in result.output
+
+
 def test_warp_no_reproject(runner, tmpdir):
     """ When called without parameters, output should be same as source """
     srcname = 'tests/data/shade.tif'
@@ -121,15 +165,6 @@ def test_warp_reproject_dst_crs(runner, tmpdir):
                                    -76.5759177302349, 25.550873767433984])
 
 
-def test_warp_reproject_dst_crs_error(runner, tmpdir):
-    srcname = 'tests/data/RGB.byte.tif'
-    outputname = str(tmpdir.join('test.tif'))
-    result = runner.invoke(warp.warp, [srcname, outputname,
-                                       '--dst-crs', '{foo: bar}'])
-    assert result.exit_code == 2
-    assert 'invalid crs format' in result.output
-
-
 def test_warp_reproject_dst_crs_proj4(runner, tmpdir):
     proj4 = '+proj=longlat +ellps=WGS84 +datum=WGS84'
     srcname = 'tests/data/shade.tif'
diff --git a/tests/test_tags.py b/tests/test_tags.py
index 514d068..e3e80ae 100644
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -13,7 +13,7 @@ def test_tags_read():
         assert src.tags(ns='IMAGE_STRUCTURE') == {'INTERLEAVE': 'PIXEL'}
         assert src.tags(ns='bogus') == {}
         assert 'STATISTICS_MAXIMUM' in src.tags(1)
-        with pytest.raises(ValueError):
+        with pytest.raises(IndexError):
             tags = src.tags(4)
 
 def test_tags_update(tmpdir):
@@ -29,7 +29,7 @@ def test_tags_update(tmpdir):
 
         dst.update_tags(a='1', b='2')
         dst.update_tags(1, c=3)
-        with pytest.raises(ValueError):
+        with pytest.raises(IndexError):
             dst.update_tags(4, d=4)
 
         assert dst.tags() == {'a': '1', 'b': '2'}
diff --git a/tests/test_tool.py b/tests/test_tool.py
index 9348686..528a60e 100644
--- a/tests/test_tool.py
+++ b/tests/test_tool.py
@@ -6,7 +6,8 @@ except ImportError:
     plt = None
 
 import rasterio
-from rasterio.tool import show, show_hist, stats
+from rasterio.plot import show, show_hist
+from rasterio.rio.insp import stats
 
 def test_stats():
     with rasterio.drivers():
diff --git a/tests/test_tools_mask.py b/tests/test_tools_mask.py
index 8398f29..733b0cd 100644
--- a/tests/test_tools_mask.py
+++ b/tests/test_tools_mask.py
@@ -1,7 +1,7 @@
 import pytest
 
 import rasterio
-from rasterio.tools.mask import mask as mask_tool
+from rasterio.mask import mask as mask_tool
 
 
 def test_nodata(basic_image_file, basic_geometry):
diff --git a/tests/test_update.py b/tests/test_update.py
index a097a54..cc15c21 100644
--- a/tests/test_update.py
+++ b/tests/test_update.py
@@ -15,7 +15,7 @@ def test_update_tags(data):
         with rasterio.open(tiffname, 'r+') as f:
             f.update_tags(a='1', b='2')
             f.update_tags(1, c=3)
-            with pytest.raises(ValueError):
+            with pytest.raises(IndexError):
                 f.update_tags(4, d=4)
             assert f.tags() == {'AREA_OR_POINT': 'Area', 'a': '1', 'b': '2'}
             assert ('c', '3') in f.tags(1).items()
diff --git a/tests/test_vfs.py b/tests/test_vfs.py
index 0041a55..140a692 100644
--- a/tests/test_vfs.py
+++ b/tests/test_vfs.py
@@ -38,7 +38,7 @@ def test_parse_path_file():
 def test_parse_unknown_scheme():
     """Raise exception for unknown WFS scheme"""
     with pytest.raises(ValueError):
-        parse_path('http://foo.tif')
+        parse_path('gopher://foo.tif')
 
 
 def test_vsi_path_scheme():
diff --git a/tests/test_warp_transform.py b/tests/test_warp_transform.py
index 8019df6..ea671cc 100644
--- a/tests/test_warp_transform.py
+++ b/tests/test_warp_transform.py
@@ -1,6 +1,12 @@
+import os
+
+import pytest
+
 import rasterio
 from rasterio._warp import _calculate_default_transform
+from rasterio.errors import CRSError
 from rasterio.transform import Affine, from_bounds
+from rasterio.warp import transform_bounds
 
 
 def test_identity():
@@ -24,6 +30,18 @@ def test_identity():
         assert round(res, 7) == round(exp, 7)
 
 
+def test_transform_bounds():
+    """CRSError is raised."""
+    with rasterio.drivers():
+        left, bottom, right, top = (
+            -11740727.544603072, 4852834.0517692715, -11584184.510675032,
+            5009377.085697309)
+        src_crs = 'EPSG:3857'
+        dst_crs = {'proj': 'foobar'}
+        with pytest.raises(CRSError):
+            transform_bounds(src_crs, dst_crs, left, bottom, right, top)
+
+
 def test_gdal_transform_notnull():
     with rasterio.drivers():
         dt, dw, dh = _calculate_default_transform(
@@ -61,3 +79,20 @@ def test_gdal_transform_fail_src_crs():
             bottom=30,
             right=-80,
             top=70)
+
+
+ at pytest.mark.xfail(
+    os.environ.get('GDALVERSION', 'a.b.c').startswith('1.9'),
+                   reason="GDAL 1.9 doesn't catch this error")
+def test_gdal_transform_fail_src_crs():
+    with rasterio.drivers():
+        with pytest.raises(CRSError):
+            dt, dw, dh = _calculate_default_transform(
+                {'init': 'EPSG:4326'},
+                {'proj': 'foobar'},
+                width=80,
+                height=80,
+                left=-120,
+                bottom=30,
+                right=-80,
+                top=70)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/rasterio.git



More information about the Pkg-grass-devel mailing list