Skip to content

Commit e987fe3

Browse files
make all TileGridInterface subclasses private again #883
1 parent a45a4e2 commit e987fe3

2 files changed

Lines changed: 46 additions & 46 deletions

File tree

openeo/extra/job_management/_job_splitting.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class JobSplittingFailure(Exception):
1717
pass
1818

1919

20-
class TileGridInterface(metaclass=abc.ABCMeta):
20+
class _TileGridInterface(metaclass=abc.ABCMeta):
2121
"""
2222
Interface for tile grid classes that split a geometry into tiles.
2323
@@ -99,7 +99,7 @@ def _reproject_to_grid_crs(
9999
return src.to_crs(f"EPSG:{grid_epsg}").geometry[0]
100100

101101

102-
class SizeBasedTileGrid(TileGridInterface):
102+
class _SizeBasedTileGrid(_TileGridInterface):
103103
"""
104104
Tile grid that splits a geometry into regular tiles of a given size.
105105
@@ -175,7 +175,7 @@ def get_tiles(self, geometry: Union[Dict, Polygon, MultiPolygon]) -> gpd.GeoData
175175
return gpd.GeoDataFrame(geometry=polygons, crs=f"EPSG:{self._epsg}")
176176

177177

178-
class PredefinedTileGrid(TileGridInterface):
178+
class _PredefinedTileGrid(_TileGridInterface):
179179
"""
180180
Tile grid based on a user-supplied collection of geometries.
181181
@@ -241,18 +241,18 @@ def split_area(
241241
*,
242242
projection: Optional[str] = None,
243243
tile_size: Optional[float] = None,
244-
tile_grid: Optional[Union[TileGridInterface, gpd.GeoDataFrame]] = None,
244+
tile_grid: Optional[Union[_TileGridInterface, gpd.GeoDataFrame]] = None,
245245
) -> gpd.GeoDataFrame:
246246
"""
247247
Split an area of interest into tiles.
248248
249249
There are two ways to define how the area is tiled:
250250
251251
1. **By tile size and projection** — pass *projection* and *tile_size*.
252-
A :class:`SizeBasedTileGrid` is created under the hood.
252+
A :class:`_SizeBasedTileGrid` is created under the hood.
253253
254-
2. **By pre-defined grid** — pass a :class:`TileGridInterface` instance
255-
(e.g. :class:`PredefinedTileGrid`) or a :class:`~geopandas.GeoDataFrame`
254+
2. **By pre-defined grid** — pass a :class:`_TileGridInterface` instance
255+
(e.g. :class:`_PredefinedTileGrid`) or a :class:`~geopandas.GeoDataFrame`
256256
as *tile_grid*.
257257
258258
:param aoi: area of interest as a bounding-box dict
@@ -263,7 +263,7 @@ def split_area(
263263
Required when *tile_grid* is not supplied.
264264
:param tile_size: tile edge length in the unit of the projection.
265265
Required when *tile_grid* is not supplied.
266-
:param tile_grid: a :class:`TileGridInterface` instance or a
266+
:param tile_grid: a :class:`_TileGridInterface` instance or a
267267
:class:`~geopandas.GeoDataFrame` (with CRS set) that defines the tiling
268268
strategy. Mutually exclusive with *projection* / *tile_size*.
269269
:return: :class:`~geopandas.GeoDataFrame` with one row per tile and CRS set.
@@ -273,10 +273,10 @@ def split_area(
273273
if projection is not None or tile_size is not None:
274274
raise JobSplittingFailure(
275275
"Cannot combine 'tile_grid' with 'projection' or 'tile_size'. "
276-
"Either pass a TileGridInterface, or pass projection + tile_size."
276+
"Either pass a _TileGridInterface, or pass projection + tile_size."
277277
)
278278
if isinstance(tile_grid, gpd.GeoDataFrame):
279-
tile_grid = PredefinedTileGrid(tiles=tile_grid)
279+
tile_grid = _PredefinedTileGrid(tiles=tile_grid)
280280
return tile_grid.get_tiles(aoi)
281281

282282
# --- Size-based splitting path ---
@@ -286,5 +286,5 @@ def split_area(
286286
if projection is None:
287287
raise JobSplittingFailure("'projection' is required when using size-based tiling.")
288288

289-
grid = SizeBasedTileGrid(epsg=normalize_crs(projection), size=tile_size)
289+
grid = _SizeBasedTileGrid(epsg=normalize_crs(projection), size=tile_size)
290290
return grid.get_tiles(aoi)

tests/extra/job_management/test_job_splitting.py

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,39 @@
44

55
from openeo.extra.job_management._job_splitting import (
66
JobSplittingFailure,
7-
PredefinedTileGrid,
8-
SizeBasedTileGrid,
9-
TileGridInterface,
7+
_PredefinedTileGrid,
8+
_SizeBasedTileGrid,
9+
_TileGridInterface,
1010
split_area,
1111
)
1212
from openeo.util import BBoxDict
1313

1414

1515
class TestSizeBasedTileGrid:
1616
def test_from_size_projection(self):
17-
splitter = SizeBasedTileGrid.from_size_projection(size=0.1, projection="EPSG:4326")
17+
splitter = _SizeBasedTileGrid.from_size_projection(size=0.1, projection="EPSG:4326")
1818
assert splitter.crs == 4326
1919
assert splitter.size == 0.1
2020

2121
def test_constructor_rejects_unparseable_epsg(self):
2222
"""An invalid CRS string should be caught and raised as JobSplittingFailure."""
2323
with pytest.raises(JobSplittingFailure, match="Failed to normalize EPSG code"):
24-
SizeBasedTileGrid(epsg="not_a_crs", size=1.0)
24+
_SizeBasedTileGrid(epsg="not_a_crs", size=1.0)
2525

2626
def test_constructor_rejects_unknown_epsg(self):
2727
"""An EPSG code unknown to pyproj is caught at construction time."""
2828
with pytest.raises(JobSplittingFailure, match="Failed to normalize EPSG code"):
29-
SizeBasedTileGrid(epsg=999999, size=1.0)
29+
_SizeBasedTileGrid(epsg=999999, size=1.0)
3030

3131
def test_get_tiles_raises_exception(self):
3232
"""test get_tiles when the input geometry is not a dict or shapely.geometry.Polygon"""
33-
tile_grid = SizeBasedTileGrid(epsg=4326, size=0.1)
33+
tile_grid = _SizeBasedTileGrid(epsg=4326, size=0.1)
3434
with pytest.raises(JobSplittingFailure):
3535
tile_grid.get_tiles("invalid_geometry")
3636

3737
def test_get_tiles_returns_geodataframe(self):
3838
aoi = {"west": 0.0, "south": 0.0, "east": 1.0, "north": 1.0, "crs": "EPSG:4326"}
39-
tile_grid = SizeBasedTileGrid(epsg=4326, size=1.0)
39+
tile_grid = _SizeBasedTileGrid(epsg=4326, size=1.0)
4040
result = tile_grid.get_tiles(aoi)
4141
assert isinstance(result, gpd.GeoDataFrame)
4242
assert result.crs is not None
@@ -45,55 +45,55 @@ def test_get_tiles_returns_geodataframe(self):
4545
def test_simple_get_tiles_dict(self):
4646
"""tile grid size equals input geometry size -> single tile returned."""
4747
aoi = {"west": 0.0, "south": 0.0, "east": 100_000.0, "north": 100_000.0, "crs": "EPSG:3857"}
48-
tile_grid = SizeBasedTileGrid(epsg=3857, size=100_000)
48+
tile_grid = _SizeBasedTileGrid(epsg=3857, size=100_000)
4949
result = tile_grid.get_tiles(aoi)
5050
assert len(result) == 1
5151
assert result.geometry[0].equals(shapely.geometry.box(0.0, 0.0, 100_000.0, 100_000.0))
5252

5353
def test_multiple_get_tile_dict(self):
5454
"""tile grid smaller than input -> multiple tiles."""
5555
aoi = {"west": 0.0, "south": 0.0, "east": 100_000.0, "north": 100_000.0, "crs": "EPSG:3857"}
56-
tile_grid = SizeBasedTileGrid(epsg=3857, size=20_000)
56+
tile_grid = _SizeBasedTileGrid(epsg=3857, size=20_000)
5757
result = tile_grid.get_tiles(aoi)
5858
assert len(result) == 25
5959
assert result.geometry[0].equals(shapely.geometry.box(0.0, 0.0, 20_000.0, 20_000.0))
6060

6161
def test_larger_get_tile_dict(self):
6262
"""tile grid larger than input -> single tile clipped to input."""
6363
aoi = {"west": 0.0, "south": 0.0, "east": 100_000.0, "north": 100_000.0, "crs": "EPSG:3857"}
64-
tile_grid = SizeBasedTileGrid(epsg=3857, size=200_000)
64+
tile_grid = _SizeBasedTileGrid(epsg=3857, size=200_000)
6565
result = tile_grid.get_tiles(aoi)
6666
assert len(result) == 1
6767
assert result.geometry[0].equals(shapely.geometry.box(0.0, 0.0, 100_000.0, 100_000.0))
6868

6969
def test_get_tiles_polygon_wgs(self):
7070
"""polygon in WGS84 with WGS84 tile grid."""
7171
polygon = shapely.geometry.box(0.0, 0.0, 1.0, 1.0)
72-
tile_grid = SizeBasedTileGrid(epsg=4326, size=0.1)
72+
tile_grid = _SizeBasedTileGrid(epsg=4326, size=0.1)
7373
result = tile_grid.get_tiles(polygon)
7474
assert len(result) == 100
7575
assert result.geometry[0].equals(shapely.geometry.box(0.0, 0.0, 0.1, 0.1))
7676

7777
def test_simple_get_tiles_polygon(self):
7878
"""tile grid size equals polygon size -> single tile."""
7979
polygon = shapely.geometry.box(0.0, 0.0, 100_000.0, 100_000.0)
80-
tile_grid = SizeBasedTileGrid(epsg=3857, size=100_000.0)
80+
tile_grid = _SizeBasedTileGrid(epsg=3857, size=100_000.0)
8181
result = tile_grid.get_tiles(polygon)
8282
assert len(result) == 1
8383
assert result.geometry[0].equals(polygon)
8484

8585
def test_larger_get_tiles_polygon(self):
8686
"""tile grid larger than polygon -> single tile clipped to polygon."""
8787
polygon = shapely.geometry.box(0.0, 0.0, 100_000.0, 100_000.0)
88-
tile_grid = SizeBasedTileGrid(epsg=3857, size=200_000.0)
88+
tile_grid = _SizeBasedTileGrid(epsg=3857, size=200_000.0)
8989
result = tile_grid.get_tiles(polygon)
9090
assert len(result) == 1
9191
assert result.geometry[0].equals(polygon)
9292

9393
def test_edge_tiles_clipped_to_aoi(self):
9494
"""When the AOI is not a multiple of tile_size, edge tiles are smaller."""
9595
aoi = {"west": 0.0, "south": 0.0, "east": 150_000.0, "north": 150_000.0, "crs": "EPSG:3857"}
96-
grid = SizeBasedTileGrid(epsg=3857, size=100_000)
96+
grid = _SizeBasedTileGrid(epsg=3857, size=100_000)
9797
result = grid.get_tiles(aoi)
9898
assert len(result) == 4 # 2x2 grid
9999
# The right/top edge tiles should be 50k wide/tall, not 100k
@@ -107,7 +107,7 @@ def test_reprojects_bbox_when_crs_differs(self):
107107
"""AOI in EPSG:4326 should be reprojected to the tile grid's CRS (EPSG:3857) before splitting."""
108108
# A small bbox around lon=0, lat=0 in WGS84
109109
aoi = {"west": -1.0, "south": -1.0, "east": 1.0, "north": 1.0, "crs": "EPSG:4326"}
110-
grid = SizeBasedTileGrid(epsg=3857, size=500_000)
110+
grid = _SizeBasedTileGrid(epsg=3857, size=500_000)
111111
result = grid.get_tiles(aoi)
112112
assert result.crs.to_epsg() == 3857
113113
# AOI-relative tiling: ~222 km bbox fits in one 500 km tile
@@ -120,7 +120,7 @@ def test_reprojects_bbox_when_crs_differs(self):
120120
def test_no_reprojection_when_crs_matches(self):
121121
"""When AOI CRS matches the tile grid CRS, no reprojection should happen."""
122122
aoi = {"west": 0.0, "south": 0.0, "east": 100_000.0, "north": 100_000.0, "crs": "EPSG:3857"}
123-
grid = SizeBasedTileGrid(epsg=3857, size=100_000)
123+
grid = _SizeBasedTileGrid(epsg=3857, size=100_000)
124124
result = grid.get_tiles(aoi)
125125
assert len(result) == 1
126126
assert result.geometry[0].equals(shapely.geometry.box(0.0, 0.0, 100_000.0, 100_000.0))
@@ -129,7 +129,7 @@ def test_no_reprojection_when_crs_matches(self):
129129
class TestPredefinedTileGrid:
130130
def test_basic_from_list(self):
131131
tiles = [shapely.geometry.box(0, 0, 1, 1), shapely.geometry.box(1, 0, 2, 1)]
132-
grid = PredefinedTileGrid(tiles=tiles, crs=4326)
132+
grid = _PredefinedTileGrid(tiles=tiles, crs=4326)
133133
result = grid.get_tiles(shapely.geometry.box(0.5, 0.5, 1.5, 0.75))
134134
assert isinstance(result, gpd.GeoDataFrame)
135135
assert len(result) == 2
@@ -139,7 +139,7 @@ def test_from_geodataframe(self):
139139
geometry=[shapely.geometry.box(0, 0, 1, 1), shapely.geometry.box(1, 0, 2, 1)],
140140
crs="EPSG:4326",
141141
)
142-
grid = PredefinedTileGrid(tiles=gdf)
142+
grid = _PredefinedTileGrid(tiles=gdf)
143143
result = grid.get_tiles(shapely.geometry.box(0.5, 0.5, 1.5, 0.75))
144144
assert len(result) == 2
145145
assert result.crs.to_epsg() == 4326
@@ -149,14 +149,14 @@ def test_from_geoseries(self):
149149
[shapely.geometry.box(0, 0, 1, 1), shapely.geometry.box(1, 0, 2, 1)],
150150
crs="EPSG:4326",
151151
)
152-
grid = PredefinedTileGrid(tiles=gs)
152+
grid = _PredefinedTileGrid(tiles=gs)
153153
result = grid.get_tiles(shapely.geometry.box(0.5, 0.5, 1.5, 0.75))
154154
assert len(result) == 2
155155

156156
def test_multipolygon_tiles(self):
157157
"""Tiles may be MultiPolygons."""
158158
mp = shapely.geometry.MultiPolygon([shapely.geometry.box(0, 0, 1, 1), shapely.geometry.box(2, 2, 3, 3)])
159-
grid = PredefinedTileGrid(tiles=[mp], crs=4326)
159+
grid = _PredefinedTileGrid(tiles=[mp], crs=4326)
160160
result = grid.get_tiles(shapely.geometry.box(0.5, 0.5, 0.8, 0.8))
161161
assert len(result) == 1
162162

@@ -166,7 +166,7 @@ def test_preserves_extra_columns(self):
166166
{"name": ["A", "B"], "geometry": [shapely.geometry.box(0, 0, 1, 1), shapely.geometry.box(5, 5, 6, 6)]},
167167
crs="EPSG:4326",
168168
)
169-
grid = PredefinedTileGrid(tiles=gdf)
169+
grid = _PredefinedTileGrid(tiles=gdf)
170170
result = grid.get_tiles(shapely.geometry.box(0, 0, 2, 2))
171171
assert len(result) == 1
172172
assert result["name"].iloc[0] == "A"
@@ -177,65 +177,65 @@ def test_filters_non_intersecting(self):
177177
shapely.geometry.box(1, 0, 2, 1),
178178
shapely.geometry.box(5, 5, 6, 6),
179179
]
180-
grid = PredefinedTileGrid(tiles=tiles, crs=4326)
180+
grid = _PredefinedTileGrid(tiles=tiles, crs=4326)
181181
result = grid.get_tiles(shapely.geometry.box(0.5, 0.5, 1.5, 0.75))
182182
assert len(result) == 2
183183

