blissdata 0.3.4__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.
Files changed (67) hide show
  1. blissdata/__init__.py +17 -0
  2. blissdata/beacon/__init__.py +1 -0
  3. blissdata/beacon/_base.py +106 -0
  4. blissdata/beacon/config.py +24 -0
  5. blissdata/beacon/data.py +45 -0
  6. blissdata/beacon/files.py +141 -0
  7. blissdata/client.py +62 -0
  8. blissdata/common/__init__.py +15 -0
  9. blissdata/common/utils.py +159 -0
  10. blissdata/data/__init__.py +15 -0
  11. blissdata/data/events/__init__.py +22 -0
  12. blissdata/data/events/channel.py +149 -0
  13. blissdata/data/events/lima.py +478 -0
  14. blissdata/data/events/node.py +28 -0
  15. blissdata/data/events/scan.py +49 -0
  16. blissdata/data/events/walk.py +42 -0
  17. blissdata/data/expiration.py +42 -0
  18. blissdata/data/lima_image.py +465 -0
  19. blissdata/data/node.py +1619 -0
  20. blissdata/data/nodes/__init__.py +11 -0
  21. blissdata/data/nodes/channel.py +394 -0
  22. blissdata/data/nodes/dataset.py +82 -0
  23. blissdata/data/nodes/dataset_collection.py +12 -0
  24. blissdata/data/nodes/lima.py +422 -0
  25. blissdata/data/nodes/node_ref_channel.py +34 -0
  26. blissdata/data/nodes/proposal.py +12 -0
  27. blissdata/data/nodes/scan.py +192 -0
  28. blissdata/data/nodes/scan_group.py +12 -0
  29. blissdata/data/remote_node.py +204 -0
  30. blissdata/data/scan.py +666 -0
  31. blissdata/h5api/__init__.py +1 -0
  32. blissdata/h5api/abstract.py +97 -0
  33. blissdata/h5api/dynamic_hdf5.py +153 -0
  34. blissdata/h5api/file_arguments.py +28 -0
  35. blissdata/h5api/static_hdf5.py +139 -0
  36. blissdata/h5api/utils/__init__.py +0 -0
  37. blissdata/h5api/utils/bliss.py +138 -0
  38. blissdata/h5api/utils/hdf5.py +280 -0
  39. blissdata/h5api/utils/hdf5_retry.py +98 -0
  40. blissdata/h5api/utils/lima.py +286 -0
  41. blissdata/h5api/utils/types.py +13 -0
  42. blissdata/redis/__init__.py +12 -0
  43. blissdata/redis/caching.py +390 -0
  44. blissdata/redis/connection.py +169 -0
  45. blissdata/redis/manager.py +164 -0
  46. blissdata/redis/proxy.py +971 -0
  47. blissdata/redis/scripting.py +24 -0
  48. blissdata/settings.py +1174 -0
  49. blissdata/streaming.py +819 -0
  50. blissdata/streaming_events.py +355 -0
  51. blissdata/tests/__init__.py +0 -0
  52. blissdata/tests/beacon/__init__.py +0 -0
  53. blissdata/tests/beacon/test_config.py +26 -0
  54. blissdata/tests/beacon/test_data.py +50 -0
  55. blissdata/tests/beacon/test_files.py +80 -0
  56. blissdata/tests/conftest.py +0 -0
  57. blissdata/tests/h5api/__init__.py +0 -0
  58. blissdata/tests/h5api/scanner.py +390 -0
  59. blissdata/tests/h5api/test_dynamic_files.py +309 -0
  60. blissdata/tests/h5api/test_static_files.py +169 -0
  61. blissdata/tests/redis/__init__.py +0 -0
  62. blissdata/tests/redis/manager.py +23 -0
  63. blissdata-0.3.4.dist-info/LICENSE +165 -0
  64. blissdata-0.3.4.dist-info/METADATA +79 -0
  65. blissdata-0.3.4.dist-info/RECORD +67 -0
  66. blissdata-0.3.4.dist-info/WHEEL +5 -0
  67. blissdata-0.3.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env python
