Skip to content

Commit 37785bc

Browse files
authored
Merge branch 'master' into forman-617-levels_on_s3
2 parents 14a558d + 6e40e2f commit 37785bc

File tree

11 files changed

+186
-90
lines changed

11 files changed

+186
-90
lines changed

CHANGES.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,21 @@
1010

1111
### Fixes
1212

13+
* Fixed `FsDataAccessor.write_data()` implementations,
14+
which now always return the passed in `data_id`. (#623)
15+
16+
* Fixes an issue where some datasets seemed to be shifted in the
17+
y-(latitude-) direction and were misplaced on maps whose tiles
18+
are served by `xcube serve`. Images with ascending y-values are
19+
now tiled correctly. (#626)
20+
1321
### Other
1422

23+
* Replace the dependency on the rfc3339-validator PyPI package with a
24+
dependency on its recently created conda-forge package.
25+
26+
* Remove unneeded dependency on the no longer used strict-rfc3339 package.
27+
1528
## Changes in 0.10.1
1629

1730
### Fixes

environment.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ dependencies:
3131
- rasterio >=1.2
3232
- requests >=2.25
3333
- requests-oauthlib >=1.3
34+
- rfc3339-validator >=0.1.4 # for python-jsonschema date-time format validation
3435
- s3fs >=2021.6
3536
- scipy >=1.6.0
3637
- setuptools >=41.0
3738
- shapely >=1.6
38-
- strict-rfc3339 >=0.7 # for python-jsonschema date-time format validation
3939
- tornado >=6.0
4040
- urllib3 >=1.26
4141
- xarray >=0.19
@@ -46,6 +46,3 @@ dependencies:
4646
- pytest >=4.4
4747
- pytest-cov >=2.6
4848
- requests-mock >=1.8.0
49-
- pip
50-
- pip:
51-
- rfc3339-validator

test/core/store/fs/test_registry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ def assertDatasetSupported(
169169
self.assertNotIn(data_id, set(data_store.get_data_ids()))
170170

171171
data = self.new_cube_data()
172-
data_store.write_data(data, data_id, **write_params)
172+
written_data_id = data_store.write_data(data, data_id, **write_params)
173+
self.assertEqual(data_id, written_data_id)
174+
173175
self.assertEqual({expected_data_type_alias},
174176
set(data_store.get_data_types_for_data(data_id)))
175177
self.assertEqual(True, data_store.has_data(data_id))

test/util/test_tiledimage.py

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import numpy as np
44
import PIL.Image
55

6-
from xcube.util.tiledimage import ArrayImage
6+
from xcube.util.tiledimage import SourceArrayImage
77
from xcube.util.tiledimage import ColorMappedRgbaImage
88
from xcube.util.tiledimage import DirectRgbaImage
99
from xcube.util.tiledimage import OpImage
10-
from xcube.util.tiledimage import TransformArrayImage
10+
from xcube.util.tiledimage import NormalizeArrayImage
1111
from xcube.util.tiledimage import trim_tile
1212

1313

@@ -26,7 +26,7 @@ def compute_tile(self, tile_x, tile_y, rectangle):
2626
class ColorMappedRgbaImageTest(TestCase):
2727
def test_default(self):
2828
a = np.linspace(0, 255, 24, dtype=np.int32).reshape((4, 6))
29-
source_image = ArrayImage(a, (2, 2))
29+
source_image = SourceArrayImage(a, (2, 2))
3030
cm_rgb_image = ColorMappedRgbaImage(source_image, format='PNG')
3131
self.assertEqual(cm_rgb_image.size, (6, 4))
3232
self.assertEqual(cm_rgb_image.tile_size, (2, 2))
@@ -41,9 +41,9 @@ def test_default(self):
4141
b = np.linspace(255, 0, 24, dtype=np.int32).reshape((4, 6))
4242
c = np.linspace(50, 200, 24, dtype=np.int32).reshape((4, 6))
4343
source_images = [
44-
ArrayImage(a, (2, 2)),
45-
ArrayImage(b, (2, 2)),
46-
ArrayImage(c, (2, 2)),
44+
SourceArrayImage(a, (2, 2)),
45+
SourceArrayImage(b, (2, 2)),
46+
SourceArrayImage(c, (2, 2)),
4747
]
4848
cm_rgb_image = DirectRgbaImage(source_images, format='PNG')
4949
self.assertEqual(cm_rgb_image.size, (6, 4))
@@ -53,11 +53,11 @@ def test_default(self):
5353
self.assertIsInstance(tile, PIL.Image.Image)
5454

5555

56-
class TransformArrayImageTest(TestCase):
56+
class NormalizeArrayImageTest(TestCase):
5757
def test_default(self):
5858
a = np.arange(0, 24, dtype=np.int32).reshape((4, 6))
59-
source_image = ArrayImage(a, (2, 2))
60-
target_image = TransformArrayImage(source_image)
59+
source_image = SourceArrayImage(a, (2, 2))
60+
target_image = NormalizeArrayImage(source_image)
6161

6262
self.assertEqual(target_image.size, (6, 4))
6363
self.assertEqual(target_image.tile_size, (2, 2))
@@ -77,37 +77,86 @@ def test_default(self):
7777
self.assertEqual(target_image.get_tile(2, 1).tolist(), [[16, 17],
7878
[22, 23]])
7979

80-
def test_flip_y(self):
81-
a = np.arange(0, 24, dtype=np.int32).reshape((4, 6))
82-
source_image = ArrayImage(a, (2, 2))
83-
target_image = TransformArrayImage(source_image, flip_y=True)
80+
def test_force_2d(self):
81+
a = np.arange(0, 48, dtype=np.int32).reshape((2, 4, 6))
82+
source_image = SourceArrayImage(a[0], (2, 2))
83+
target_image = NormalizeArrayImage(source_image, force_2d=True)
8484

8585
self.assertEqual(target_image.size, (6, 4))
8686
self.assertEqual(target_image.tile_size, (2, 2))
8787
self.assertEqual(target_image.num_tiles, (3, 2))
8888

89-
self.assertEqual(target_image.get_tile(0, 0).tolist(), [[18, 19],
90-
[12, 13]])
91-
self.assertEqual(target_image.get_tile(1, 0).tolist(), [[20, 21],
92-
[14, 15]])
93-
self.assertEqual(target_image.get_tile(2, 0).tolist(), [[22, 23],
94-
[16, 17]])
9589

96-
self.assertEqual(target_image.get_tile(0, 1).tolist(), [[6, 7],
97-
[0, 1]])
98-
self.assertEqual(target_image.get_tile(1, 1).tolist(), [[8, 9],
99-
[2, 3]])
100-
self.assertEqual(target_image.get_tile(2, 1).tolist(), [[10, 11],
101-
[4, 5]])
102-
103-
def test_force_2d(self):
104-
a = np.arange(0, 48, dtype=np.int32).reshape((2, 4, 6))
105-
source_image = ArrayImage(a[0], (2, 2))
106-
target_image = TransformArrayImage(source_image, force_2d=True)
90+
class SourceArrayImageTest(TestCase):
10791

108-
self.assertEqual(target_image.size, (6, 4))
109-
self.assertEqual(target_image.tile_size, (2, 2))
110-
self.assertEqual(target_image.num_tiles, (3, 2))
92+
def test_flip_y_tiled_evenly(self):
93+
a = np.arange(0, 24, dtype=np.int32).reshape((4, 6))
94+
source_array_image = SourceArrayImage(a, (2, 2), flip_y=True)
95+
96+
self.assertEqual(source_array_image.size, (6, 4))
97+
self.assertEqual(source_array_image.tile_size, (2, 2))
98+
self.assertEqual(source_array_image.num_tiles, (3, 2))
99+
100+
self.assertEqual(source_array_image.get_tile(0, 0).tolist(),
101+
[[18, 19],
102+
[12, 13]])
103+
self.assertEqual(source_array_image.get_tile(1, 0).tolist(),
104+
[[20, 21],
105+
[14, 15]])
106+
self.assertEqual(source_array_image.get_tile(2, 0).tolist(),
107+
[[22, 23],
108+
[16, 17]])
109+
110+
self.assertEqual(source_array_image.get_tile(0, 1).tolist(),
111+
[[6, 7],
112+
[0, 1]])
113+
self.assertEqual(source_array_image.get_tile(1, 1).tolist(),
114+
[[8, 9],
115+
[2, 3]])
116+
self.assertEqual(source_array_image.get_tile(2, 1).tolist(),
117+
[[10, 11],
118+
[4, 5]])
119+
120+
def test_flip_y_not_tiled_evenly(self):
121+
a = np.arange(0, 30, dtype=np.int32).reshape((5, 6))
122+
source_array_image = SourceArrayImage(a, (2, 2), flip_y=True)
123+
124+
self.assertEqual(source_array_image.size, (6, 5))
125+
self.assertEqual(source_array_image.tile_size, (2, 2))
126+
self.assertEqual(source_array_image.num_tiles, (3, 3))
127+
128+
self.assertEqual(source_array_image.get_tile(0, 0).tolist(),
129+
[[24, 25],
130+
[18, 19]])
131+
self.assertEqual(source_array_image.get_tile(1, 0).tolist(),
132+
[[26, 27],
133+
[20, 21]])
134+
self.assertEqual(source_array_image.get_tile(2, 0).tolist(),
135+
[[28, 29],
136+
[22, 23]])
137+
138+
self.assertEqual(source_array_image.get_tile(0, 1).tolist(),
139+
[[12, 13],
140+
[6, 7]])
141+
self.assertEqual(source_array_image.get_tile(1, 1).tolist(),
142+
[[14, 15],
143+
[8, 9]])
144+
self.assertEqual(source_array_image.get_tile(2, 1).tolist(),
145+
[[16, 17],
146+
[10, 11]])
147+
148+
tile_0_2 = source_array_image.get_tile(0, 2).tolist()
149+
self.assertEqual(tile_0_2[0], [0, 1])
150+
self.assertTrue(np.isnan(tile_0_2[1][0]))
151+
self.assertTrue(np.isnan(tile_0_2[1][1]))
152+
tile_1_2 = source_array_image.get_tile(1, 2).tolist()
153+
self.assertEqual(tile_1_2[0], [2, 3])
154+
self.assertTrue(tile_1_2[1][0])
155+
self.assertTrue(tile_1_2[1][1])
156+
tile_2_2 = source_array_image.get_tile(2, 2).tolist()
157+
self.assertEqual(tile_2_2[0], [4, 5])
158+
self.assertTrue(tile_2_2[1][0])
159+
self.assertTrue(tile_2_2[1][1])
111160

112161

113162
class TrimTileTest(TestCase):

xcube/core/store/fs/impl/dataset.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def write_data(self,
195195
data: xr.Dataset,
196196
data_id: str,
197197
replace=False,
198-
**write_params):
198+
**write_params) -> str:
199199
assert_instance(data, xr.Dataset, name='data')
200200
assert_instance(data_id, str, name='data_id')
201201
fs, root, write_params = self.load_fs(write_params)
@@ -213,6 +213,7 @@ def write_data(self,
213213
except ValueError as e:
214214
raise DataStoreError(f'Failed to write'
215215
f' dataset {data_id!r}: {e}') from e
216+
return data_id
216217

217218
def delete_data(self,
218219
data_id: str,
@@ -281,7 +282,7 @@ def write_data(self,
281282
data: xr.Dataset,
282283
data_id: str,
283284
replace=False,
284-
**write_params):
285+
**write_params) -> str:
285286
assert_instance(data, xr.Dataset, name='data')
286287
assert_instance(data_id, str, name='data_id')
287288
fs, root, write_params = self.load_fs(write_params)
@@ -302,3 +303,4 @@ def write_data(self,
302303
data.to_netcdf(file_path, engine=engine, **write_params)
303304
if not is_local:
304305
fs.put_file(file_path, data_id)
306+
return data_id

xcube/core/store/fs/impl/geodataframe.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def get_write_data_params_schema(self) -> JsonObjectSchema:
8282
def write_data(self,
8383
data: gpd.GeoDataFrame,
8484
data_id: str,
85-
**write_params):
85+
**write_params) -> str:
8686
# TODO: implement me correctly,
8787
# this is not valid for shapefile AND geojson
8888
assert_instance(data, (gpd.GeoDataFrame, pd.DataFrame), 'data')
@@ -95,6 +95,7 @@ def write_data(self,
9595
data.to_file(file_path, driver=self.get_driver_name(), **write_params)
9696
if not is_local:
9797
fs.put_file(file_path, data_id)
98+
return data_id
9899

99100

100101
class GeoDataFrameShapefileFsDataAccessor(GeoDataFrameFsDataAccessor, ABC):

xcube/core/store/fs/impl/mldataset.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
21+
2122
import os.path
2223
from typing import Dict, Any, List, Union, Tuple, Optional
2324

@@ -80,7 +81,7 @@ def write_data(self,
8081
data: Union[xr.Dataset, MultiLevelDataset],
8182
data_id: str,
8283
replace: bool = False,
83-
**write_params):
84+
**write_params) -> str:
8485
assert_instance(data, (xr.Dataset, MultiLevelDataset), name='data')
8586
assert_instance(data_id, str, name='data_id')
8687
if isinstance(data, MultiLevelDataset):
@@ -100,7 +101,6 @@ def write_data(self,
100101

101102
for index in range(ml_dataset.num_levels):
102103
level_dataset = ml_dataset.get_dataset(index)
103-
104104
if base_dataset_id and index == 0:
105105
# Write file "0.link" instead of copying
106106
# level-0 dataset to "0.zarr"
@@ -128,6 +128,9 @@ def write_data(self,
128128
consolidated=consolidated)
129129
ml_dataset.set_dataset(index, level_dataset)
130130

131+
return data_id
132+
133+
131134

132135
class FsMultiLevelDataset(LazyMultiLevelDataset):
133136
def __init__(self,

xcube/core/store/fs/store.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@
3434
from xcube.util.assertions import assert_given
3535
from xcube.util.assertions import assert_in
3636
from xcube.util.assertions import assert_instance
37+
from xcube.util.assertions import assert_true
3738
from xcube.util.extension import Extension
3839
from xcube.util.jsonschema import JsonBooleanSchema
3940
from xcube.util.jsonschema import JsonIntegerSchema
4041
from xcube.util.jsonschema import JsonObjectSchema
4142
from xcube.util.jsonschema import JsonStringSchema
42-
from .accessor import STORAGE_OPTIONS_PARAM_NAME
4343
from .accessor import FsAccessor
44+
from .accessor import STORAGE_OPTIONS_PARAM_NAME
4445
from .helpers import is_local_fs
4546
from .helpers import normalize_path
4647
from ..accessor import DataOpener
@@ -299,12 +300,23 @@ def write_data(self,
299300
data_id = self._ensure_valid_data_id(writer_id, data_id=data_id)
300301
fs_path = self._convert_data_id_into_fs_path(data_id)
301302
self.fs.makedirs(self.root, exist_ok=True)
302-
writer.write_data(data,
303-
fs_path,
304-
replace=replace,
305-
fs=self.fs,
306-
root=self.root,
307-
**write_params)
303+
written_fs_path = writer.write_data(data,
304+
fs_path,
305+
replace=replace,
306+
fs=self.fs,
307+
root=self.root,
308+
**write_params)
309+
# Verify, accessors fulfill their write_data() contract
310+
assert_true(fs_path == written_fs_path,
311+
message='FsDataAccessor implementations must '
312+
'return the data_id passed in.')
313+
# Return original data_id (which is a relative path).
314+
# Note: it would be cleaner to return written_fs_path
315+
# here, but it is an absolute path.
316+
# --> Possible solution: FsDataAccessor implementations
317+
# should be responsible for resolving their data_id into a
318+
# filesystem path by providing them both with fs and root
319+
# arguments.
308320
return data_id
309321

310322
def get_delete_data_params_schema(self, data_id: str = None) \

0 commit comments

Comments
 (0)