184184
def test_reprojects_query_geometry_when_crs_differs(self):
185185
"""AOI dict in EPSG:4326 with tile grid in EPSG:3857 should reproject before intersection."""
186186
# Tile covering roughly lon [-1, 1], lat [-1, 1] in 3857 meters
187187
tile_3857 = shapely.geometry.box(-111_320, -111_325, 111_320, 111_325)
188-
grid = PredefinedTileGrid(tiles=[tile_3857], crs=3857)
188+
grid = _PredefinedTileGrid(tiles=[tile_3857], crs=3857)
189189
# Query in WGS84 degrees — overlaps the tile after reprojection
190190
result = grid.get_tiles({"west": -0.5, "south": -0.5, "east": 0.5, "north": 0.5, "crs": "EPSG:4326"})
191191
assert len(result) == 1
192192

193193
def test_no_match_after_reprojection(self):
194194
"""AOI that doesn't overlap tiles after reprojection should return empty."""
195195
tile_3857 = shapely.geometry.box(-111_320, -111_325, 111_320, 111_325)
196-
grid = PredefinedTileGrid(tiles=[tile_3857], crs=3857)
196+
grid = _PredefinedTileGrid(tiles=[tile_3857], crs=3857)
197197
# Query far away from the tile
198198
result = grid.get_tiles({"west": 50, "south": 50, "east": 51, "north": 51, "crs": "EPSG:4326"})
199199
assert len(result) == 0
200200

