karton-core 5.6.1__py3-none-any.whl → 5.8.0__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.
karton/core/backend.py CHANGED
@@ -21,6 +21,7 @@ from urllib3.response import HTTPResponse
21
21
 
22
22
  from .config import Config
23
23
  from .exceptions import InvalidIdentityError
24
+ from .resource import RemoteResource
24
25
  from .task import Task, TaskPriority, TaskState
25
26
  from .utils import chunks, chunks_iter
26
27
 
@@ -33,12 +34,20 @@ KARTON_OUTPUTS_NAMESPACE = "karton.outputs"
33
34
 
34
35
  KartonBind = namedtuple(
35
36
  "KartonBind",
36
- ["identity", "info", "version", "persistent", "filters", "service_version"],
37
+ [
38
+ "identity",
39
+ "info",
40
+ "version",
41
+ "persistent",
42
+ "filters",
43
+ "service_version",
44
+ "is_async",
45
+ ],
37
46
  )
38
47
 
39
48
 
40
49
  KartonOutputs = namedtuple("KartonOutputs", ["identity", "outputs"])
41
- logger = logging.getLogger("karton.core.backend")
50
+ logger = logging.getLogger(__name__)
42
51
 
43
52
 
44
53
  class KartonMetrics(enum.Enum):
@@ -103,13 +112,13 @@ class KartonServiceInfo:
103
112
  )
104
113
 
105
114
 
106
- class KartonBackend:
115
+ class KartonBackendBase:
107
116
  def __init__(
108
117
  self,
109
118
  config: Config,
110
119
  identity: Optional[str] = None,
111
120
  service_info: Optional[KartonServiceInfo] = None,
112
- ) -> None:
121
+ ):
113
122
  self.config = config
114
123
 
115
124
  if identity is not None:
@@ -117,59 +126,6 @@ class KartonBackend:
117
126
  self.identity = identity
118
127
 
119
128
  self.service_info = service_info
120
- self.redis = self.make_redis(
121
- config, identity=identity, service_info=service_info
122
- )
123
-
124
- endpoint = config.get("s3", "address")
125
- access_key = config.get("s3", "access_key")
126
- secret_key = config.get("s3", "secret_key")
127
- iam_auth = config.getboolean("s3", "iam_auth")
128
-
129
- if not endpoint:
130
- raise RuntimeError("Attempting to get S3 client without an endpoint set")
131
-
132
- if access_key and secret_key and iam_auth:
133
- logger.warning(
134
- "Warning: iam is turned on and both S3 access key and secret key are"
135
- " provided"
136
- )
137
-
138
- if iam_auth:
139
- s3_client = self.iam_auth_s3(endpoint)
140
- if s3_client:
141
- self.s3 = s3_client
142
- return
143
-
144
- if access_key is None or secret_key is None:
145
- raise RuntimeError(
146
- "Attempting to get S3 client without an access_key/secret_key set"
147
- )
148
-
149
- self.s3 = boto3.client(
150
- "s3",
151
- endpoint_url=endpoint,
152
- aws_access_key_id=access_key,
153
- aws_secret_access_key=secret_key,
154
- )
155
-
156
- def iam_auth_s3(self, endpoint: str):
157
- boto_session = get_session()
158
- iam_providers = [
159
- ContainerProvider(),
160
- InstanceMetadataProvider(
161
- iam_role_fetcher=InstanceMetadataFetcher(timeout=1000, num_attempts=2)
162
- ),
163
- ]
164
-
165
- for provider in iam_providers:
166
- creds = provider.load()
167
- if creds:
168
- boto_session._credentials = creds # type: ignore
169
- return boto3.Session(botocore_session=boto_session).client(
170
- "s3",
171
- endpoint_url=endpoint,
172
- )
173
129
 
174
130
  @staticmethod
175
131
  def _validate_identity(identity: str):
@@ -179,48 +135,6 @@ class KartonBackend:
179
135
  f"Karton identity should not contain {disallowed_chars}"
180
136
  )
181
137
 
