-
Notifications
You must be signed in to change notification settings - Fork 6
/
errors.py
620 lines (469 loc) · 23.2 KB
/
errors.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
"""
Implementation of the openEO error handling API
https://open-eo.github.io/openeo-api/errors/
To avoid brittle exception handling, we don't want to parse the error
spec at run time or dynamically generate openEO compliant exceptions.
Instead, we define a straightforward Python exception class for each
openEO error code.
To keep the implementation in sync with the spec, the following semi-automated
approach is followed:
- there are unit tests that do a compliance and health check of the exception
classes (see test_errors.py). They will complain for example about
missing exceptions or status code mismatches.
- executing this module directly will automatically generate source code
for missing exceptions:
python openeo_driver/errors.py
This approach also allows to customize certain exceptions a bit
where necessary or useful.
"""
import json
import re
import textwrap
from typing import List, Set, Optional
from openeo_driver.specs import SPECS_ROOT
from openeo_driver.util.logging import FlaskRequestCorrelationIdLogging
class OpenEOApiException(Exception):
"""
Exception that wraps the fields/data necessary for OpenEO API compliant status/error handling
see https://open-eo.github.io/openeo-api/errors/#json-error-object:
required fields:
- code: standardized textual openEO error code
- message: human readable explanation
optional:
- id: unique identifier for referencing between API responses and server logs
- url: link to resource that explains error/solutions
"""
status_code = 500
code = "Internal"
message = "Unspecified Internal server error"
_description = "Internal/generic error"
_tags = ["General"]
url = None
def __init__(
self,
message: Optional[str] = None,
code: Optional[str] = None,
status_code: Optional[int] = None,
id: Optional[str] = None,
url: Optional[str] = None,
):
super().__init__(message or self.message)
self.message = message or self.message
# (Standardized) textual openEO error code
self.code = code or self.code
# HTTP status code
self.status_code = status_code or self.status_code
# Use request correlation id as error id to simplify post-mortem analysis.
self.id = id or FlaskRequestCorrelationIdLogging.get_request_id()
self.url = url or self.url
def to_dict(self):
"""Generate OpenEO API compliant error dict to JSONify"""
d = {"message": str(self), "code": self.code, "id": self.id}
if self.url:
d['url'] = self.url
return d
def __repr__(self):
return f"{type(self).__name__}(status_code={self.status_code!r}, code={self.code!r}, message={self.message!r}, id={self.id!r})"
# --- Begin of semi-autogenerated openEO exception classes ------------------------------------------------
class TokenInvalidException(OpenEOApiException):
status_code = 403
code = 'TokenInvalid'
message = 'Authorization token has expired or is invalid. Please authenticate again.'
_description = None
_tags = ['Account Management']
class AuthenticationRequiredException(OpenEOApiException):
status_code = 401
code = 'AuthenticationRequired'
message = 'Unauthorized.'
_description = 'The client did not provide any authentication details for a resource requiring authentication or the provided authentication details are not correct.'
_tags = ['Account Management']
class CredentialsInvalidException(OpenEOApiException):
status_code = 403
code = 'CredentialsInvalid'
message = 'Credentials are not correct.'
_description = None
_tags = ['Account Management']
class AuthenticationSchemeInvalidException(OpenEOApiException):
status_code = 403
code = 'AuthenticationSchemeInvalid'
message = 'Authentication method not supported.'
_description = 'Invalid authentication scheme (e.g. Bearer).'
_tags = ['Account Management']
class PermissionsInsufficientException(OpenEOApiException):
status_code = 403
code = 'PermissionsInsufficient'
message = 'Forbidden. The permissions of the authenticated account do not allow to request the requested resource.'
_description = 'Forbidden. The client did provided correct authentication details, but the privileges/permissions of the provided credentials do not allow to request the resource.'
_tags = ['Account Management']
class CollectionNotFoundException(OpenEOApiException):
status_code = 404
code = 'CollectionNotFound'
message = "Collection '{identifier}' does not exist."
_description = 'The requested collection does not exist.'
_tags = ['EO Data Discovery']
def __init__(self, collection_id: str):
super().__init__(message=self.message.format(identifier=collection_id))
class FileLockedException(OpenEOApiException):
status_code = 400
code = 'FileLocked'
message = "File '{file}' is locked by another process."
_description = 'The file is locked by a running job or another process.'
_tags = ['File Management']
def __init__(self, file: str):
super().__init__(message=self.message.format(file=file))
class FileSizeExceededException(OpenEOApiException):
status_code = 400
code = 'FileSizeExceeded'
message = 'File size it too large. Maximum file size: {size}'
_description = 'File exceeds allowed maximum file size.'
_tags = ['File Management']
def __init__(self, size: str):
super().__init__(message=self.message.format(size=size))
class FilePathInvalidException(OpenEOApiException):
status_code = 400
code = 'FilePathInvalid'
message = 'File path is invalid: {reason}'
_description = 'The specified path is invalid or not accessible. Path could contain invalid characters, point to an existing folder or a location outside of the user folder.'
_tags = ['File Management']
def __init__(self, reason: str):
super().__init__(message=self.message.format(reason=reason))
class FileTypeInvalidException(OpenEOApiException):
status_code = 400
code = 'FileTypeInvalid'
message = 'File format {type} not allowed. Allowed file formats: {types}'
_description = 'File format or file extension is not allowed.'
_tags = ['File Management']
def __init__(self, type: str, types: str):
super().__init__(message=self.message.format(type=type, types=types))
class FileOperationUnsupportedException(OpenEOApiException):
status_code = 400
code = 'FileOperationUnsupported'
message = 'The file operation is not supported for the specified path.'
_description = None
_tags = ['File Management']
class ContentTypeInvalidException(OpenEOApiException):
status_code = 400
code = 'ContentTypeInvalid'
message = 'The media type is not supported. Allowed: {types}'
_description = 'The specified media (MIME) type used in the Content-Type header is not allowed.'
_tags = ['File Management', 'General']
def __init__(self, types: str):
super().__init__(message=self.message.format(types=types))
class StorageFailureException(OpenEOApiException):
status_code = 500
code = 'StorageFailure'
message = 'Unable to store files due to a server error. Please try again later or contact our support.'
_description = "Server couldn't store file(s) due to server-side reasons."
_tags = ['Batch Jobs', 'File Management']
class StorageQuotaExceededException(OpenEOApiException):
status_code = 400
code = 'StorageQuotaExceeded'
message = 'Your storage quota has been exceeded.'
_description = 'The storage quota has been exceeded by the user.'
_tags = ['Batch Jobs', 'File Management']
class FileNotFoundException(OpenEOApiException):
status_code = 404
code = 'FileNotFound'
message = "File '{file}' does not exist."
_description = 'The requested file does not exist.'
_tags = ['File Management']
def __init__(self, filename: str):
super().__init__(message=self.message.format(file=filename))
class FileContentInvalidException(OpenEOApiException):
status_code = 400
code = 'FileContentInvalid'
message = 'File content is invalid.'
_description = 'The content of the file is invalid.'
_tags = ['File Management']
class FolderOperationUnsupportedException(OpenEOApiException):
status_code = 400
code = 'FolderOperationUnsupported'
message = 'Operation is only supported for files, not folders.'
_description = 'The specified path is a folder and the operation is only supported for files.'
_tags = ['File Management']
class UnsupportedApiVersionException(OpenEOApiException):
status_code = 404
code = 'UnsupportedApiVersion'
message = "The requested API version '{version}' is not supported."
_description = "The service doesn't support the openEO API version specified in the request URL. Clients should check well-known document for supported versions."
_tags = ['General']
def __init__(self, version: str):
super().__init__(message=self.message.format(version=version))
class InternalException(OpenEOApiException):
status_code = 500
code = 'Internal'
message = 'Server error: {message}'
_description = 'An internal server error with a proprietary message.'
_tags = ['General']
def __init__(self, message: str):
super().__init__(message=self.message.format(message=message))
class InfrastructureMaintenanceException(OpenEOApiException):
status_code = 503
code = 'InfrastructureMaintenance'
message = 'Service is not available at the moment due to maintenance work. Please try again later or contact our support.'
_description = 'Service is currently not available as the infrastructure is currently undergoing maintenance work.'
_tags = ['General']
class InfrastructureBusyException(OpenEOApiException):
status_code = 503
code = 'InfrastructureBusy'
message = 'Service is not available at the moment due to overloading. Please try again later or contact our support.'
_description = "Service is generally available, but the infrastructure can't handle it at the moment as too many requests are processed."
_tags = ['General']
class FeatureUnsupportedException(OpenEOApiException):
status_code = 501
code = 'FeatureUnsupported'
message = 'Feature not supported.'
_description = 'The back-end responds with this error whenever an endpoint is specified in the openEO API, but is not supported.'
_tags = ['General']
class NotFoundException(OpenEOApiException):
status_code = 404
code = 'NotFound'
message = 'Resource not found.'
_description = "To be used if the requested resource does not exist. Note: There are specialized errors for missing jobs (JobNotFound), files (FileNotFound), etc. Unsupported endpoints MAY send an 'FeatureUnsupported' (501) error."
_tags = ['General']
class RequestTimeoutException(OpenEOApiException):
status_code = 408
code = 'RequestTimeout'
message = 'Request timed out.'
_description = 'The request took too long and timed out.'
_tags = ['Data Processing', 'General']
class ProcessGraphComplexityException(OpenEOApiException):
status_code = 400
code = 'ProcessGraphComplexity'
message = 'The process is too complex for for synchronous processing. Please use a batch job instead.'
_description = 'The process graph is too complex for synchronous processing and will likely time out. Please use a batch job instead.'
_tags = ['Data Processing']
class JobNotFinishedException(OpenEOApiException):
status_code = 400
code = 'JobNotFinished'
message = 'Batch job has not finished computing the results yet. Please try again later or contact our support.'
_description = None
_tags = ['Batch Jobs']
class JobLockedException(OpenEOApiException):
status_code = 400
code = 'JobLocked'
message = 'Batch job is locked due to a queued or running batch computation.'
_description = "The job is currently locked due to a running batch computation and can't be modified meanwhile."
_tags = ['Batch Jobs']
class JobNotStartedException(OpenEOApiException):
status_code = 400
code = 'JobNotStarted'
message = 'Batch job must be started first.'
_description = 'Job has not been queued or started yet or was canceled and not restarted by the user.'
_tags = ['Batch Jobs']
class PropertyNotEditableException(OpenEOApiException):
status_code = 400
code = 'PropertyNotEditable'
message = "The specified property '{property}' is read-only."
_description = "For PATCH requests: The specified parameter can't be updated. It is read-only."
_tags = ['Batch Jobs', 'Secondary Services']
def __init__(self, property: str):
super().__init__(message=self.message.format(property=property))
class ProcessInvalidException(OpenEOApiException):
status_code = 400
code = 'ProcessInvalid'
message = 'Invalid process specified.'
_description = 'The process given is invalid, which ususlly means that the process metadata is invalid.'
_tags = ['Batch Jobs', 'Data Processing', 'Secondary Services', 'User-Defined Processes']
class ProcessGraphMissingException(OpenEOApiException):
status_code = 400
code = 'ProcessGraphMissing'
message = "Invalid process specified. It doesn't contain a process graph."
_description = "The parameter `process` doesn't contain a valid process."
_tags = ['Batch Jobs', 'Data Processing', 'Secondary Services', 'User-Defined Processes']
class ProcessGraphInvalidException(OpenEOApiException):
status_code = 400
code = 'ProcessGraphInvalid'
message = 'Invalid process graph specified.'
_description = "The process doesn't contain a valid process graph, which means it doesn't comply to the general structure / schema."
_tags = ['Batch Jobs', 'Data Processing', 'Secondary Services', 'User-Defined Processes']
class NoDataForUpdateException(OpenEOApiException):
status_code = 400
code = 'NoDataForUpdate'
message = 'No data specified to be updated.'
_description = 'For PATCH requests: No valid data specified at all.'
_tags = ['Batch Jobs', 'Secondary Services']
class JobNotFoundException(OpenEOApiException):
status_code = 404
code = 'JobNotFound'
message = "The batch job '{identifier}' does not exist."
_description = 'The requested job does not exist.'
_tags = ['Batch Jobs']
def __init__(self, job_id: str):
super().__init__(message=self.message.format(identifier=job_id))
self.job_id = job_id
class BillingPlanInvalidException(OpenEOApiException):
status_code = 400
code = 'BillingPlanInvalid'
message = 'The billing plan is invalid.'
_description = 'The billing plan is not on the list of available plans.'
_tags = ['Batch Jobs', 'Data Processing', 'Secondary Services']
class PaymentRequiredException(OpenEOApiException):
status_code = 402
code = 'PaymentRequired'
message = 'The budget required to fulfil the request is not sufficient. A payment is required first.'
_description = 'The budget required to fulfil the request is insufficient.'
_tags = ['Batch Jobs', 'Secondary Services']
class BudgetInvalidException(OpenEOApiException):
status_code = 400
code = 'BudgetInvalid'
message = 'The specified budget is too low.'
_description = 'The budget is too low as it is either smaller than or equal to 0 or below the costs.'
_tags = ['Batch Jobs', 'Data Processing', 'Secondary Services']
class ProcessGraphNotFoundException(OpenEOApiException):
status_code = 404
code = 'ProcessGraphNotFound'
message = "User-defined process '{identifier}' does not exist."
_description = 'The requested user-defined process does not exist. To be used for all endpoints starting with `/process_graphs`.'
_tags = ['User-Defined Processes']
def __init__(self, process_graph_id: str):
super().__init__(message=self.message.format(identifier=process_graph_id))
class ProcessParameterInvalidException(OpenEOApiException):
status_code = 400
code = 'ProcessParameterInvalid'
message = "The value passed for parameter '{parameter}' in process '{process}' is invalid: {reason}"
_description = None
_tags = ['Data Processing']
def __init__(self, parameter: str, process: str, reason: str):
super().__init__(message=self.message.format(parameter=parameter, process=process, reason=reason))
class ProcessUnsupportedException(OpenEOApiException):
status_code = 400
code = 'ProcessUnsupported'
message = "Process with identifier '{process}' is not available in namespace '{namespace}'."
_description = 'A process (pre-defined or user-defined) with the specified identifier is not available. To be used when validating or executing process graphs.'
_tags = ['Data Processing']
def __init__(self, process: str, namespace: str = "backend"):
super().__init__(message=self.message.format(process=process, namespace=namespace))
class PredefinedProcessExistsException(OpenEOApiException):
status_code = 400
code = 'PredefinedProcessExists'
message = 'A predefined process with the given identifier exists.'
_description = 'If a user wants to store a user-defined process with the id of a pre-defined process.'
_tags = ['User-Defined Processes']
class ProcessParameterRequiredException(OpenEOApiException):
status_code = 400
code = 'ProcessParameterRequired'
message = "Process '{process}' parameter '{parameter}' is required."
_description = None
_tags = ['Data Processing']
def __init__(self, process: str, parameter: str):
super().__init__(message=self.message.format(process=process, parameter=parameter))
class ProcessParameterUnsupportedException(OpenEOApiException):
status_code = 400
code = 'ProcessParameterUnsupported'
message = "Process '{process}' does not support parameter '{parameter}'."
_description = None
_tags = ['Data Processing']
def __init__(self, process: str, parameter: str):
super().__init__(message=self.message.format(process=process, parameter=parameter))
class ResultLinkExpiredException(OpenEOApiException):
status_code = 410
code = 'ResultLinkExpired'
message = 'The link to the batch job result has expired. Please request the results again.'
_description = 'The signed URLs for batch job results have expired. Please send a request to `GET /jobs/{job_id}/results` to refresh the links.'
_tags = ['Batch Jobs']
class ServiceConfigUnsupportedException(OpenEOApiException):
status_code = 400
code = 'ServiceConfigUnsupported'
message = "Service parameter '{parameter}' is not supported."
_description = 'Refers to the secondary service `configuration` object.'
_tags = ['Secondary Services']
def __init__(self, parameter: str):
super().__init__(message=self.message.format(parameter=parameter))
class ServiceUnsupportedException(OpenEOApiException):
status_code = 400
code = 'ServiceUnsupported'
message = "Service type '{type}' is not supported."
_description = None
_tags = ['Secondary Services']
def __init__(self, type: str):
super().__init__(message=self.message.format(type=type))
class ServiceConfigRequiredException(OpenEOApiException):
status_code = 400
code = 'ServiceConfigRequired'
message = "Service parameter '{parameter}' is required."
_description = 'Refers to the secondary service `configuration` object.'
_tags = ['Secondary Services']
def __init__(self, parameter: str):
super().__init__(message=self.message.format(parameter=parameter))
class ServiceNotFoundException(OpenEOApiException):
status_code = 404
code = 'ServiceNotFound'
message = "Service '{identifier}' does not exist."
_description = 'The requested secondary service does not exist.'
_tags = ['Secondary Services']
def __init__(self, service_id: str):
super().__init__(message=self.message.format(identifier=service_id))
class ServiceConfigInvalidException(OpenEOApiException):
status_code = 400
code = 'ServiceConfigInvalid'
message = "The value passed for the service parameter '{parameter}' is invalid: {reason}"
_description = 'Refers to the secondary service `configuration` object.'
_tags = ['Secondary Services']
def __init__(self, parameter: str, reason: str):
super().__init__(message=self.message.format(parameter=parameter, reason=reason))
# --- End of semi-autogenerated openEO exception classes ------------------------------------------------
class OpenEOApiErrorSpecHelper:
"""
Helper class around OpenEO API error handling spec to support automated
generation of Python exception classes
"""
_placeholder_regex = re.compile(r"{(\w+)}")
def __init__(self, spec: dict = None):
if spec is None:
with (SPECS_ROOT / 'openeo-api/1.x/errors.json').open('r', encoding='utf-8') as f:
spec = json.load(f)
self._spec = spec
def get(self, error_code: str) -> dict:
return self._spec[error_code]
def get_error_codes(self) -> List[str]:
return list(self._spec.keys())
def generate_exception_class(self, error_code: str) -> str:
"""Generate source code for given OpenEO error code"""
spec = self._spec[error_code]
message = spec["message"]
src = textwrap.dedent("""\
class {code}Exception({parent}):
status_code = {status}
code = {code!r}
message = {message!r}
_description = {description!r}
_tags = {tags!r}
""".format(
code=error_code, parent=OpenEOApiException.__name__,
status=spec["http"],
message=message,
description=spec["description"],
tags=sorted(spec["tags"]),
))
placeholders = self.extract_placeholders(message)
if placeholders:
init = textwrap.dedent("""\
def __init__(self, {args}):
super().__init__(message=self.message.format({format_args}))
""".format(
args=", ".join("{p}: str".format(p=p) for p in placeholders),
format_args=", ".join("{p}={p}".format(p=p) for p in placeholders)
))
src += "\n" + textwrap.indent(init, prefix=" " * 4)
return src
@classmethod
def extract_placeholders(cls, message) -> Set[str]:
return set(cls._placeholder_regex.findall(message))
if __name__ == '__main__':
# Print suggested exception class implementations for missing errors
spec_helper = OpenEOApiErrorSpecHelper()
implemented_codes = [
v.code for v in globals().values()
if isinstance(v, type) and issubclass(v, OpenEOApiException) and v is not OpenEOApiException
]
expected_codes = set(spec_helper.get_error_codes())
unimplemented = expected_codes.difference(implemented_codes)
# Sort on tag
unimplemented = sorted(unimplemented, key=lambda code: sorted(spec_helper.get(code)["tags"]))
if unimplemented:
print("### Found {c} unimplemented openEO error codes. Suggested implementation:".format(c=len(unimplemented)))
for code in unimplemented:
print(spec_helper.generate_exception_class(code) + "\n")
else:
print("### No unimplemented openEO error codes.")