openfilter 0.1.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.
- openfilter/__init__.py +0 -0
- openfilter/cli/__init__.py +0 -0
- openfilter/cli/__main__.py +31 -0
- openfilter/cli/cmd_info.py +70 -0
- openfilter/cli/cmd_logs_metrics.py +212 -0
- openfilter/cli/cmd_run.py +111 -0
- openfilter/cli/common.py +374 -0
- openfilter/filter_runtime/__init__.py +2 -0
- openfilter/filter_runtime/dlcache.py +180 -0
- openfilter/filter_runtime/filter.py +989 -0
- openfilter/filter_runtime/filters/__init__.py +0 -0
- openfilter/filter_runtime/filters/mqtt_out.py +465 -0
- openfilter/filter_runtime/filters/recorder.py +283 -0
- openfilter/filter_runtime/filters/rest.py +365 -0
- openfilter/filter_runtime/filters/util.py +321 -0
- openfilter/filter_runtime/filters/video.py +24 -0
- openfilter/filter_runtime/filters/video_in.py +648 -0
- openfilter/filter_runtime/filters/video_out.py +478 -0
- openfilter/filter_runtime/filters/webvis.py +140 -0
- openfilter/filter_runtime/frame.py +492 -0
- openfilter/filter_runtime/logging.py +166 -0
- openfilter/filter_runtime/metrics.py +255 -0
- openfilter/filter_runtime/mq.py +284 -0
- openfilter/filter_runtime/rolllog.py +591 -0
- openfilter/filter_runtime/test.py +246 -0
- openfilter/filter_runtime/utils.py +610 -0
- openfilter/filter_runtime/zeromq.py +948 -0
- openfilter-0.1.0.dist-info/METADATA +193 -0
- openfilter-0.1.0.dist-info/RECORD +33 -0
- openfilter-0.1.0.dist-info/WHEEL +5 -0
- openfilter-0.1.0.dist-info/entry_points.txt +2 -0
- openfilter-0.1.0.dist-info/licenses/LICENSE +201 -0
- openfilter-0.1.0.dist-info/top_level.txt +1 -0
openfilter/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from .common import SCRIPT
|
|
4
|
+
from .cmd_logs_metrics import cmd_logs, cmd_metrics
|
|
5
|
+
from .cmd_info import cmd_info
|
|
6
|
+
from .cmd_run import cmd_run
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
args = sys.argv[2:]
|
|
11
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else ''
|
|
12
|
+
|
|
13
|
+
if cmd_func := getattr(sys.modules[__name__], f'cmd_{cmd}', None):
|
|
14
|
+
cmd_func(args)
|
|
15
|
+
|
|
16
|
+
else:
|
|
17
|
+
print(f"""
|
|
18
|
+
usage: {SCRIPT} COMMAND ...
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
run Directly run one or more filter(s)
|
|
22
|
+
logs Show filter(s) logs
|
|
23
|
+
metrics Show filter(s) metrics
|
|
24
|
+
info Get help on a specific filter
|
|
25
|
+
|
|
26
|
+
Run '{SCRIPT} COMMAND --help' for more information on a command.
|
|
27
|
+
""".strip())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == '__main__':
|
|
31
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from openfilter.filter_runtime.filter import Filter
|
|
7
|
+
|
|
8
|
+
from .common import SCRIPT, get_filter
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
logger.setLevel(int(getattr(logging, (os.getenv('LOG_LEVEL') or 'INFO').upper())))
|
|
13
|
+
|
|
14
|
+
SHORTERHAND = {
|
|
15
|
+
'filter': 'Filter',
|
|
16
|
+
'mqtt': 'MQTTOut',
|
|
17
|
+
'mqttout': 'MQTTOut',
|
|
18
|
+
'mqtt_out': 'MQTTOut',
|
|
19
|
+
'recorder': 'Recorder',
|
|
20
|
+
'rest': 'REST',
|
|
21
|
+
'util': 'Util',
|
|
22
|
+
'video': 'Video',
|
|
23
|
+
'vidin': 'VideoIn',
|
|
24
|
+
'vid_in': 'VideoIn',
|
|
25
|
+
'videoin': 'VideoIn',
|
|
26
|
+
'video_in': 'VideoIn',
|
|
27
|
+
'vidout': 'VideoOut',
|
|
28
|
+
'vid_out': 'VideoOut',
|
|
29
|
+
'videoout': 'VideoOut',
|
|
30
|
+
'video_out': 'VideoOut',
|
|
31
|
+
'webvis': 'Webvis',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# --- info -------------------------------------------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def cmd_info(args):
|
|
38
|
+
parser = argparse.ArgumentParser(prog=f'{SCRIPT} info', description='Get info on a specific Filter.')
|
|
39
|
+
|
|
40
|
+
parser.add_argument('FILTER',
|
|
41
|
+
help='Filter to show info on',
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
opts = parser.parse_args(args)
|
|
45
|
+
|
|
46
|
+
logger.debug(f'opts: {opts}')
|
|
47
|
+
|
|
48
|
+
# do the thing
|
|
49
|
+
|
|
50
|
+
module_name, filter_name, module, filter_cls = get_filter(SHORTERHAND.get(opts.FILTER.lower(), opts.FILTER))
|
|
51
|
+
|
|
52
|
+
for cls in inspect.getmro(filter_cls):
|
|
53
|
+
if not issubclass(cls, Filter):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if cls is not filter_cls:
|
|
57
|
+
if cls is not Filter:
|
|
58
|
+
print('\n\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nThe filter above is a subclass of the following:\n\n')
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
print("\n\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nThe filter above is a subclass of the base Filter, see 'Filter' for information on that.")
|
|
62
|
+
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
print(f'{(s := f"{cls.__module__}.{cls.__qualname__}:")}\n{"-" * len(s)}\n')
|
|
66
|
+
|
|
67
|
+
if not cls.__doc__:
|
|
68
|
+
print('\nFilter class does not have a docstring.')
|
|
69
|
+
else:
|
|
70
|
+
print(inspect.cleandoc(cls.__doc__).strip())
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
from openfilter.filter_runtime.filter import LOG_UTC
|
|
8
|
+
from openfilter.filter_runtime.rolllog import RollLog
|
|
9
|
+
from openfilter.filter_runtime.logging import LOG_PATH, Logger
|
|
10
|
+
from openfilter.filter_runtime.utils import sanitize_filename, parse_date_and_or_time
|
|
11
|
+
|
|
12
|
+
from .common import SCRIPT
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
logger.setLevel(int(getattr(logging, (os.getenv('LOG_LEVEL') or 'INFO').upper())))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# --- logs OR metrics --------------------------------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def _cmd_logs_or_metrics(args, category, default_path, setup_func, entry_func):
|
|
22
|
+
|
|
23
|
+
# parse options from command line
|
|
24
|
+
|
|
25
|
+
parser = argparse.ArgumentParser(prog=f'{SCRIPT} {category}', formatter_class=argparse.RawTextHelpFormatter,
|
|
26
|
+
description=f'Show Filter {category}.',
|
|
27
|
+
epilog=f"""
|
|
28
|
+
notes:
|
|
29
|
+
* Dates are in year/month/day order, separator is '/' or '-', accepted formats are 'yyyy/mm/dd', 'yy/mm/dd', 'mm/dd' or 'dd'.
|
|
30
|
+
* Accepted times are 24 hour clock 'hh:mm:ss.ms', 'hh:mm:ss', 'hh:mm', 'mm:ss.ms' or 'ss.ms'.
|
|
31
|
+
* Date with time is accepted separated by a space or a 'T', e.g. 'yy-mm-dd hh:mm:ss' or 'mm/ddTss.ms'.
|
|
32
|
+
* Datetime can also be ISO format.
|
|
33
|
+
""".strip(),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
parser.add_argument('-f', '--from',
|
|
37
|
+
type = str,
|
|
38
|
+
help = f"date/time to show {category} from, 'start' from very beginning (default: show last file)",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument('-t', '--to',
|
|
41
|
+
type = str,
|
|
42
|
+
help = f'date/time to show {category} to (default: show last file)',
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument('-p', '--path',
|
|
45
|
+
type = str,
|
|
46
|
+
default = default_path,
|
|
47
|
+
help = f'path to {category} (default: %(default)s)',
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument('--utc',
|
|
50
|
+
action = 'store_true',
|
|
51
|
+
default = None,
|
|
52
|
+
help = 'all dates/times in UTC regardless of environment setting',
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument('--no-utc',
|
|
55
|
+
action = 'store_false',
|
|
56
|
+
dest = 'utc',
|
|
57
|
+
help = 'all dates/times in local time regardless of environment setting',
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument('FILTER',
|
|
60
|
+
nargs='*',
|
|
61
|
+
help='Filters to show, all if nothing specified',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
opts = parser.parse_args(args)
|
|
65
|
+
|
|
66
|
+
logger.debug(f'opts: {opts}')
|
|
67
|
+
|
|
68
|
+
# do the thing
|
|
69
|
+
|
|
70
|
+
setup_func(opts.utc)
|
|
71
|
+
|
|
72
|
+
path = opts.path
|
|
73
|
+
ts_from = None if (f := getattr(opts, 'from')) is None else \
|
|
74
|
+
(dt_from := parse_date_and_or_time(f, opts.utc)).timestamp() if f.lower() != 'start' else 0
|
|
75
|
+
ts_to = None if (t := opts.to) is None else (dt_to := parse_date_and_or_time(t, opts.utc)).timestamp()
|
|
76
|
+
FILTERS = [sanitize_filename(f) for f in opts.FILTER]
|
|
77
|
+
filters = set()
|
|
78
|
+
|
|
79
|
+
if ts_from: # also checks for 0
|
|
80
|
+
logger.debug(f'from: {dt_from}')
|
|
81
|
+
|
|
82
|
+
if ts_to is not None:
|
|
83
|
+
logger.debug(f'to: {dt_to}')
|
|
84
|
+
|
|
85
|
+
if not os.path.isdir(path):
|
|
86
|
+
print(f'Path does not exist: {path}')
|
|
87
|
+
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if ts_from is not None and ts_to is not None and ts_to <= ts_from:
|
|
91
|
+
raise ValueError("'--to' timestamp can not be before or same as '--from' timestamp")
|
|
92
|
+
|
|
93
|
+
for name in os.listdir(path):
|
|
94
|
+
if os.path.isdir(os.path.join(path, name)):
|
|
95
|
+
filters.add(name)
|
|
96
|
+
|
|
97
|
+
if not FILTERS:
|
|
98
|
+
if not filters:
|
|
99
|
+
print(f'No {category} directories')
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
for filter in FILTERS:
|
|
103
|
+
if filter not in filters:
|
|
104
|
+
print(f'No {category} directory for: {filter}')
|
|
105
|
+
|
|
106
|
+
filters &= set(FILTERS)
|
|
107
|
+
|
|
108
|
+
if not filters:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
filters = sorted(filters)
|
|
112
|
+
filter_logs = {} # {'filter': RollLog, ...}
|
|
113
|
+
|
|
114
|
+
for filter in filters:
|
|
115
|
+
rl = RollLog(mode='json', rdonly=True, **Logger.path_prefix_and_suffix(path, filter, category))
|
|
116
|
+
|
|
117
|
+
if rl.logfiles:
|
|
118
|
+
filter_logs[filter] = rl
|
|
119
|
+
else:
|
|
120
|
+
print(f'No {category} for: {filter}')
|
|
121
|
+
|
|
122
|
+
for filter, rl in filter_logs.items():
|
|
123
|
+
if len(filter_logs) > 1:
|
|
124
|
+
print(f'\n{filter}\n')
|
|
125
|
+
|
|
126
|
+
rl.seek_block(rl.logfiles[-1].timestamp if ts_from is None else ts_from)
|
|
127
|
+
|
|
128
|
+
if ts_from is not None:
|
|
129
|
+
while entry := rl.read():
|
|
130
|
+
entry['ts'] = dt = datetime.fromisoformat(entry['ts'])
|
|
131
|
+
|
|
132
|
+
if dt.timestamp() >= ts_from:
|
|
133
|
+
if ts_to is None or dt.timestamp() < ts_to:
|
|
134
|
+
entry_func(entry)
|
|
135
|
+
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
else:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
while entry := rl.read():
|
|
142
|
+
entry['ts'] = dt = datetime.fromisoformat(entry['ts'])
|
|
143
|
+
|
|
144
|
+
if ts_to is None or dt.timestamp() < ts_to:
|
|
145
|
+
entry_func(entry)
|
|
146
|
+
else:
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# --- logs -------------------------------------------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def cmd_logs(args):
|
|
153
|
+
LogRecord = logging.LogRecord
|
|
154
|
+
levels = {
|
|
155
|
+
'CRITICAL': 50,
|
|
156
|
+
'FATAL': 50,
|
|
157
|
+
'ERROR': 40,
|
|
158
|
+
'WARN': 30,
|
|
159
|
+
'WARNING': 30,
|
|
160
|
+
'INFO': 20,
|
|
161
|
+
'DEBUG': 10,
|
|
162
|
+
'NOTSET': 0,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
format = None
|
|
166
|
+
|
|
167
|
+
def setup_func(utc: bool | None):
|
|
168
|
+
nonlocal format
|
|
169
|
+
|
|
170
|
+
root_formatter = logging.getLogger().handlers[0].formatter # root formatter
|
|
171
|
+
formatter = logging.Formatter( # clone it
|
|
172
|
+
fmt = root_formatter._fmt,
|
|
173
|
+
datefmt = root_formatter.datefmt,
|
|
174
|
+
style = root_formatter._style._fmt[0],
|
|
175
|
+
)
|
|
176
|
+
formatter.converter = root_formatter.converter if utc is None else time.gmtime if utc else time.localtime
|
|
177
|
+
format = formatter.format
|
|
178
|
+
|
|
179
|
+
def entry_func(entry: dict): # format it exactly the same way as the logger
|
|
180
|
+
log_record = LogRecord(None, levels.get(entry['lvl'], 0), None, None, entry['msg'], None, None)
|
|
181
|
+
log_record.process = entry['pid']
|
|
182
|
+
log_record.thread = log_record.threadName = entry['thid']
|
|
183
|
+
log_record.created = ct = entry['ts'].timestamp()
|
|
184
|
+
log_record.msecs = int((ct - int(ct)) * 1000) + 0.0 # see gh-89047
|
|
185
|
+
log_record.lineno = 0
|
|
186
|
+
|
|
187
|
+
# log_record.threadName = ''
|
|
188
|
+
# log_record.filename = ''
|
|
189
|
+
# log_record.funcName = ''
|
|
190
|
+
|
|
191
|
+
print(format(log_record))
|
|
192
|
+
|
|
193
|
+
return _cmd_logs_or_metrics(args, 'logs', LOG_PATH, setup_func, entry_func)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --- metrics ----------------------------------------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def cmd_metrics(args):
|
|
199
|
+
tz = datefmt = None
|
|
200
|
+
|
|
201
|
+
def setup_func(utc: bool | None):
|
|
202
|
+
nonlocal tz, datefmt
|
|
203
|
+
|
|
204
|
+
tz = timezone.utc if (LOG_UTC if utc is None else utc) else datetime.now().astimezone().tzinfo
|
|
205
|
+
datefmt = logging.getLogger().handlers[0].formatter.datefmt
|
|
206
|
+
|
|
207
|
+
def entry_func(entry: dict):
|
|
208
|
+
entry['ts'] = entry['ts'].astimezone(tz).strftime(datefmt)
|
|
209
|
+
|
|
210
|
+
print(', '.join(f'{k}: {v}' for k, v in entry.items()))
|
|
211
|
+
|
|
212
|
+
return _cmd_logs_or_metrics(args, 'metrics', LOG_PATH, setup_func, entry_func)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import multiprocessing as mp
|
|
4
|
+
import os
|
|
5
|
+
from pprint import pp
|
|
6
|
+
|
|
7
|
+
from openfilter.filter_runtime.filter import Filter, PROP_EXIT_FLAGS, PROP_EXIT, OBEY_EXIT
|
|
8
|
+
from openfilter.filter_runtime.utils import dict_without
|
|
9
|
+
|
|
10
|
+
from .common import SCRIPT, parse_filters
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
logger.setLevel(int(getattr(logging, (os.getenv('LOG_LEVEL') or 'INFO').upper())))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- run --------------------------------------------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
def cmd_run(args):
|
|
20
|
+
try:
|
|
21
|
+
idx = args.index('-')
|
|
22
|
+
except ValueError:
|
|
23
|
+
idx = len(args)
|
|
24
|
+
|
|
25
|
+
opts = args[:idx]
|
|
26
|
+
parser = argparse.ArgumentParser(prog=f'{SCRIPT} run', formatter_class=argparse.RawTextHelpFormatter,
|
|
27
|
+
usage=f"""
|
|
28
|
+
usage: {SCRIPT} run [-h] [--ipc] [-s] [-f] [-p {{all,clean,error,none}}] [-o {{all,clean,error,none}}] [--dry] FILTER [FILTER ...]
|
|
29
|
+
""".strip(),
|
|
30
|
+
description="""
|
|
31
|
+
Run one or more Filters.
|
|
32
|
+
|
|
33
|
+
positional arguments:
|
|
34
|
+
FILTER specified as "- package.filter.Filter [--config_param value ...]"
|
|
35
|
+
""".strip(),
|
|
36
|
+
epilog=f"""
|
|
37
|
+
examples:
|
|
38
|
+
Plug into a running pipeline Filter at localhost:5554 and log its output without disturbint it too much:
|
|
39
|
+
{SCRIPT} run - Util --sources tcp://localhost:5554?? --log
|
|
40
|
+
|
|
41
|
+
Read a video file and visualize it:
|
|
42
|
+
{SCRIPT} run - VideoIn --sources file://video.mp4 --outputs tcp://0 - Webvis --sources tcp://localhost
|
|
43
|
+
autochain filters:
|
|
44
|
+
{SCRIPT} run - VideoIn --sources file://video.mp4 - Webvis
|
|
45
|
+
|
|
46
|
+
Connect via ids:
|
|
47
|
+
{SCRIPT} run - VideoIn --id myvideo --sources file://video.mp4 - webvis --sources myvideo
|
|
48
|
+
autogenerated id:
|
|
49
|
+
{SCRIPT} run - VideoIn --sources file://video.mp4 - Webvis --sources VideoIn
|
|
50
|
+
|
|
51
|
+
notes:
|
|
52
|
+
* For --outputs, 'tcp://*', 'tcp://0.0.0.0' and 'tcp://0' are all equivalent.
|
|
53
|
+
""".strip(),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
parser.add_argument('--ipc',
|
|
57
|
+
action = 'store_true',
|
|
58
|
+
help = 'when creating new connections make them ipc:// instead of tcp://',
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument('-s', '--solo',
|
|
61
|
+
action = 'store_true',
|
|
62
|
+
help = 'run a single Filter in same process',
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument('-f', '--fork',
|
|
65
|
+
action = 'store_true',
|
|
66
|
+
help = "run Filters using 'fork' method instead of 'spawn', doesn't work with CUDA",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument('-p', '--prop-exit',
|
|
69
|
+
type = str,
|
|
70
|
+
default = PROP_EXIT,
|
|
71
|
+
choices = list(PROP_EXIT_FLAGS),
|
|
72
|
+
help = 'exit conditions Filters will propagate (default: %(default)s)',
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument('-o', '--obey-exit',
|
|
75
|
+
type = str,
|
|
76
|
+
default = OBEY_EXIT,
|
|
77
|
+
choices = list(PROP_EXIT_FLAGS),
|
|
78
|
+
help = 'exit conditions Filters will obey (default: %(default)s)',
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument('--dry',
|
|
81
|
+
action = 'store_true',
|
|
82
|
+
help = 'dry run, just prepare and show the configs that would be used',
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
opts = parser.parse_args(opts)
|
|
86
|
+
|
|
87
|
+
logger.debug(f'opts: {opts}')
|
|
88
|
+
|
|
89
|
+
# parse filters
|
|
90
|
+
|
|
91
|
+
filters = parse_filters(args[:idx:-1], opts.ipc)
|
|
92
|
+
|
|
93
|
+
# run
|
|
94
|
+
|
|
95
|
+
if not opts.fork:
|
|
96
|
+
mp.set_start_method('spawn')
|
|
97
|
+
|
|
98
|
+
if opts.solo and len(filters) > 1:
|
|
99
|
+
raise ValueError("can only run a single Filter '--solo'")
|
|
100
|
+
|
|
101
|
+
if opts.dry:
|
|
102
|
+
for _, config, name in filters:
|
|
103
|
+
print(f'\n{name}:\n{"-" * (len(name) + 1)}')
|
|
104
|
+
pp(config)
|
|
105
|
+
|
|
106
|
+
elif opts.solo:
|
|
107
|
+
filters[0][0].run(dict_without(filters[0][1], '__env_compose'),
|
|
108
|
+
prop_exit=opts.prop_exit, obey_exit=opts.obey_exit)
|
|
109
|
+
else:
|
|
110
|
+
Filter.run_multi([(cls, dict_without(config, '__env_compose')) for cls, config, _ in filters],
|
|
111
|
+
prop_exit=opts.prop_exit, obey_exit=opts.obey_exit)
|