182
- @staticmethod
183
- def make_redis(
184
- config,
185
- identity: Optional[str] = None,
186
- service_info: Optional[KartonServiceInfo] = None,
187
- ) -> StrictRedis:
188
- """
189
- Create and test a Redis connection.
190
-
191
- :param config: The karton configuration
192
- :param identity: Karton service identity
193
- :param service_info: Additional service identity metadata
194
- :return: Redis connection
195
- """
196
- if service_info is not None:
197
- client_name: Optional[str] = service_info.make_client_name()
198
- else:
199
- client_name = identity
200
-
201
- redis_args = {
202
- "host": config["redis"]["host"],
203
- "port": config.getint("redis", "port", 6379),
204
- "db": config.getint("redis", "db", 0),
205
- "username": config.get("redis", "username"),
206
- "password": config.get("redis", "password"),
207
- "client_name": client_name,
208
- # set socket_timeout to None if set to 0
209
- "socket_timeout": config.getint("redis", "socket_timeout", 30) or None,
210
- "decode_responses": True,
211
- }
212
- try:
213
- redis = StrictRedis(**redis_args)
214
- redis.ping()
215
- except AuthenticationError:
216
- # Maybe we've sent a wrong password.
217
- # Or maybe the server is not (yet) password protected
218
- # To make smooth transition possible, try to login insecurely
219
- del redis_args["password"]
220
- redis = StrictRedis(**redis_args)
221
- redis.ping()
222
- return redis
223
-
224
138
  @property
225
139
  def default_bucket_name(self) -> str:
226
140
  bucket_name = self.config.get("s3", "bucket")
@@ -270,6 +184,7 @@ class KartonBackend:
270
184
  "filters": bind.filters,
271
185
  "persistent": bind.persistent,
272
186
  "service_version": bind.service_version,
187
+ "is_async": bind.is_async,
273
188
  },
274
189
  sort_keys=True,
275
190
  )
@@ -294,6 +209,7 @@ class KartonBackend:
294
209
  persistent=not identity.endswith(".test"),
295
210
  filters=bind,
296
211
  service_version=None,
212
+ is_async=False,
297
213
  )
298
214
  return KartonBind(
299
215
  identity=identity,
@@ -302,6 +218,7 @@ class KartonBackend:
302
218
  persistent=bind["persistent"],
303
219
  filters=bind["filters"],
304
220
  service_version=bind.get("service_version"),
221
+ is_async=bind.get("is_async", False),
305
222
  )
306
223
 
307
224
  @staticmethod
@@ -316,6 +233,126 @@ class KartonBackend:
316
233
  output = [json.loads(output_type) for output_type in output_data]
317
234
  return KartonOutputs(identity=identity, outputs=output)
318
235
 
