karton-core 5.7.0__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],
karton/core/base.py CHANGED
@@ -9,32 +9,16 @@ 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()
39
23
  self.enable_publish_log = self.config.getboolean(
40
24
  "logging", "enable_publish", True
@@ -50,25 +34,81 @@ class KartonBase(abc.ABC):
50
34
 
51
35
  self.debug = self.config.getboolean("karton", "debug", False)
52
36
 
53
- if self.debug:
37
+ if self.debug and self.identity:
54
38
  self.identity += "-" + os.urandom(4).hex() + "-dev"
55
39
 
56
- self.service_info = None
57
- if self.identity is not None and self.with_service_info:
58
- self.service_info = KartonServiceInfo(
59
- identity=self.identity,
60
- karton_version=__version__,
61
- service_version=self.version,
62
- )
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]
63
46
 
64
- self.backend = backend or KartonBackend(
65
- 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"
66
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
68
+
69
+ @classmethod
70
+ def config_from_args(cls, config: Config, args: argparse.Namespace) -> None:
71
+ """
72
+ Updates configuration with settings from arguments
67
73
 
68
- self._log_handler = KartonLogHandler(
69
- backend=self.backend, channel=self.identity
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
+ }
70
85
  )
71
- 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
72
112
 
73
113
  def setup_logger(self, level: Optional[Union[str, int]] = None) -> None:
74
114
  """
@@ -94,6 +134,7 @@ class KartonBase(abc.ABC):
94
134
  self._log_handler.setFormatter(logging.Formatter())
95
135
 
96
136
  logger = logging.getLogger(self.identity)
137
+ logger.addFilter(TaskContextFilter())
97
138
 
98
139
  if logger.handlers:
99
140
  # If logger already have handlers set: clear them
@@ -106,16 +147,14 @@ class KartonBase(abc.ABC):
106
147
 
107
148
  logger.setLevel(log_level)
108
149
  stream_handler = logging.StreamHandler()
109
- stream_handler.setFormatter(
110
- logging.Formatter("[%(asctime)s][%(levelname)s] %(message)s")
111
- )
150
+ stream_handler.setFormatter(logging.Formatter(self._log_format))
112
151
  logger.addHandler(stream_handler)
113
152
 
114
153
  if not self.debug and self.enable_publish_log:
115
154
  logger.addHandler(self._log_handler)
116
155
 
117
156
  @property
118
- def log_handler(self) -> KartonLogHandler:
157
+ def log_handler(self) -> logging.Handler:
119
158
  """
120
159
  Return KartonLogHandler bound to this Karton service.
121
160
 
@@ -141,67 +180,54 @@ class KartonBase(abc.ABC):
141
180
  """
142
181
  return logging.getLogger(self.identity)
143
182
 
144
- @classmethod
145
- def args_description(cls) -> str:
146
- """Return short description for argument parser."""
147
- if not cls.__doc__:
148
- return ""
149
- return textwrap.dedent(cls.__doc__).strip().splitlines()[0]
150
183
 
151
- @classmethod
152
- def args_parser(cls) -> argparse.ArgumentParser:
153
- """
154
- Return ArgumentParser for main() class method.
184
+ class KartonBase(abc.ABC, ConfigMixin, LoggingMixin):
185
+ """
186
+ Base class for all Karton services
155
187
 
156
- This method should be overridden and call super methods
157
- if you want to add more arguments.
158
- """
159
- parser = argparse.ArgumentParser(description=cls.args_description())
160
- parser.add_argument(
161
- "--version", action="version", version=cast(str, cls.version)
162
- )
163
- parser.add_argument("--config-file", help="Alternative configuration path")
164
- parser.add_argument(
165
- "--identity", help="Alternative identity for Karton service"
166
- )
167
- parser.add_argument("--log-level", help="Logging level of Karton logger")
168
- parser.add_argument(
169
- "--debug", help="Enable debugging mode", action="store_true", default=None
170
- )
171
- return parser
188
+ You can set an informative version information by setting the ``version`` class
189
+ attribute.
190
+ """
172
191
 
173
- @classmethod
174
- def config_from_args(cls, config: Config, args: argparse.Namespace) -> None:
175
- """
176
- 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
177
198
 
178
- This method should be overridden and call super methods
179
- if you want to add more arguments.
180
- """
181
- config.load_from_dict(
182
- {
183
- "karton": {
184
- "identity": args.identity,
185
- "debug": args.debug,
186
- },
187
- "logging": {"level": args.log_level},
188
- }
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
189
217
  )
190
218
 
191
- @classmethod
192
- def karton_from_args(cls, args: Optional[argparse.Namespace] = None):
193
- """
194
- Returns Karton instance configured using configuration files
195
- 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
+ )
196
223
 
197
- Used by :py:meth:`KartonServiceBase.main` method
198
- """
199
- if args is None:
200
- parser = cls.args_parser()
201
- args = parser.parse_args()
202
- config = Config(path=args.config_file)
203
- cls.config_from_args(config, args)
204
- 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)
205
231
 
206
232
 
207
233
  class KartonServiceBase(KartonBase):
karton/core/config.py CHANGED
@@ -116,6 +116,11 @@ class Config(object):
116
116
  @overload
117
117
  def getint(self, section_name: str, option_name: str) -> Optional[int]: ...
118
118
 
119
+ @overload
120
+ def getint(
121
+ self, section_name: str, option_name: str, fallback: Optional[int]
122
+ ) -> Optional[int]: ...
123
+
119
124
  def getint(
120
125
  self, section_name: str, option_name: str, fallback: Optional[int] = None
121
126
  ) -> Optional[int]: