Repository: catmaid/catpy

Issue:
Instantiating ImageFetcher from CatMaid fails due to integer orientation

Discussion:

Patch:
diff --git a/catpy/__init__.py b/catpy/__init__.py
index feba40e..895073e 100644
--- a/catpy/__init__.py
+++ b/catpy/__init__.py
@@ -10,3 +10,5 @@ __all__ = ['client']
 
 
 from catpy.client import CatmaidClient, CoordinateTransformer, CatmaidUrl  # noqa
+from catpy import image  # noqa
+from catpy import export  # noqa
diff --git a/catpy/image.py b/catpy/image.py
index a601889..cd25378 100644
--- a/catpy/image.py
+++ b/catpy/image.py
@@ -5,6 +5,8 @@ from __future__ import division, unicode_literals
 import logging
 from io import BytesIO
 from collections import OrderedDict
+
+from requests import HTTPError
 from timeit import timeit
 import itertools
 from warnings import warn
@@ -392,6 +394,12 @@ class Stack(object):
 
 
 class ProjectStack(Stack):
+    orientation_choices = {
+        0: "xy",
+        1: "xz",
+        2: "zy",
+    }
+
     def __init__(self, dimension, translation, resolution, orientation, broken_slices=None, canary_location=None):
         """
         Representation of an image stack as it pertains to a CATMAID project
@@ -430,7 +438,8 @@ class ProjectStack(Stack):
         """
         stack = cls(
             stack_info['dimension'], stack_info['translation'], stack_info['resolution'],
-            stack_info['orientation'], stack_info['broken_slices'], stack_info['canary_location']
+            cls.orientation_choices[stack_info['orientation']], stack_info['broken_slices'],
+            stack_info['canary_location']
         )
         mirrors = [StackMirror.from_dict(d) for d in stack_info['mirrors']]
 
@@ -670,6 +679,12 @@ class ImageFetcher(object):
             raise ValueError('Unknown dimension of volume: should be 2D or 3D')
         return np.moveaxis(arr, (0, 1, 2), self._dimension_mappings)
 
+    def _make_empty_tile(self, width, height=None):
+        height = height or width
+        tile = np.empty((height, width), dtype=np.uint8)
+        tile.fill(self.cval)
+        return tile
+
     def _get_tile(self, tile_index):
         """
         Get the tile from the cache, handle broken slices, or fetch.
@@ -689,9 +704,7 @@ class ImageFetcher(object):
 
         if tile_index.depth in self.stack.broken_slices:
             if self.broken_slice_handling == BrokenSliceHandling.FILL and self.cval is not None:
-                tile = np.empty((tile_index.width, tile_index.height))
-                tile.fill(self.cval)
-                return tile
+                return self._make_empty_tile(tile_index.width, tile_index.height)
             else:
                 raise NotImplementedError(
                     "'fill' with a non-None cval is the only implemented broken slice handling mode"
@@ -813,7 +826,14 @@ class ImageFetcher(object):
         Future of np.ndarray in source orientation
         """
         url = self.mirror.generate_url(tile_index)
-        return response_to_array(self._session.get(url, timeout=self.timeout))
+        try:
+            return response_to_array(self._session.get(url, timeout=self.timeout))
+        except HTTPError as e:
+            if e.response.status_code == 404:
+                logger.warning("Tile not found at %s (error 404), returning blank tile", url)
+                return self._make_empty_tile(tile_index.width, tile_index.height)
+            else:
+                raise
 
     def _reorient_roi_tgt_to_src(self, roi_tgt):
         return roi_tgt[:, self._dimension_mappings]


Test patch:
diff --git a/tests/test_image.py b/tests/test_image.py
index 7c02cf8..b73fa41 100644
--- a/tests/test_image.py
+++ b/tests/test_image.py
@@ -6,6 +6,7 @@ import requests
 from PIL import Image
 from io import BytesIO
 from concurrent.futures import Future
+from requests import HTTPError
 
 try:
     import mock
@@ -508,7 +509,7 @@ def test_imagefetcher_set_mirror_title_warns_no_match(min_fetcher):
 
 def test_imagefetcher_set_mirror_title_warns_too_many(min_fetcher):
     min_fetcher.stack.mirrors.append(StackMirror(IMAGE_BASE, 1, 1, TILE_SOURCE_TYPE, 'png', 'title0', 10))
-    with pytest.warns(UserWarning, match='does not exist'):
+    with pytest.warns(UserWarning, match='ore than one'):
         min_fetcher.mirror = 'title0'
     assert min_fetcher._mirror == min_fetcher.stack.mirrors[0]
 
@@ -746,3 +747,17 @@ def test_imagefetcher_get_wrappers(min_fetcher, space):
     min_fetcher.get = mock.Mock()
     getattr(min_fetcher, 'get_{}_space'.format(space.value))('roi', 'zoom_level')
     min_fetcher.get.assert_called_with('roi', space, 'zoom_level', None)
+
+
+def test_404_handled_correctly(min_fetcher):
+    idx = TileIndex(0, 0, 0, 0, 100, 100)
+    min_fetcher._session.get = mock.Mock(side_effect=HTTPError(response=mock.Mock(status_code=404)))
+    with mock.patch('catpy.image.response_to_array', mock.Mock()):
+        tile = min_fetcher._fetch(idx)
+    assert tile.shape == (100, 100)
+    assert (tile == 0).sum() == tile.size
+
+
+@pytest.mark.xfail(reason="404 handling not implemented for threaded fetcher")
+def test_404_handled_correctly_threaded(min_fetcher):
+    assert False