blackant-sdk 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blackant/__init__.py +31 -0
- blackant/auth/__init__.py +10 -0
- blackant/auth/blackant_auth.py +518 -0
- blackant/auth/keycloak_manager.py +363 -0
- blackant/auth/request_id.py +52 -0
- blackant/auth/role_assignment.py +443 -0
- blackant/auth/tokens.py +57 -0
- blackant/client.py +400 -0
- blackant/config/__init__.py +0 -0
- blackant/config/docker_config.py +457 -0
- blackant/config/keycloak_admin_config.py +107 -0
- blackant/docker/__init__.py +12 -0
- blackant/docker/builder.py +616 -0
- blackant/docker/client.py +983 -0
- blackant/docker/dao.py +462 -0
- blackant/docker/registry.py +172 -0
- blackant/exceptions.py +111 -0
- blackant/http/__init__.py +8 -0
- blackant/http/client.py +125 -0
- blackant/patterns/__init__.py +1 -0
- blackant/patterns/singleton.py +20 -0
- blackant/services/__init__.py +10 -0
- blackant/services/dao.py +414 -0
- blackant/services/registry.py +635 -0
- blackant/utils/__init__.py +8 -0
- blackant/utils/initialization.py +32 -0
- blackant/utils/logging.py +337 -0
- blackant/utils/request_id.py +13 -0
- blackant/utils/store.py +50 -0
- blackant_sdk-1.0.2.dist-info/METADATA +117 -0
- blackant_sdk-1.0.2.dist-info/RECORD +70 -0
- blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
- blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
- calculation/__init__.py +0 -0
- calculation/base.py +26 -0
- calculation/errors.py +2 -0
- calculation/impl/__init__.py +0 -0
- calculation/impl/my_calculation.py +144 -0
- calculation/impl/simple_calc.py +53 -0
- calculation/impl/test.py +1 -0
- calculation/impl/test_calc.py +36 -0
- calculation/loader.py +227 -0
- notifinations/__init__.py +8 -0
- notifinations/mail_sender.py +212 -0
- storage/__init__.py +0 -0
- storage/errors.py +10 -0
- storage/factory.py +26 -0
- storage/interface.py +19 -0
- storage/minio.py +106 -0
- task/__init__.py +0 -0
- task/dao.py +38 -0
- task/errors.py +10 -0
- task/log_adapter.py +11 -0
- task/parsers/__init__.py +0 -0
- task/parsers/base.py +13 -0
- task/parsers/callback.py +40 -0
- task/parsers/cmd_args.py +52 -0
- task/parsers/freetext.py +19 -0
- task/parsers/objects.py +50 -0
- task/parsers/request.py +56 -0
- task/resource.py +84 -0
- task/states/__init__.py +0 -0
- task/states/base.py +14 -0
- task/states/error.py +47 -0
- task/states/idle.py +12 -0
- task/states/ready.py +51 -0
- task/states/running.py +21 -0
- task/states/set_up.py +40 -0
- task/states/tear_down.py +29 -0
- task/task.py +358 -0
storage/errors.py
ADDED
storage/factory.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import urllib3
|
|
4
|
+
|
|
5
|
+
from storage.errors import StorageCreationFailedException
|
|
6
|
+
from storage.minio import MinioStorageManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StorageFactory: # pylint: disable=too-few-public-methods
|
|
10
|
+
__instance = None
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def create(cls, logger=None):
|
|
14
|
+
if cls.__instance is None:
|
|
15
|
+
try:
|
|
16
|
+
cls.__instance = MinioStorageManager(
|
|
17
|
+
os.environ["OBJECT_STORAGE_URL"],
|
|
18
|
+
os.environ["CALCULATION_NAME"],
|
|
19
|
+
os.environ["OBJECT_STORAGE_ACCESS_KEY"],
|
|
20
|
+
os.environ["OBJECT_STORAGE_SECRET_KEY"],
|
|
21
|
+
bool(int(os.environ["OBJECT_STORAGE_USE_SSL"])),
|
|
22
|
+
logger=logger,
|
|
23
|
+
)
|
|
24
|
+
except urllib3.exceptions.MaxRetryError as exc:
|
|
25
|
+
raise StorageCreationFailedException from exc
|
|
26
|
+
return cls.__instance
|
storage/interface.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StorageInterface(metaclass=abc.ABCMeta):
|
|
5
|
+
@abc.abstractmethod
|
|
6
|
+
def download_file(self, local_path, file_id):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
def upload_file(self, local_path, file_id):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def download_string(self, file_id):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def upload_string(self, input_string, file_id):
|
|
19
|
+
pass
|
storage/minio.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
import urllib3
|
|
4
|
+
import minio
|
|
5
|
+
|
|
6
|
+
from blackant.utils.logging import get_logger
|
|
7
|
+
from storage.interface import StorageInterface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MinioStorageManager(StorageInterface):
|
|
11
|
+
__instance = None
|
|
12
|
+
|
|
13
|
+
def __init__(self, url, bucket_id, access_key, secret_key, secure, logger=None): # pylint: disable=too-many-arguments
|
|
14
|
+
self.__bucket_id = "".join(letter for letter in bucket_id if letter.isalnum()).lower()
|
|
15
|
+
self.__logger = logger or get_logger("minio_client")
|
|
16
|
+
|
|
17
|
+
self.__logger.debug("Creating minio client for %s with user %s", url, access_key)
|
|
18
|
+
if not secure:
|
|
19
|
+
self.__logger.warning(
|
|
20
|
+
"Minio client is not using SSL. "
|
|
21
|
+
"If the client does NOT communicate via Docker inside network, "
|
|
22
|
+
"it should NOT run in production."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
timeout = timedelta(minutes=5).seconds
|
|
26
|
+
http_client_pool_manager = urllib3.PoolManager(
|
|
27
|
+
timeout=urllib3.util.Timeout(connect=timeout, read=timeout),
|
|
28
|
+
maxsize=10,
|
|
29
|
+
cert_reqs="CERT_NONE",
|
|
30
|
+
retries=urllib3.Retry(
|
|
31
|
+
total=5, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504]
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
self.__client = minio.Minio(
|
|
36
|
+
url,
|
|
37
|
+
access_key=access_key,
|
|
38
|
+
secret_key=secret_key,
|
|
39
|
+
secure=secure,
|
|
40
|
+
http_client=http_client_pool_manager,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
if not self.__client.bucket_exists(self.__bucket_id):
|
|
45
|
+
self.__client.make_bucket(self.__bucket_id)
|
|
46
|
+
except minio.error.AccessDenied:
|
|
47
|
+
self.__logger.critical(
|
|
48
|
+
"Cannot access object storage. Access is denied. "
|
|
49
|
+
"Object storage url: %s , "
|
|
50
|
+
"Access key: %s",
|
|
51
|
+
url,
|
|
52
|
+
access_key,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def download_file(self, local_path, file_id):
|
|
56
|
+
try:
|
|
57
|
+
self.__client.fget_object(self.__bucket_id, file_id, local_path)
|
|
58
|
+
except minio.error.AccessDenied:
|
|
59
|
+
self.__logger.critical(
|
|
60
|
+
"Cannot download file from object storage. Access is denied. "
|
|
61
|
+
"Bucket: %s, remote: %s, local: %s",
|
|
62
|
+
self.__bucket_id,
|
|
63
|
+
file_id,
|
|
64
|
+
local_path,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def upload_file(self, local_path, file_id):
|
|
68
|
+
try:
|
|
69
|
+
self.__client.fput_object(self.__bucket_id, file_id, local_path)
|
|
70
|
+
except minio.error.AccessDenied:
|
|
71
|
+
self.__logger.critical(
|
|
72
|
+
"Cannot upload file to object storage. Access is denied. "
|
|
73
|
+
"Bucket: %s, remote: %s, local: %s",
|
|
74
|
+
self.__bucket_id,
|
|
75
|
+
file_id,
|
|
76
|
+
local_path,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def download_string(self, file_id):
|
|
80
|
+
try:
|
|
81
|
+
obj = self.__client.get_object(self.__bucket_id, file_id)
|
|
82
|
+
return obj.data.decode("utf-8")
|
|
83
|
+
except minio.error.AccessDenied:
|
|
84
|
+
self.__logger.critical(
|
|
85
|
+
"Cannot download file as string from object storage. Access is denied. "
|
|
86
|
+
"Bucket: %s, remote: %s",
|
|
87
|
+
self.__bucket_id,
|
|
88
|
+
file_id,
|
|
89
|
+
)
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
def upload_string(self, input_string, file_id):
|
|
93
|
+
try:
|
|
94
|
+
value_as_bytes = input_string.encode("utf-8")
|
|
95
|
+
value_as_a_stream = io.BytesIO(value_as_bytes)
|
|
96
|
+
self.__client.put_object(
|
|
97
|
+
self.__bucket_id, file_id, value_as_a_stream, length=len(value_as_bytes)
|
|
98
|
+
)
|
|
99
|
+
except minio.error.AccessDenied:
|
|
100
|
+
self.__logger.critical(
|
|
101
|
+
"Cannot upload string to file to object storage. Access is denied. "
|
|
102
|
+
"Bucket: %s, remote: %s, string: %s",
|
|
103
|
+
self.__bucket_id,
|
|
104
|
+
file_id,
|
|
105
|
+
input_string,
|
|
106
|
+
)
|
task/__init__.py
ADDED
|
File without changes
|
task/dao.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from task.errors import InvalidDAOUsageError, TaskDoesNotExistsError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TaskDao:
|
|
7
|
+
__instance = None
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
if self.__instance is not None:
|
|
11
|
+
raise InvalidDAOUsageError("Singleton object was created multiple times")
|
|
12
|
+
self.__data = {}
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def get_instance(cls):
|
|
16
|
+
if cls.__instance is None:
|
|
17
|
+
cls.__instance = TaskDao()
|
|
18
|
+
return cls.__instance
|
|
19
|
+
|
|
20
|
+
def get_task_ids(self):
|
|
21
|
+
return list(self.__data.keys())
|
|
22
|
+
|
|
23
|
+
def get_task(self, task_id):
|
|
24
|
+
if task_id not in self.__data.keys():
|
|
25
|
+
raise TaskDoesNotExistsError
|
|
26
|
+
return self.__data.get(task_id)
|
|
27
|
+
|
|
28
|
+
def add_task(self, task):
|
|
29
|
+
task_id = uuid.uuid4().hex
|
|
30
|
+
task.task_id = task_id
|
|
31
|
+
self.__data[task_id] = task
|
|
32
|
+
return task_id
|
|
33
|
+
|
|
34
|
+
def delete_task(self, task_id):
|
|
35
|
+
if task_id not in self.__data.keys():
|
|
36
|
+
raise TaskDoesNotExistsError
|
|
37
|
+
self.__data.pop(task_id)
|
|
38
|
+
return task_id
|
task/errors.py
ADDED
task/log_adapter.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TaskIdAdapter(logging.LoggerAdapter):
|
|
5
|
+
def set_task_id(self, task_id):
|
|
6
|
+
self.extra["task_id"] = task_id
|
|
7
|
+
|
|
8
|
+
def process(self, msg, kwargs):
|
|
9
|
+
if "task_id" in self.extra:
|
|
10
|
+
return "[%s] %s" % (self.extra["task_id"], msg), kwargs
|
|
11
|
+
return "%s" % msg, kwargs
|
task/parsers/__init__.py
ADDED
|
File without changes
|
task/parsers/base.py
ADDED
task/parsers/callback.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from task.parsers.base import RequestParserBase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class CallbackParameter:
|
|
8
|
+
url: str
|
|
9
|
+
method: str
|
|
10
|
+
headers: dict
|
|
11
|
+
request_body: dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CallbackParser(RequestParserBase):
|
|
15
|
+
def __init__(self, loc):
|
|
16
|
+
super().__init__()
|
|
17
|
+
|
|
18
|
+
self.add_argument("url", type=str, location=loc)
|
|
19
|
+
self.add_argument(
|
|
20
|
+
"method",
|
|
21
|
+
type=str,
|
|
22
|
+
location=loc,
|
|
23
|
+
default="GET",
|
|
24
|
+
choices=("GET", "POST", "PUT", "DELETE"),
|
|
25
|
+
)
|
|
26
|
+
self.add_argument("headers", type=dict, location=loc, default={})
|
|
27
|
+
self.add_argument("request_body", type=dict, location=loc, default={})
|
|
28
|
+
|
|
29
|
+
def validate(self, request):
|
|
30
|
+
self.parse_args(req=request)
|
|
31
|
+
|
|
32
|
+
def parse(self, request):
|
|
33
|
+
result = self.parse_args(req=request)
|
|
34
|
+
|
|
35
|
+
return CallbackParameter(
|
|
36
|
+
url=result["url"],
|
|
37
|
+
method=result["method"],
|
|
38
|
+
headers=result["headers"],
|
|
39
|
+
request_body=result["request_body"],
|
|
40
|
+
)
|
task/parsers/cmd_args.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import ast
|
|
3
|
+
|
|
4
|
+
import werkzeug.exceptions
|
|
5
|
+
|
|
6
|
+
from blackant.utils.logging import get_logger
|
|
7
|
+
from task.parsers.base import RequestParserBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommandLineArgumentParser(RequestParserBase):
|
|
11
|
+
def __init__(self, loc):
|
|
12
|
+
super().__init__()
|
|
13
|
+
|
|
14
|
+
self.__loc = loc
|
|
15
|
+
self.__logger = get_logger("cmd_args")
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def check_size_of_data(request_field, cmd_data):
|
|
19
|
+
size_of_data = sys.getsizeof(cmd_data)
|
|
20
|
+
if size_of_data > 35000000:
|
|
21
|
+
raise werkzeug.exceptions.BadRequest(
|
|
22
|
+
"The getting '{}' parameter size is too big. "
|
|
23
|
+
"The current data size is {} bytes. "
|
|
24
|
+
"35000000 bytes allows".format(request_field, size_of_data)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def validate(self, request):
|
|
28
|
+
if not isinstance(request[self.__loc], dict):
|
|
29
|
+
raise werkzeug.exceptions.BadRequest("Command line argument input is not a dictionary")
|
|
30
|
+
|
|
31
|
+
self.check_size_of_data(self.__loc, request)
|
|
32
|
+
|
|
33
|
+
for key, value in request[self.__loc].items():
|
|
34
|
+
if not isinstance(key, str) or not isinstance(value, str):
|
|
35
|
+
raise werkzeug.exceptions.BadRequest(
|
|
36
|
+
"Command line argument input has invalid key or value"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def parse(self, request):
|
|
40
|
+
return_data = {}
|
|
41
|
+
|
|
42
|
+
if not request or self.__loc not in request.keys():
|
|
43
|
+
return return_data
|
|
44
|
+
|
|
45
|
+
self.check_size_of_data(self.__loc, request)
|
|
46
|
+
|
|
47
|
+
request_data = request[self.__loc]
|
|
48
|
+
|
|
49
|
+
if isinstance(request_data, str):
|
|
50
|
+
request_data = ast.literal_eval(request_data)
|
|
51
|
+
|
|
52
|
+
return request_data
|
task/parsers/freetext.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import werkzeug.exceptions
|
|
2
|
+
|
|
3
|
+
from task.parsers.base import RequestParserBase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FreeTextInputParser(RequestParserBase):
|
|
7
|
+
def __init__(self, loc):
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
self.__loc = loc
|
|
11
|
+
|
|
12
|
+
def validate(self, request):
|
|
13
|
+
if not isinstance(request[self.__loc], str):
|
|
14
|
+
raise werkzeug.exceptions.BadRequest("Freetext input must be text type")
|
|
15
|
+
|
|
16
|
+
def parse(self, request):
|
|
17
|
+
if not request or self.__loc not in request.keys():
|
|
18
|
+
return ""
|
|
19
|
+
return request[self.__loc]
|
task/parsers/objects.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import werkzeug.exceptions
|
|
4
|
+
|
|
5
|
+
from task.parsers.base import RequestParserBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ObjectParameter:
|
|
10
|
+
remote: str
|
|
11
|
+
local: str
|
|
12
|
+
preserve: bool = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ObjectListParser(RequestParserBase):
|
|
16
|
+
def __init__(self, loc):
|
|
17
|
+
super().__init__()
|
|
18
|
+
|
|
19
|
+
self.__loc = loc
|
|
20
|
+
|
|
21
|
+
def validate(self, request):
|
|
22
|
+
if not isinstance(request[self.__loc], list):
|
|
23
|
+
raise werkzeug.exceptions.BadRequest("Objects input is not a list")
|
|
24
|
+
|
|
25
|
+
for obj in request[self.__loc].items():
|
|
26
|
+
if not isinstance(obj, dict):
|
|
27
|
+
raise werkzeug.exceptions.BadRequest("Objects input has invalid value")
|
|
28
|
+
|
|
29
|
+
if "remote" not in obj.keys() or "local" not in obj.keys():
|
|
30
|
+
raise werkzeug.exceptions.BadRequest("Object input has missing mandatory element")
|
|
31
|
+
|
|
32
|
+
if not isinstance(obj["remote"], str) or not isinstance(obj["local"], str):
|
|
33
|
+
raise werkzeug.exceptions.BadRequest("Object input has invalid mandatory element")
|
|
34
|
+
|
|
35
|
+
def parse(self, request):
|
|
36
|
+
try:
|
|
37
|
+
request = request[self.__loc]
|
|
38
|
+
if not request:
|
|
39
|
+
return []
|
|
40
|
+
return [
|
|
41
|
+
ObjectParameter(
|
|
42
|
+
remote=request["remote"],
|
|
43
|
+
local=request["local"],
|
|
44
|
+
preserve=request["preserve"] if "preserve" in request.keys() else False,
|
|
45
|
+
)
|
|
46
|
+
for obj in request[self.__loc]
|
|
47
|
+
]
|
|
48
|
+
except KeyError:
|
|
49
|
+
pass
|
|
50
|
+
return []
|
task/parsers/request.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
from task.parsers.base import RequestParserBase
|
|
4
|
+
|
|
5
|
+
from task.parsers.callback import CallbackParser, CallbackParameter
|
|
6
|
+
from task.parsers.cmd_args import CommandLineArgumentParser
|
|
7
|
+
from task.parsers.objects import ObjectListParser, ObjectParameter
|
|
8
|
+
from task.parsers.freetext import FreeTextInputParser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class TaskConfiguration:
|
|
13
|
+
callback: CallbackParameter
|
|
14
|
+
cmd_args: typing.List[typing.Dict]
|
|
15
|
+
objects: typing.List[ObjectParameter]
|
|
16
|
+
input: str
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def json(self):
|
|
20
|
+
return {
|
|
21
|
+
"callback": dataclasses.asdict(self.callback),
|
|
22
|
+
"cmd_args": self.cmd_args,
|
|
23
|
+
"objects": [dataclasses.asdict(obj) for obj in self.objects],
|
|
24
|
+
"input": self.input,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RequestParser(RequestParserBase):
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__()
|
|
31
|
+
|
|
32
|
+
self.add_argument("callback", type=dict)
|
|
33
|
+
self.add_argument("input")
|
|
34
|
+
self.add_argument("objects")
|
|
35
|
+
self.add_argument("cmd_args")
|
|
36
|
+
|
|
37
|
+
self.__callback_parser = CallbackParser("callback")
|
|
38
|
+
self.__cmd_args_parser = CommandLineArgumentParser("cmd_args")
|
|
39
|
+
self.__objects_parser = ObjectListParser("objects")
|
|
40
|
+
self.__freetext_parser = FreeTextInputParser("input")
|
|
41
|
+
|
|
42
|
+
def validate(self, request):
|
|
43
|
+
self.__callback_parser.validate(request)
|
|
44
|
+
self.__cmd_args_parser.validate(request)
|
|
45
|
+
self.__objects_parser.validate(request)
|
|
46
|
+
self.__freetext_parser.validate(request)
|
|
47
|
+
|
|
48
|
+
def parse(self, request):
|
|
49
|
+
request = self.parse_args()
|
|
50
|
+
|
|
51
|
+
return TaskConfiguration(
|
|
52
|
+
callback=self.__callback_parser.parse(request),
|
|
53
|
+
cmd_args=self.__cmd_args_parser.parse(request),
|
|
54
|
+
objects=self.__objects_parser.parse(request),
|
|
55
|
+
input=self.__freetext_parser.parse(request),
|
|
56
|
+
)
|
task/resource.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from flask import jsonify, Response
|
|
7
|
+
from flask_restful import Resource
|
|
8
|
+
|
|
9
|
+
from blackant.utils.logging import get_logger
|
|
10
|
+
from task.task import Task
|
|
11
|
+
from task.dao import TaskDao
|
|
12
|
+
from task.errors import TaskDoesNotExistsError
|
|
13
|
+
from task.log_adapter import TaskIdAdapter
|
|
14
|
+
from task.parsers.request import RequestParser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskResource(Resource):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.__task_dao = TaskDao.get_instance()
|
|
20
|
+
|
|
21
|
+
def get(self, task_id=None):
|
|
22
|
+
if task_id is not None:
|
|
23
|
+
try:
|
|
24
|
+
task = self.__task_dao.get_task(task_id)
|
|
25
|
+
response = jsonify(task.response)
|
|
26
|
+
except TaskDoesNotExistsError:
|
|
27
|
+
response = Response(status=404)
|
|
28
|
+
else:
|
|
29
|
+
response = jsonify(self.__task_dao.get_task_ids())
|
|
30
|
+
return response
|
|
31
|
+
|
|
32
|
+
def post(self):
|
|
33
|
+
configuration = RequestParser().parse(None)
|
|
34
|
+
unique_uuid = uuid.uuid4().hex
|
|
35
|
+
tmp_dir = tempfile.gettempdir()
|
|
36
|
+
|
|
37
|
+
log_file_handler = logging.FileHandler(
|
|
38
|
+
os.path.join(tmp_dir, "user_log_{}.log".format(unique_uuid)), mode="w"
|
|
39
|
+
)
|
|
40
|
+
log_file_handler.setLevel(logging.DEBUG)
|
|
41
|
+
|
|
42
|
+
calculation_logger_instance = get_logger("calculation_{}".format(unique_uuid))
|
|
43
|
+
calculation_logger_instance.setLevel(logging.DEBUG)
|
|
44
|
+
calculation_logger_instance.addHandler(log_file_handler)
|
|
45
|
+
calculation_logger = TaskIdAdapter(calculation_logger_instance, {})
|
|
46
|
+
|
|
47
|
+
task_logger_instance = get_logger("task_{}".format(unique_uuid))
|
|
48
|
+
task_logger_instance.setLevel(logging.DEBUG)
|
|
49
|
+
task_logger_instance.addHandler(log_file_handler)
|
|
50
|
+
task_logger = TaskIdAdapter(task_logger_instance, {})
|
|
51
|
+
|
|
52
|
+
cmd_args = configuration.cmd_args if configuration.cmd_args else {}
|
|
53
|
+
try:
|
|
54
|
+
# Dynamic calculation loading using CalculationLoader
|
|
55
|
+
# Supports environment variable configuration (CALCULATION_MODULE, CALCULATION_CLASS)
|
|
56
|
+
from calculation.loader import CalculationLoader # pylint: disable=import-outside-toplevel
|
|
57
|
+
|
|
58
|
+
loader = CalculationLoader()
|
|
59
|
+
CalculationClass = loader.get_calculation()
|
|
60
|
+
calculation = CalculationClass(calculation_logger, **cmd_args)
|
|
61
|
+
# The too-broad exception warning is suppressed because we want to handle exceptions
|
|
62
|
+
except Exception: # pylint: disable=broad-except
|
|
63
|
+
task_logger.exception("Cannot create the calculation instance.")
|
|
64
|
+
calculation = None
|
|
65
|
+
new_task = Task(calculation, configuration, log_file_handler, task_logger)
|
|
66
|
+
|
|
67
|
+
new_task_id = self.__task_dao.add_task(new_task)
|
|
68
|
+
task_logger.info("The calculated task UUID: {}".format(new_task_id))
|
|
69
|
+
calculation_logger.set_task_id(new_task_id)
|
|
70
|
+
task_logger.set_task_id(new_task_id)
|
|
71
|
+
|
|
72
|
+
new_task.run()
|
|
73
|
+
|
|
74
|
+
response = jsonify(new_task_id)
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
def delete(self, task_id):
|
|
78
|
+
try:
|
|
79
|
+
task = self.__task_dao.get_task(task_id)
|
|
80
|
+
task.stop()
|
|
81
|
+
response = jsonify(self.__task_dao.delete_task(task_id))
|
|
82
|
+
except TaskDoesNotExistsError:
|
|
83
|
+
response = Response(status=404)
|
|
84
|
+
return response
|
task/states/__init__.py
ADDED
|
File without changes
|
task/states/base.py
ADDED
task/states/error.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from threading import Lock
|
|
3
|
+
|
|
4
|
+
from task.states.base import TaskStateBase
|
|
5
|
+
from task.errors import TaskCallbackFailedError
|
|
6
|
+
|
|
7
|
+
log_lock = Lock()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskErrorState(TaskStateBase):
|
|
11
|
+
@property
|
|
12
|
+
def name(self):
|
|
13
|
+
return "error"
|
|
14
|
+
|
|
15
|
+
def run(self):
|
|
16
|
+
self._task.logger.info("Calculation went to error state")
|
|
17
|
+
|
|
18
|
+
# Get the log file path and read its content
|
|
19
|
+
log_file_path = Path(self._task.log_file_handler.baseFilename)
|
|
20
|
+
log_content = log_file_path.read_text()
|
|
21
|
+
|
|
22
|
+
# Truncate the log if it exceeds the maximum allowed size (20,000 characters)
|
|
23
|
+
if len(log_content) > 20000:
|
|
24
|
+
truncated_log = f"{log_content[:10000]}\n...\n...\n...\n{log_content[-10000:]}"
|
|
25
|
+
else:
|
|
26
|
+
truncated_log = log_content
|
|
27
|
+
|
|
28
|
+
# Attempt to send callbacks with the error and log information
|
|
29
|
+
try:
|
|
30
|
+
self._task.send_callbacks(
|
|
31
|
+
{"error": "Calculation went to error state", "log": truncated_log}
|
|
32
|
+
)
|
|
33
|
+
except TaskCallbackFailedError as exception:
|
|
34
|
+
self._task.logger.critical("Cannot send callback")
|
|
35
|
+
self._task.logger.exception(exception)
|
|
36
|
+
|
|
37
|
+
self._task.set_finished(True)
|
|
38
|
+
|
|
39
|
+
# Safely remove log handler and delete the log file
|
|
40
|
+
with log_lock:
|
|
41
|
+
try:
|
|
42
|
+
# Remove the log file handler from the logger
|
|
43
|
+
self._task.logger.logger.handlers.remove(self._task.log_file_handler)
|
|
44
|
+
if log_file_path.exists():
|
|
45
|
+
log_file_path.unlink() # Delete the log file
|
|
46
|
+
except (OSError, ValueError) as exc:
|
|
47
|
+
self._task.logger.warning(f"Cannot safely remove or delete the log file: {exc}")
|
task/states/idle.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from task.states.base import TaskStateBase
|
|
2
|
+
from task.states.set_up import TaskSetUpState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TaskIdleState(TaskStateBase):
|
|
6
|
+
@property
|
|
7
|
+
def name(self):
|
|
8
|
+
return "idle"
|
|
9
|
+
|
|
10
|
+
def run(self):
|
|
11
|
+
self._task.logger.info("Start processing")
|
|
12
|
+
self._task.change_state(TaskSetUpState(self._task))
|
task/states/ready.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from threading import Lock
|
|
3
|
+
|
|
4
|
+
from task.errors import TaskCallbackFailedError
|
|
5
|
+
from task.states.base import TaskStateBase
|
|
6
|
+
from task.states.error import TaskErrorState
|
|
7
|
+
|
|
8
|
+
log_lock = Lock()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskReadyState(TaskStateBase):
|
|
12
|
+
@property
|
|
13
|
+
def name(self):
|
|
14
|
+
return "ready"
|
|
15
|
+
|
|
16
|
+
def run(self):
|
|
17
|
+
self._task.logger.info("Calculation has finished")
|
|
18
|
+
|
|
19
|
+
# Read and truncate the log file if necessary
|
|
20
|
+
log_file_path = Path(self._task.log_file_handler.baseFilename)
|
|
21
|
+
calculation_log = log_file_path.read_text()
|
|
22
|
+
|
|
23
|
+
if len(calculation_log) > 20000:
|
|
24
|
+
calculation_log = (
|
|
25
|
+
f"{calculation_log[:10000]}\n...\n...\n...\n{calculation_log[-10000:]}"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
# Send callback with result and truncated log
|
|
30
|
+
self._task.send_callbacks(
|
|
31
|
+
{"result": self._task.calculation.result, "log": calculation_log}
|
|
32
|
+
)
|
|
33
|
+
self._task.set_finished(True)
|
|
34
|
+
except TaskCallbackFailedError as ex:
|
|
35
|
+
self._task.logger.critical("Cannot send callback. Transitioning to error state.")
|
|
36
|
+
self._task.logger.exception(ex)
|
|
37
|
+
self._task.change_state(TaskErrorState(self._task))
|
|
38
|
+
# Early return to avoid cleaning up the log file; error state may need it.
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Safely remove log handler and delete log file
|
|
42
|
+
with log_lock:
|
|
43
|
+
try:
|
|
44
|
+
self._task.logger.logger.handlers.remove(self._task.log_file_handler)
|
|
45
|
+
if log_file_path.exists():
|
|
46
|
+
log_file_path.unlink() # Delete log file
|
|
47
|
+
self._task.logger.info(f"Log file {log_file_path} successfully removed.")
|
|
48
|
+
except (OSError, ValueError) as exc:
|
|
49
|
+
self._task.logger.warning(
|
|
50
|
+
f"Failed to remove or delete log file ({log_file_path}): {exc}"
|
|
51
|
+
)
|