edd-core 260907.1__tar.gz
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.
- edd_core-260907.1/PKG-INFO +26 -0
- edd_core-260907.1/edd/core/__init__.py +112 -0
- edd_core-260907.1/edd/core/async_util.py +24 -0
- edd_core-260907.1/edd/core/blank_image.py +2 -0
- edd_core-260907.1/edd/core/core_manager.py +128 -0
- edd_core-260907.1/edd/core/data_stream.py +137 -0
- edd_core-260907.1/edd/core/datastore.py +555 -0
- edd_core-260907.1/edd/core/edd_pipeline_aio.py +993 -0
- edd_core-260907.1/edd/core/ip_manager.py +126 -0
- edd_core-260907.1/edd/core/ip_utils.py +242 -0
- edd_core-260907.1/edd/core/katcp_client.py +30 -0
- edd_core-260907.1/edd/core/logger.py +120 -0
- edd_core-260907.1/edd/core/numa.py +473 -0
- edd_core-260907.1/edd/core/output.py +91 -0
- edd_core-260907.1/edd/core/pipe_monitor.py +50 -0
- edd_core-260907.1/edd/core/pipeline_exceptions.py +24 -0
- edd_core-260907.1/edd/core/pipeline_statemodel.py +127 -0
- edd_core-260907.1/edd/core/pipelinetest_runner.py +127 -0
- edd_core-260907.1/edd/core/process_io.py +174 -0
- edd_core-260907.1/edd/core/process_tools.py +397 -0
- edd_core-260907.1/edd/core/product_controller.py +193 -0
- edd_core-260907.1/edd/core/redis_sender.py +86 -0
- edd_core-260907.1/edd/core/scpi.py +278 -0
- edd_core-260907.1/edd/core/sensor_watchdog.py +41 -0
- edd_core-260907.1/edd/core/telescope_meta_information_server.py +296 -0
- edd_core-260907.1/edd/core/testing/__init__.py +15 -0
- edd_core-260907.1/edd/core/testing/mock_katcp_server.py +133 -0
- edd_core-260907.1/edd/core/testing/mock_pipelines.py +46 -0
- edd_core-260907.1/edd/core/testing/provision_tests/HTMLOutput.py +173 -0
- edd_core-260907.1/edd/core/testing/provision_tests/OutputProcessor.py +60 -0
- edd_core-260907.1/edd/core/testing/provision_tests/TerminalOutput.py +56 -0
- edd_core-260907.1/edd/core/testing/provision_tests/__init__.py +37 -0
- edd_core-260907.1/edd/core/testing/provision_tests/common.py +135 -0
- edd_core-260907.1/edd/core/testing/provision_tests/default_tests.py +44 -0
- edd_core-260907.1/edd/core/testing/provision_tests/execution.py +87 -0
- edd_core-260907.1/edd/core/testing/provision_tests/json_output.py +86 -0
- edd_core-260907.1/edd/core/testing/provision_tests/registry.py +121 -0
- edd_core-260907.1/edd/core/testing/server_thread.py +61 -0
- edd_core-260907.1/edd/core/timestamp_handler.py +77 -0
- edd_core-260907.1/edd/core/version.py +82 -0
- edd_core-260907.1/edd/core/visualisations.py +372 -0
- edd_core-260907.1/edd_core.egg-info/PKG-INFO +26 -0
- edd_core-260907.1/edd_core.egg-info/SOURCES.txt +47 -0
- edd_core-260907.1/edd_core.egg-info/dependency_links.txt +1 -0
- edd_core-260907.1/edd_core.egg-info/entry_points.txt +4 -0
- edd_core-260907.1/edd_core.egg-info/requires.txt +13 -0
- edd_core-260907.1/edd_core.egg-info/top_level.txt +2 -0
- edd_core-260907.1/pyproject.toml +62 -0
- edd_core-260907.1/setup.cfg +4 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edd-core
|
|
3
|
+
Version: 260907.1
|
|
4
|
+
Summary: Core control-layer library for the EDD backend.
|
|
5
|
+
Author-email: Ewan Barr <ebarr@mpifr.de>, Niclas Esser <nesser@mpifr.de>, Tobias Winchen <twinchen@mpifr.de>, Jason Wu <jwu@mpifr.de>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.mpcdf.mpg.de/mpifr-bdg/edd/edd-core
|
|
8
|
+
Project-URL: Documentation, https://mpifr-bdg.pages.mpcdf.de/edd_documentation
|
|
9
|
+
Project-URL: Repository, https://gitlab.mpcdf.mpg.de/mpifr-bdg/edd/edd-core.git
|
|
10
|
+
Project-URL: Bug Tracker, https://gitlab.mpcdf.mpg.de/mpifr-bdg/edd/edd-core/issues
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: aiokatcp<2.0.0
|
|
16
|
+
Requires-Dist: setuptools_scm
|
|
17
|
+
Requires-Dist: pynvml
|
|
18
|
+
Requires-Dist: coloredlogs
|
|
19
|
+
Requires-Dist: netaddr
|
|
20
|
+
Requires-Dist: transitions
|
|
21
|
+
Requires-Dist: redis
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Requires-Dist: psutil
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: fakeredis; extra == "test"
|
|
26
|
+
Requires-Dist: pygraphviz; extra == "test"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import socket
|
|
3
|
+
import copy
|
|
4
|
+
import fnmatch
|
|
5
|
+
|
|
6
|
+
from edd.core.logger import getLogger
|
|
7
|
+
from edd.core.version import get_version
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_log = getLogger('edd.core')
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from edd.core._version import __version__
|
|
15
|
+
except ModuleNotFoundError:
|
|
16
|
+
_log.debug('EDD.core version not determined from installed _version file.')
|
|
17
|
+
__version__ = get_version('edd-core')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def config_update(cfg, update):
|
|
23
|
+
"""
|
|
24
|
+
Updates the config dictionary cfg with values in update. Keys in update containing *,? are applied to all cfgs that match the glob pattern
|
|
25
|
+
"""
|
|
26
|
+
result = copy.deepcopy(cfg)
|
|
27
|
+
for k, v in update.items():
|
|
28
|
+
match = fnmatch.filter(cfg.keys(), k)
|
|
29
|
+
for m in match:
|
|
30
|
+
result[m].update(v)
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def updateConfig(oldo, new):
|
|
37
|
+
"""
|
|
38
|
+
Merge retrieved config [new] into [old] via recursive dict merge
|
|
39
|
+
|
|
40
|
+
Example::
|
|
41
|
+
|
|
42
|
+
>>> old = {'a:': 0, 'b': {'ba': 0, 'bb': 0}}
|
|
43
|
+
>>> new = {'a:': 1, 'b': {'ba': 2}}
|
|
44
|
+
|
|
45
|
+
>>> print(updateConfig(old, new))
|
|
46
|
+
{'a:': 1, 'b': {'ba': 2, 'bb': 0}}
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
old = copy.deepcopy(oldo)
|
|
50
|
+
for k, v in new.items():
|
|
51
|
+
match = fnmatch.filter(old.keys(), k)
|
|
52
|
+
_log.trace("Key %s matches %s", k, match)
|
|
53
|
+
if not match:
|
|
54
|
+
raise KeyError(f"No match for key {k} found in config")
|
|
55
|
+
for m in match:
|
|
56
|
+
if isinstance(old[m], dict):
|
|
57
|
+
_log.trace("update sub dict for key: {}".format(m))
|
|
58
|
+
old[m] = updateConfig(old[m], v)
|
|
59
|
+
else:
|
|
60
|
+
_log.trace('Updating key %s with type %s', m, type(old[m]))
|
|
61
|
+
if type(old[m]) != type(v): # pylint: disable=unidiomatic-typecheck
|
|
62
|
+
_log.warning("Update option {} with different type! Old value(type) {}({}), new {}({}) ".format(m, old[m], type(old[m]), v, type(v)))
|
|
63
|
+
old[m] = v
|
|
64
|
+
return old
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cfgjson2dict(config_json: str) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Returns the provided config as dict if a JSON object or returns the object
|
|
70
|
+
if it already is a dict.
|
|
71
|
+
"""
|
|
72
|
+
if isinstance(config_json, str):
|
|
73
|
+
_log.debug("Received config as string:\n {}".format(config_json))
|
|
74
|
+
if (not config_json.strip()) or config_json.strip() == '""':
|
|
75
|
+
_log.debug("String empty, returning empty dict.")
|
|
76
|
+
return {}
|
|
77
|
+
cfg = json.loads(config_json)
|
|
78
|
+
elif isinstance(config_json, dict):
|
|
79
|
+
_log.debug("Received config as dict")
|
|
80
|
+
cfg = config_json
|
|
81
|
+
else:
|
|
82
|
+
raise TypeError(f"Cannot handle config type {type(config_json)}. Config has to be either json formatted string or dict!")
|
|
83
|
+
|
|
84
|
+
_log.debug("Got cfg: {}, {}".format(cfg, type(cfg)))
|
|
85
|
+
return cfg
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def value_list(d):
|
|
91
|
+
"""
|
|
92
|
+
Return list of values for a dict, list directly otherwise.
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(d, dict):
|
|
95
|
+
return d.values()
|
|
96
|
+
return d
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_port() -> int:
|
|
100
|
+
"""
|
|
101
|
+
Return a free port from the kernel port range
|
|
102
|
+
"""
|
|
103
|
+
with socket.socket() as sock:
|
|
104
|
+
sock.bind(('localhost', 0))
|
|
105
|
+
port = sock.getsockname()[1]
|
|
106
|
+
_log.debug("Got port %d", port)
|
|
107
|
+
return port
|
|
108
|
+
|
|
109
|
+
def get_host() -> str:
|
|
110
|
+
host = socket.gethostname()
|
|
111
|
+
_log.debug("Hostname is %s", host)
|
|
112
|
+
return host
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
if sys.version_info < (3, 9):
|
|
5
|
+
import functools
|
|
6
|
+
import contextvars
|
|
7
|
+
from asyncio import events
|
|
8
|
+
|
|
9
|
+
__all__ = "to_thread",
|
|
10
|
+
|
|
11
|
+
async def to_thread(func, /, *args, **kwargs):
|
|
12
|
+
"""Asynchronously run function *func* in a separate thread.
|
|
13
|
+
Any *args and **kwargs supplied for this function are directly passed
|
|
14
|
+
to *func*. Also, the current :class:`contextvars.Context` is propagated,
|
|
15
|
+
allowing context variables from the main thread to be accessed in the
|
|
16
|
+
separate thread.
|
|
17
|
+
Return a coroutine that can be awaited to get the eventual result of *func*.
|
|
18
|
+
"""
|
|
19
|
+
loop = events.get_running_loop()
|
|
20
|
+
ctx = contextvars.copy_context()
|
|
21
|
+
func_call = functools.partial(ctx.run, func, *args, **kwargs)
|
|
22
|
+
return await loop.run_in_executor(None, func_call)
|
|
23
|
+
|
|
24
|
+
asyncio.to_thread = to_thread
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
|
|
2
|
+
BLANK_IMAGE = "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"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
from edd.core import logger
|
|
4
|
+
from edd.core import numa
|
|
5
|
+
|
|
6
|
+
log = logger.getLogger('edd.core_manager')
|
|
7
|
+
|
|
8
|
+
class CoreManager:
|
|
9
|
+
"""
|
|
10
|
+
Manage cores on one numa node to reserve for one tasksets
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, numa_node='0', task_override=None):
|
|
13
|
+
"""
|
|
14
|
+
Args:
|
|
15
|
+
numa_node: Numa node to place tasks on
|
|
16
|
+
task_override: Dictionary of tasks with explicit core placing
|
|
17
|
+
"""
|
|
18
|
+
self.__numa_node = numa_node
|
|
19
|
+
self.__tasks = {}
|
|
20
|
+
self.__new_task = True
|
|
21
|
+
if task_override is None:
|
|
22
|
+
task_override = {}
|
|
23
|
+
self.__task_override = task_override
|
|
24
|
+
|
|
25
|
+
def add_task(self, taskname, requested_cores, prefer_isolated=False, require_isolated=False):
|
|
26
|
+
"""
|
|
27
|
+
Add a new task to the core manager.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
taskname: name of the task
|
|
31
|
+
requested_cores: number of cores required for the task
|
|
32
|
+
prefer_isolated: If true, prefer isolated cores over non isolated (THIS IS A TYPO must be prefer*)
|
|
33
|
+
require_isolated: If true, will be only placed on isolated cores
|
|
34
|
+
"""
|
|
35
|
+
log.debug("Task {}: Requested cores {} ".format(taskname, requested_cores))
|
|
36
|
+
if not self.__new_task:
|
|
37
|
+
log.warning("Core Management for node {} was already finalized, reevaluating core usage!".format(self.__numa_node))
|
|
38
|
+
self.__new_task = True
|
|
39
|
+
self.__tasks[taskname] = {"requested_cores": requested_cores, "prefer_isolated": prefer_isolated, "require_isolated": require_isolated}
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def tasknames(self) -> list:
|
|
43
|
+
return list(self.__tasks.keys())
|
|
44
|
+
|
|
45
|
+
def __finalize_cores(self):
|
|
46
|
+
self.__new_task = False
|
|
47
|
+
numa_info = numa.getInfo()[self.__numa_node]
|
|
48
|
+
|
|
49
|
+
cores = copy.deepcopy(numa_info['cores'])
|
|
50
|
+
iso_cores = copy.deepcopy(numa_info['isolated_cores'])
|
|
51
|
+
# ToDo: Add advanced checks so that tasks requiring isolated cores get them first etc.
|
|
52
|
+
|
|
53
|
+
for taskname, taskprops in self.__tasks.items():
|
|
54
|
+
if taskname in self.__task_override and self.__task_override[taskname] != 'auto':
|
|
55
|
+
log.debug("Task %s: Using override %s", taskname, self.__task_override[taskname])
|
|
56
|
+
taskprops['reserved_cores'] = []
|
|
57
|
+
for k in self.__task_override[taskname].split(","):
|
|
58
|
+
if '-' in k:
|
|
59
|
+
b,e = k.split('-')
|
|
60
|
+
taskprops['reserved_cores'].extend([str(x) for x in range(int(b), int(e))])
|
|
61
|
+
else:
|
|
62
|
+
taskprops['reserved_cores'].append(k)
|
|
63
|
+
|
|
64
|
+
elif taskprops['require_isolated']:
|
|
65
|
+
log.debug("Task {}: Requested cores {} - available: {}".format(taskname, taskprops['requested_cores'], len(iso_cores)))
|
|
66
|
+
if len(iso_cores) < taskprops['requested_cores']:
|
|
67
|
+
raise RuntimeError("Task {} requires {} isolated cores, but only {} are available.".format(taskname, taskprops['requested_cores'], len(iso_cores)))
|
|
68
|
+
taskprops['reserved_cores'] = iso_cores[:taskprops['requested_cores']]
|
|
69
|
+
iso_cores = iso_cores[taskprops['requested_cores']:]
|
|
70
|
+
else:
|
|
71
|
+
if taskprops['prefer_isolated']:
|
|
72
|
+
if len(iso_cores) < taskprops['requested_cores']:
|
|
73
|
+
cores = iso_cores + cores
|
|
74
|
+
iso_cores = []
|
|
75
|
+
else:
|
|
76
|
+
cores = iso_cores[:taskprops['requested_cores']] + cores
|
|
77
|
+
iso_cores = iso_cores[taskprops['requested_cores']:]
|
|
78
|
+
|
|
79
|
+
if len(cores) < taskprops['requested_cores']:
|
|
80
|
+
cores = cores + iso_cores
|
|
81
|
+
iso_cores = []
|
|
82
|
+
if len(cores) < taskprops['requested_cores']:
|
|
83
|
+
log.warning("Requested reservation of {} cores for task {}, but only {} available on node!".format(taskprops['requested_cores'], taskname, len(cores)))
|
|
84
|
+
taskprops['reserved_cores'] = numa_info['cores']
|
|
85
|
+
else:
|
|
86
|
+
taskprops['reserved_cores'] = cores[:taskprops['requested_cores']]
|
|
87
|
+
cores = cores[taskprops['requested_cores']:]
|
|
88
|
+
log.debug("Assigned cores: %s", ", ".join(cores))
|
|
89
|
+
|
|
90
|
+
def available_cores(self, isolated=False) -> list:
|
|
91
|
+
"""
|
|
92
|
+
Return the list of unreserved cores on this NUMA node.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
isolated (bool): If True, return only available isolated cores.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
list: Sorted list of unreserved core IDs (as strings).
|
|
99
|
+
"""
|
|
100
|
+
if self.__new_task:
|
|
101
|
+
self.__finalize_cores()
|
|
102
|
+
|
|
103
|
+
numa_info = numa.getInfo()[self.__numa_node]
|
|
104
|
+
all_cores = set(numa_info['cores'])
|
|
105
|
+
all_isolated = set(numa_info['isolated_cores'])
|
|
106
|
+
|
|
107
|
+
reserved = set()
|
|
108
|
+
for task in self.__tasks.values():
|
|
109
|
+
reserved.update(task.get('reserved_cores', []))
|
|
110
|
+
|
|
111
|
+
if isolated:
|
|
112
|
+
return sorted(all_isolated - reserved, key=int)
|
|
113
|
+
return sorted((all_cores | all_isolated) - reserved, key=int)
|
|
114
|
+
|
|
115
|
+
def get_cores(self, taskname):
|
|
116
|
+
"""
|
|
117
|
+
Get list of cores reserved for the task
|
|
118
|
+
"""
|
|
119
|
+
if self.__new_task:
|
|
120
|
+
self.__finalize_cores()
|
|
121
|
+
|
|
122
|
+
return self.__tasks[taskname]['reserved_cores']
|
|
123
|
+
|
|
124
|
+
def get_coresstr(self, taskname):
|
|
125
|
+
"""
|
|
126
|
+
Get list of cores reserved for the task as comma separated string ready to be used e.g. for task-set.
|
|
127
|
+
"""
|
|
128
|
+
return ",".join(self.get_cores(taskname))
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import warnings
|
|
4
|
+
import numpy as np
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
import edd.core.logger
|
|
7
|
+
|
|
8
|
+
_log = edd.core.logger.getLogger("edd.core.data_stream")
|
|
9
|
+
|
|
10
|
+
def convert48_64(arr: list) -> int:
|
|
11
|
+
"""
|
|
12
|
+
Converts 48 bit to 64 bit integers.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
arr (list): array of 6 bytes.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
64bit integer representation of the 48 bit list
|
|
19
|
+
"""
|
|
20
|
+
assert len(arr) == 6
|
|
21
|
+
npd = np.array(arr, dtype=np.uint64) # pylint: disable=no-member
|
|
22
|
+
# We need to cast the result to python int, as numpy default behavior is
|
|
23
|
+
# uint64 + int -> float64, see: https://github.com/numpy/numpy/issues/7126
|
|
24
|
+
# The 48 bit can, however, be safely stored in a python int.
|
|
25
|
+
|
|
26
|
+
return int(np.sum(256**np.arange(0, 6, dtype=np.uint64)[::-1] * npd)) # pylint: disable=no-member
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def convert64_48(num: int) -> np.ndarray:
|
|
30
|
+
"""
|
|
31
|
+
Converts 64 bit number to 48 bit number
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
num (int): 64-bit number
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
array of lower 6 bytes
|
|
38
|
+
"""
|
|
39
|
+
return np.frombuffer(num.to_bytes(6, byteorder='big'), dtype=np.uint8)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def deserialize_array(serialized: str, dtype: str="float32", encoding: str="utf-8"):
|
|
43
|
+
"""Decodes a b64 string into a numpy array
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
serialized (str): the b64 encoded string
|
|
47
|
+
dtype (str, optional): The data type of the numpy array. Defaults to "float32".
|
|
48
|
+
encoding (str, optional): The encoding type. Defaults to "utf-8".
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
np.ndarray: The decoded numpy array
|
|
52
|
+
"""
|
|
53
|
+
return np.frombuffer(base64.b64decode(serialized.encode(encoding)), dtype=dtype)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def serialize_array(array: np.ndarray, encoding: str="utf-8") -> str:
|
|
57
|
+
"""Encodes a numpy array into b64 encoded string
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
array (np.ndarray): The numpy array to encode
|
|
61
|
+
encoding (str, optional): The encoding type. Defaults to "utf-8".
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
str: the encoded string
|
|
65
|
+
"""
|
|
66
|
+
return base64.b64encode(array.tobytes()).decode(encoding)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DataStreamFormat(Mapping):
|
|
72
|
+
"""
|
|
73
|
+
Data stream format definition.
|
|
74
|
+
basically a dict with a key 'format' that defines its name
|
|
75
|
+
"""
|
|
76
|
+
def __init__(self, **kwargs):
|
|
77
|
+
# Everything with an "id" is an instance of the stream, and not a
|
|
78
|
+
# format.
|
|
79
|
+
self.__definition = kwargs.copy()
|
|
80
|
+
self.__definition.pop("id", "UNDEFINED")
|
|
81
|
+
self.format = self.__definition.get("format")
|
|
82
|
+
for k, v in self.__definition.items():
|
|
83
|
+
setattr(self, k, v)
|
|
84
|
+
|
|
85
|
+
def items(self):
|
|
86
|
+
return self.__definition.items()
|
|
87
|
+
|
|
88
|
+
def __getitem__(self, v):
|
|
89
|
+
return getattr(self, v)
|
|
90
|
+
|
|
91
|
+
def __iter__(self):
|
|
92
|
+
for k in self.__definition:
|
|
93
|
+
yield k
|
|
94
|
+
|
|
95
|
+
def __len__(self):
|
|
96
|
+
return len(self.__definition)
|
|
97
|
+
|
|
98
|
+
def __repr__(self):
|
|
99
|
+
return json.dumps(self.__definition, indent=2)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_stream(self):
|
|
103
|
+
d = {"id": "UNDEFINED"}
|
|
104
|
+
d.update({k: v for k,v in self.__definition.items()})
|
|
105
|
+
return d
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class DataStreamFormatRegistry:
|
|
111
|
+
"""
|
|
112
|
+
Registry to manage data stream formats.
|
|
113
|
+
"""
|
|
114
|
+
_data_streams = {}
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def register(cls, inp):
|
|
118
|
+
"""
|
|
119
|
+
Register data-stream-format by meta dict or class.
|
|
120
|
+
"""
|
|
121
|
+
_log.debug('Registering format %s', inp.format)
|
|
122
|
+
cls._data_streams[inp.format] = inp
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def get(cls, format_name):
|
|
126
|
+
"""
|
|
127
|
+
Return data stream with given name.
|
|
128
|
+
"""
|
|
129
|
+
_log.debug('Retrieving format %s', format_name)
|
|
130
|
+
return cls._data_streams[format_name]
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def clear(cls):
|
|
134
|
+
"""
|
|
135
|
+
Clear the registry.
|
|
136
|
+
"""
|
|
137
|
+
cls._data_streams.clear()
|