|
1 |
| -""" Helper functions for generating request dictionaries |
| 1 | +""" SentinelHubRequest for the Processing API |
2 | 2 |
|
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/ |
4 | 4 | """
|
5 | 5 |
|
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 |
7 | 12 |
|
8 | 13 |
|
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 |
11 | 16 | """
|
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>`_. |
19 | 22 |
|
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 | + """ |
22 | 42 |
|
23 |
| - return request_body |
| 43 | + if size is None and resolution is None: |
| 44 | + raise ValueError("Either size or resolution argument should be given") |
24 | 45 |
|
| 46 | + if not isinstance(evalscript, str): |
| 47 | + raise ValueError("'evalscript' should be a string") |
25 | 48 |
|
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 | + } |
33 | 120 | }
|
34 |
| - } |
35 | 121 |
|
| 122 | + if data_type == 'CUSTOM': |
| 123 | + input_data_object['dataFilter']['collectionId'] = data_source.value |
36 | 124 |
|
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) |
45 | 127 |
|
| 128 | + return input_data_object |
46 | 129 |
|
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 | + } |
52 | 153 |
|
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 |
55 | 156 |
|
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) |
58 | 159 |
|
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 |
62 | 161 |
|
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 | + } |
66 | 179 | }
|
67 |
| - } |
68 | 180 |
|
69 |
| - if bbox: |
70 |
| - request_bounds['bbox'] = list(bbox) |
| 181 | + if other_args: |
| 182 | + output_response.update(other_args) |
71 | 183 |
|
72 |
| - if geometry: |
73 |
| - request_bounds['geometry'] = geometry.geojson |
| 184 | + return output_response |
74 | 185 |
|
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 |
76 | 189 |
|
| 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.") |
77 | 202 |
|
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 |
87 | 249 | }
|
88 | 250 | }
|
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