236
+ @staticmethod
237
+ def _log_channel(logger_name: Optional[str], level: Optional[str]) -> str:
238
+ return ".".join(
239
+ [KARTON_LOG_CHANNEL, (level or "*").lower(), logger_name or "*"]
240
+ )
241
+
242
+
243
+ class KartonBackend(KartonBackendBase):
244
+ def __init__(
245
+ self,
246
+ config: Config,
247
+ identity: Optional[str] = None,
248
+ service_info: Optional[KartonServiceInfo] = None,
249
+ ) -> None:
250
+ super().__init__(config, identity, service_info)
251
+ self.redis = self.make_redis(
252
+ config, identity=identity, service_info=service_info
253
+ )
254
+
255
+ endpoint = config.get("s3", "address")
256
+ access_key = config.get("s3", "access_key")
257
+ secret_key = config.get("s3", "secret_key")
258
+ iam_auth = config.getboolean("s3", "iam_auth")
259
+
260
+ if not endpoint:
261
+ raise RuntimeError("Attempting to get S3 client without an endpoint set")
262
+
263
+ if access_key and secret_key and iam_auth:
264
+ logger.warning(
265
+ "Warning: iam is turned on and both S3 access key and secret key are"
266
+ " provided"
267
+ )
268
+
269
+ if iam_auth:
270
+ s3_client = self.iam_auth_s3(endpoint)
271
+ if s3_client:
272
+ self.s3 = s3_client
273
+ return
274
+
275
+ if access_key is None or secret_key is None:
276
+ raise RuntimeError(
277
+ "Attempting to get S3 client without an access_key/secret_key set"
278
+ )
279
+
280
+ self.s3 = boto3.client(
281
+ "s3",
282
+ endpoint_url=endpoint,
283
+ aws_access_key_id=access_key,
284
+ aws_secret_access_key=secret_key,
285
+ )
286
+
287
+ def iam_auth_s3(self, endpoint: str):
288
+ boto_session = get_session()
289
+ iam_providers = [
290
+ ContainerProvider(),
291
+ InstanceMetadataProvider(
292
+ iam_role_fetcher=InstanceMetadataFetcher(timeout=1000, num_attempts=2)
293
+ ),
294
+ ]
295
+
296
+ for provider in iam_providers:
297
+ creds = provider.load()
298
+ if creds:
299
+ boto_session._credentials = creds # type: ignore
300
+ return boto3.Session(botocore_session=boto_session).client(
301
+ "s3",
302
+ endpoint_url=endpoint,
303
+ )
304
+
305
+ @staticmethod
306
+ def make_redis(
307
+ config,
308
+ identity: Optional[str] = None,
309
+ service_info: Optional[KartonServiceInfo] = None,
310
+ ) -> StrictRedis:
311
+ """
312
+ Create and test a Redis connection.
313
+
314
+ :param config: The karton configuration
315
+ :param identity: Karton service identity
316
+ :param service_info: Additional service identity metadata
317
+ :return: Redis connection
318
+ """
319
+ if service_info is not None:
320
+ client_name: Optional[str] = service_info.make_client_name()
321
+ else:
322
+ client_name = identity
323
+
324
+ redis_args = {
325
+ "host": config["redis"]["host"],
326
+ "port": config.getint("redis", "port", 6379),
327
+ "db": config.getint("redis", "db", 0),
328
+ "username": config.get("redis", "username"),
329
+ "password": config.get("redis", "password"),
330
+ "client_name": client_name,
331
+ # set socket_timeout to None if set to 0
332
+ "socket_timeout": config.getint("redis", "socket_timeout", 30) or None,
333
+ "decode_responses": True,
334
+ }
335
+ try:
336
+ redis = StrictRedis(**redis_args)
337
+ redis.ping()
338
+ except AuthenticationError:
339
+ # Maybe we've sent a wrong password.
340
+ # Or maybe the server is not (yet) password protected
341
+ # To make smooth transition possible, try to login insecurely
342
+ del redis_args["password"]
343
+ redis = StrictRedis(**redis_args)
344
+ redis.ping()
345
+ return redis
346
+
347
+ def unserialize_resource(self, resource_spec: Dict[str, Any]) -> RemoteResource:
348
+ """
349
+ Unserializes resource into a RemoteResource object bound with current backend
350
+
351
+ :param resource_spec: Resource specification
352
+ :return: RemoteResource object
353
+ """
354
+ return RemoteResource.from_dict(resource_spec, backend=self)
355
+
319
356
  def get_bind(self, identity: str) -> KartonBind:
320
357
  """
321
358
  Get bind object for given identity
@@ -426,7 +463,9 @@ class KartonBackend:
426
463
  task_data = self.redis.get(f"{KARTON_TASK_NAMESPACE}:{task_uid}")
427
464
  if not task_data:
428
465
  return None
429
- return Task.unserialize(task_data, backend=self)
466
+ return Task.unserialize(
467
+ task_data, resource_unserializer=self.unserialize_resource
468
+ )
430
469
 
431
470
  def get_tasks(
432
471
  self,
@@ -449,7 +488,11 @@ class KartonBackend:
449
488
  chunk_size,
450
489
  )
451
490
  return [
452
- Task.unserialize(task_data, backend=self, parse_resources=parse_resources)
491
+ Task.unserialize(
492
+ task_data,
493
+ parse_resources=parse_resources,
494
+ resource_unserializer=self.unserialize_resource,
495
+ )
453
496
  for chunk in keys
454
497
  for task_data in self.redis.mget(chunk)
455
498
  if task_data is not None
@@ -464,7 +507,9 @@ class KartonBackend:
464
507
  for chunk in chunks_iter(task_keys, chunk_size):
465
508
  yield from (
466
509
  Task.unserialize(
467
- task_data, backend=self, parse_resources=parse_resources
510
+ task_data,
511
+ parse_resources=parse_resources,
512
+ resource_unserializer=self.unserialize_resource,
468
513
  )
469
514
  for task_data in self.redis.mget(chunk)
470
515
  if task_data is not None
@@ -550,7 +595,9 @@ class KartonBackend:
550
595
  lambda task: task.root_uid == root_uid,
551
596
  (
552
597
  Task.unserialize(
553
- task_data, backend=self, parse_resources=parse_resources
598
+ task_data,
599
+ parse_resources=parse_resources,
600
+ resource_unserializer=self.unserialize_resource,
554
601
  )
555
602
  for task_data in self.redis.mget(chunk)
556
603
  if task_data is not None
@@ -798,12 +845,6 @@ class KartonBackend:
798
845
  p.execute()
799
846
  return new_task
800
847
 
801
- @staticmethod
802
- def _log_channel(logger_name: Optional[str], level: Optional[str]) -> str:
803
- return ".".join(
804
- [KARTON_LOG_CHANNEL, (level or "*").lower(), logger_name or "*"]
805
- )
806
-
807
848
  def produce_log(
808
849
  self,
809
850
  log_record: Dict[str, Any],
@@ -1040,9 +1081,11 @@ class KartonBackend:
1040
1081
  """
