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/__version__.py +1 -1
- karton/core/asyncio/__init__.py +21 -0
- karton/core/asyncio/backend.py +379 -0
- karton/core/asyncio/base.py +133 -0
- karton/core/asyncio/karton.py +364 -0
- karton/core/asyncio/logger.py +57 -0
- karton/core/asyncio/resource.py +384 -0
- karton/core/backend.py +192 -107
- karton/core/base.py +121 -94
- karton/core/config.py +13 -1
- karton/core/karton.py +35 -22
- karton/core/logger.py +33 -15
- karton/core/main.py +26 -6
- karton/core/resource.py +32 -30
- karton/core/task.py +24 -2
- karton/core/test.py +6 -2
- {karton_core-5.7.0.dist-info → karton_core-5.9.0.dist-info}/METADATA +30 -6
- karton_core-5.9.0.dist-info/RECORD +31 -0
- {karton_core-5.7.0.dist-info → karton_core-5.9.0.dist-info}/WHEEL +1 -1
- karton_core-5.7.0-nspkg.pth +0 -1
- karton_core-5.7.0.dist-info/RECORD +0 -27
- karton_core-5.7.0.dist-info/namespace_packages.txt +0 -1
- {karton_core-5.7.0.dist-info → karton_core-5.9.0.dist-info}/entry_points.txt +0 -0
- {karton_core-5.7.0.dist-info → karton_core-5.9.0.dist-info/licenses}/LICENSE +0 -0
- {karton_core-5.7.0.dist-info → karton_core-5.9.0.dist-info}/top_level.txt +0 -0
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
|
-
[
|
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(
|
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
|
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
|
-
)
|
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
|
184
|
-
config,
|
147
|
+
def get_redis_configuration(
|
148
|
+
config: Config,
|
185
149
|
identity: Optional[str] = None,
|
186
150
|
service_info: Optional[KartonServiceInfo] = None,
|
187
|
-
) ->
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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(
|
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(
|
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,
|
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,
|
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],
|