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.
Files changed (70) hide show
  1. blackant/__init__.py +31 -0
  2. blackant/auth/__init__.py +10 -0
  3. blackant/auth/blackant_auth.py +518 -0
  4. blackant/auth/keycloak_manager.py +363 -0
  5. blackant/auth/request_id.py +52 -0
  6. blackant/auth/role_assignment.py +443 -0
  7. blackant/auth/tokens.py +57 -0
  8. blackant/client.py +400 -0
  9. blackant/config/__init__.py +0 -0
  10. blackant/config/docker_config.py +457 -0
  11. blackant/config/keycloak_admin_config.py +107 -0
  12. blackant/docker/__init__.py +12 -0
  13. blackant/docker/builder.py +616 -0
  14. blackant/docker/client.py +983 -0
  15. blackant/docker/dao.py +462 -0
  16. blackant/docker/registry.py +172 -0
  17. blackant/exceptions.py +111 -0
  18. blackant/http/__init__.py +8 -0
  19. blackant/http/client.py +125 -0
  20. blackant/patterns/__init__.py +1 -0
  21. blackant/patterns/singleton.py +20 -0
  22. blackant/services/__init__.py +10 -0
  23. blackant/services/dao.py +414 -0
  24. blackant/services/registry.py +635 -0
  25. blackant/utils/__init__.py +8 -0
  26. blackant/utils/initialization.py +32 -0
  27. blackant/utils/logging.py +337 -0
  28. blackant/utils/request_id.py +13 -0
  29. blackant/utils/store.py +50 -0
  30. blackant_sdk-1.0.2.dist-info/METADATA +117 -0
  31. blackant_sdk-1.0.2.dist-info/RECORD +70 -0
  32. blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
  33. blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
  34. calculation/__init__.py +0 -0
  35. calculation/base.py +26 -0
  36. calculation/errors.py +2 -0
  37. calculation/impl/__init__.py +0 -0
  38. calculation/impl/my_calculation.py +144 -0
  39. calculation/impl/simple_calc.py +53 -0
  40. calculation/impl/test.py +1 -0
  41. calculation/impl/test_calc.py +36 -0
  42. calculation/loader.py +227 -0
  43. notifinations/__init__.py +8 -0
  44. notifinations/mail_sender.py +212 -0
  45. storage/__init__.py +0 -0
  46. storage/errors.py +10 -0
  47. storage/factory.py +26 -0
  48. storage/interface.py +19 -0
  49. storage/minio.py +106 -0
  50. task/__init__.py +0 -0
  51. task/dao.py +38 -0
  52. task/errors.py +10 -0
  53. task/log_adapter.py +11 -0
  54. task/parsers/__init__.py +0 -0
  55. task/parsers/base.py +13 -0
  56. task/parsers/callback.py +40 -0
  57. task/parsers/cmd_args.py +52 -0
  58. task/parsers/freetext.py +19 -0
  59. task/parsers/objects.py +50 -0
  60. task/parsers/request.py +56 -0
  61. task/resource.py +84 -0
  62. task/states/__init__.py +0 -0
  63. task/states/base.py +14 -0
  64. task/states/error.py +47 -0
  65. task/states/idle.py +12 -0
  66. task/states/ready.py +51 -0
  67. task/states/running.py +21 -0
  68. task/states/set_up.py +40 -0
  69. task/states/tear_down.py +29 -0
  70. task/task.py +358 -0
storage/errors.py ADDED
@@ -0,0 +1,10 @@
1
+ class InvalidStorageUsageException(Exception):
2
+ pass
3
+
4
+
5
+ class StorageOperationFailedException(Exception):
6
+ pass
7
+
8
+
9
+ class StorageCreationFailedException(Exception):
10
+ pass
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
@@ -0,0 +1,10 @@
1
+ class InvalidDAOUsageError(Exception):
2
+ pass
3
+
4
+
5
+ class TaskDoesNotExistsError(Exception):
6
+ pass
7
+
8
+
9
+ class TaskCallbackFailedError(Exception):
10
+ pass
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
File without changes
task/parsers/base.py ADDED
@@ -0,0 +1,13 @@
1
+ import abc
2
+
3
+ from flask_restful import reqparse
4
+
5
+
6
+ class RequestParserBase(reqparse.RequestParser):
7
+ @abc.abstractmethod
8
+ def validate(self, request):
9
+ pass
10
+
11
+ @abc.abstractmethod
12
+ def parse(self, request):
13
+ pass
@@ -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
+ )
@@ -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
@@ -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]
@@ -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 []
@@ -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
File without changes
task/states/base.py ADDED
@@ -0,0 +1,14 @@
1
+ import abc
2
+
3
+
4
+ class TaskStateBase:
5
+ def __init__(self, task):
6
+ self._task = task
7
+
8
+ @abc.abstractproperty
9
+ def name(self):
10
+ raise NotImplementedError
11
+
12
+ @abc.abstractmethod
13
+ def run(self):
14
+ raise NotImplementedError
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
+ )