1041
1082
  deletion_chunks = chunks(
1042
1083
  [
1043
- {"Key": uid, "VersionId": version_id}
1044
- if version_id != "null" or explicit_version_null
1045
- else {"Key": uid}
1084
+ (
1085
+ {"Key": uid, "VersionId": version_id}
1086
+ if version_id != "null" or explicit_version_null
1087
+ else {"Key": uid}
1088
+ )
1046
1089
  for uid, versions in object_versions.items()
1047
1090
  for version_id in versions
1048
1091
  ],
@@ -1070,17 +1113,20 @@ class KartonBackend:
1070
1113
  raise e
1071
1114
  return False
1072
1115
 
1073
- def log_identity_output(self, identity: str, headers: Dict[str, Any]) -> None:
1116
+ def log_identity_output(
1117
+ self, identity: str, headers: Dict[str, Any], task_tracking_ttl: int
1118
+ ) -> None:
1074
1119
  """
1075
1120
  Store the type of task outputted for given producer to
1076
1121
  be used in tracking karton service connections.
1077
1122
 
1078
1123
  :param identity: producer identity
1079
1124
  :param headers: outputted headers
1125
+ :param task_tracking_ttl: expire time (in seconds)
1080
1126
  """
1081
1127
 
1082
1128
  self.redis.sadd(f"{KARTON_OUTPUTS_NAMESPACE}:{identity}", json.dumps(headers))
1083
- self.redis.expire(f"{KARTON_OUTPUTS_NAMESPACE}:{identity}", 60 * 60 * 24 * 30)
1129
+ self.redis.expire(f"{KARTON_OUTPUTS_NAMESPACE}:{identity}", task_tracking_ttl)
1084
1130
 
1085
1131
  def get_outputs(self) -> List[KartonOutputs]:
1086
1132
  """
karton/core/base.py CHANGED
@@ -9,33 +9,20 @@ from typing import Optional, Union, cast
9
9
  from .__version__ import __version__
10
10
  from .backend import KartonBackend, KartonServiceInfo
11
11
  from .config import Config
12
- from .logger import KartonLogHandler
13
- from .task import Task
12
+ from .logger import KartonLogHandler, TaskContextFilter
13
+ from .task import Task, get_current_task, set_current_task
14
14
  from .utils import HardShutdownInterrupt, StrictClassMethod, graceful_killer
15
15
 
16
16
 
17
- class KartonBase(abc.ABC):
18
- """
19
- Base class for all Karton services
20
-
21
- You can set an informative version information by setting the ``version`` class
22
- attribute.
23
- """
24
-
25
- #: Karton service identity
26
- identity: str = ""
27
- #: Karton service version
28
- version: Optional[str] = None
29
- #: Include extended service information for non-consumer services
30
- with_service_info: bool = False
17
+ class ConfigMixin:
18
+ identity: Optional[str]
19
+ version: Optional[str]
31
20
 
32
- def __init__(
33
- self,
34
- config: Optional[Config] = None,
35
- identity: Optional[str] = None,
36
- backend: Optional[KartonBackend] = None,
37
- ) -> None:
21
+ def __init__(self, config: Optional[Config] = None, identity: Optional[str] = None):
38
22
  self.config = config or Config()
23
+ self.enable_publish_log = self.config.getboolean(
24
+ "logging", "enable_publish", True
25
+ )
39
26
 
40
27
  # If not passed via constructor - get it from class
41
28
  if identity is not None:
@@ -47,25 +34,81 @@ class KartonBase(abc.ABC):
47
34
 
48
35
  self.debug = self.config.getboolean("karton", "debug", False)
49
36
 
50
- if self.debug:
37
+ if self.debug and self.identity:
51
38
  self.identity += "-" + os.urandom(4).hex() + "-dev"
52
39
 
53
- self.service_info = None
54
- if self.identity is not None and self.with_service_info:
55
- self.service_info = KartonServiceInfo(
56
- identity=self.identity,
57
- karton_version=__version__,
58
- service_version=self.version,
59
- )
40
+ @classmethod
41
+ def args_description(cls) -> str:
42
+ """Return short description for argument parser."""
43
+ if not cls.__doc__:
44
+ return ""
45
+ return textwrap.dedent(cls.__doc__).strip().splitlines()[0]
60
46
 
61
- self.backend = backend or KartonBackend(
62
- self.config, identity=self.identity, service_info=self.service_info
47
+ @classmethod
48
+ def args_parser(cls) -> argparse.ArgumentParser:
49
+ """
50
+ Return ArgumentParser for main() class method.
51
+
52
+ This method should be overridden and call super methods
53
+ if you want to add more arguments.
54
+ """
55
+ parser = argparse.ArgumentParser(description=cls.args_description())
56
+ parser.add_argument(
57
+ "--version", action="version", version=cast(str, cls.version)
58
+ )
59
+ parser.add_argument("--config-file", help="Alternative configuration path")
60
+ parser.add_argument(
61
+ "--identity", help="Alternative identity for Karton service"
63
62
  )
63
+ parser.add_argument("--log-level", help="Logging level of Karton logger")
64
+ parser.add_argument(
65
+ "--debug", help="Enable debugging mode", action="store_true", default=None
66
+ )
67
+ return parser
64
68
 
65
- self._log_handler = KartonLogHandler(
66
- backend=self.backend, channel=self.identity
69
+ @classmethod
70
+ def config_from_args(cls, config: Config, args: argparse.Namespace) -> None:
71
+ """
72
+ Updates configuration with settings from arguments
73
+
74
+ This method should be overridden and call super methods
75
+ if you want to add more arguments.
76
+ """
77
+ config.load_from_dict(
78
+ {
79
+ "karton": {
80
+ "identity": args.identity,
81
+ "debug": args.debug,
82
+ },
83
+ "logging": {"level": args.log_level},
84
+ }
67
85
  )
68
- self.current_task: Optional[Task] = None
86
+
87
+ @classmethod
88
+ def karton_from_args(cls, args: Optional[argparse.Namespace] = None):
89
+ """
90
+ Returns Karton instance configured using configuration files
91
+ and provided arguments
92
+
93
+ Used by :py:meth:`KartonServiceBase.main` method
94
+ """
95
+ if args is None:
96
+ parser = cls.args_parser()
97
+ args = parser.parse_args()
98
+ config = Config(path=args.config_file)
99
+ cls.config_from_args(config, args)
100
+ return cls(config=config)
101
+
102
+
103
+ class LoggingMixin:
104
+ config: Config
105
+ identity: Optional[str]
106
+ debug: bool
107
+ enable_publish_log: bool
108
+
109
+ def __init__(self, log_handler: logging.Handler, log_format: str) -> None:
110
+ self._log_handler = log_handler
111
+ self._log_format = log_format
69
112
 
70
113
  def setup_logger(self, level: Optional[Union[str, int]] = None) -> None:
71
114
  """
@@ -91,6 +134,7 @@ class KartonBase(abc.ABC):
91
134
  self._log_handler.setFormatter(logging.Formatter())
92
135
 
93
136
  logger = logging.getLogger(self.identity)
137
+ logger.addFilter(TaskContextFilter())
94
138
 
95
139
  if logger.handlers:
96
140
  # If logger already have handlers set: clear them
@@ -103,16 +147,14 @@ class KartonBase(abc.ABC):
103
147
 
104
148
  logger.setLevel(log_level)
105
149
  stream_handler = logging.StreamHandler()
106
- stream_handler.setFormatter(
107
- logging.Formatter("[%(asctime)s][%(levelname)s] %(message)s")
108
- )
150
+ stream_handler.setFormatter(logging.Formatter(self._log_format))
109
151
  logger.addHandler(stream_handler)
110
152
 
111
- if not self.debug:
153
+ if not self.debug and self.enable_publish_log:
112
154
  logger.addHandler(self._log_handler)
113
155
 
114
156
  @property
115
- def log_handler(self) -> KartonLogHandler:
157
+ def log_handler(self) -> logging.Handler:
116
158
  """
117
159
  Return KartonLogHandler bound to this Karton service.
118
160
 
@@ -138,67 +180,54 @@ class KartonBase(abc.ABC):
138
180
  """
139
181
  return logging.getLogger(self.identity)
140
182
 
141
- @classmethod
142
- def args_description(cls) -> str:
143
- """Return short description for argument parser."""
144
- if not cls.__doc__:
145
- return ""
146
- return textwrap.dedent(cls.__doc__).strip().splitlines()[0]
147
183
 
148
- @classmethod
149
- def args_parser(cls) -> argparse.ArgumentParser:
150
- """
151
- Return ArgumentParser for main() class method.
184
+ class KartonBase(abc.ABC, ConfigMixin, LoggingMixin):
185
+ """
186
+ Base class for all Karton services
152
187
 
153
- This method should be overridden and call super methods
154
- if you want to add more arguments.
155
- """
156
- parser = argparse.ArgumentParser(description=cls.args_description())
157
- parser.add_argument(
158
- "--version", action="version", version=cast(str, cls.version)
159
- )
160
- parser.add_argument("--config-file", help="Alternative configuration path")
161
- parser.add_argument(
162
- "--identity", help="Alternative identity for Karton service"
163
- )
164
- parser.add_argument("--log-level", help="Logging level of Karton logger")
165
- parser.add_argument(
166
- "--debug", help="Enable debugging mode", action="store_true", default=None
167
- )
168
- return parser
188
+ You can set an informative version information by setting the ``version`` class
189
+ attribute.
190
+ """
169
191
 
170
- @classmethod
171
- def config_from_args(cls, config: Config, args: argparse.Namespace) -> None:
172
- """
173
- Updates configuration with settings from arguments
192
+ #: Karton service identity
193
+ identity: str = ""
194
+ #: Karton service version
195
+ version: Optional[str] = None
196
+ #: Include extended service information for non-consumer services
197
+ with_service_info: bool = False
174
198
 
175
- This method should be overridden and call super methods
176
- if you want to add more arguments.
177
- """
178
- config.load_from_dict(
179
- {
180
- "karton": {
181
- "identity": args.identity,
182
- "debug": args.debug,
183
- },
184
- "logging": {"level": args.log_level},
185
- }
199
+ def __init__(
200
+ self,
201
+ config: Optional[Config] = None,
202
+ identity: Optional[str] = None,
203
+ backend: Optional[KartonBackend] = None,
204
+ ) -> None:
205
+ ConfigMixin.__init__(self, config, identity)
206
+
207
+ self.service_info = None
208
+ if self.identity is not None and self.with_service_info:
209
+ self.service_info = KartonServiceInfo(
210
+ identity=self.identity,
211
+ karton_version=__version__,
212
+ service_version=self.version,
213
+ )
214
+
215
+ self.backend = backend or KartonBackend(
216
+ self.config, identity=self.identity, service_info=self.service_info
186
217
  )
187
218
 
188
- @classmethod
189
- def karton_from_args(cls, args: Optional[argparse.Namespace] = None):
190
- """
191
- Returns Karton instance configured using configuration files
192
- and provided arguments
219
+ log_handler = KartonLogHandler(backend=self.backend, channel=self.identity)
220
+ LoggingMixin.__init__(
221
+ self, log_handler, log_format="[%(asctime)s][%(levelname)s] %(message)s"
222
+ )
193
223
 
194
- Used by :py:meth:`KartonServiceBase.main` method
195
- """
196
- if args is None:
197
- parser = cls.args_parser()
198
- args = parser.parse_args()
199
- config = Config(path=args.config_file)
200
- cls.config_from_args(config, args)
201
- return cls(config=config)
224
+ @property
225
+ def current_task(self) -> Optional[Task]:
226
+ return get_current_task()
227
+
228
+ @current_task.setter
229
+ def current_task(self, task: Optional[Task]):
230
+ set_current_task(task)
202
231
 
203
232
 
204
233
  class KartonServiceBase(KartonBase):