2
+ import os
3
+ import sys
4
+ import signal
5
+ import gevent
6
+ import multiprocessing
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ from queue import Empty
10
+
11
+ from blissdata.client import configure_with_beacon_address
12
+ from blissdata.data.events import Event, EventType
13
+ from blissdata.data.node import get_node, get_session_node, DataNodeContainer
14
+ from blissdata.redis.manager import RedisAddress
15
+
16
+
17
+ @contextmanager
18
+ def run_process(process):
19
+ try:
20
+ process.start()
21
+ yield
22
+ finally:
23
+ process.terminate()
24
+ process.join()
25
+
26
+
27
+ @dataclass
28
+ class NodeContent:
29
+ """A pickle-able class to embed a node's data attributes (no implementation related stuff)"""
30
+
31
+ type: EventType
32
+ name: str
33
+ db_name: str
34
+ info: dict
35
+ redis_url: str
36
+
37
+
38
+ class RemoteNodeWalker:
39
+ """Node-like object implementing walking functions through a subprocess, thus monkey-patching
40
+ can be avoided locally.
41
+ RemoteNodeWalker and NodeContent objects represent the node's implementation and data, this way
42
+ interprocess communication only sees the data part.
43
+
44
+ USER-PROCESS SUB-PROCESS
45
+ ┌─────────────────────────────┐ ┌───────────────┐
46
+ │ ┌──────────────────┐ │ │ BLISSDATA │
47
+ │USER │ RemoteNodeWalker │ │ │ ┌─────────┐ │
48
+ │CODE: │ │ │ │ │real Node│ │
49
+ │walk()<┼───── queue <─────┼──┼───────┼──┤ walk() │ │
50
+ │ └──────────────────┘ │ │ └─────────┘ │
51
+ │ (GEVENT-FREE) │ │ (GEVENT) │
52
+ └─────────────────────────────┘ └───────────────┘
53
+ """
54
+
55
+ def __init__(self, beacon_host: str, beacon_port: int, node_db_name: str):
56
+ self._queue = multiprocessing.Queue()
57
+ self._beacon_host = beacon_host
58
+ self._beacon_port = beacon_port
59
+ self._node_db_name = node_db_name
60
+ self._keep_running = True
61
+
62
+ def stop(self):
63
+ self._keep_running = False
64
+
65
+ def walk(self, *args, **kwargs):
66
+ yield from self._run_remote_walk("walk", *args, **kwargs)
67
+
68
+ def walk_from_last(self, *args, **kwargs):
69
+ yield from self._run_remote_walk("walk_from_last", *args, **kwargs)
70
+
71
+ def walk_events(self, *args, **kwargs):
72
+ yield from self._run_remote_walk("walk_events", *args, **kwargs)
73
+
74
+ def walk_on_new_events(self, *args, **kwargs):
75
+ yield from self._run_remote_walk("walk_on_new_events", *args, **kwargs)
76
+
77
+ def _run_remote_walk(
78
+ self,
79
+ walk_func_name,
80
+ *walk_func_args,
81
+ started_event=None,
82
+ stop_handler=None,
83
+ **walk_func_kwargs,
84
+ ):
85
+ if stop_handler is not None:
86
+ raise RuntimeError("RemoteNodeWalker doesn't support `stop_handler`")
87
+
88
+ user_started_event = None
89
+ if started_event is not None:
90
+ user_started_event = started_event
91
+ started_event = multiprocessing.Event()
92
+ walk_func_kwargs["started_event"] = started_event
93
+
94
+ process = multiprocessing.Process(
95
+ target=RemoteNodeWalker._remote_task,
96
+ daemon=True,
97
+ args=(
98
+ self._beacon_host,
99
+ self._beacon_port,
100
+ self._node_db_name,
101
+ self._queue,
102
+ walk_func_name,
103
+ walk_func_args,
104
+ walk_func_kwargs,
105
+ ),
106
+ )
107
+
108
+ with run_process(process):
109
+ if started_event is not None:
110
+ while process.is_alive() and self._keep_running:
111
+ if started_event.wait(timeout=0.2):
112
+ user_started_event.set()
113
+ break
114
+ while process.is_alive() and self._keep_running:
115
+ try:
116
+ yield self._queue.get(timeout=0.2)
117
+ except Empty:
118
+ pass
119
+ if not process.is_alive():
120
+ raise RuntimeError("RemoteNodeWalker subprocess failed.")
121
+
122
+ @staticmethod
123
+ def _remote_task(
124
+ beacon_host,
125
+ beacon_port,
126
+ node_db_name,
127
+ queue,
128
+ walk_func_name,
129
+ walk_func_args,
130
+ walk_func_kwargs,
131
+ ):
132
+ """Suprocess main function, don't use it on user side to prevent gevent patching"""
133
+ gevent.monkey.patch_all(thread=False)
134
+
135
+ # A watchdog looking at parent's PID to exit if parent dies. If the parent encounters an
136
+ # exception below python level, context managers or "try" statements won't exit properly.
137
+ # Thus, the child has to keep an eye on its parent.
138
+ def watchdog():
139
+ parent_pid = os.getppid()
140
+ while True:
141
+ try:
142
+ # send null signal to test pid
143
+ os.kill(parent_pid, 0)
144
+ except OSError:
145
+ sys.exit(0)
146
+ gevent.sleep(1)
147
+
148
+ # Inhibit SIGINT handler before running watchdog, so it is protected from Ctrl-C
149
+ def dummy_handler(signum, frame):
150
+ pass
151
+
152
+ signal.signal(signal.SIGINT, dummy_handler)
153
+
154
+ gevent.spawn(watchdog)
155
+
156
+ configure_with_beacon_address(beacon_host, beacon_port)
157
+
158
+ # if a node_db_name contains no ":", it is a session, therefore it should be
159
+ # accessed with get_session_node(), which allows to start walking on it before
160
+ # it even exists in Redis.
161
+ if ":" in node_db_name:
162
+ target_node = get_node(node_db_name)
163
+ if target_node is None:
164
+ raise RuntimeError(f'Node "{node_db_name}" doesn\'t exist.')
165
+ else:
166
+ target_node = get_session_node(node_db_name)
167
+
168
+ walk_function = getattr(target_node, walk_func_name)
169
+
170
+ for result in walk_function(*walk_func_args, **walk_func_kwargs):
171
+ if isinstance(result, Event):
172
+ ev_type, node, data = result
173
+ packed_event = Event(ev_type, RemoteNodeWalker.pack_node(node), data)
174
+ queue.put(packed_event)
175
+ elif isinstance(result, DataNodeContainer):
176
+ packed_node = RemoteNodeWalker.pack_node(result)
177
+ queue.put(packed_node)
178
+ else:
179
+ RuntimeError(
180
+ f"Oops, unexpected walk function return type: {type(result)}"
181
+ )
182
+
183
+ @staticmethod
184
+ def pack_node(node):
185
+ """Pack revelant node's information to be pickled, without embedding Gevent or Redis related stuff."""
186
+ redis_kw = node.db_connection.connection_pool.connection_kwargs
187
+ if "path" in redis_kw:
188
+ redis_url = (
189
+ RedisAddress.factory(f"localhost:{redis_kw['path']}").url
190
+ + f"?db={redis_kw['db']}"
191
+ )
192
+ else:
193
+ redis_url = (
194
+ RedisAddress.factory(f"{redis_kw['host']}:{redis_kw['port']}").url
195
+ + f"/{redis_kw['db']}"
196
+ )
197
+
198
+ return NodeContent(
199
+ node.node_type,
200
+ node.name,
201
+ node.db_name,
202
+ node.info.get_all(),
203
+ redis_url,
204
+ )