1
+ import re
1
2
from pathlib import Path
2
- from typing import List
3
3
4
4
import pytest
5
5
6
+ from openeo import Connection
6
7
from openeo .internal .graph_building import PGNode
7
8
from openeo .rest .vectorcube import VectorCube
8
9
@@ -13,42 +14,101 @@ def vector_cube(con100) -> VectorCube:
13
14
return VectorCube (graph = pgnode , connection = con100 )
14
15
15
16
16
- class DownloadSpy :
17
+ class DummyBackend :
17
18
"""
18
- Test helper to track download requests and optionally override next response to return.
19
+ Dummy backend that handles sync/batch execution requests
20
+ and allows inspection of posted process graphs
19
21
"""
20
22
21
- __slots__ = ["requests" , "next_response" ]
23
+ def __init__ (self , requests_mock , connection : Connection ):
24
+ self .connection = connection
25
+ self .sync_requests = []
26
+ self .batch_jobs = {}
27
+ self .next_result = b"Result data"
28
+ requests_mock .post (connection .build_url ("/result" ), content = self ._handle_post_result )
29
+ requests_mock .post (connection .build_url ("/jobs" ), content = self ._handle_post_jobs )
30
+ requests_mock .post (
31
+ re .compile (connection .build_url (r"/jobs/(job-\d+)/results$" )), content = self ._handle_post_job_results
32
+ )
33
+ requests_mock .get (re .compile (connection .build_url (r"/jobs/(job-\d+)$" )), json = self ._handle_get_job )
34
+ requests_mock .get (
35
+ re .compile (connection .build_url (r"/jobs/(job-\d+)/results$" )), json = self ._handle_get_job_results
36
+ )
37
+ requests_mock .get (
38
+ re .compile (connection .build_url ("/jobs/(.*?)/results/result.data$" )),
39
+ content = self ._handle_get_job_result_asset ,
40
+ )
22
41
23
- def __init__ (self ):
24
- self .requests : List [dict ] = []
25
- self .next_response : bytes = b"Spy data"
42
+ def _handle_post_result (self , request , context ):
43
+ """handler of `POST /result` (synchronous execute)"""
44
+ pg = request .json ()["process" ]["process_graph" ]
45
+ self .sync_requests .append (pg )
46
+ return self .next_result
26
47
27
- @property
28
- def only_request (self ) -> dict :
29
- """Get progress graph of only request done"""
30
- assert len (self .requests ) == 1
31
- return self .requests [- 1 ]
48
+ def _handle_post_jobs (self , request , context ):
49
+ """handler of `POST /jobs` (create batch job)"""
50
+ pg = request .json ()["process" ]["process_graph" ]
51
+ job_id = f"job-{ len (self .batch_jobs ):03d} "
52
+ self .batch_jobs [job_id ] = {"job_id" : job_id , "pg" : pg , "status" : "created" }
53
+ context .status_code = 201
54
+ context .headers ["openeo-identifier" ] = job_id
32
55
33
- @property
34
- def last_request (self ) -> dict :
35
- """Get last progress graph"""
36
- assert len (self .requests ) > 0
37
- return self .requests [- 1 ]
56
+ def _get_job_id (self , request ) -> str :
57
+ match = re .match (r"^/jobs/(job-\d+)(/|$)" , request .path )
58
+ if not match :
59
+ raise ValueError (f"Failed to extract job_id from { request .path } " )
60
+ job_id = match .group (1 )
61
+ assert job_id in self .batch_jobs
62
+ return job_id
38
63
64
+ def _handle_post_job_results (self , request , context ):
65
+ """Handler of `POST /job/{job_id}/results` (start batch job)."""
66
+ job_id = self ._get_job_id (request )
67
+ assert self .batch_jobs [job_id ]["status" ] == "created"
68
+ # TODO: support custom status sequence (instead of directly going to status "finished")?
69
+ self .batch_jobs [job_id ]["status" ] = "finished"
70
+ context .status_code = 202
39
71
40
- @ pytest . fixture
41
- def download_spy ( requests_mock , con100 ) -> DownloadSpy :
42
- """Test fixture to spy on (and mock) `POST /result` (download) requests."""
43
- spy = DownloadSpy ()
72
+ def _handle_get_job ( self , request , context ):
73
+ """Handler of `GET /job/{job_id}` (get batch job status and metadata)."""
74
+ job_id = self . _get_job_id ( request )
75
+ return { "id" : job_id , "status" : self . batch_jobs [ job_id ][ "status" ]}
44
76
45
- def post_result (request , context ):
46
- pg = request .json ()["process" ]["process_graph" ]
47
- spy .requests .append (pg )
48
- return spy .next_response
77
+ def _handle_get_job_results (self , request , context ):
78
+ """Handler of `GET /job/{job_id}/results` (list batch job results)."""
79
+ job_id = self ._get_job_id (request )
80
+ assert self .batch_jobs [job_id ]["status" ] == "finished"
81
+ return {
82
+ "id" : job_id ,
83
+ "assets" : {"result.data" : {"href" : self .connection .build_url (f"/jobs/{ job_id } /results/result.data" )}},
84
+ }
85
+
86
+ def _handle_get_job_result_asset (self , request , context ):
87
+ """Handler of `GET /job/{job_id}/results/result.data` (get batch job result asset)."""
88
+ job_id = self ._get_job_id (request )
89
+ assert self .batch_jobs [job_id ]["status" ] == "finished"
90
+ return self .next_result
91
+
92
+ def get_sync_pg (self ) -> dict :
93
+ """Get one and only synchronous process graph"""
94
+ assert len (self .sync_requests ) == 1
95
+ return self .sync_requests [0 ]
96
+
97
+ def get_batch_pg (self ) -> dict :
98
+ """Get one and only batch process graph"""
99
+ assert len (self .batch_jobs ) == 1
100
+ return self .batch_jobs [max (self .batch_jobs .keys ())]["pg" ]
49
101
50
- requests_mock .post (con100 .build_url ("/result" ), content = post_result )
51
- yield spy
102
+ def get_pg (self ) -> dict :
103
+ """Get one and only batch process graph (sync or batch)"""
104
+ pgs = self .sync_requests + [b ["pg" ] for b in self .batch_jobs .values ()]
105
+ assert len (pgs ) == 1
106
+ return pgs [0 ]
107
+
108
+
109
+ @pytest .fixture
110
+ def dummy_backend (requests_mock , con100 ) -> DummyBackend :
111
+ yield DummyBackend (requests_mock = requests_mock , connection = con100 )
52
112
53
113
54
114
def test_raster_to_vector (con100 ):
@@ -91,13 +151,19 @@ def test_raster_to_vector(con100):
91
151
],
92
152
)
93
153
@pytest .mark .parametrize ("path_class" , [str , Path ])
154
+ @pytest .mark .parametrize ("exec_mode" , ["sync" , "batch" ])
94
155
def test_download_auto_save_result_only_file (
95
- vector_cube , download_spy , tmp_path , filename , expected_format , path_class
156
+ vector_cube , dummy_backend , tmp_path , filename , expected_format , path_class , exec_mode
96
157
):
97
158
output_path = tmp_path / filename
98
- vector_cube .download (path_class (output_path ))
159
+ if exec_mode == "sync" :
160
+ vector_cube .download (path_class (output_path ))
161
+ elif exec_mode == "batch" :
162
+ vector_cube .execute_batch (outputfile = path_class (output_path ))
163
+ else :
164
+ raise ValueError (exec_mode )
99
165
100
- assert download_spy . only_request == {
166
+ assert dummy_backend . get_pg () == {
101
167
"createvectorcube1" : {"process_id" : "create_vector_cube" , "arguments" : {}},
102
168
"saveresult1" : {
103
169
"process_id" : "save_result" ,
@@ -109,7 +175,7 @@ def test_download_auto_save_result_only_file(
109
175
"result" : True ,
110
176
},
111
177
}
112
- assert output_path .read_bytes () == b"Spy data"
178
+ assert output_path .read_bytes () == b"Result data"
113
179
114
180
115
181
@pytest .mark .parametrize (
@@ -126,11 +192,19 @@ def test_download_auto_save_result_only_file(
126
192
# TODO #449 more formats to autodetect?
127
193
],
128
194
)
129
- def test_download_auto_save_result_with_format (vector_cube , download_spy , tmp_path , filename , format , expected_format ):
195
+ @pytest .mark .parametrize ("exec_mode" , ["sync" , "batch" ])
196
+ def test_download_auto_save_result_with_format (
197
+ vector_cube , dummy_backend , tmp_path , filename , format , expected_format , exec_mode
198
+ ):
130
199
output_path = tmp_path / filename
131
- vector_cube .download (output_path , format = format )
200
+ if exec_mode == "sync" :
201
+ vector_cube .download (output_path , format = format )
202
+ elif exec_mode == "batch" :
203
+ vector_cube .execute_batch (outputfile = output_path , out_format = format )
204
+ else :
205
+ raise ValueError (exec_mode )
132
206
133
- assert download_spy . only_request == {
207
+ assert dummy_backend . get_pg () == {
134
208
"createvectorcube1" : {"process_id" : "create_vector_cube" , "arguments" : {}},
135
209
"saveresult1" : {
136
210
"process_id" : "save_result" ,
@@ -142,14 +216,23 @@ def test_download_auto_save_result_with_format(vector_cube, download_spy, tmp_pa
142
216
"result" : True ,
143
217
},
144
218
}
145
- assert output_path .read_bytes () == b"Spy data"
219
+ assert output_path .read_bytes () == b"Result data"
146
220
147
221
148
- def test_download_auto_save_result_with_options (vector_cube , download_spy , tmp_path ):
222
+ @pytest .mark .parametrize ("exec_mode" , ["sync" , "batch" ])
223
+ def test_download_auto_save_result_with_options (vector_cube , dummy_backend , tmp_path , exec_mode ):
149
224
output_path = tmp_path / "result.json"
150
- vector_cube .download (output_path , format = "GeoJSON" , options = {"precision" : 7 })
225
+ format = "GeoJSON"
226
+ options = {"precision" : 7 }
151
227
152
- assert download_spy .only_request == {
228
+ if exec_mode == "sync" :
229
+ vector_cube .download (output_path , format = format , options = options )
230
+ elif exec_mode == "batch" :
231
+ vector_cube .execute_batch (outputfile = output_path , out_format = format , ** options )
232
+ else :
233
+ raise ValueError (exec_mode )
234
+
235
+ assert dummy_backend .get_pg () == {
153
236
"createvectorcube1" : {"process_id" : "create_vector_cube" , "arguments" : {}},
154
237
"saveresult1" : {
155
238
"process_id" : "save_result" ,
@@ -161,7 +244,7 @@ def test_download_auto_save_result_with_options(vector_cube, download_spy, tmp_p
161
244
"result" : True ,
162
245
},
163
246
}
164
- assert output_path .read_bytes () == b"Spy data"
247
+ assert output_path .read_bytes () == b"Result data"
165
248
166
249
167
250
@pytest .mark .parametrize (
@@ -173,17 +256,26 @@ def test_download_auto_save_result_with_options(vector_cube, download_spy, tmp_p
173
256
("result.nc" , "netCDF" , "netCDF" ),
174
257
],
175
258
)
176
- def test_save_result_and_download (vector_cube , download_spy , tmp_path , output_file , format , expected_format ):
259
+ @pytest .mark .parametrize ("exec_mode" , ["sync" , "batch" ])
260
+ def test_save_result_and_download (
261
+ vector_cube , dummy_backend , tmp_path , output_file , format , expected_format , exec_mode
262
+ ):
177
263
"""e.g. https://github.com/Open-EO/openeo-geopyspark-driver/issues/477"""
178
264
vector_cube = vector_cube .save_result (format = format )
179
265
output_path = tmp_path / output_file
180
- vector_cube .download (output_path )
181
- assert download_spy .only_request == {
266
+ if exec_mode == "sync" :
267
+ vector_cube .download (output_path )
268
+ elif exec_mode == "batch" :
269
+ vector_cube .execute_batch (outputfile = output_path )
270
+ else :
271
+ raise ValueError (exec_mode )
272
+
273
+ assert dummy_backend .get_pg () == {
182
274
"createvectorcube1" : {"process_id" : "create_vector_cube" , "arguments" : {}},
183
275
"saveresult1" : {
184
276
"process_id" : "save_result" ,
185
277
"arguments" : {"data" : {"from_node" : "createvectorcube1" }, "format" : expected_format , "options" : {}},
186
278
"result" : True ,
187
279
},
188
280
}
189
- assert output_path .read_bytes () == b"Spy data"
281
+ assert output_path .read_bytes () == b"Result data"
0 commit comments