201201
def test_dict_geometry(self):
202202
tiles = [shapely.geometry.box(0, 0, 1, 1), shapely.geometry.box(2, 2, 3, 3)]
203-
grid = PredefinedTileGrid(tiles=tiles, crs=4326)
203+
grid = _PredefinedTileGrid(tiles=tiles, crs=4326)
204204
result = grid.get_tiles({"west": 0.5, "south": 0.5, "east": 0.8, "north": 0.8})
205205
assert len(result) == 1
206206
assert result.geometry[0].equals(shapely.geometry.box(0, 0, 1, 1))
207207

208208
def test_empty_list_raises(self):
209209
with pytest.raises(JobSplittingFailure, match="At least one tile"):
210-
PredefinedTileGrid(tiles=[], crs=4326)
210+
_PredefinedTileGrid(tiles=[], crs=4326)
211211

212212
def test_empty_geodataframe_raises(self):
213213
gdf = gpd.GeoDataFrame(geometry=[], crs="EPSG:4326")
214214
with pytest.raises(JobSplittingFailure, match="at least one row"):
215-
PredefinedTileGrid(tiles=gdf)
215+
_PredefinedTileGrid(tiles=gdf)
216216

217217
def test_invalid_tile_type_raises(self):
218218
with pytest.raises(JobSplittingFailure, match="All tiles must be"):
219-
PredefinedTileGrid(tiles=["not_a_polygon"], crs=4326)
219+
_PredefinedTileGrid(tiles=["not_a_polygon"], crs=4326)
220220

