karton-core 5.7.0__py3-none-any.whl → 5.9.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
@@ -17,10 +17,12 @@ from botocore.credentials import (
17
17
  from botocore.session import get_session
18
18
  from redis import AuthenticationError, StrictRedis
19
19
  from redis.client import Pipeline
20
+ from redis.connection import parse_url as parse_redis_url
20
21
  from urllib3.response import HTTPResponse
21
22
 
22
23
  from .config import Config
23
24
  from .exceptions import InvalidIdentityError
25
+ from .resource import LocalResource, RemoteResource
24
26
  from .task import Task, TaskPriority, TaskState
25
27
  from .utils import chunks, chunks_iter
26
28
 
@@ -33,12 +35,20 @@ KARTON_OUTPUTS_NAMESPACE = "karton.outputs"
33
35
 
34
36
  KartonBind = namedtuple(
35
37
  "KartonBind",
36
- ["identity", "info", "version", "persistent", "filters", "service_version"],
38
+ [
39
+ "identity",
40
+ "info",
41
+ "version",
42
+ "persistent",
43
+ "filters",
44
+ "service_version",
45
+ "is_async",
46
+ ],
37
47
  )
38
48
 
39
49
 
40
50
  KartonOutputs = namedtuple("KartonOutputs", ["identity", "outputs"])
41
- logger = logging.getLogger("karton.core.backend")
51
+ logger = logging.getLogger(__name__)
42
52
 
43
53
 
44
54
  class KartonMetrics(enum.Enum):
@@ -103,13 +113,13 @@ class KartonServiceInfo:
103
113
  )
104
114
 
105
115
 
106
- class KartonBackend:
116
+ class KartonBackendBase:
107
117
  def __init__(
108
118
  self,
109
119
  config: Config,
110
120
  identity: Optional[str] = None,
111
121
  service_info: Optional[KartonServiceInfo] = None,
112
- ) -> None:
122
+ ):
113
123
  self.config = config
114
124
 
115
125
  if identity is not None:
@@ -117,59 +127,6 @@ class KartonBackend:
117
127
  self.identity = identity
118
128
 
119
129
  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
130
 
174
131
  @staticmethod
175
132
  def _validate_identity(identity: str):
@@ -179,54 +136,47 @@ class KartonBackend:
179
136
  f"Karton identity should not contain {disallowed_chars}"
180
137
  )
181
138
 
139
+ @property
140
+ def default_bucket_name(self) -> str:
141
+ bucket_name = self.config.get("s3", "bucket")
142
+ if not bucket_name:
143
+ raise RuntimeError("S3 default bucket is not defined in configuration")
144
+ return bucket_name
145
+
182
146
  @staticmethod
