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/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
|
18
|
-
|
19
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
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
|
"""
|
@@ -91,10 +131,11 @@ class KartonBase(abc.ABC):
|
|
91
131
|
if not self.identity:
|
92
132
|
raise ValueError("Can't setup logger without identity")
|
93
133
|
|
134
|
+
task_context_filter = TaskContextFilter()
|
94
135
|
self._log_handler.setFormatter(logging.Formatter())
|
136
|
+
self._log_handler.addFilter(task_context_filter)
|
95
137
|
|
96
138
|
logger = logging.getLogger(self.identity)
|
97
|
-
|
98
139
|
if logger.handlers:
|
99
140
|
# If logger already have handlers set: clear them
|
100
141
|
logger.handlers.clear()
|
@@ -106,16 +147,15 @@ class KartonBase(abc.ABC):
|
|
106
147
|
|
107
148
|
logger.setLevel(log_level)
|
108
149
|
stream_handler = logging.StreamHandler()
|
109
|
-
stream_handler.setFormatter(
|
110
|
-
|
111
|
-
)
|
150
|
+
stream_handler.setFormatter(logging.Formatter(self._log_format))
|
151
|
+
stream_handler.addFilter(task_context_filter)
|
112
152
|
logger.addHandler(stream_handler)
|
113
153
|
|
114
154
|
if not self.debug and self.enable_publish_log:
|
115
155
|
logger.addHandler(self._log_handler)
|
116
156
|
|
117
157
|
@property
|
118
|
-
def log_handler(self) ->
|
158
|
+
def log_handler(self) -> logging.Handler:
|
119
159
|
"""
|
120
160
|
Return KartonLogHandler bound to this Karton service.
|
121
161
|
|
@@ -141,67 +181,54 @@ class KartonBase(abc.ABC):
|
|
141
181
|
"""
|
142
182
|
return logging.getLogger(self.identity)
|
143
183
|
|
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
184
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
Return ArgumentParser for main() class method.
|
185
|
+
class KartonBase(abc.ABC, ConfigMixin, LoggingMixin):
|
186
|
+
"""
|
187
|
+
Base class for all Karton services
|
155
188
|
|
156
|
-
|
157
|
-
|
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
|
189
|
+
You can set an informative version information by setting the ``version`` class
|
190
|
+
attribute.
|
191
|
+
"""
|
172
192
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
193
|
+
#: Karton service identity
|
194
|
+
identity: str = ""
|
195
|
+
#: Karton service version
|
196
|
+
version: Optional[str] = None
|
197
|
+
#: Include extended service information for non-consumer services
|
198
|
+
with_service_info: bool = False
|
177
199
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
200
|
+
def __init__(
|
201
|
+
self,
|
202
|
+
config: Optional[Config] = None,
|
203
|
+
identity: Optional[str] = None,
|
204
|
+
backend: Optional[KartonBackend] = None,
|
205
|
+
) -> None:
|
206
|
+
ConfigMixin.__init__(self, config, identity)
|
207
|
+
|
208
|
+
self.service_info = None
|
209
|
+
if self.identity is not None and self.with_service_info:
|
210
|
+
self.service_info = KartonServiceInfo(
|
211
|
+
identity=self.identity,
|
212
|
+
karton_version=__version__,
|
213
|
+
service_version=self.version,
|
214
|
+
)
|
215
|
+
|
216
|
+
self.backend = backend or KartonBackend(
|
217
|
+
self.config, identity=self.identity, service_info=self.service_info
|
189
218
|
)
|
190
219
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
and provided arguments
|
220
|
+
log_handler = KartonLogHandler(backend=self.backend, channel=self.identity)
|
221
|
+
LoggingMixin.__init__(
|
222
|
+
self, log_handler, log_format="[%(asctime)s][%(levelname)s] %(message)s"
|
223
|
+
)
|
196
224
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
return cls(config=config)
|
225
|
+
@property
|
226
|
+
def current_task(self) -> Optional[Task]:
|
227
|
+
return get_current_task()
|
228
|
+
|
229
|
+
@current_task.setter
|
230
|
+
def current_task(self, task: Optional[Task]):
|
231
|
+
set_current_task(task)
|
205
232
|
|
206
233
|
|
207
234
|
class KartonServiceBase(KartonBase):
|
karton/core/config.py
CHANGED
@@ -14,6 +14,7 @@ class Config(object):
|
|
14
14
|
- ``/etc/karton/karton.ini`` (global)
|
15
15
|
- ``~/.config/karton/karton.ini`` (user local)
|
16
16
|
- ``./karton.ini`` (subsystem local)
|
17
|
+
- path from ``KARTON_CONFIG_FILE`` environment variable
|
17
18
|
- ``<path>`` optional, additional path provided in arguments
|
18
19
|
|
19
20
|
It is also possible to pass configuration via environment variables.
|
@@ -38,6 +39,12 @@ class Config(object):
|
|
38
39
|
) -> None:
|
39
40
|
self._config: Dict[str, Dict[str, Any]] = {}
|
40
41
|
|
42
|
+
path_from_env = os.getenv("KARTON_CONFIG_FILE")
|
43
|
+
if path_from_env:
|
44
|
+
if not os.path.isfile(path_from_env):
|
45
|
+
raise IOError(f"Configuration file not found in {path_from_env}")
|
46
|
+
self.SEARCH_PATHS = self.SEARCH_PATHS + [path_from_env]
|
47
|
+
|
41
48
|
if path is not None:
|
42
49
|
if not os.path.isfile(path):
|
43
50
|
raise IOError("Configuration file not found in " + path)
|
@@ -116,6 +123,11 @@ class Config(object):
|
|
116
123
|
@overload
|
117
124
|
def getint(self, section_name: str, option_name: str) -> Optional[int]: ...
|
118
125
|
|
126
|
+
@overload
|
127
|
+
def getint(
|
128
|
+
self, section_name: str, option_name: str, fallback: Optional[int]
|
129
|
+
) -> Optional[int]: ...
|
130
|
+
|
119
131
|
def getint(
|
120
132
|
self, section_name: str, option_name: str, fallback: Optional[int] = None
|
121
133
|
) -> Optional[int]:
|
@@ -217,7 +229,7 @@ class Config(object):
|
|
217
229
|
for name, value in os.environ.items():
|
218
230
|
# Load env variables named KARTON_[section]_[key]
|
219
231
|
# to match ConfigParser structure
|
220
|
-
result = re.fullmatch(r"KARTON_([A-Z0-9
|
232
|
+
result = re.fullmatch(r"KARTON_([A-Z0-9\-\.]+)_([A-Z0-9_]+)", name)
|
221
233
|
|
222
234
|
if not result:
|
223
235
|
continue
|
karton/core/karton.py
CHANGED
@@ -80,13 +80,8 @@ class Producer(KartonBase):
|
|
80
80
|
task.last_update = time.time()
|
81
81
|
task.headers.update({"origin": self.identity})
|
82
82
|
|
83
|
-
# Ensure all local resources have good buckets
|
84
|
-
for resource in task.iterate_resources():
|
85
|
-
if isinstance(resource, LocalResource) and not resource.bucket:
|
86
|
-
resource.bucket = self.backend.default_bucket_name
|
87
|
-
|
88
83
|
# Register new task
|
89
|
-
self.backend.
|
84
|
+
self.backend.declare_task(task)
|
90
85
|
|
91
86
|
# Upload local resources
|
92
87
|
for resource in task.iterate_resources():
|
@@ -137,7 +132,7 @@ class Consumer(KartonServiceBase):
|
|
137
132
|
)
|
138
133
|
if self.task_timeout is None:
|
139
134
|
self.task_timeout = self.config.getint("karton", "task_timeout")
|
140
|
-
|
135
|
+
|
141
136
|
self._pre_hooks: List[Tuple[Optional[str], Callable[[Task], None]]] = []
|
142
137
|
self._post_hooks: List[
|
143
138
|
Tuple[
|
@@ -170,19 +165,22 @@ class Consumer(KartonServiceBase):
|
|
170
165
|
"""
|
171
166
|
|
172
167
|
self.current_task = task
|
173
|
-
self.log_handler.set_task(self.current_task)
|
174
168
|
|
175
|
-
if not
|
176
|
-
self.log.info(
|
177
|
-
|
169
|
+
if not task.matches_filters(self.filters):
|
170
|
+
self.log.info(
|
171
|
+
"Task rejected because binds are no longer valid. "
|
172
|
+
"Rejected ask headers: %s",
|
173
|
+
task.headers,
|
174
|
+
)
|
175
|
+
self.backend.set_task_status(task, TaskState.FINISHED)
|
178
176
|
# Task rejected: end of processing
|
179
177
|
return
|
180
178
|
|
181
179
|
exception_str = None
|
182
180
|
|
183
181
|
try:
|
184
|
-
self.log.info("Received new task - %s",
|
185
|
-
self.backend.set_task_status(
|
182
|
+
self.log.info("Received new task - %s", task.uid)
|
183
|
+
self.backend.set_task_status(task, TaskState.STARTED)
|
186
184
|
|
187
185
|
self._run_pre_hooks()
|
188
186
|
|
@@ -190,22 +188,22 @@ class Consumer(KartonServiceBase):
|
|
190
188
|
try:
|
191
189
|
if self.task_timeout:
|
192
190
|
with timeout(self.task_timeout):
|
193
|
-
self.process(
|
191
|
+
self.process(task)
|
194
192
|
else:
|
195
|
-
self.process(
|
193
|
+
self.process(task)
|
196
194
|
except (Exception, TaskTimeoutError) as exc:
|
197
195
|
saved_exception = exc
|
198
196
|
raise
|
199
197
|
finally:
|
200
198
|
self._run_post_hooks(saved_exception)
|
201
199
|
|
202
|
-
self.log.info("Task done - %s",
|
200
|
+
self.log.info("Task done - %s", task.uid)
|
203
201
|
except (Exception, TaskTimeoutError):
|
204
202
|
exc_info = sys.exc_info()
|
205
203
|
exception_str = traceback.format_exception(*exc_info)
|
206
204
|
|
207
205
|
self.backend.increment_metrics(KartonMetrics.TASK_CRASHED, self.identity)
|
208
|
-
self.log.exception("Failed to process task - %s",
|
206
|
+
self.log.exception("Failed to process task - %s", task.uid)
|
209
207
|
finally:
|
210
208
|
self.backend.increment_metrics(KartonMetrics.TASK_CONSUMED, self.identity)
|
211
209
|
|
@@ -215,9 +213,10 @@ class Consumer(KartonServiceBase):
|
|
215
213
|
# if an exception was caught while processing
|
216
214
|
if exception_str is not None:
|
217
215
|
task_state = TaskState.CRASHED
|
218
|
-
|
216
|
+
task.error = exception_str
|
219
217
|
|
220
|
-
self.backend.set_task_status(
|
218
|
+
self.backend.set_task_status(task, task_state)
|
219
|
+
self.current_task = None
|
221
220
|
|
222
221
|
@property
|
223
222
|
def _bind(self) -> KartonBind:
|
@@ -228,6 +227,7 @@ class Consumer(KartonServiceBase):
|
|
228
227
|
filters=self.filters,
|
229
228
|
persistent=self.persistent,
|
230
229
|
service_version=self.__class__.version,
|
230
|
+
is_async=False,
|
231
231
|
)
|
232
232
|
|
233
233
|
@classmethod
|
@@ -338,15 +338,28 @@ class Consumer(KartonServiceBase):
|
|
338
338
|
if not old_bind:
|
339
339
|
self.log.info("Service binds created.")
|
340
340
|
elif old_bind != self._bind:
|
341
|
-
self.log.info(
|
341
|
+
self.log.info(
|
342
|
+
"Binds changed, old service instances should exit soon. "
|
343
|
+
"Old binds: %s "
|
344
|
+
"New binds: %s",
|
345
|
+
old_bind,
|
346
|
+
self._bind,
|
347
|
+
)
|
342
348
|
|
343
349
|
for task_filter in self.filters:
|
344
350
|
self.log.info("Binding on: %s", task_filter)
|
345
351
|
|
346
352
|
with self.graceful_killer():
|
347
353
|
while not self.shutdown:
|
348
|
-
|
349
|
-
|
354
|
+
current_bind = self.backend.get_bind(self.identity)
|
355
|
+
if current_bind != self._bind:
|
356
|
+
self.log.info(
|
357
|
+
"Binds changed, shutting down. "
|
358
|
+
"Old binds: %s "
|
359
|
+
"New binds: %s",
|
360
|
+
self._bind,
|
361
|
+
current_bind,
|
362
|
+
)
|
350
363
|
break
|
351
364
|
task = self.backend.consume_routed_task(self.identity)
|
352
365
|
if task:
|
karton/core/logger.py
CHANGED
@@ -2,30 +2,32 @@ import logging
|
|
2
2
|
import platform
|
3
3
|
import traceback
|
4
4
|
import warnings
|
5
|
-
from typing import
|
5
|
+
from typing import Any, Callable, Dict
|
6
6
|
|
7
7
|
from .backend import KartonBackend
|
8
|
-
from .task import
|
8
|
+
from .task import get_current_task
|
9
9
|
|
10
10
|
HOSTNAME = platform.node()
|
11
11
|
|
12
12
|
|
13
|
-
class
|
13
|
+
class TaskContextFilter(logging.Filter):
|
14
14
|
"""
|
15
|
-
|
15
|
+
This is a filter which injects information about current task ID to the log.
|
16
16
|
"""
|
17
17
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
19
|
+
current_task = get_current_task()
|
20
|
+
if current_task is not None:
|
21
|
+
record.task_id = current_task.task_uid
|
22
|
+
else:
|
23
|
+
record.task_id = "(no task)"
|
24
|
+
return True
|
24
25
|
|
25
|
-
def set_task(self, task: Task) -> None:
|
26
|
-
self.task = task
|
27
26
|
|
28
|
-
|
27
|
+
class LogLineFormatterMixin:
|
28
|
+
format: Callable[[logging.LogRecord], str]
|
29
|
+
|
30
|
+
def prepare_log_line(self, record: logging.LogRecord) -> Dict[str, Any]:
|
29
31
|
ignore_fields = [
|
30
32
|
"args",
|
31
33
|
"asctime",
|
@@ -54,11 +56,27 @@ class KartonLogHandler(logging.Handler):
|
|
54
56
|
log_line["type"] = "log"
|
55
57
|
log_line["message"] = self.format(record)
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
+
current_task = get_current_task()
|
60
|
+
if current_task is not None:
|
61
|
+
log_line["task"] = current_task.serialize()
|
59
62
|
|
60
63
|
log_line["hostname"] = HOSTNAME
|
64
|
+
return log_line
|
65
|
+
|
66
|
+
|
67
|
+
class KartonLogHandler(logging.Handler, LogLineFormatterMixin):
|
68
|
+
"""
|
69
|
+
logging.Handler that passes logs to the Karton backend.
|
70
|
+
"""
|
71
|
+
|
72
|
+
def __init__(self, backend: KartonBackend, channel: str) -> None:
|
73
|
+
logging.Handler.__init__(self)
|
74
|
+
self.backend = backend
|
75
|
+
self.is_consumer_active: bool = True
|
76
|
+
self.channel: str = channel
|
61
77
|
|
78
|
+
def emit(self, record: logging.LogRecord) -> None:
|
79
|
+
log_line = self.prepare_log_line(record)
|
62
80
|
log_consumed = self.backend.produce_log(
|
63
81
|
log_line, logger_name=self.channel, level=record.levelname
|
64
82
|
)
|
karton/core/main.py
CHANGED
@@ -144,10 +144,23 @@ def configuration_wizard(config_filename: str) -> None:
|
|
144
144
|
log.info("Saved the new configuration file in %s", os.path.abspath(config_filename))
|
145
145
|
|
146
146
|
|
147
|
-
def print_bind_list(config: Config) -> None:
|
147
|
+
def print_bind_list(config: Config, output_format: str) -> None:
|
148
148
|
backend = KartonBackend(config=config)
|
149
|
-
|
150
|
-
|
149
|
+
|
150
|
+
if output_format == "table":
|
151
|
+
# Print a human-readable table-like version
|
152
|
+
print(f"{'karton name':50} {'version':10} {'karton':10}")
|
153
|
+
print("-" * 72)
|
154
|
+
for bind in backend.get_binds():
|
155
|
+
print(
|
156
|
+
f"{bind.identity:50} {bind.service_version or "-":10} {bind.version:10}"
|
157
|
+
)
|
158
|
+
elif output_format == "json":
|
159
|
+
# Use JSONL, each line is a JSON representing next bind
|
160
|
+
for bind in backend.get_binds():
|
161
|
+
print(backend.serialize_bind(bind))
|
162
|
+
else:
|
163
|
+
raise RuntimeError(f"Invalid output format: {output_format}")
|
151
164
|
|
152
165
|
|
153
166
|
def delete_bind(config: Config, karton_name: str) -> None:
|
@@ -180,7 +193,7 @@ def delete_bind(config: Config, karton_name: str) -> None:
|
|
180
193
|
|
181
194
|
def main() -> None:
|
182
195
|
|
183
|
-
parser = argparse.ArgumentParser(description="
|
196
|
+
parser = argparse.ArgumentParser(description="Karton-core management utility")
|
184
197
|
parser.add_argument("--version", action="version", version=__version__)
|
185
198
|
parser.add_argument("-c", "--config-file", help="Alternative configuration path")
|
186
199
|
parser.add_argument(
|
@@ -189,7 +202,14 @@ def main() -> None:
|
|
189
202
|
|
190
203
|
subparsers = parser.add_subparsers(dest="command", help="sub-command help")
|
191
204
|
|
192
|
-
subparsers.add_parser("list", help="List active karton binds")
|
205
|
+
list_parser = subparsers.add_parser("list", help="List active karton binds")
|
206
|
+
list_parser.add_argument(
|
207
|
+
"-o",
|
208
|
+
"--output",
|
209
|
+
help="Short, human readable output, with names and versions only.",
|
210
|
+
default="table",
|
211
|
+
choices=("table", "json"),
|
212
|
+
)
|
193
213
|
|
194
214
|
logs_parser = subparsers.add_parser("logs", help="Start streaming logs")
|
195
215
|
logs_parser.add_argument(
|
@@ -253,7 +273,7 @@ def main() -> None:
|
|
253
273
|
return
|
254
274
|
|
255
275
|
if args.command == "list":
|
256
|
-
print_bind_list(config)
|
276
|
+
print_bind_list(config, args.output)
|
257
277
|
elif args.command == "delete":
|
258
278
|
karton_name = args.identity
|
259
279
|
print(
|
karton/core/resource.py
CHANGED
@@ -150,34 +150,7 @@ class ResourceBase(object):
|
|
150
150
|
}
|
151
151
|
|
152
152
|
|
153
|
-
class
|
154
|
-
"""
|
155
|
-
Represents local resource with arbitrary binary data e.g. file contents.
|
156
|
-
|
157
|
-
Local resources will be uploaded to object hub (S3) during
|
158
|
-
task dispatching.
|
159
|
-
|
160
|
-
.. code-block:: python
|
161
|
-
|
162
|
-
# Creating resource from bytes
|
163
|
-
sample = Resource("original_name.exe", content=b"X5O!P%@AP[4\\
|
164
|
-
PZX54(P^)7CC)7}$EICAR-STANDARD-ANT...")
|
165
|
-
|
166
|
-
# Creating resource from path
|
167
|
-
sample = Resource("original_name.exe", path="sample/original_name.exe")
|
168
|
-
|
169
|
-
:param name: Name of the resource (e.g. name of file)
|
170
|
-
:param content: Resource content
|
171
|
-
:param path: Path of file with resource content
|
172
|
-
:param bucket: Alternative S3 bucket for resource
|
173
|
-
:param metadata: Resource metadata
|
174
|
-
:param uid: Alternative S3 resource id
|
175
|
-
:param sha256: Resource sha256 hash
|
176
|
-
:param fd: Seekable file descriptor
|
177
|
-
:param _flags: Resource flags
|
178
|
-
:param _close_fd: Close file descriptor after upload (default: False)
|
179
|
-
"""
|
180
|
-
|
153
|
+
class LocalResourceBase(ResourceBase):
|
181
154
|
def __init__(
|
182
155
|
self,
|
183
156
|
name: str,
|
@@ -194,7 +167,7 @@ class LocalResource(ResourceBase):
|
|
194
167
|
if len(list(filter(None, [path, content, fd]))) != 1:
|
195
168
|
raise ValueError("You must exclusively provide a path, content or fd")
|
196
169
|
|
197
|
-
super(
|
170
|
+
super().__init__(
|
198
171
|
name,
|
199
172
|
content=content,
|
200
173
|
path=path,
|
@@ -247,7 +220,7 @@ class LocalResource(ResourceBase):
|
|
247
220
|
bucket: Optional[str] = None,
|
248
221
|
metadata: Optional[Dict[str, Any]] = None,
|
249
222
|
uid: Optional[str] = None,
|
250
|
-
) -> "
|
223
|
+
) -> "LocalResourceBase":
|
251
224
|
"""
|
252
225
|
Resource extension, allowing to pass whole directory as a zipped resource.
|
253
226
|
|
@@ -305,6 +278,35 @@ class LocalResource(ResourceBase):
|
|
305
278
|
_close_fd=True,
|
306
279
|
)
|
307
280
|
|
281
|
+
|
282
|
+
class LocalResource(LocalResourceBase):
|
283
|
+
"""
|
284
|
+
Represents local resource with arbitrary binary data e.g. file contents.
|
285
|
+
|
286
|
+
Local resources will be uploaded to object hub (S3) during
|
287
|
+
task dispatching.
|
288
|
+
|
289
|
+
.. code-block:: python
|
290
|
+
|
291
|
+
# Creating resource from bytes
|
292
|
+
sample = Resource("original_name.exe", content=b"X5O!P%@AP[4\\
|
293
|
+
PZX54(P^)7CC)7}$EICAR-STANDARD-ANT...")
|
294
|
+
|
295
|
+
# Creating resource from path
|
296
|
+
sample = Resource("original_name.exe", path="sample/original_name.exe")
|
297
|
+
|
298
|
+
:param name: Name of the resource (e.g. name of file)
|
299
|
+
:param content: Resource content
|
300
|
+
:param path: Path of file with resource content
|
301
|
+
:param bucket: Alternative S3 bucket for resource
|
302
|
+
:param metadata: Resource metadata
|
303
|
+
:param uid: Alternative S3 resource id
|
304
|
+
:param sha256: Resource sha256 hash
|
305
|
+
:param fd: Seekable file descriptor
|
306
|
+
:param _flags: Resource flags
|
307
|
+
:param _close_fd: Close file descriptor after upload (default: False)
|
308
|
+
"""
|
309
|
+
|
308
310
|
def _upload(self, backend: "KartonBackend") -> None:
|
309
311
|
"""Internal function for uploading resources
|
310
312
|
|