@@ -260,6 +260,7 @@ def __init__(
260
260
refresh_token_store : Optional [RefreshTokenStore ] = None ,
261
261
slow_response_threshold : Optional [float ] = None ,
262
262
oidc_auth_renewer : Optional [OidcAuthenticator ] = None ,
263
+ auto_validate : bool = True ,
263
264
):
264
265
"""
265
266
Constructor of Connection, authenticates user.
@@ -282,6 +283,7 @@ def __init__(
282
283
self ._auth_config = auth_config
283
284
self ._refresh_token_store = refresh_token_store
284
285
self ._oidc_auth_renewer = oidc_auth_renewer
286
+ self ._auto_validate = auto_validate
285
287
286
288
@classmethod
287
289
def version_discovery (
@@ -1052,8 +1054,8 @@ def validate_process_graph(self, process_graph: dict) -> List[dict]:
1052
1054
:param process_graph: (flat) dict representing process graph
1053
1055
:return: list of errors (dictionaries with "code" and "message" fields)
1054
1056
"""
1055
- request = { " process_graph" : process_graph }
1056
- return self .post (path = "/validation" , json = request , expected_status = 200 ).json ()["errors" ]
1057
+ pg_with_metadata = self . _build_request_with_process_graph ( process_graph )[ "process" ]
1058
+ return self .post (path = "/validation" , json = pg_with_metadata , expected_status = 200 ).json ()["errors" ]
1057
1059
1058
1060
@property
1059
1061
def _api_version (self ) -> ComparableVersion :
@@ -1393,8 +1395,9 @@ def load_url(self, url: str, format: str, options: Optional[dict] = None):
1393
1395
1394
1396
def create_service (self , graph : dict , type : str , ** kwargs ) -> Service :
1395
1397
# TODO: type hint for graph: is it a nested or a flat one?
1396
- req = self ._build_request_with_process_graph (process_graph = graph , type = type , ** kwargs )
1397
- response = self .post (path = "/services" , json = req , expected_status = 201 )
1398
+ pg_with_metadata = self ._build_request_with_process_graph (process_graph = graph , type = type , ** kwargs )
1399
+ self ._preflight_validation (pg_with_metadata = pg_with_metadata )
1400
+ response = self .post (path = "/services" , json = pg_with_metadata , expected_status = 201 )
1398
1401
service_id = response .headers .get ("OpenEO-Identifier" )
1399
1402
return Service (service_id , self )
1400
1403
@@ -1463,23 +1466,55 @@ def upload_file(
1463
1466
def _build_request_with_process_graph (self , process_graph : Union [dict , FlatGraphableMixin , Any ], ** kwargs ) -> dict :
1464
1467
"""
1465
1468
Prepare a json payload with a process graph to submit to /result, /services, /jobs, ...
1466
- :param process_graph: flat dict representing a process graph
1469
+ :param process_graph: flat dict representing a " process graph with metadata" ({"process": {"process_graph": ...}, ...})
1467
1470
"""
1468
1471
# TODO: make this a more general helper (like `as_flat_graph`)
1469
1472
result = kwargs
1470
1473
process_graph = as_flat_graph (process_graph )
1471
1474
if "process_graph" not in process_graph :
1472
1475
process_graph = {"process_graph" : process_graph }
1473
- # TODO: also check if `process_graph` already has "process" key (i.e. is a "process graph with metadata already)
1476
+ # TODO: also check if `process_graph` already has "process" key (i.e. is a "process graph with metadata" already)
1474
1477
result ["process" ] = process_graph
1475
1478
return result
1476
1479
1480
+ def _preflight_validation (self , pg_with_metadata : dict , * , validate : Optional [bool ] = None ):
1481
+ """
1482
+ Preflight validation of process graph to execute.
1483
+
1484
+ :param pg_with_metadata: flat dict representation of process graph with metadata,
1485
+ e.g. as produced by `_build_request_with_process_graph`
1486
+ :param validate: Optional toggle to enable/prevent validation of the process graphs before execution
1487
+ (overruling the connection's ``auto_validate`` setting).
1488
+
1489
+ :return:
1490
+ """
1491
+ if validate is None :
1492
+ validate = self ._auto_validate
1493
+ if validate and self .capabilities ().supports_endpoint ("/validation" , "POST" ):
1494
+ # At present, the intention is that a failed validation does not block
1495
+ # the job from running, it is only reported as a warning.
1496
+ # Therefor we also want to continue when something *else* goes wrong
1497
+ # *during* the validation.
1498
+ try :
1499
+ resp = self .post (path = "/validation" , json = pg_with_metadata ["process" ], expected_status = 200 )
1500
+ validation_errors = resp .json ()["errors" ]
1501
+ if validation_errors :
1502
+ _log .warning (
1503
+ "Preflight process graph validation raised: "
1504
+ + (" " .join (f"[{ e .get ('code' )} ] { e .get ('message' )} " for e in validation_errors ))
1505
+ )
1506
+ except Exception as e :
1507
+ _log .error (f"Preflight process graph validation failed: { e } " , exc_info = True )
1508
+
1509
+ # TODO: additional validation and sanity checks: e.g. is there a result node, are all process_ids valid, ...?
1510
+
1477
1511
# TODO: unify `download` and `execute` better: e.g. `download` always writes to disk, `execute` returns result (raw or as JSON decoded dict)
1478
1512
def download (
1479
1513
self ,
1480
1514
graph : Union [dict , FlatGraphableMixin , str , Path ],
1481
1515
outputfile : Union [Path , str , None ] = None ,
1482
1516
timeout : Optional [int ] = None ,
1517
+ validate : Optional [bool ] = None ,
1483
1518
) -> Union [None , bytes ]:
1484
1519
"""
1485
1520
Downloads the result of a process graph synchronously,
@@ -1490,11 +1525,14 @@ def download(
1490
1525
or as local file path or URL
1491
1526
:param outputfile: output file
1492
1527
:param timeout: timeout to wait for response
1528
+ :param validate: Optional toggle to enable/prevent validation of the process graphs before execution
1529
+ (overruling the connection's ``auto_validate`` setting).
1493
1530
"""
1494
- request = self ._build_request_with_process_graph (process_graph = graph )
1531
+ pg_with_metadata = self ._build_request_with_process_graph (process_graph = graph )
1532
+ self ._preflight_validation (pg_with_metadata = pg_with_metadata , validate = validate )
1495
1533
response = self .post (
1496
1534
path = "/result" ,
1497
- json = request ,
1535
+ json = pg_with_metadata ,
1498
1536
expected_status = 200 ,
1499
1537
stream = True ,
1500
1538
timeout = timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE ,
@@ -1511,21 +1549,26 @@ def execute(
1511
1549
self ,
1512
1550
process_graph : Union [dict , str , Path ],
1513
1551
timeout : Optional [int ] = None ,
1552
+ validate : Optional [bool ] = None ,
1514
1553
):
1515
1554
"""
1516
1555
Execute a process graph synchronously and return the result (assumed to be JSON).
1517
1556
1518
1557
:param process_graph: (flat) dict representing a process graph, or process graph as raw JSON string,
1519
1558
or as local file path or URL
1559
+ :param validate: Optional toggle to enable/prevent validation of the process graphs before execution
1560
+ (overruling the connection's ``auto_validate`` setting).
1561
+
1520
1562
:return: parsed JSON response
1521
1563
"""
1522
- req = self ._build_request_with_process_graph (process_graph = process_graph )
1564
+ pg_with_metadata = self ._build_request_with_process_graph (process_graph = process_graph )
1565
+ self ._preflight_validation (pg_with_metadata = pg_with_metadata , validate = validate )
1523
1566
return self .post (
1524
1567
path = "/result" ,
1525
- json = req ,
1568
+ json = pg_with_metadata ,
1526
1569
expected_status = 200 ,
1527
1570
timeout = timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE ,
1528
- ).json ()
1571
+ ).json () # TODO: only do JSON decoding when mimetype is actually JSON?
1529
1572
1530
1573
def create_job (
1531
1574
self ,
@@ -1536,6 +1579,7 @@ def create_job(
1536
1579
plan : Optional [str ] = None ,
1537
1580
budget : Optional [float ] = None ,
1538
1581
additional : Optional [dict ] = None ,
1582
+ validate : Optional [bool ] = None ,
1539
1583
) -> BatchJob :
1540
1584
"""
1541
1585
Create a new job from given process graph on the back-end.
@@ -1547,18 +1591,22 @@ def create_job(
1547
1591
:param plan: billing plan
1548
1592
:param budget: maximum cost the request is allowed to produce
1549
1593
:param additional: additional job options to pass to the backend
1594
+ :param validate: Optional toggle to enable/prevent validation of the process graphs before execution
1595
+ (overruling the connection's ``auto_validate`` setting).
1550
1596
:return: Created job
1551
1597
"""
1552
1598
# TODO move all this (BatchJob factory) logic to BatchJob?
1553
- req = self ._build_request_with_process_graph (
1599
+
1600
+ pg_with_metadata = self ._build_request_with_process_graph (
1554
1601
process_graph = process_graph ,
1555
1602
** dict_no_none (title = title , description = description , plan = plan , budget = budget )
1556
1603
)
1557
1604
if additional :
1558
1605
# TODO: get rid of this non-standard field? https://github.com/Open-EO/openeo-api/issues/276
1559
- req ["job_options" ] = additional
1606
+ pg_with_metadata ["job_options" ] = additional
1560
1607
1561
- response = self .post ("/jobs" , json = req , expected_status = 201 )
1608
+ self ._preflight_validation (pg_with_metadata = pg_with_metadata , validate = validate )
1609
+ response = self .post ("/jobs" , json = pg_with_metadata , expected_status = 201 )
1562
1610
1563
1611
job_id = None
1564
1612
if "openeo-identifier" in response .headers :
@@ -1636,8 +1684,8 @@ def as_curl(
1636
1684
cmd += ["-H" , "Content-Type: application/json" ]
1637
1685
if isinstance (self .auth , BearerAuth ):
1638
1686
cmd += ["-H" , f"Authorization: Bearer { '...' if obfuscate_auth else self .auth .bearer } " ]
1639
- post_data = self ._build_request_with_process_graph (data )
1640
- post_json = json .dumps (post_data , separators = (',' , ':' ))
1687
+ pg_with_metadata = self ._build_request_with_process_graph (data )
1688
+ post_json = json .dumps (pg_with_metadata , separators = ("," , ":" ))
1641
1689
cmd += ["--data" , post_json ]
1642
1690
cmd += [self .build_url (path )]
1643
1691
return " " .join (shlex .quote (c ) for c in cmd )
@@ -1657,17 +1705,20 @@ def version_info(self):
1657
1705
1658
1706
1659
1707
def connect (
1660
- url : Optional [str ] = None ,
1661
- auth_type : Optional [str ] = None , auth_options : Optional [dict ] = None ,
1662
- session : Optional [requests .Session ] = None ,
1663
- default_timeout : Optional [int ] = None ,
1708
+ url : Optional [str ] = None ,
1709
+ * ,
1710
+ auth_type : Optional [str ] = None ,
1711
+ auth_options : Optional [dict ] = None ,
1712
+ session : Optional [requests .Session ] = None ,
1713
+ default_timeout : Optional [int ] = None ,
1714
+ auto_validate : bool = True ,
1664
1715
) -> Connection :
1665
1716
"""
1666
1717
This method is the entry point to OpenEO.
1667
1718
You typically create one connection object in your script or application
1668
1719
and re-use it for all calls to that backend.
1669
1720
1670
- If the backend requires authentication, you can pass authentication data directly to this function
1721
+ If the backend requires authentication, you can pass authentication data directly to this function,
1671
1722
but it could be easier to authenticate as follows:
1672
1723
1673
1724
>>> # For basic authentication
@@ -1679,7 +1730,10 @@ def connect(
1679
1730
:param auth_type: Which authentication to use: None, "basic" or "oidc" (for OpenID Connect)
1680
1731
:param auth_options: Options/arguments specific to the authentication type
1681
1732
:param default_timeout: default timeout (in seconds) for requests
1682
- :rtype: openeo.connections.Connection
1733
+ :param auto_validate: toggle to automatically validate process graphs before execution
1734
+
1735
+ .. versionadded:: 0.24.0
1736
+ added ``auto_validate`` argument
1683
1737
"""
1684
1738
1685
1739
def _config_log (message ):
@@ -1704,7 +1758,7 @@ def _config_log(message):
1704
1758
1705
1759
if not url :
1706
1760
raise OpenEoClientException ("No openEO back-end URL given or known to connect to." )
1707
- connection = Connection (url , session = session , default_timeout = default_timeout )
1761
+ connection = Connection (url , session = session , default_timeout = default_timeout , auto_validate = auto_validate )
1708
1762
1709
1763
auth_type = auth_type .lower () if isinstance (auth_type , str ) else auth_type
1710
1764
if auth_type in {None , False , 'null' , 'none' }:
0 commit comments