221221
def test_list_without_crs_raises(self):
222222
with pytest.raises(JobSplittingFailure, match="'crs' is required"):
223-
PredefinedTileGrid(tiles=[shapely.geometry.box(0, 0, 1, 1)])
223+
_PredefinedTileGrid(tiles=[shapely.geometry.box(0, 0, 1, 1)])
224224

225225
def test_geodataframe_without_crs_and_no_crs_arg_raises(self):
226226
gdf = gpd.GeoDataFrame(geometry=[shapely.geometry.box(0, 0, 1, 1)])
227227
with pytest.raises(JobSplittingFailure, match="no CRS set"):
228-
PredefinedTileGrid(tiles=gdf)
228+
_PredefinedTileGrid(tiles=gdf)
229229

230230
def test_geodataframe_without_crs_uses_crs_arg(self):
231231
gdf = gpd.GeoDataFrame(geometry=[shapely.geometry.box(0, 0, 1, 1)])
232-
grid = PredefinedTileGrid(tiles=gdf, crs=4326)
232+
grid = _PredefinedTileGrid(tiles=gdf, crs=4326)
233233
result = grid.get_tiles(shapely.geometry.box(0, 0, 1, 1))
234234
assert result.crs.to_epsg() == 4326
235235

236236
def test_invalid_geometry_type_raises(self):
237237
tiles = [shapely.geometry.box(0, 0, 1, 1)]
238-
grid = PredefinedTileGrid(tiles=tiles, crs=4326)
238+
grid = _PredefinedTileGrid(tiles=tiles, crs=4326)
239239
with pytest.raises(JobSplittingFailure, match="Expected a bounding-box dict"):
240240
grid.get_tiles("invalid")
241241

@@ -270,13 +270,13 @@ def test_no_projection_no_crs_raises(self):
270270
split_area(aoi, tile_size=1.0)
271271

272272
def test_tile_grid_mutually_exclusive(self):
273-
grid = PredefinedTileGrid(tiles=[shapely.geometry.box(0, 0, 1, 1)], crs=4326)
273+
grid = _PredefinedTileGrid(tiles=[shapely.geometry.box(0, 0, 1, 1)], crs=4326)
274274
with pytest.raises(JobSplittingFailure, match="Cannot combine"):
275275
split_area(shapely.geometry.box(0, 0, 1, 1), projection="EPSG:4326", tile_grid=grid)
276276

277277
def test_with_predefined_tile_grid(self):
278278
grid_tiles = [shapely.geometry.box(0, 0, 0.5, 0.5), shapely.geometry.box(0.5, 0, 1, 0.5)]
279-
grid = PredefinedTileGrid(tiles=grid_tiles, crs=4326)
279+
grid = _PredefinedTileGrid(tiles=grid_tiles, crs=4326)
280280
aoi = shapely.geometry.box(0, 0, 1, 0.5)
281281
result = split_area(aoi, tile_grid=grid)
282282
assert len(result) == 2

0 commit comments

Comments
 (0)