Skip to content

Commit 4585eac

Browse files
authored
Merge pull request #107 from sentinel-hub/feat/proc-api-request
Convenience request for the Processing API
2 parents fd3222a + fb29c27 commit 4585eac

File tree

6 files changed

+567
-361
lines changed

6 files changed

+567
-361
lines changed

docs/source/docs.rst

+1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ Modules
2323
ogc
2424
opensearch
2525
os_utils
26+
sentinelhub_request
2627
testing_utils
2728
time_utils

docs/source/sentinelhub_request.rst

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
sentinelhub_request
2+
===================
3+
4+
.. automodule:: sentinelhub.sentinelhub_request
5+
:members:
6+
:show-inheritance:

sentinelhub/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
from .sentinelhub_session import SentinelHubSession
3737

38-
from .sentinelhub_request import body, bounds, data, output, response
38+
from .sentinelhub_request import SentinelHubRequest
3939

4040
from .time_utils import parse_time_interval, filter_times
4141

sentinelhub/sentinelhub_request.py

+234-62
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,261 @@
1-
""" Helper functions for generating request dictionaries
1+
""" SentinelHubRequest for the Processing API
22
3-
Request structure documentation is available at: https://docs.sentinel-hub.com/api/latest/reference/
3+
Documentation: https://docs.sentinel-hub.com/api/latest/reference/
44
"""
55

6-
from .geometry import Geometry
6+
from .config import SHConfig
7+
from .constants import MimeType, DataSource, RequestType
8+
from .download import DownloadRequest, SentinelHubDownloadClient
9+
from .data_request import DataRequest
10+
from .geometry import Geometry, BBox
11+
from .time_utils import parse_time_interval
712

813

9-
def body(request_bounds, request_data, evalscript, request_output=None):
10-
""" Generate request body
14+
class SentinelHubRequest(DataRequest):
15+
""" Sentinel Hub API request class
1116
"""
12-
request_body = {
13-
"input": {
14-
"bounds": request_bounds,
15-
"data": request_data
16-
},
17-
"evalscript": evalscript
18-
}
17+
def __init__(self, evalscript, input_data, responses, bbox=None, geometry=None, size=None, resolution=None,
18+
config=None, **kwargs):
19+
"""
20+
For details of certain parameters check the
21+
`Processing API reference <https://docs.sentinel-hub.com/api/latest/reference/#operation/process>`_.
1922
20-
if request_output is not None:
21-
request_body['output'] = request_output
23+
:param evalscript: `Evalscript <https://docs.sentinel-hub.com/api/latest/#/Evalscript/>`_.
24+
:type evalscript: str
25+
:param input_data: A list of input dictionary objects as described in the API reference. It can be generated
26+
with the helper function `SentinelHubRequest.input_data`
27+
:type input_data: List[dict]
28+
:param responses: A list of `output.responses` objects as described in the API reference. It can be generated
29+
with the helper function `SentinelHubRequest.output_response`
30+
:type responses: List[dict]
31+
:param bbox: Bounding box describing the area of interest.
32+
:type bbox: sentinelhub.BBox
33+
:param geometry: Geometry describing the area of interest.
34+
:type geometry: sentinelhub.Geometry
35+
:param size: Size of the image.
36+
:type size: Tuple[int, int]
37+
:param resolution: Resolution of the image. It has to be in units compatible with the given CRS.
38+
:type resolution: Tuple[float, float]
39+
:param config: SHConfig object containing desired sentinel-hub configuration parameters.
40+
:type config: sentinelhub.SHConfig
41+
"""
2242

23-
return request_body
43+
if size is None and resolution is None:
44+
raise ValueError("Either size or resolution argument should be given")
2445

46+
if not isinstance(evalscript, str):
47+
raise ValueError("'evalscript' should be a string")
2548

