bluesky-tiled-plugins 2.0.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.
- bluesky_tiled_plugins/__init__.py +11 -0
- bluesky_tiled_plugins/_version.py +34 -0
- bluesky_tiled_plugins/clients/__init__.py +0 -0
- bluesky_tiled_plugins/clients/_common.py +9 -0
- bluesky_tiled_plugins/clients/bluesky_event_stream.py +387 -0
- bluesky_tiled_plugins/clients/bluesky_run.py +441 -0
- bluesky_tiled_plugins/clients/catalog_of_bluesky_runs.py +256 -0
- bluesky_tiled_plugins/clients/document.py +175 -0
- bluesky_tiled_plugins/exporters.py +171 -0
- bluesky_tiled_plugins/queries.py +313 -0
- bluesky_tiled_plugins/utils.py +28 -0
- bluesky_tiled_plugins/writing/__init__.py +0 -0
- bluesky_tiled_plugins/writing/_dispatcher.py +370 -0
- bluesky_tiled_plugins/writing/_json_writer.py +65 -0
- bluesky_tiled_plugins/writing/consolidators.py +668 -0
- bluesky_tiled_plugins/writing/tiled_writer.py +1094 -0
- bluesky_tiled_plugins/writing/validator.py +276 -0
- bluesky_tiled_plugins-2.0.0.dist-info/METADATA +59 -0
- bluesky_tiled_plugins-2.0.0.dist-info/RECORD +21 -0
- bluesky_tiled_plugins-2.0.0.dist-info/WHEEL +4 -0
- bluesky_tiled_plugins-2.0.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .clients.bluesky_event_stream import BlueskyEventStream
|
|
2
|
+
from .clients.bluesky_run import BlueskyRun
|
|
3
|
+
from .clients.catalog_of_bluesky_runs import CatalogOfBlueskyRuns
|
|
4
|
+
from .writing.tiled_writer import TiledWriter
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BlueskyEventStream",
|
|
8
|
+
"BlueskyRun",
|
|
9
|
+
"CatalogOfBlueskyRuns",
|
|
10
|
+
"TiledWriter",
|
|
11
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '2.0.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 0, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# There are methods that IPython will try to call.
|
|
2
|
+
# We special-case them because we want to avoid the getattr
|
|
3
|
+
# resulting in an unnecessary network hit just to raise
|
|
4
|
+
# AttributeError.
|
|
5
|
+
|
|
6
|
+
IPYTHON_METHODS = {
|
|
7
|
+
"_ipython_canary_method_should_not_exist_",
|
|
8
|
+
"_repr_mimebundle__ipython_display_",
|
|
9
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import keyword
|
|
3
|
+
import warnings
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
import numpy
|
|
7
|
+
import xarray
|
|
8
|
+
from tiled.client.composite import CompositeClient
|
|
9
|
+
from tiled.client.container import DEFAULT_STRUCTURE_CLIENT_DISPATCH, Container
|
|
10
|
+
from tiled.utils import DictView, OneShotCachedMap, Sentinel, node_repr
|
|
11
|
+
|
|
12
|
+
from ._common import IPYTHON_METHODS
|
|
13
|
+
|
|
14
|
+
DATAVALUES = Sentinel("DATAVALUES")
|
|
15
|
+
TIMESTAMPS = Sentinel("TIMESTAMPS")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlueskyEventStream(Container):
|
|
19
|
+
_ipython_display_ = None
|
|
20
|
+
_repr_mimebundle__ = None
|
|
21
|
+
|
|
22
|
+
def __new__(cls, context, *, item, structure_clients, **kwargs):
|
|
23
|
+
# When inheriting from BlueskyEventStream, return the class itself
|
|
24
|
+
if cls is not BlueskyEventStream:
|
|
25
|
+
return super().__new__(cls)
|
|
26
|
+
|
|
27
|
+
# Set the version based on the specs
|
|
28
|
+
_cls = BlueskyEventStreamV3 if cls._is_sql(item) else BlueskyEventStreamV2Mongo
|
|
29
|
+
return _cls(context, item=item, structure_clients=structure_clients, **kwargs)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _is_sql(item):
|
|
33
|
+
for spec in item["attributes"]["specs"]:
|
|
34
|
+
if spec["name"] == "BlueskyEventStream":
|
|
35
|
+
if spec["version"].startswith("3."):
|
|
36
|
+
return True
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BlueskyEventStreamV2Mongo(BlueskyEventStream):
|
|
41
|
+
"""
|
|
42
|
+
This encapsulates the data and metadata for one 'stream' in a Bluesky 'run'.
|
|
43
|
+
|
|
44
|
+
This adds for bluesky-specific conveniences to the standard client Container.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __repr__(self):
|
|
48
|
+
stream_name = self.metadata.get("stream_name") or self.item["id"]
|
|
49
|
+
return f"<BlueskyEventStream {set(self)!r} stream_name={stream_name!r}>"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def descriptors(self):
|
|
53
|
+
return self.metadata["descriptors"]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def _descriptors(self):
|
|
57
|
+
# For backward-compatibility.
|
|
58
|
+
# We do not normally worry about backward-compatibility of _ methods, but
|
|
59
|
+
# for a time databroker.v2 *only* have _descriptors and not descriptors,
|
|
60
|
+
# and I know there is useer code that relies on that.
|
|
61
|
+
warnings.warn("Use `.descriptors` instead of `._descriptors`.", stacklevel=2)
|
|
62
|
+
return self.descriptors
|
|
63
|
+
|
|
64
|
+
def __getattr__(self, key):
|
|
65
|
+
"""
|
|
66
|
+
Let run.X be a synonym for run['X'] unless run.X already exists.
|
|
67
|
+
|
|
68
|
+
This behavior is the same as with pandas.DataFrame.
|
|
69
|
+
"""
|
|
70
|
+
# The wisdom of this kind of "magic" is arguable, but we
|
|
71
|
+
# need to support it for backward-compatibility reasons.
|
|
72
|
+
if key in IPYTHON_METHODS:
|
|
73
|
+
raise AttributeError(key)
|
|
74
|
+
if key in self:
|
|
75
|
+
return self[key]
|
|
76
|
+
raise AttributeError(key)
|
|
77
|
+
|
|
78
|
+
def __dir__(self):
|
|
79
|
+
# Build a list of entries that are valid attribute names
|
|
80
|
+
# and add them to __dir__ so that they tab-complete.
|
|
81
|
+
tab_completable_entries = [
|
|
82
|
+
entry
|
|
83
|
+
for entry in self
|
|
84
|
+
if (entry.isidentifier() and (not keyword.iskeyword(entry)))
|
|
85
|
+
]
|
|
86
|
+
return super().__dir__() + tab_completable_entries
|
|
87
|
+
|
|
88
|
+
def read(self, *args, **kwargs):
|
|
89
|
+
"""
|
|
90
|
+
Shortcut for reading the 'data' (as opposed to timestamps or config).
|
|
91
|
+
|
|
92
|
+
That is:
|
|
93
|
+
|
|
94
|
+
>>> stream.read(...)
|
|
95
|
+
|
|
96
|
+
is equivalent to
|
|
97
|
+
|
|
98
|
+
>>> stream["data"].read(...)
|
|
99
|
+
"""
|
|
100
|
+
return self["data"].read(*args, **kwargs)
|
|
101
|
+
|
|
102
|
+
def to_dask(self):
|
|
103
|
+
warnings.warn(
|
|
104
|
+
"""Do not use this method.
|
|
105
|
+
Instead, set dask or when first creating the client, as in
|
|
106
|
+
|
|
107
|
+
>>> catalog = from_uri("...", "dask")
|
|
108
|
+
|
|
109
|
+
and then read() will return dask objects.""",
|
|
110
|
+
DeprecationWarning,
|
|
111
|
+
stacklevel=2,
|
|
112
|
+
)
|
|
113
|
+
return self.new_variation(
|
|
114
|
+
structure_clients=DEFAULT_STRUCTURE_CLIENT_DISPATCH["dask"]
|
|
115
|
+
).read()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class BlueskyEventStreamV2SQL(OneShotCachedMap):
|
|
119
|
+
def __init__(self, internal_dict, metadata=None):
|
|
120
|
+
super().__init__(internal_dict)
|
|
121
|
+
self.metadata = metadata or {}
|
|
122
|
+
|
|
123
|
+
def __repr__(self):
|
|
124
|
+
stream_name = self.metadata.get("stream_name")
|
|
125
|
+
return f"<BlueskyEventStream {set(self)!r} stream_name={stream_name!r}>"
|
|
126
|
+
|
|
127
|
+
def __getitem__(self, key):
|
|
128
|
+
if "/" in key:
|
|
129
|
+
key, rest = key.split("/", 1)
|
|
130
|
+
return self[key][rest]
|
|
131
|
+
|
|
132
|
+
return super().__getitem__(key)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_stream_client(cls, stream_client, metadata=None):
|
|
136
|
+
stream_parts = set(stream_client.base.keys())
|
|
137
|
+
data_keys = [k for k in stream_parts if k != "internal"]
|
|
138
|
+
ts_keys = ["time"]
|
|
139
|
+
if "internal" in stream_parts:
|
|
140
|
+
internal_cols = stream_client.base["internal"].columns
|
|
141
|
+
data_keys += [
|
|
142
|
+
col
|
|
143
|
+
for col in internal_cols
|
|
144
|
+
if col != "seq_num" and not col.startswith("ts_")
|
|
145
|
+
]
|
|
146
|
+
ts_keys += [col for col in internal_cols if col.startswith("ts_")]
|
|
147
|
+
|
|
148
|
+
# Construct clients for the configuration data
|
|
149
|
+
cf_vals, cf_time = defaultdict(dict), defaultdict(dict)
|
|
150
|
+
if config := stream_client.metadata.get("configuration", {}):
|
|
151
|
+
updates = stream_client.metadata.get("_config_updates", [])
|
|
152
|
+
for obj_name, obj in config.items():
|
|
153
|
+
for key in obj["data"].keys():
|
|
154
|
+
_vs, _ts = [obj["data"][key]], [obj["timestamps"][key]]
|
|
155
|
+
|
|
156
|
+
# Add values and timestamps from config_updates
|
|
157
|
+
for upd in updates:
|
|
158
|
+
if upd_config := upd.get("configuration", {}):
|
|
159
|
+
_vs.append(upd_config.get("data", {}).get(key))
|
|
160
|
+
_ts.append(upd_config.get("timestamps", {}).get(key))
|
|
161
|
+
|
|
162
|
+
cf_vals[obj_name][key] = VirtualArrayClient(_vs)
|
|
163
|
+
cf_time[obj_name][key] = VirtualArrayClient(_ts)
|
|
164
|
+
|
|
165
|
+
internal_dict = {
|
|
166
|
+
"data": lambda: CompositeSubsetClient(stream_client, data_keys),
|
|
167
|
+
"timestamps": lambda: CompositeSubsetClient(stream_client, ts_keys),
|
|
168
|
+
"config": lambda: VirtualContainer(
|
|
169
|
+
{k: ConfigDatasetClient(v) for k, v in cf_vals.items()}
|
|
170
|
+
),
|
|
171
|
+
"config_timestamps": lambda: VirtualContainer(
|
|
172
|
+
{k: ConfigDatasetClient(v) for k, v in cf_time.items()}
|
|
173
|
+
),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Construct the metadata
|
|
177
|
+
metadata = {
|
|
178
|
+
"descriptors": [],
|
|
179
|
+
"stream_name": stream_client.item["id"],
|
|
180
|
+
**stream_client.metadata,
|
|
181
|
+
**(metadata or {}),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return cls(internal_dict, metadata=metadata)
|
|
185
|
+
|
|
186
|
+
@functools.cached_property
|
|
187
|
+
def descriptors(self):
|
|
188
|
+
# Go back to the BlueskyRun node and request the documents
|
|
189
|
+
# the path is: bs_run_node/streams/current_stream (old) or bs_run_node/current_stream (new)
|
|
190
|
+
bs_run_node = self["data"].parent
|
|
191
|
+
if bs_run_node.item["id"] == "streams" and (
|
|
192
|
+
"BlueskyRun" not in {s.name for s in bs_run_node.specs}
|
|
193
|
+
):
|
|
194
|
+
# The parent is the old "streams" node, go up one more level
|
|
195
|
+
bs_run_node = bs_run_node.parent
|
|
196
|
+
stream_name = self.metadata.get("stream_name") or self["data"].item["id"]
|
|
197
|
+
return [
|
|
198
|
+
doc
|
|
199
|
+
for name, doc in bs_run_node.documents()
|
|
200
|
+
if name == "descriptor" and doc["name"] == stream_name
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def _descriptors(self):
|
|
205
|
+
# For backward-compatibility.
|
|
206
|
+
# We do not normally worry about backward-compatibility of _ methods, but
|
|
207
|
+
# for a time databroker.v2 *only* have _descriptors and not descriptors,
|
|
208
|
+
# and I know there is useer code that relies on that.
|
|
209
|
+
warnings.warn("Use `.descriptors` instead of `._descriptors`.", stacklevel=2)
|
|
210
|
+
return self.descriptors
|
|
211
|
+
|
|
212
|
+
def __getattr__(self, key):
|
|
213
|
+
"""
|
|
214
|
+
Let run.X be a synonym for run['X'] unless run.X already exists.
|
|
215
|
+
|
|
216
|
+
This behavior is the same as with pandas.DataFrame.
|
|
217
|
+
"""
|
|
218
|
+
# The wisdom of this kind of "magic" is arguable, but we
|
|
219
|
+
# need to support it for backward-compatibility reasons.
|
|
220
|
+
if key in IPYTHON_METHODS:
|
|
221
|
+
raise AttributeError(key)
|
|
222
|
+
if key in self:
|
|
223
|
+
return self[key]
|
|
224
|
+
raise AttributeError(key)
|
|
225
|
+
|
|
226
|
+
def read(self, *args, **kwargs):
|
|
227
|
+
"""Read the data from the stream.
|
|
228
|
+
|
|
229
|
+
This is a shortcut for reading the 'data' (as opposed to timestamps or config).
|
|
230
|
+
"""
|
|
231
|
+
return self["data"].read(*args, **kwargs)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class ConfigDatasetClient(DictView):
|
|
235
|
+
def __repr__(self):
|
|
236
|
+
tiled_repr = node_repr(self, self._internal_dict.keys())
|
|
237
|
+
return tiled_repr.replace(type(self).__name__, "DatasetClient")
|
|
238
|
+
|
|
239
|
+
def read(self):
|
|
240
|
+
# Delay this import for fast startup. In some cases only metadata
|
|
241
|
+
# is handled, and we can avoid the xarray import altogether.
|
|
242
|
+
|
|
243
|
+
d = {
|
|
244
|
+
k: {"dims": "time", "data": v.read()}
|
|
245
|
+
for k, v in self._internal_dict.items()
|
|
246
|
+
}
|
|
247
|
+
return xarray.Dataset.from_dict(d)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class CompositeSubsetClient(CompositeClient):
|
|
251
|
+
"""A composite client with only a subset of its keys exposed."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, client, keys=None):
|
|
254
|
+
super().__init__(
|
|
255
|
+
context=client.context,
|
|
256
|
+
item=client.item,
|
|
257
|
+
structure_clients=client.structure_clients,
|
|
258
|
+
)
|
|
259
|
+
self._keys = keys or list(client.keys())
|
|
260
|
+
|
|
261
|
+
def __repr__(self):
|
|
262
|
+
return node_repr(self, self._keys).replace(type(self).__name__, "DatasetClient")
|
|
263
|
+
|
|
264
|
+
def _keys_slice(
|
|
265
|
+
self, start, stop, direction, page_size: int | None = None, **kwargs
|
|
266
|
+
):
|
|
267
|
+
yield from self._keys[start : stop : -1 if direction < 0 else 1] # noqa: 203
|
|
268
|
+
|
|
269
|
+
def _items_slice(
|
|
270
|
+
self, start, stop, direction, page_size: int | None = None, **kwargs
|
|
271
|
+
):
|
|
272
|
+
for key in self._keys[start : stop : -1 if direction < 0 else 1]: # noqa: 203
|
|
273
|
+
yield key, self[key]
|
|
274
|
+
|
|
275
|
+
def __iter__(self):
|
|
276
|
+
yield from self._keys
|
|
277
|
+
|
|
278
|
+
def __getitem__(self, key):
|
|
279
|
+
if key in self._keys:
|
|
280
|
+
return super().__getitem__(key)
|
|
281
|
+
raise KeyError(key)
|
|
282
|
+
|
|
283
|
+
def __len__(self):
|
|
284
|
+
return len(self._keys)
|
|
285
|
+
|
|
286
|
+
def __contains__(self, key):
|
|
287
|
+
return key in self._keys
|
|
288
|
+
|
|
289
|
+
def read(self, variables=None, dim0=None):
|
|
290
|
+
variables = set(self._keys).intersection(variables or self._keys)
|
|
291
|
+
|
|
292
|
+
return super().read(variables, dim0=dim0)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class VirtualContainer(DictView):
|
|
296
|
+
def __repr__(self):
|
|
297
|
+
tiled_repr = node_repr(self, self._internal_dict.keys())
|
|
298
|
+
return tiled_repr.replace(type(self).__name__, "ContainerClient")
|
|
299
|
+
|
|
300
|
+
def __getitem__(self, key):
|
|
301
|
+
if "/" in key:
|
|
302
|
+
key, rest = key.split("/", 1)
|
|
303
|
+
return self[key][rest]
|
|
304
|
+
|
|
305
|
+
return super().__getitem__(key)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class VirtualArrayClient:
|
|
309
|
+
def __init__(self, data, dims=None):
|
|
310
|
+
# Delay this import for fast startup. In some cases only metadata
|
|
311
|
+
# is handled, and we can avoid the numpy import altogether.
|
|
312
|
+
|
|
313
|
+
# Ensure data is an array-like object
|
|
314
|
+
if not hasattr(data, "__iter__") or isinstance(data, str):
|
|
315
|
+
data = [data]
|
|
316
|
+
if not hasattr(data, "__array__"):
|
|
317
|
+
data = numpy.asanyarray(data)
|
|
318
|
+
|
|
319
|
+
self._data = data
|
|
320
|
+
self._dims = dims
|
|
321
|
+
|
|
322
|
+
def __getitem__(self, slice):
|
|
323
|
+
return self.read(slice)
|
|
324
|
+
|
|
325
|
+
def __repr__(self):
|
|
326
|
+
attrs = {"shape": self.shape, "dtype": self.dtype}
|
|
327
|
+
if dims := self.dims:
|
|
328
|
+
attrs["dims"] = dims
|
|
329
|
+
return "<ArrayClient" + "".join(f" {k}={v}" for k, v in attrs.items()) + ">"
|
|
330
|
+
|
|
331
|
+
def read(self, slice=None):
|
|
332
|
+
return self._data if slice is None else self._data[slice]
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def size(self):
|
|
336
|
+
return self._data.size
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def shape(self):
|
|
340
|
+
return self._data.shape
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def dtype(self):
|
|
344
|
+
return self._data.dtype
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def dims(self):
|
|
348
|
+
return self._dims
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class BlueskyEventStreamV3(BlueskyEventStream, CompositeClient):
|
|
352
|
+
def __repr__(self):
|
|
353
|
+
stream_name = self.metadata.get("stream_name") or self.item["id"]
|
|
354
|
+
return f"<BlueskyEventStream {self._var_keys!r} stream_name={stream_name!r}>"
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def _var_keys(self):
|
|
358
|
+
return {k for k in self if not k.startswith("ts_") and k != "seq_num"}
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def _ts_keys(self):
|
|
362
|
+
return {k for k in self if k.startswith("ts_")}
|
|
363
|
+
|
|
364
|
+
def read(self, variables=(DATAVALUES,), dim0=None):
|
|
365
|
+
if DATAVALUES in variables:
|
|
366
|
+
variables = self._var_keys.union(variables) - {DATAVALUES}
|
|
367
|
+
if TIMESTAMPS in variables:
|
|
368
|
+
variables = self._ts_keys.union(variables) - {TIMESTAMPS}
|
|
369
|
+
|
|
370
|
+
return super().read(variables=variables, dim0=dim0)
|
|
371
|
+
|
|
372
|
+
@functools.cached_property
|
|
373
|
+
def descriptors(self):
|
|
374
|
+
# Go back to the BlueskyRun node and requests the documents
|
|
375
|
+
stream_name = self.metadata.get("stream_name") or self.item["id"]
|
|
376
|
+
# the path is: bs_run_node/streams/current_stream (old) or bs_run_node/current_stream (new)
|
|
377
|
+
bs_run_node = self.parent
|
|
378
|
+
if bs_run_node.item["id"] == "streams" and (
|
|
379
|
+
"BlueskyRun" not in {s.name for s in bs_run_node.specs}
|
|
380
|
+
):
|
|
381
|
+
# The parent is the old "streams" node, go up one more level
|
|
382
|
+
bs_run_node = bs_run_node.parent
|
|
383
|
+
return [
|
|
384
|
+
doc
|
|
385
|
+
for name, doc in bs_run_node.documents()
|
|
386
|
+
if name == "descriptor" and doc["name"] == stream_name
|
|
387
|
+
]
|