183
- def make_redis(
184
- config,
147
+ def get_redis_configuration(
148
+ config: Config,
185
149
  identity: Optional[str] = None,
186
150
  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
- """
151
+ ) -> Dict[str, Any]:
196
152
  if service_info is not None:
197
153
  client_name: Optional[str] = service_info.make_client_name()
198
154
  else:
199
155
  client_name = identity
200
156
 
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
- @property
225
- def default_bucket_name(self) -> str:
226
- bucket_name = self.config.get("s3", "bucket")
227
- if not bucket_name:
228
- raise RuntimeError("S3 default bucket is not defined in configuration")
229
- return bucket_name
157
+ redis_url = config.get("redis", "url")
158
+ if redis_url is not None:
159
+ redis_conf = parse_redis_url(redis_url)
160
+ else:
161
+ redis_conf = {
162
+ "host": config["redis"]["host"],
163
+ "port": config.getint("redis", "port", 6379),
164
+ "db": config.getint("redis", "db", 0),
165
+ "ssl": config.getboolean("redis", "ssl", False),
166
+ }
167
+
168
+ if username := config.get("redis", "username"):
169
+ redis_conf["username"] = username
170
+ password = config.get("redis", "password")
171
+ if password is None:
172
+ raise RuntimeError("You must set both username and password, or none")
173
+ redis_conf["password"] = password
174
+ # Don't set if set to 0
175
+ if socket_timeout := config.get("redis", "socket_timeout", 30):
176
+ redis_conf["socket_timeout"] = socket_timeout
177
+ redis_conf["client_name"] = client_name
178
+ redis_conf["decode_responses"] = True
179
+ return redis_conf
230
180
 
231
181
  @staticmethod
232
182
  def get_queue_name(identity: str, priority: TaskPriority) -> str:
@@ -270,6 +220,7 @@ class KartonBackend:
270
220
  "filters": bind.filters,
271
221
  "persistent": bind.persistent,
272
222
  "service_version": bind.service_version,
223
+ "is_async": bind.is_async,
273
224
  },
274
225
  sort_keys=True,
275
226
  )
@@ -294,6 +245,7 @@ class KartonBackend:
294
245
  persistent=not identity.endswith(".test"),
295
246
  filters=bind,
296
247
  service_version=None,
248
+ is_async=False,
297
249
  )
298
250
  return KartonBind(
299
251
  identity=identity,
@@ -302,6 +254,7 @@ class KartonBackend:
302
254
  persistent=bind["persistent"],
303
255
  filters=bind["filters"],
304
256
  service_version=bind.get("service_version"),
257
+ is_async=bind.get("is_async", False),
305
258
  )
306
259
 
307
260
  @staticmethod
@@ -316,6 +269,115 @@ class KartonBackend:
316
269
  output = [json.loads(output_type) for output_type in output_data]
317
270
  return KartonOutputs(identity=identity, outputs=output)
318
271
 
272
+ @staticmethod
273
+ def _log_channel(logger_name: Optional[str], level: Optional[str]) -> str:
274
+ return ".".join(
275
+ [KARTON_LOG_CHANNEL, (level or "*").lower(), logger_name or "*"]
276
+ )
277
+
278
+
279
+ class KartonBackend(KartonBackendBase):
280
+ def __init__(
281
+ self,
282
+ config: Config,
283
+ identity: Optional[str] = None,
284
+ service_info: Optional[KartonServiceInfo] = None,
285
+ ) -> None:
286
+ super().__init__(config, identity, service_info)
287
+ self.redis = self.make_redis(
288
+ config, identity=identity, service_info=service_info
289
+ )
290
+
291
+ endpoint = config.get("s3", "address")
292
+ access_key = config.get("s3", "access_key")
293
+ secret_key = config.get("s3", "secret_key")
294
+ iam_auth = config.getboolean("s3", "iam_auth")
295
+
296
+ if not endpoint:
297
+ raise RuntimeError("Attempting to get S3 client without an endpoint set")
298
+
299
+ if access_key and secret_key and iam_auth:
300
+ logger.warning(
301
+ "Warning: iam is turned on and both S3 access key and secret key are"
302
+ " provided"
303
+ )
304
+
305
+ if iam_auth:
306
+ s3_client = self.iam_auth_s3(endpoint)
307
+ if s3_client:
308
+ self.s3 = s3_client
309
+ return
310
+
311
+ if access_key is None or secret_key is None:
312
+ raise RuntimeError(
313
+ "Attempting to get S3 client without an access_key/secret_key set"
314
+ )
315
+
316
+ self.s3 = boto3.client(
317
+ "s3",
318
+ endpoint_url=endpoint,
319
+ aws_access_key_id=access_key,
320
+ aws_secret_access_key=secret_key,
321
+ )
322
+
323
+ def iam_auth_s3(self, endpoint: str):
324
+ boto_session = get_session()
325
+ iam_providers = [
326
+ ContainerProvider(),
327
+ InstanceMetadataProvider(
328
+ iam_role_fetcher=InstanceMetadataFetcher(timeout=1000, num_attempts=2)
329
+ ),
330
+ ]
331
+
332
+ for provider in iam_providers:
333
+ creds = provider.load()
334
+ if creds:
335
+ boto_session._credentials = creds # type: ignore
336
+ return boto3.Session(botocore_session=boto_session).client(
337
+ "s3",
338
+ endpoint_url=endpoint,
339
+ )
340
+
341
+ @classmethod
342
+ def make_redis(
343
+ cls,
344
+ config,
345
+ identity: Optional[str] = None,
346
+ service_info: Optional[KartonServiceInfo] = None,
347
+ ) -> StrictRedis:
348
+ """
349
+ Create and test a Redis connection.
350
+
351
+ :param config: The karton configuration
352
+ :param identity: Karton service identity
353
+ :param service_info: Additional service identity metadata
354
+ :return: Redis connection
355
+ """
356
+ redis_args = cls.get_redis_configuration(
357
+ config, identity=identity, service_info=service_info
358
+ )
359
+ try:
360
+ redis = StrictRedis(**redis_args)
361
+ redis.ping()
362
+ except AuthenticationError:
363
+ # Maybe we've sent a wrong password.
364
+ # Or maybe the server is not (yet) password protected
365
+ # To make smooth transition possible, try to login insecurely
366
+ del redis_args["username"]
367
+ del redis_args["password"]
368
+ redis = StrictRedis(**redis_args)
369
+ redis.ping()
370
+ return redis
371
+
372
+ def unserialize_resource(self, resource_spec: Dict[str, Any]) -> RemoteResource:
373
+ """
374
+ Unserializes resource into a RemoteResource object bound with current backend
375
+
376
+ :param resource_spec: Resource specification
377
+ :return: RemoteResource object
378
+ """
379
+ return RemoteResource.from_dict(resource_spec, backend=self)
380
+
319
381
  def get_bind(self, identity: str) -> KartonBind:
320
382
  """
321
383
  Get bind object for given identity
@@ -426,7 +488,9 @@ class KartonBackend:
426
488
  task_data = self.redis.get(f"{KARTON_TASK_NAMESPACE}:{task_uid}")
427
489
  if not task_data:
428
490
  return None
429
- return Task.unserialize(task_data, backend=self)
491
+ return Task.unserialize(
492
+ task_data, resource_unserializer=self.unserialize_resource
493
+ )
430
494
 
431
495
  def get_tasks(
432
496
  self,
@@ -449,7 +513,11 @@ class KartonBackend:
449
513
  chunk_size,
450
514
  )
451
515
  return [
452
- Task.unserialize(task_data, backend=self, parse_resources=parse_resources)
516
+ Task.unserialize(
517
+ task_data,
518
+ parse_resources=parse_resources,
519
+ resource_unserializer=self.unserialize_resource,
520
+ )
453
521
  for chunk in keys
454
522
  for task_data in self.redis.mget(chunk)
455
523
  if task_data is not None
@@ -464,7 +532,9 @@ class KartonBackend:
464
532
  for chunk in chunks_iter(task_keys, chunk_size):
465
533
  yield from (
466
534
  Task.unserialize(
467
- task_data, backend=self, parse_resources=parse_resources
535
+ task_data,
536
+ parse_resources=parse_resources,
537
+ resource_unserializer=self.unserialize_resource,
468
538
  )
469
539
  for task_data in self.redis.mget(chunk)
470
540
  if task_data is not None
@@ -550,7 +620,9 @@ class KartonBackend:
550
620
  lambda task: task.root_uid == root_uid,
551
621
  (
552
622
  Task.unserialize(
553
- task_data, backend=self, parse_resources=parse_resources
623
+ task_data,
624
+ parse_resources=parse_resources,
625
+ resource_unserializer=self.unserialize_resource,
554
626
  )
555
627
  for task_data in self.redis.mget(chunk)
556
628
  if task_data is not None
@@ -584,10 +656,29 @@ class KartonBackend:
584
656
  task_keys, chunk_size=chunk_size, parse_resources=parse_resources
585
657
  )
586
658
 
659
+ def declare_task(self, task: Task) -> None:
660
+ """
661
+ Declares a new task to send it to the queue.
662
+
663
+ Task producers should use this method for new tasks.
664
+
665
+ :param task: Task to declare
666
+ """
667
+ # Ensure all local resources have good buckets
668
+ for resource in task.iterate_resources():
669
+ if isinstance(resource, LocalResource) and not resource.bucket:
670
+ resource.bucket = self.default_bucket_name
671
+
672
+ # Register new task
673
+ self.register_task(task)
674
+
587
675
  def register_task(self, task: Task, pipe: Optional[Pipeline] = None) -> None:
588
676
  """
589
677
  Register or update task in Redis.
590
678
 
679
+ This method is used internally to alter task data. If you want to declare new
680
+ task in Redis, use declare_task.
681
+
591
682
  :param task: Task object
592
683
  :param pipe: Optional pipeline object if operation is a part of pipeline
593
684
  """
@@ -798,12 +889,6 @@ class KartonBackend:
798
889
  p.execute()
799
890
  return new_task
800
891
 
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
892
  def produce_log(
808
893
  self,
809
894
  log_record: Dict[str, Any],