diff --git a/rest/app.py b/rest/app.py index 73896fe1..a15c04c8 100644 --- a/rest/app.py +++ b/rest/app.py @@ -31,13 +31,14 @@ from dynamodb import JobsPersistence, ProcessGraphsPersistence, ServicesPersistence from processing.processing import ( check_process_graph_conversion_validity, + get_batch_job_estimate, process_data_synchronously, create_batch_job, start_batch_job, cancel_batch_job, modify_batch_job, - get_batch_job_estimate, get_batch_job_status, + create_or_get_estimate_values_from_db, ) from post_processing.post_processing import parse_sh_gtiff_to_format from processing.utils import inject_variables_in_process_graph, overwrite_spatial_extent_without_parameters @@ -480,17 +481,28 @@ def api_batch_job(job_id): if flask.request.method == "GET": status, error = get_batch_job_status(job["batch_request_id"], job["deployment_endpoint"]) + data_to_jsonify = { + "id": job_id, + "title": job.get("title", None), + "description": job.get("description", None), + "process": {"process_graph": json.loads(job["process"])["process_graph"]}, + "status": status.value, + "error": error, + "created": convert_timestamp_to_simpler_format(job["created"]), + "updated": convert_timestamp_to_simpler_format(job["last_updated"]), + } + + if status is not openEOBatchJobStatus.CREATED: + data_to_jsonify["costs"] = float(job.get("sum_costs", 0)) + data_to_jsonify["usage"] = { + "Platform Credits": {"unit": "credits", "value": round(float(job.get("sum_costs", 0)) * 0.15, 3)}, + "Sentinel Hub": { + "unit": "sentinelhub_processing_unit", + "value": float(job.get("sum_costs", 0)), + }, + } return flask.make_response( - jsonify( - id=job_id, - title=job.get("title", None), - description=job.get("description", None), - process={"process_graph": json.loads(job["process"])["process_graph"]}, - status=status.value, - error=error, - created=convert_timestamp_to_simpler_format(job["created"]), - updated=convert_timestamp_to_simpler_format(job["last_updated"]), - ), + jsonify(data_to_jsonify), 200, ) @@ -513,6 +525,19 @@ def api_batch_job(job_id): update_batch_request_id(job_id, job, new_batch_request_id) data["deployment_endpoint"] = deployment_endpoint + if json.dumps(data.get("process"), sort_keys=True) != json.dumps( + json.loads(job.get("process")), sort_keys=True + ): + estimated_sentinelhub_pu, estimated_file_size = get_batch_job_estimate( + new_batch_request_id, data.get("process"), deployment_endpoint + ) + estimated_platform_credits = round(estimated_sentinelhub_pu * 0.15, 3) + JobsPersistence.update_key( + job["id"], "estimated_sentinelhub_pu", str(round(estimated_sentinelhub_pu, 3)) + ) + JobsPersistence.update_key(job["id"], "estimated_platform_credits", str(estimated_platform_credits)) + JobsPersistence.update_key(job["id"], "estimated_file_size", str(estimated_file_size)) + for key in data: JobsPersistence.update_key(job_id, key, data[key]) @@ -612,7 +637,6 @@ def add_job_to_queue(job_id): # we can create a /results_metadata.json file here # the contents of the batch job folder in the bucket isn't revealed anywhere else anyway - metadata_creation_time = datetime.utcnow().strftime(ISO8601_UTC_FORMAT) batch_job_metadata = { "type": "Feature", @@ -627,6 +651,13 @@ def add_job_to_queue(job_id): "title": job.get("title", None), "datetime": metadata_creation_time, "expires": metadata_valid, + "usage": { + "Platform credits": {"unit": "credits", "value": job.get("estimated_platform_credits", 0)}, + "Sentinel Hub": { + "unit": "sentinelhub_processing_unit", + "value": job.get("estimated_sentinelhub_pu", 0), + }, + }, "processing:expression": {"format": "openeo", "expression": json.loads(job["process"])}, }, "links": links, @@ -663,11 +694,12 @@ def estimate_job_cost(job_id): if job is None: raise JobNotFound() - estimated_pu, estimated_file_size = get_batch_job_estimate( - job["batch_request_id"], json.loads(job["process"]), job["deployment_endpoint"] + estimated_sentinelhub_pu, _, estimated_file_size = create_or_get_estimate_values_from_db( + job, job["batch_request_id"] ) + return flask.make_response( - jsonify(costs=estimated_pu, size=estimated_file_size), + jsonify(costs=estimated_sentinelhub_pu, size=estimated_file_size), 200, ) diff --git a/rest/authentication/authentication.py b/rest/authentication/authentication.py index 02ad1cf6..22764cdf 100644 --- a/rest/authentication/authentication.py +++ b/rest/authentication/authentication.py @@ -15,6 +15,7 @@ Internal, CredentialsInvalid, BillingPlanInvalid, + TokenInvalid, ) from authentication.oidc_providers import oidc_providers from authentication.user import OIDCUser, SHUser @@ -62,7 +63,7 @@ def authenticate_user_oidc(self, access_token, oidc_provider_id): user_id = userinfo["sub"] try: - user = OIDCUser(user_id, oidc_userinfo=userinfo) + user = OIDCUser(user_id, oidc_userinfo=userinfo, access_token=access_token) except BillingPlanInvalid: return None diff --git a/rest/authentication/user.py b/rest/authentication/user.py index 410e1570..95faebc4 100644 --- a/rest/authentication/user.py +++ b/rest/authentication/user.py @@ -25,12 +25,15 @@ def get_user_info(self): user_info["default_plan"] = self.default_plan.name return user_info + def get_leftover_credits(self): + pass + def report_usage(self, pu_spent, job_id=None): pass class OIDCUser(User): - def __init__(self, user_id=None, oidc_userinfo={}): + def __init__(self, user_id=None, oidc_userinfo={}, access_token=None): super().__init__(user_id) self.entitlements = [ self.convert_entitlement(entitlement) for entitlement in oidc_userinfo.get("eduperson_entitlement", []) @@ -38,6 +41,7 @@ def __init__(self, user_id=None, oidc_userinfo={}): self.oidc_userinfo = oidc_userinfo self.default_plan = OpenEOPBillingPlan.get_billing_plan(self.entitlements) self.session = central_user_sentinelhub_session + self.access_token = access_token def __str__(self): return f"{self.__class__.__name__}: {self.user_id}" @@ -60,6 +64,9 @@ def get_user_info(self): user_info["info"] = {"oidc_userinfo": self.oidc_userinfo} return user_info + def get_leftover_credits(self): + return usageReporting.get_leftover_credits_for_user(self.access_token) + def report_usage(self, pu_spent, job_id=None): usageReporting.report_usage(self.user_id, pu_spent, job_id) diff --git a/rest/dynamodb/dynamodb.py b/rest/dynamodb/dynamodb.py index 9fbc4429..c117cb5f 100644 --- a/rest/dynamodb/dynamodb.py +++ b/rest/dynamodb/dynamodb.py @@ -16,6 +16,8 @@ FAKE_AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE" FAKE_AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +USED_RESERVED_WORDS = ["plan"] + class DeploymentTypes(Enum): PRODUCTION = "production" @@ -121,12 +123,17 @@ def update_key(cls, record_id, key, new_value): else: new_value = str(new_value) - updated_item = cls.dynamodb.update_item( + kwargs = dict( TableName=cls.TABLE_NAME, Key={"id": {"S": record_id}}, UpdateExpression="SET {} = :new_content".format(key), ExpressionAttributeValues={":new_content": {data_type: new_value}}, ) + if key in USED_RESERVED_WORDS: + kwargs["UpdateExpression"] = "SET #{} = :new_content".format(key) + kwargs["ExpressionAttributeNames"] = {"#{}".format(key): "{}".format(key)} + + updated_item = cls.dynamodb.update_item(**kwargs) return updated_item @classmethod @@ -204,6 +211,10 @@ def create(cls, data): "http_code": {"N": data.get("http_code", "200")}, "results": {"S": json.dumps(data.get("results"))}, "deployment_endpoint": {"S": data.get("deployment_endpoint", "https://services.sentinel-hub.com")}, + "estimated_sentinelhub_pu": {"N": data.get("estimated_sentinelhub_pu", "0")}, + "estimated_platform_credits": {"N": data.get("estimated_platform_credits", "0")}, + "estimated_file_size": {"N": data.get("estimated_file_size", "0")}, + "sum_costs": {"N": data.get("sum_costs", "0")}, } if data.get("title"): item["title"] = {"S": str(data.get("title"))} @@ -316,7 +327,6 @@ def create(cls, data): if __name__ == "__main__": - # To create tables, run: # $ pipenv shell # $ DEPLOYMENT_TYPE="production" ./dynamodb.py diff --git a/rest/openeo_collections/commercial_collections/SKYSAT.json b/rest/openeo_collections/commercial_collections/SKYSAT.json new file mode 100644 index 00000000..771ff291 --- /dev/null +++ b/rest/openeo_collections/commercial_collections/SKYSAT.json @@ -0,0 +1,539 @@ +{ + "type": "Collection", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/datacube/v1.0.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "id": "SKYSAT", + "datasource_type": "byoc-ID", + "title": "SkySat", + "links": [], + "description": "SkySat is one of the satellite constellations operated by Planet. SkySat satellite constellation consists of 21 satellites, which were launched between 2013 and 2020. The satellites are based on a CubeSat concept but are a bit bigger comparing to the PlanetScope's satellites. Because of its rapid revisit time, this data is suitable to monitor fast changes on earth's surface. However, note that the data acquisition must be tasked, data is not acquired systematically.", + "keywords": [ + "sentinel hub", + "SkySat", + "vhr", + "commercial data" + ], + "license": "various", + "providers": [ + { + "description": "", + "name": "Sentinel Hub", + "roles": [ + "processor" + ], + "url": "https://services.sentinel-hub.com/" + }, + { + "description": "", + "name": "Planet", + "roles": [ + "producer" + ], + "url": "https://www.planet.com/products/planet-imagery/" + } + ], + "extent": { + "spatial": { + "bbox": [ + [ + -180, + -90, + 180, + 90 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2014-01-01T00:00:00Z", + null + ] + ] + } + }, + "cube:dimensions": { + "bands": { + "type": "bands", + "values": [ + "Blue", + "Green", + "Red", + "NIR", + "UDM", + "UDM2_Clear", + "UDM2_Snow", + "UDM2_Shadow", + "UDM2_LightHaze", + "UDM2_HeavyHaze", + "UDM2_Cloud", + "UDM2_Confidence", + "PAN", + "dataMask" + ] + }, + "t": { + "extent": [ + "2014-01-01T00:00:00Z", + null + ], + "type": "temporal" + }, + "x": { + "axis": "x", + "extent": [ + -180, + 180 + ], + "reference_system": { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "area": "World", + "base_crs": { + "coordinate_system": { + "axis": [ + { + "abbreviation": "Lat", + "direction": "north", + "name": "Geodetic latitude", + "unit": "degree" + }, + { + "abbreviation": "Lon", + "direction": "east", + "name": "Geodetic longitude", + "unit": "degree" + } + ], + "subtype": "ellipsoidal" + }, + "datum": { + "ellipsoid": { + "inverse_flattening": 298.257223563, + "name": "WGS 84", + "semi_major_axis": 6378137 + }, + "name": "World Geodetic System 1984", + "type": "GeodeticReferenceFrame" + }, + "name": "WGS 84" + }, + "bbox": { + "east_longitude": 180, + "north_latitude": 90, + "south_latitude": -90, + "west_longitude": -180 + }, + "coordinate_system": { + "axis": [ + { + "abbreviation": "E", + "direction": "east", + "name": "Easting", + "unit": "metre" + }, + { + "abbreviation": "N", + "direction": "north", + "name": "Northing", + "unit": "metre" + } + ], + "subtype": "Cartesian" + }, + "id": { + "authority": "OGC", + "code": "Auto42001", + "version": "1.3" + }, + "name": "AUTO 42001 (Universal Transverse Mercator)", + "type": "ProjectedCRS" + }, + "type": "spatial" + }, + "y": { + "axis": "y", + "extent": [ + -90, + 90 + ], + "reference_system": { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "area": "World", + "base_crs": { + "coordinate_system": { + "axis": [ + { + "abbreviation": "Lat", + "direction": "north", + "name": "Geodetic latitude", + "unit": "degree" + }, + { + "abbreviation": "Lon", + "direction": "east", + "name": "Geodetic longitude", + "unit": "degree" + } + ], + "subtype": "ellipsoidal" + }, + "datum": { + "ellipsoid": { + "inverse_flattening": 298.257223563, + "name": "WGS 84", + "semi_major_axis": 6378137 + }, + "name": "World Geodetic System 1984", + "type": "GeodeticReferenceFrame" + }, + "name": "WGS 84" + }, + "bbox": { + "east_longitude": 180, + "north_latitude": 90, + "south_latitude": -90, + "west_longitude": -180 + }, + "coordinate_system": { + "axis": [ + { + "abbreviation": "E", + "direction": "east", + "name": "Easting", + "unit": "metre" + }, + { + "abbreviation": "N", + "direction": "north", + "name": "Northing", + "unit": "metre" + } + ], + "subtype": "Cartesian" + }, + "id": { + "authority": "OGC", + "code": "Auto42001", + "version": "1.3" + }, + "name": "AUTO 42001 (Universal Transverse Mercator)", + "type": "ProjectedCRS" + }, + "type": "spatial" + } + }, + "sci:citation": "\u00a9 Planet (YYYY), contains SkySat data processed by Sentinel Hub", + "summaries": { + "eo:bands": [ + { + "center_wavelength": 0.4825, + "common_name": "blue", + "description": "Blue", + "full_width_half_max": 0.325, + "name": "Blue", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.545, + "common_name": "green", + "description": "Green", + "full_width_half_max": 0.4, + "name": "Green", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.650, + "common_name": "red", + "description": "Red", + "full_width_half_max": 0.45, + "name": "Red", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.82, + "common_name": "nir08", + "description": "Near Infrared", + "full_width_half_max": 0.8, + "name": "NIR", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Unusable Data Mask", + "name": "UDM", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Clear mask", + "name": "UDM2_Clear", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Snow mask", + "name": "UDM2_Snow", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Shadow mask", + "name": "UDM2_Shadow", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Light haze mask", + "name": "UDM2_LightHaze", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Heavy haze mask", + "name": "UDM2_HeavyHaze", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Cloud mask", + "name": "UDM2_Cloud", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Confidence map", + "name": "UDM2_Confidence", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.675, + "common_name": "nir08", + "description": "Panchromatic", + "full_width_half_max": 0.225, + "name": "PAN", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "The mask of data/no data pixels", + "name": "dataMask" + } + ] + }, + "crs": [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "http://www.opengis.net/def/crs/EPSG/0/2154", + "http://www.opengis.net/def/crs/EPSG/0/2180", + "http://www.opengis.net/def/crs/EPSG/0/2193", + "http://www.opengis.net/def/crs/EPSG/0/3003", + "http://www.opengis.net/def/crs/EPSG/0/3004", + "http://www.opengis.net/def/crs/EPSG/0/3031", + "http://www.opengis.net/def/crs/EPSG/0/3035", + "http://www.opengis.net/def/crs/EPSG/0/4326", + "http://www.opengis.net/def/crs/EPSG/0/4346", + "http://www.opengis.net/def/crs/EPSG/0/4416", + "http://www.opengis.net/def/crs/EPSG/0/4765", + "http://www.opengis.net/def/crs/EPSG/0/4794", + "http://www.opengis.net/def/crs/EPSG/0/4844", + "http://www.opengis.net/def/crs/EPSG/0/4857", + "http://www.opengis.net/def/crs/EPSG/0/3912", + "http://www.opengis.net/def/crs/EPSG/0/3995", + "http://www.opengis.net/def/crs/EPSG/0/4026", + "http://www.opengis.net/def/crs/EPSG/0/5514", + "http://www.opengis.net/def/crs/EPSG/0/28992", + "http://www.opengis.net/def/crs/EPSG/0/32601", + "http://www.opengis.net/def/crs/EPSG/0/32602", + "http://www.opengis.net/def/crs/EPSG/0/32603", + "http://www.opengis.net/def/crs/EPSG/0/32604", + "http://www.opengis.net/def/crs/EPSG/0/32605", + "http://www.opengis.net/def/crs/EPSG/0/32606", + "http://www.opengis.net/def/crs/EPSG/0/32607", + "http://www.opengis.net/def/crs/EPSG/0/32608", + "http://www.opengis.net/def/crs/EPSG/0/32609", + "http://www.opengis.net/def/crs/EPSG/0/32610", + "http://www.opengis.net/def/crs/EPSG/0/32611", + "http://www.opengis.net/def/crs/EPSG/0/32612", + "http://www.opengis.net/def/crs/EPSG/0/32613", + "http://www.opengis.net/def/crs/EPSG/0/32614", + "http://www.opengis.net/def/crs/EPSG/0/32615", + "http://www.opengis.net/def/crs/EPSG/0/32616", + "http://www.opengis.net/def/crs/EPSG/0/32617", + "http://www.opengis.net/def/crs/EPSG/0/32618", + "http://www.opengis.net/def/crs/EPSG/0/32619", + "http://www.opengis.net/def/crs/EPSG/0/32620", + "http://www.opengis.net/def/crs/EPSG/0/32621", + "http://www.opengis.net/def/crs/EPSG/0/32622", + "http://www.opengis.net/def/crs/EPSG/0/32623", + "http://www.opengis.net/def/crs/EPSG/0/32624", + "http://www.opengis.net/def/crs/EPSG/0/32625", + "http://www.opengis.net/def/crs/EPSG/0/32626", + "http://www.opengis.net/def/crs/EPSG/0/32627", + "http://www.opengis.net/def/crs/EPSG/0/32628", + "http://www.opengis.net/def/crs/EPSG/0/32629", + "http://www.opengis.net/def/crs/EPSG/0/32630", + "http://www.opengis.net/def/crs/EPSG/0/32631", + "http://www.opengis.net/def/crs/EPSG/0/32632", + "http://www.opengis.net/def/crs/EPSG/0/32633", + "http://www.opengis.net/def/crs/EPSG/0/32634", + "http://www.opengis.net/def/crs/EPSG/0/32635", + "http://www.opengis.net/def/crs/EPSG/0/32636", + "http://www.opengis.net/def/crs/EPSG/0/32637", + "http://www.opengis.net/def/crs/EPSG/0/32638", + "http://www.opengis.net/def/crs/EPSG/0/32639", + "http://www.opengis.net/def/crs/EPSG/0/32640", + "http://www.opengis.net/def/crs/EPSG/0/32641", + "http://www.opengis.net/def/crs/EPSG/0/32642", + "http://www.opengis.net/def/crs/EPSG/0/32643", + "http://www.opengis.net/def/crs/EPSG/0/32644", + "http://www.opengis.net/def/crs/EPSG/0/32645", + "http://www.opengis.net/def/crs/EPSG/0/32646", + "http://www.opengis.net/def/crs/EPSG/0/32647", + "http://www.opengis.net/def/crs/EPSG/0/32648", + "http://www.opengis.net/def/crs/EPSG/0/32649", + "http://www.opengis.net/def/crs/EPSG/0/32650", + "http://www.opengis.net/def/crs/EPSG/0/32651", + "http://www.opengis.net/def/crs/EPSG/0/32652", + "http://www.opengis.net/def/crs/EPSG/0/32653", + "http://www.opengis.net/def/crs/EPSG/0/32654", + "http://www.opengis.net/def/crs/EPSG/0/32655", + "http://www.opengis.net/def/crs/EPSG/0/32656", + "http://www.opengis.net/def/crs/EPSG/0/32657", + "http://www.opengis.net/def/crs/EPSG/0/32658", + "http://www.opengis.net/def/crs/EPSG/0/32659", + "http://www.opengis.net/def/crs/EPSG/0/32660", + "http://www.opengis.net/def/crs/EPSG/0/32701", + "http://www.opengis.net/def/crs/EPSG/0/32702", + "http://www.opengis.net/def/crs/EPSG/0/32703", + "http://www.opengis.net/def/crs/EPSG/0/32704", + "http://www.opengis.net/def/crs/EPSG/0/32705", + "http://www.opengis.net/def/crs/EPSG/0/32706", + "http://www.opengis.net/def/crs/EPSG/0/32707", + "http://www.opengis.net/def/crs/EPSG/0/32708", + "http://www.opengis.net/def/crs/EPSG/0/32709", + "http://www.opengis.net/def/crs/EPSG/0/32710", + "http://www.opengis.net/def/crs/EPSG/0/32711", + "http://www.opengis.net/def/crs/EPSG/0/32712", + "http://www.opengis.net/def/crs/EPSG/0/32713", + "http://www.opengis.net/def/crs/EPSG/0/32714", + "http://www.opengis.net/def/crs/EPSG/0/32715", + "http://www.opengis.net/def/crs/EPSG/0/32716", + "http://www.opengis.net/def/crs/EPSG/0/32717", + "http://www.opengis.net/def/crs/EPSG/0/32718", + "http://www.opengis.net/def/crs/EPSG/0/32719", + "http://www.opengis.net/def/crs/EPSG/0/32720", + "http://www.opengis.net/def/crs/EPSG/0/32721", + "http://www.opengis.net/def/crs/EPSG/0/32722", + "http://www.opengis.net/def/crs/EPSG/0/32723", + "http://www.opengis.net/def/crs/EPSG/0/32724", + "http://www.opengis.net/def/crs/EPSG/0/32725", + "http://www.opengis.net/def/crs/EPSG/0/32726", + "http://www.opengis.net/def/crs/EPSG/0/32727", + "http://www.opengis.net/def/crs/EPSG/0/32728", + "http://www.opengis.net/def/crs/EPSG/0/32729", + "http://www.opengis.net/def/crs/EPSG/0/32730", + "http://www.opengis.net/def/crs/EPSG/0/32731", + "http://www.opengis.net/def/crs/EPSG/0/32732", + "http://www.opengis.net/def/crs/EPSG/0/32733", + "http://www.opengis.net/def/crs/EPSG/0/32734", + "http://www.opengis.net/def/crs/EPSG/0/32735", + "http://www.opengis.net/def/crs/EPSG/0/32736", + "http://www.opengis.net/def/crs/EPSG/0/32737", + "http://www.opengis.net/def/crs/EPSG/0/32738", + "http://www.opengis.net/def/crs/EPSG/0/32739", + "http://www.opengis.net/def/crs/EPSG/0/32740", + "http://www.opengis.net/def/crs/EPSG/0/32741", + "http://www.opengis.net/def/crs/EPSG/0/32742", + "http://www.opengis.net/def/crs/EPSG/0/32743", + "http://www.opengis.net/def/crs/EPSG/0/32744", + "http://www.opengis.net/def/crs/EPSG/0/32745", + "http://www.opengis.net/def/crs/EPSG/0/32746", + "http://www.opengis.net/def/crs/EPSG/0/32746", + "http://www.opengis.net/def/crs/EPSG/0/32748", + "http://www.opengis.net/def/crs/EPSG/0/32749", + "http://www.opengis.net/def/crs/EPSG/0/32750", + "http://www.opengis.net/def/crs/EPSG/0/32751", + "http://www.opengis.net/def/crs/EPSG/0/32752", + "http://www.opengis.net/def/crs/EPSG/0/32753", + "http://www.opengis.net/def/crs/EPSG/0/32754", + "http://www.opengis.net/def/crs/EPSG/0/32755", + "http://www.opengis.net/def/crs/EPSG/0/32756", + "http://www.opengis.net/def/crs/EPSG/0/32757", + "http://www.opengis.net/def/crs/EPSG/0/32758", + "http://www.opengis.net/def/crs/EPSG/0/32759", + "http://www.opengis.net/def/crs/EPSG/0/32760", + "http://www.opengis.net/def/crs/SR-ORG/0/98739" + ] +} \ No newline at end of file diff --git a/rest/openeoerrors.py b/rest/openeoerrors.py index 5117f689..e04b74af 100644 --- a/rest/openeoerrors.py +++ b/rest/openeoerrors.py @@ -153,3 +153,9 @@ def __init__(self, width, height) -> None: error_code = "ImageDimensionInvalid" http_code = 400 + + +class InsufficientCredits(SHOpenEOError): + error_code = "InsufficientCredits" + http_code = 402 + message = "You do not have sufficient credits to perform this request. Please visit https://portal.terrascope.be/pages/pricing to find more information on how to buy additional credits." diff --git a/rest/processing/processing.py b/rest/processing/processing.py index 5a2aa096..3a49e136 100644 --- a/rest/processing/processing.py +++ b/rest/processing/processing.py @@ -1,3 +1,4 @@ +import json import time from pg_to_evalscript import convert_from_process_graph @@ -9,8 +10,9 @@ from processing.sentinel_hub import SentinelHub from processing.partially_supported_processes import partially_supported_processes from dynamodb.utils import get_user_defined_processes_graphs +from dynamodb import JobsPersistence from const import openEOBatchJobStatus -from openeoerrors import Timeout +from openeoerrors import InsufficientCredits, JobNotFound, Timeout def check_process_graph_conversion_validity(process_graph): @@ -54,10 +56,21 @@ def create_batch_job(process): def start_new_batch_job(sentinel_hub, process, job_id): - new_batch_request_id, deployment_endpoint = create_batch_job(process) - estimated_pu, _ = get_batch_job_estimate(new_batch_request_id, process, deployment_endpoint) + new_batch_request_id, _ = create_batch_job(process) + + job = JobsPersistence.get_by_id(job_id) + if job is None: + raise JobNotFound() + + estimated_sentinelhub_pu, _, _ = create_or_get_estimate_values_from_db(job, new_batch_request_id) + + check_leftover_credits(estimated_sentinelhub_pu) + + JobsPersistence.update_key( + job["id"], "sum_costs", str(round(float(job.get("sum_costs", 0)) + estimated_sentinelhub_pu, 3)) + ) sentinel_hub.start_batch_job(new_batch_request_id) - g.user.report_usage(estimated_pu, job_id) + g.user.report_usage(estimated_sentinelhub_pu, job_id) return new_batch_request_id @@ -85,9 +98,19 @@ def start_batch_job(batch_request_id, process, deployment_endpoint, job_id): if batch_request_info is None: return start_new_batch_job(sentinel_hub, process, job_id) elif batch_request_info.status in [BatchRequestStatus.CREATED, BatchRequestStatus.ANALYSIS_DONE]: - estimated_pu, _ = get_batch_job_estimate(batch_request_id, process, deployment_endpoint) + job = JobsPersistence.get_by_id(job_id) + if job is None: + raise JobNotFound() + + estimated_sentinelhub_pu, _, _ = create_or_get_estimate_values_from_db(job, job["batch_request_id"]) + + check_leftover_credits(estimated_sentinelhub_pu) + + JobsPersistence.update_key( + job["id"], "sum_costs", str(round(float(job.get("sum_costs", 0)) + estimated_sentinelhub_pu, 3)) + ) sentinel_hub.start_batch_job(batch_request_id) - g.user.report_usage(estimated_pu, job_id) + g.user.report_usage(estimated_sentinelhub_pu, job_id) elif batch_request_info.status == BatchRequestStatus.PARTIAL: sentinel_hub.restart_batch_job(batch_request_id) elif batch_request_info.status in [ @@ -188,3 +211,27 @@ def get_batch_job_status(batch_request_id, deployment_endpoint): ) else: return openEOBatchJobStatus.FINISHED, None + + +def create_or_get_estimate_values_from_db(job, batch_request_id): + if float(job.get("estimated_sentinelhub_pu", 0)) == 0 and float(job.get("estimated_file_size", 0)) == 0: + estimated_sentinelhub_pu, estimated_file_size = get_batch_job_estimate( + batch_request_id, json.loads(job["process"]), job["deployment_endpoint"] + ) + estimated_platform_credits = round(estimated_sentinelhub_pu * 0.15, 3) + JobsPersistence.update_key(job["id"], "estimated_sentinelhub_pu", str(round(estimated_sentinelhub_pu, 3))) + JobsPersistence.update_key(job["id"], "estimated_platform_credits", str(estimated_platform_credits)) + JobsPersistence.update_key(job["id"], "estimated_file_size", str(estimated_file_size)) + else: + estimated_sentinelhub_pu = float(job.get("estimated_sentinelhub_pu", 0)) + estimated_platform_credits = float(job.get("estimated_platform_credits", 0)) + estimated_file_size = float(job.get("estimated_file_size", 0)) + + return estimated_sentinelhub_pu, estimated_platform_credits, estimated_file_size + + +def check_leftover_credits(estimated_pu): + leftover_credits = g.user.get_leftover_credits() + estimated_pu_as_credits = estimated_pu * 0.15 # platform credits === SH PU's * 0.15 + if leftover_credits is not None and leftover_credits < estimated_pu_as_credits: + raise InsufficientCredits() diff --git a/rest/usage_reporting/report_usage.py b/rest/usage_reporting/report_usage.py index b86bb0e7..e85cbf7f 100644 --- a/rest/usage_reporting/report_usage.py +++ b/rest/usage_reporting/report_usage.py @@ -15,6 +15,22 @@ def __init__(self): self.auth_client_secret = os.environ.get("USAGE_REPORTING_AUTH_CLIENT_SECRET") self.base_url = os.environ.get("USAGE_REPORTING_BASE_URL") + if self.auth_url is None: + log(ERROR, "USAGE_REPORTING_AUTH_URL environment variable is not set") + raise Internal("USAGE_REPORTING_AUTH_URL environment variable is not set") + + if self.auth_client_id is None: + log(ERROR, "USAGE_REPORTING_AUTH_CLIENT_ID environment variable is not set") + raise Internal("USAGE_REPORTING_AUTH_CLIENT_ID environment variable is not set") + + if self.auth_client_secret is None: + log(ERROR, "USAGE_REPORTING_AUTH_CLIENT_SECRET environment variable is not set") + raise Internal("USAGE_REPORTING_AUTH_CLIENT_SECRET environment variable is not set") + + if self.base_url is None: + log(ERROR, "USAGE_REPORTING_BASE_URL environment variable is not set") + raise Internal("USAGE_REPORTING_BASE_URL environment variable is not set") + self.authenticate() def authenticate(self, max_tries=5): @@ -58,6 +74,26 @@ def reporting_check_health(self): return r.status_code == 200 and content["status"] == "ok" + def get_leftover_credits_for_user(self, user_access_token): + user_url = f"{self.base_url}user" + + headers = {"content-type": "application/json", "Authorization": f"Bearer {user_access_token}"} + + if not self.reporting_check_health(): + log(ERROR, "Services for usage reporting are not healthy") + raise Internal("Services for usage reporting are not healthy") + + r = requests.get(user_url, headers=headers) + + if r.status_code == 200: + content = r.json() + platform_credits = content.get("credits") + + return platform_credits + else: + log(ERROR, f"Error fetching leftover credits: {r.status_code} {r.text}") + raise Internal(f"Problems during fetching leftover credits: {r.status_code} {r.text}") + def report_usage(self, user_id, pu_spent, job_id=None, max_tries=5): reporting_token = self.get_token() diff --git a/tests/fixtures/collection_information/SKYSAT.json b/tests/fixtures/collection_information/SKYSAT.json new file mode 100644 index 00000000..771ff291 --- /dev/null +++ b/tests/fixtures/collection_information/SKYSAT.json @@ -0,0 +1,539 @@ +{ + "type": "Collection", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/datacube/v1.0.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json" + ], + "id": "SKYSAT", + "datasource_type": "byoc-ID", + "title": "SkySat", + "links": [], + "description": "SkySat is one of the satellite constellations operated by Planet. SkySat satellite constellation consists of 21 satellites, which were launched between 2013 and 2020. The satellites are based on a CubeSat concept but are a bit bigger comparing to the PlanetScope's satellites. Because of its rapid revisit time, this data is suitable to monitor fast changes on earth's surface. However, note that the data acquisition must be tasked, data is not acquired systematically.", + "keywords": [ + "sentinel hub", + "SkySat", + "vhr", + "commercial data" + ], + "license": "various", + "providers": [ + { + "description": "", + "name": "Sentinel Hub", + "roles": [ + "processor" + ], + "url": "https://services.sentinel-hub.com/" + }, + { + "description": "", + "name": "Planet", + "roles": [ + "producer" + ], + "url": "https://www.planet.com/products/planet-imagery/" + } + ], + "extent": { + "spatial": { + "bbox": [ + [ + -180, + -90, + 180, + 90 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2014-01-01T00:00:00Z", + null + ] + ] + } + }, + "cube:dimensions": { + "bands": { + "type": "bands", + "values": [ + "Blue", + "Green", + "Red", + "NIR", + "UDM", + "UDM2_Clear", + "UDM2_Snow", + "UDM2_Shadow", + "UDM2_LightHaze", + "UDM2_HeavyHaze", + "UDM2_Cloud", + "UDM2_Confidence", + "PAN", + "dataMask" + ] + }, + "t": { + "extent": [ + "2014-01-01T00:00:00Z", + null + ], + "type": "temporal" + }, + "x": { + "axis": "x", + "extent": [ + -180, + 180 + ], + "reference_system": { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "area": "World", + "base_crs": { + "coordinate_system": { + "axis": [ + { + "abbreviation": "Lat", + "direction": "north", + "name": "Geodetic latitude", + "unit": "degree" + }, + { + "abbreviation": "Lon", + "direction": "east", + "name": "Geodetic longitude", + "unit": "degree" + } + ], + "subtype": "ellipsoidal" + }, + "datum": { + "ellipsoid": { + "inverse_flattening": 298.257223563, + "name": "WGS 84", + "semi_major_axis": 6378137 + }, + "name": "World Geodetic System 1984", + "type": "GeodeticReferenceFrame" + }, + "name": "WGS 84" + }, + "bbox": { + "east_longitude": 180, + "north_latitude": 90, + "south_latitude": -90, + "west_longitude": -180 + }, + "coordinate_system": { + "axis": [ + { + "abbreviation": "E", + "direction": "east", + "name": "Easting", + "unit": "metre" + }, + { + "abbreviation": "N", + "direction": "north", + "name": "Northing", + "unit": "metre" + } + ], + "subtype": "Cartesian" + }, + "id": { + "authority": "OGC", + "code": "Auto42001", + "version": "1.3" + }, + "name": "AUTO 42001 (Universal Transverse Mercator)", + "type": "ProjectedCRS" + }, + "type": "spatial" + }, + "y": { + "axis": "y", + "extent": [ + -90, + 90 + ], + "reference_system": { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "area": "World", + "base_crs": { + "coordinate_system": { + "axis": [ + { + "abbreviation": "Lat", + "direction": "north", + "name": "Geodetic latitude", + "unit": "degree" + }, + { + "abbreviation": "Lon", + "direction": "east", + "name": "Geodetic longitude", + "unit": "degree" + } + ], + "subtype": "ellipsoidal" + }, + "datum": { + "ellipsoid": { + "inverse_flattening": 298.257223563, + "name": "WGS 84", + "semi_major_axis": 6378137 + }, + "name": "World Geodetic System 1984", + "type": "GeodeticReferenceFrame" + }, + "name": "WGS 84" + }, + "bbox": { + "east_longitude": 180, + "north_latitude": 90, + "south_latitude": -90, + "west_longitude": -180 + }, + "coordinate_system": { + "axis": [ + { + "abbreviation": "E", + "direction": "east", + "name": "Easting", + "unit": "metre" + }, + { + "abbreviation": "N", + "direction": "north", + "name": "Northing", + "unit": "metre" + } + ], + "subtype": "Cartesian" + }, + "id": { + "authority": "OGC", + "code": "Auto42001", + "version": "1.3" + }, + "name": "AUTO 42001 (Universal Transverse Mercator)", + "type": "ProjectedCRS" + }, + "type": "spatial" + } + }, + "sci:citation": "\u00a9 Planet (YYYY), contains SkySat data processed by Sentinel Hub", + "summaries": { + "eo:bands": [ + { + "center_wavelength": 0.4825, + "common_name": "blue", + "description": "Blue", + "full_width_half_max": 0.325, + "name": "Blue", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.545, + "common_name": "green", + "description": "Green", + "full_width_half_max": 0.4, + "name": "Green", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.650, + "common_name": "red", + "description": "Red", + "full_width_half_max": 0.45, + "name": "Red", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.82, + "common_name": "nir08", + "description": "Near Infrared", + "full_width_half_max": 0.8, + "name": "NIR", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Unusable Data Mask", + "name": "UDM", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Clear mask", + "name": "UDM2_Clear", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Snow mask", + "name": "UDM2_Snow", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Shadow mask", + "name": "UDM2_Shadow", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Light haze mask", + "name": "UDM2_LightHaze", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Heavy haze mask", + "name": "UDM2_HeavyHaze", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Cloud mask", + "name": "UDM2_Cloud", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "Usable Data mask - Confidence map", + "name": "UDM2_Confidence", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "center_wavelength": 0.675, + "common_name": "nir08", + "description": "Panchromatic", + "full_width_half_max": 0.225, + "name": "PAN", + "openeo:gsd": { + "unit": "m", + "value": [ + 0.5, + 0.5 + ] + } + }, + { + "description": "The mask of data/no data pixels", + "name": "dataMask" + } + ] + }, + "crs": [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "http://www.opengis.net/def/crs/EPSG/0/2154", + "http://www.opengis.net/def/crs/EPSG/0/2180", + "http://www.opengis.net/def/crs/EPSG/0/2193", + "http://www.opengis.net/def/crs/EPSG/0/3003", + "http://www.opengis.net/def/crs/EPSG/0/3004", + "http://www.opengis.net/def/crs/EPSG/0/3031", + "http://www.opengis.net/def/crs/EPSG/0/3035", + "http://www.opengis.net/def/crs/EPSG/0/4326", + "http://www.opengis.net/def/crs/EPSG/0/4346", + "http://www.opengis.net/def/crs/EPSG/0/4416", + "http://www.opengis.net/def/crs/EPSG/0/4765", + "http://www.opengis.net/def/crs/EPSG/0/4794", + "http://www.opengis.net/def/crs/EPSG/0/4844", + "http://www.opengis.net/def/crs/EPSG/0/4857", + "http://www.opengis.net/def/crs/EPSG/0/3912", + "http://www.opengis.net/def/crs/EPSG/0/3995", + "http://www.opengis.net/def/crs/EPSG/0/4026", + "http://www.opengis.net/def/crs/EPSG/0/5514", + "http://www.opengis.net/def/crs/EPSG/0/28992", + "http://www.opengis.net/def/crs/EPSG/0/32601", + "http://www.opengis.net/def/crs/EPSG/0/32602", + "http://www.opengis.net/def/crs/EPSG/0/32603", + "http://www.opengis.net/def/crs/EPSG/0/32604", + "http://www.opengis.net/def/crs/EPSG/0/32605", + "http://www.opengis.net/def/crs/EPSG/0/32606", + "http://www.opengis.net/def/crs/EPSG/0/32607", + "http://www.opengis.net/def/crs/EPSG/0/32608", + "http://www.opengis.net/def/crs/EPSG/0/32609", + "http://www.opengis.net/def/crs/EPSG/0/32610", + "http://www.opengis.net/def/crs/EPSG/0/32611", + "http://www.opengis.net/def/crs/EPSG/0/32612", + "http://www.opengis.net/def/crs/EPSG/0/32613", + "http://www.opengis.net/def/crs/EPSG/0/32614", + "http://www.opengis.net/def/crs/EPSG/0/32615", + "http://www.opengis.net/def/crs/EPSG/0/32616", + "http://www.opengis.net/def/crs/EPSG/0/32617", + "http://www.opengis.net/def/crs/EPSG/0/32618", + "http://www.opengis.net/def/crs/EPSG/0/32619", + "http://www.opengis.net/def/crs/EPSG/0/32620", + "http://www.opengis.net/def/crs/EPSG/0/32621", + "http://www.opengis.net/def/crs/EPSG/0/32622", + "http://www.opengis.net/def/crs/EPSG/0/32623", + "http://www.opengis.net/def/crs/EPSG/0/32624", + "http://www.opengis.net/def/crs/EPSG/0/32625", + "http://www.opengis.net/def/crs/EPSG/0/32626", + "http://www.opengis.net/def/crs/EPSG/0/32627", + "http://www.opengis.net/def/crs/EPSG/0/32628", + "http://www.opengis.net/def/crs/EPSG/0/32629", + "http://www.opengis.net/def/crs/EPSG/0/32630", + "http://www.opengis.net/def/crs/EPSG/0/32631", + "http://www.opengis.net/def/crs/EPSG/0/32632", + "http://www.opengis.net/def/crs/EPSG/0/32633", + "http://www.opengis.net/def/crs/EPSG/0/32634", + "http://www.opengis.net/def/crs/EPSG/0/32635", + "http://www.opengis.net/def/crs/EPSG/0/32636", + "http://www.opengis.net/def/crs/EPSG/0/32637", + "http://www.opengis.net/def/crs/EPSG/0/32638", + "http://www.opengis.net/def/crs/EPSG/0/32639", + "http://www.opengis.net/def/crs/EPSG/0/32640", + "http://www.opengis.net/def/crs/EPSG/0/32641", + "http://www.opengis.net/def/crs/EPSG/0/32642", + "http://www.opengis.net/def/crs/EPSG/0/32643", + "http://www.opengis.net/def/crs/EPSG/0/32644", + "http://www.opengis.net/def/crs/EPSG/0/32645", + "http://www.opengis.net/def/crs/EPSG/0/32646", + "http://www.opengis.net/def/crs/EPSG/0/32647", + "http://www.opengis.net/def/crs/EPSG/0/32648", + "http://www.opengis.net/def/crs/EPSG/0/32649", + "http://www.opengis.net/def/crs/EPSG/0/32650", + "http://www.opengis.net/def/crs/EPSG/0/32651", + "http://www.opengis.net/def/crs/EPSG/0/32652", + "http://www.opengis.net/def/crs/EPSG/0/32653", + "http://www.opengis.net/def/crs/EPSG/0/32654", + "http://www.opengis.net/def/crs/EPSG/0/32655", + "http://www.opengis.net/def/crs/EPSG/0/32656", + "http://www.opengis.net/def/crs/EPSG/0/32657", + "http://www.opengis.net/def/crs/EPSG/0/32658", + "http://www.opengis.net/def/crs/EPSG/0/32659", + "http://www.opengis.net/def/crs/EPSG/0/32660", + "http://www.opengis.net/def/crs/EPSG/0/32701", + "http://www.opengis.net/def/crs/EPSG/0/32702", + "http://www.opengis.net/def/crs/EPSG/0/32703", + "http://www.opengis.net/def/crs/EPSG/0/32704", + "http://www.opengis.net/def/crs/EPSG/0/32705", + "http://www.opengis.net/def/crs/EPSG/0/32706", + "http://www.opengis.net/def/crs/EPSG/0/32707", + "http://www.opengis.net/def/crs/EPSG/0/32708", + "http://www.opengis.net/def/crs/EPSG/0/32709", + "http://www.opengis.net/def/crs/EPSG/0/32710", + "http://www.opengis.net/def/crs/EPSG/0/32711", + "http://www.opengis.net/def/crs/EPSG/0/32712", + "http://www.opengis.net/def/crs/EPSG/0/32713", + "http://www.opengis.net/def/crs/EPSG/0/32714", + "http://www.opengis.net/def/crs/EPSG/0/32715", + "http://www.opengis.net/def/crs/EPSG/0/32716", + "http://www.opengis.net/def/crs/EPSG/0/32717", + "http://www.opengis.net/def/crs/EPSG/0/32718", + "http://www.opengis.net/def/crs/EPSG/0/32719", + "http://www.opengis.net/def/crs/EPSG/0/32720", + "http://www.opengis.net/def/crs/EPSG/0/32721", + "http://www.opengis.net/def/crs/EPSG/0/32722", + "http://www.opengis.net/def/crs/EPSG/0/32723", + "http://www.opengis.net/def/crs/EPSG/0/32724", + "http://www.opengis.net/def/crs/EPSG/0/32725", + "http://www.opengis.net/def/crs/EPSG/0/32726", + "http://www.opengis.net/def/crs/EPSG/0/32727", + "http://www.opengis.net/def/crs/EPSG/0/32728", + "http://www.opengis.net/def/crs/EPSG/0/32729", + "http://www.opengis.net/def/crs/EPSG/0/32730", + "http://www.opengis.net/def/crs/EPSG/0/32731", + "http://www.opengis.net/def/crs/EPSG/0/32732", + "http://www.opengis.net/def/crs/EPSG/0/32733", + "http://www.opengis.net/def/crs/EPSG/0/32734", + "http://www.opengis.net/def/crs/EPSG/0/32735", + "http://www.opengis.net/def/crs/EPSG/0/32736", + "http://www.opengis.net/def/crs/EPSG/0/32737", + "http://www.opengis.net/def/crs/EPSG/0/32738", + "http://www.opengis.net/def/crs/EPSG/0/32739", + "http://www.opengis.net/def/crs/EPSG/0/32740", + "http://www.opengis.net/def/crs/EPSG/0/32741", + "http://www.opengis.net/def/crs/EPSG/0/32742", + "http://www.opengis.net/def/crs/EPSG/0/32743", + "http://www.opengis.net/def/crs/EPSG/0/32744", + "http://www.opengis.net/def/crs/EPSG/0/32745", + "http://www.opengis.net/def/crs/EPSG/0/32746", + "http://www.opengis.net/def/crs/EPSG/0/32746", + "http://www.opengis.net/def/crs/EPSG/0/32748", + "http://www.opengis.net/def/crs/EPSG/0/32749", + "http://www.opengis.net/def/crs/EPSG/0/32750", + "http://www.opengis.net/def/crs/EPSG/0/32751", + "http://www.opengis.net/def/crs/EPSG/0/32752", + "http://www.opengis.net/def/crs/EPSG/0/32753", + "http://www.opengis.net/def/crs/EPSG/0/32754", + "http://www.opengis.net/def/crs/EPSG/0/32755", + "http://www.opengis.net/def/crs/EPSG/0/32756", + "http://www.opengis.net/def/crs/EPSG/0/32757", + "http://www.opengis.net/def/crs/EPSG/0/32758", + "http://www.opengis.net/def/crs/EPSG/0/32759", + "http://www.opengis.net/def/crs/EPSG/0/32760", + "http://www.opengis.net/def/crs/SR-ORG/0/98739" + ] +} \ No newline at end of file diff --git a/tests/test_units.py b/tests/test_units.py index 04a692c3..09e4c538 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -57,6 +57,7 @@ def test_collections(get_process_graph, collection_id): [ "SPOT", "PLEIADES", + "SKYSAT", "WORLDVIEW", "PLANETSCOPE", "landsat-7-etm+-l2",