26-
def response(identifier, response_format):
27-
""" Generate request response
28-
"""
29-
return {
30-
"identifier": identifier,
31-
"format": {
32-
'type': response_format
49+
self.config = config or SHConfig()
50+
51+
self.mime_type = MimeType.TAR if len(responses) > 1 else MimeType(responses[0]['format']['type'].split('/')[1])
52+
53+
self.payload = self.body(
54+
request_bounds=self.bounds(bbox=bbox, geometry=geometry),
55+
request_data=input_data,
56+
request_output=self.output(size=size, resolution=resolution, responses=responses),
57+
evalscript=evalscript
58+
)
59+
60+
super().__init__(SentinelHubDownloadClient, **kwargs)
61+
62+
def create_request(self):
63+
""" Prepares a download request
64+
"""
65+
headers = {'content-type': MimeType.JSON.get_string(), "accept": self.mime_type.get_string()}
66+
67+
self.download_list = [DownloadRequest(
68+
request_type=RequestType.POST,
69+
url=self.config.get_sh_processing_api_url(),
70+
post_values=self.payload,
71+
data_folder=self.data_folder,
72+
save_response=bool(self.data_folder),
73+
data_type=self.mime_type,
74+
headers=headers
75+
)]
76+
77+
@staticmethod
78+
def input_data(data_source=None, time_interval=None, maxcc=1.0, mosaicking_order='mostRecent', other_args=None):
79+
""" Generate the `input` part of the Processing API request body
80+
81+
:param data_source: One of supported ProcessingAPI data sources.
82+
:type data_source: sentinelhub.DataSource
83+
:param time_interval: interval with start and end date of the form YYYY-MM-DDThh:mm:ss or YYYY-MM-DD
84+
:type time_interval: (str, str) or (datetime, datetime)
85+
:param maxcc: Maximum accepted cloud coverage of an image. Float between 0.0 and 1.0. Default is 1.0.
86+
:type maxcc: float
87+
:param mosaicking_order: Mosaicking order, which has to be either 'mostRecent', 'leastRecent' or 'leastCC'.
88+
:type mosaicking_order: str
89+
:param other_args: Additional dictionary of arguments. If provided, the resulting dictionary will get updated
90+
by it.
91+
:param other_args: dict
92+
"""
93+
94+
if not isinstance(data_source, DataSource):
95+
raise ValueError("'data_source' should be an instance of sentinelhub.DataSource")
96+
97+
if not isinstance(maxcc, float) and (maxcc < 0 or maxcc > 1):
98+
raise ValueError('maxcc should be a float on an interval [0, 1]')
99+
100+
if time_interval:
101+
date_from, date_to = parse_time_interval(time_interval)
102+
date_from, date_to = date_from + 'Z', date_to + 'Z'
103+
else:
104+
date_from, date_to = "", ""
105+
106+
mosaic_order_params = ["mostRecent", "leastRecent", "leastCC"]
107+
if mosaicking_order not in mosaic_order_params:
108+
msg = "{} is not a valid mosaickingOrder parameter, it should be one of: {}"
109+
raise ValueError(msg.format(mosaicking_order, mosaic_order_params))
110+
111+
data_type = 'CUSTOM' if data_source.is_custom() else data_source.api_identifier()
112+
113+
input_data_object = {
114+
"type": data_type,
115+
"dataFilter": {
116+
"timeRange": {"from": date_from, "to": date_to},
117+
"maxCloudCoverage": int(maxcc * 100),
118+
"mosaickingOrder": mosaicking_order,
119+
}
33120
}
34-
}
35121

122+
if data_type == 'CUSTOM':
123+
input_data_object['dataFilter']['collectionId'] = data_source.value
36124

37-
def output(responses, size_x, size_y):
38-
""" Generate request output
39-
"""
40-
return {
41-
"width": size_x,
42-
"height": size_y,
43-
"responses": responses
44-
}
125+
if other_args:
126+
input_data_object.update(other_args)
45127

128+
return input_data_object
46129

47-
def bounds(crs, bbox=None, geometry=None):
48-
""" Generate request bounds
49-
"""
50-
if bbox is None and geometry is None:
51-
raise ValueError("At least one of parameters 'bbox' and 'geometry' has to be given")
130+
@staticmethod
131+
def body(request_bounds, request_data, evalscript, request_output=None, other_args=None):
132+
""" Generate the body the Processing API request body
133+
134+
:param request_bounds: A dictionary as generated by `SentinelHubRequest.bounds` helper method.
135+
:type request_bounds: dict
136+
:param request_data: A list of dictionaries as generated by `SentinelHubRequest.input_data` helper method.
137+
:type request_data: List[dict]
138+
:param evalscript: Evalscript (https://docs.sentinel-hub.com/api/latest/#/Evalscript/)
139+
:type evalscript: str
140+
:param request_output: A dictionary as generated by `SentinelHubRequest.output` helper method.
141+
:type request_output: dict
142+
:param other_args: Additional dictionary of arguments. If provided, the resulting dictionary will get updated
143+
by it.
144+
:param other_args: dict
145+
"""
146+
request_body = {
147+
"input": {
148+
"bounds": request_bounds,
149+
"data": request_data
150+
},
151+
"evalscript": evalscript
152+
}
52153

53-
if bbox and (not isinstance(bbox, list) or len(bbox) != 4 or not all(isinstance(x, float) for x in bbox)):
54-
raise ValueError("Invalid bbox argument: {}".format(bbox))
154+
if request_output is not None:
155+
request_body['output'] = request_output
55156

56-
if geometry and not isinstance(geometry, Geometry):
57-
raise ValueError('Geometry has to be of type sentinelhub.Geometry')
157+
if other_args:
158+
request_body.update(other_args)
58159

59-
if bbox and geometry and bbox is not geometry.crs:
60-
raise ValueError('Bounding box and geometry should have the same CRS, but {} and {} '
61-
'found'.format(bbox, geometry.crs))
160+
return request_body
62161

63-
request_bounds = {
64-
"properties": {
65-
"crs": crs
162+
@staticmethod
163+
def output_response(identifier, response_format, other_args=None):
164+
""" Generate an element of `output.responses` as described in the Processing API reference.
165+
166+
:param identifier: Identifier of the output response.
167+
:type identifier: str
168+
:param response_format: A mime type of one of 'png', 'json', 'jpeg', 'tiff'.
169+
:type response_format: str or sentinelhub.MimeType
170+
:param other_args: Additional dictionary of arguments. If provided, the resulting dictionary will get updated
171+
by it.
172+
:param other_args: dict
173+
"""
174+
output_response = {
175+
"identifier": identifier,
176+
"format": {
177+
'type': MimeType(response_format).get_string()
178+
}
66179
}
67-
}
68180

69-
if bbox:
70-
request_bounds['bbox'] = list(bbox)
181+
if other_args:
182+
output_response.update(other_args)
71183

72-
if geometry:
73-
request_bounds['geometry'] = geometry.geojson
184+
return output_response
74185

75-
return request_bounds
186+
@staticmethod
187+
def output(responses, size=None, resolution=None, other_args=None):
188+
""" Generate an `output` part of the request as described in the Processing API reference
76189
190+
:param responses: A list of objects in `output.responses` as generated by `SentinelHubRequest.output_response`.
191+
:type responses: List[dict]
192+
:param size: Size of the image.
193+
:type size: Tuple[int, int]
194+
:param resolution: Resolution of the image. It has to be in units compatible with the given CRS.
195+
:type resolution: Tuple[float, float]
196+
:param other_args: Additional dictionary of arguments. If provided, the resulting dictionary will get updated
197+
by it.
198+
:param other_args: dict
199+
"""
200+
if size and resolution:
201+
raise ValueError("Either size or resolution argument should be given, not both.")
77202

78-
def data(time_from=None, time_to=None, data_type='S2L1C'):
79-
""" Generate request data
80-
"""
81-
return {
82-
"type": data_type,
83-
"dataFilter": {
84-
"timeRange": {
85-
"from": "" if time_from is None else time_from,
86-
"to": "" if time_to is None else time_to
203+
request_output = {
204+
"responses": responses
205+
}
206+
207+
if size:
208+
request_output['width'], request_output['height'] = size
209+
if resolution:
210+
request_output['resx'], request_output['resy'] = resolution
211+
212+
if other_args:
213+
request_output.update(other_args)
214+
215+
return request_output
216+
217+
@staticmethod
218+
def bounds(bbox=None, geometry=None, other_args=None):
219+
""" Generate a `bound` part of the Processing API request
220+
221+
:param bbox: Bounding box describing the area of interest.
222+
:type bbox: sentinelhub.BBox
223+
:param geometry: Geometry describing the area of interest.
224+
:type geometry: sentinelhub.Geometry
225+
:param other_args: Additional dictionary of arguments. If provided, the resulting dictionary will get updated
226+
by it.
227+
:param other_args: dict
228+
"""
229+
if bbox is None and geometry is None:
230+
raise ValueError("'bbox' and/or 'geometry' have to be provided.")
231+
232+
if bbox and not isinstance(bbox, BBox):
233+
raise ValueError("'bbox' should be an instance of sentinelhub.BBox")
234+
235+
if geometry and not isinstance(geometry, Geometry):
236+
raise ValueError("'geometry' should be an instance of sentinelhub.Geometry")
237+
238+
if bbox and geometry and bbox.crs != geometry.crs:
239+
raise ValueError("bbox and geometry should be in the same CRS")
240+
241+
if bbox is None:
242+
bbox = geometry.bbox
243+
244+
crs = bbox.crs if bbox else geometry.crs
245+
246+
request_bounds = {
247+
"properties": {
248+
"crs": crs.opengis_string
87249
}
88250
}
89-
}
251+
252+
if bbox:
253+
request_bounds['bbox'] = list(bbox)
254+
255+
if geometry:
256+
request_bounds['geometry'] = geometry.geojson
257+
258+
if other_args:
259+
request_bounds.update(other_args)
260+
261+
return request_bounds

0 commit comments

Comments
 (0)