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 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)