hat-event 0.9.27__cp310.cp311.cp312.cp313-abi3-win_amd64.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 (73) hide show
  1. hat/event/__init__.py +1 -0
  2. hat/event/adminer/__init__.py +18 -0
  3. hat/event/adminer/client.py +124 -0
  4. hat/event/adminer/common.py +27 -0
  5. hat/event/adminer/server.py +111 -0
  6. hat/event/backends/__init__.py +0 -0
  7. hat/event/backends/dummy.py +49 -0
  8. hat/event/backends/lmdb/__init__.py +9 -0
  9. hat/event/backends/lmdb/backend.py +319 -0
  10. hat/event/backends/lmdb/common.py +277 -0
  11. hat/event/backends/lmdb/conditions.py +102 -0
  12. hat/event/backends/lmdb/convert/__init__.py +0 -0
  13. hat/event/backends/lmdb/convert/__main__.py +8 -0
  14. hat/event/backends/lmdb/convert/convert_v06_to_v07.py +213 -0
  15. hat/event/backends/lmdb/convert/convert_v07_to_v09.py +175 -0
  16. hat/event/backends/lmdb/convert/main.py +88 -0
  17. hat/event/backends/lmdb/convert/v06.py +216 -0
  18. hat/event/backends/lmdb/convert/v07.py +508 -0
  19. hat/event/backends/lmdb/convert/v09.py +50 -0
  20. hat/event/backends/lmdb/convert/version.py +63 -0
  21. hat/event/backends/lmdb/environment.py +100 -0
  22. hat/event/backends/lmdb/latestdb.py +116 -0
  23. hat/event/backends/lmdb/manager/__init__.py +0 -0
  24. hat/event/backends/lmdb/manager/__main__.py +8 -0
  25. hat/event/backends/lmdb/manager/common.py +45 -0
  26. hat/event/backends/lmdb/manager/copy.py +92 -0
  27. hat/event/backends/lmdb/manager/main.py +34 -0
  28. hat/event/backends/lmdb/manager/query.py +215 -0
  29. hat/event/backends/lmdb/refdb.py +234 -0
  30. hat/event/backends/lmdb/systemdb.py +102 -0
  31. hat/event/backends/lmdb/timeseriesdb.py +486 -0
  32. hat/event/backends/memory.py +178 -0
  33. hat/event/common/__init__.py +144 -0
  34. hat/event/common/backend.py +91 -0
  35. hat/event/common/collection/__init__.py +8 -0
  36. hat/event/common/collection/common.py +28 -0
  37. hat/event/common/collection/list.py +19 -0
  38. hat/event/common/collection/tree.py +62 -0
  39. hat/event/common/common.py +176 -0
  40. hat/event/common/encoder.py +305 -0
  41. hat/event/common/json_schema_repo.json +1 -0
  42. hat/event/common/matches.py +44 -0
  43. hat/event/common/module.py +142 -0
  44. hat/event/common/sbs_repo.json +1 -0
  45. hat/event/common/subscription/__init__.py +22 -0
  46. hat/event/common/subscription/_csubscription.abi3.pyd +0 -0
  47. hat/event/common/subscription/common.py +145 -0
  48. hat/event/common/subscription/csubscription.py +47 -0
  49. hat/event/common/subscription/pysubscription.py +97 -0
  50. hat/event/component.py +284 -0
  51. hat/event/eventer/__init__.py +28 -0
  52. hat/event/eventer/client.py +260 -0
  53. hat/event/eventer/common.py +27 -0
  54. hat/event/eventer/server.py +286 -0
  55. hat/event/manager/__init__.py +0 -0
  56. hat/event/manager/__main__.py +8 -0
  57. hat/event/manager/common.py +48 -0
  58. hat/event/manager/main.py +387 -0
  59. hat/event/server/__init__.py +0 -0
  60. hat/event/server/__main__.py +8 -0
  61. hat/event/server/adminer_server.py +43 -0
  62. hat/event/server/engine.py +216 -0
  63. hat/event/server/engine_runner.py +127 -0
  64. hat/event/server/eventer_client.py +205 -0
  65. hat/event/server/eventer_client_runner.py +152 -0
  66. hat/event/server/eventer_server.py +119 -0
  67. hat/event/server/main.py +84 -0
  68. hat/event/server/main_runner.py +212 -0
  69. hat_event-0.9.27.dist-info/LICENSE +202 -0
  70. hat_event-0.9.27.dist-info/METADATA +108 -0
  71. hat_event-0.9.27.dist-info/RECORD +73 -0
  72. hat_event-0.9.27.dist-info/WHEEL +7 -0
  73. hat_event-0.9.27.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,145 @@
1
+ from collections.abc import Iterable
2
+ import abc
3
+ import itertools
4
+ import typing
5
+
6
+ from hat.event.common.common import EventTypeSegment, EventType
7
+
8
+
9
+ class Subscription(abc.ABC):
10
+ """Subscription defined by query event types"""
11
+
12
+ @abc.abstractmethod
13
+ def __init__(self, query_types: Iterable[EventType]):
14
+ """Create subscription instance"""
15
+
16
+ @abc.abstractmethod
17
+ def get_query_types(self) -> Iterable[EventType]:
18
+ """Calculate sanitized query event types"""
19
+
20
+ @abc.abstractmethod
21
+ def matches(self, event_type: EventType) -> bool:
22
+ """Does `event_type` match subscription"""
23
+
24
+ @abc.abstractmethod
25
+ def union(self, *others: 'Subscription') -> 'Subscription':
26
+ """Create new subscription including event types from this and
27
+ other subscriptions."""
28
+
29
+ @abc.abstractmethod
30
+ def intersection(self, *others: 'Subscription') -> 'Subscription':
31
+ """Create new subscription containing event types in common with
32
+ other subscriptions."""
33
+
34
+ @abc.abstractmethod
35
+ def isdisjoint(self, other: 'Subscription') -> bool:
36
+ """Return ``True`` if this subscription has no event types in common
37
+ with other subscription."""
38
+
39
+
40
+ class Node(typing.NamedTuple):
41
+ """Subscription tree node"""
42
+ is_leaf: bool
43
+ children: typing.Dict[EventTypeSegment, 'Node']
44
+
45
+
46
+ def node_from_query_types(query_types: Iterable[EventType]) -> Node:
47
+ node = Node(False, {})
48
+
49
+ for query_type in query_types:
50
+ node = _add_query_type(node, query_type)
51
+
52
+ return node
53
+
54
+
55
+ def node_to_query_types(node: Node) -> Iterable[EventType]:
56
+ is_leaf, children = node
57
+
58
+ if is_leaf and '*' not in children:
59
+ yield ()
60
+
61
+ for head, child in children.items():
62
+ for rest in node_to_query_types(child):
63
+ yield (head, *rest)
64
+
65
+
66
+ def union(first: Node, second: Node) -> Node:
67
+ is_leaf = first.is_leaf or second.is_leaf
68
+
69
+ if '*' in first.children or '*' in second.children:
70
+ return Node(is_leaf, {'*': (True, {})})
71
+
72
+ children = {}
73
+
74
+ for name, child in itertools.chain(first.children.items(),
75
+ second.children.items()):
76
+ prev_child = children.get(name)
77
+ children[name] = union(prev_child, child) if prev_child else child
78
+
79
+ return Node(is_leaf, children)
80
+
81
+
82
+ def intersection(first: Node, second: Node) -> Node:
83
+ is_leaf = first.is_leaf and second.is_leaf
84
+
85
+ if '*' in first.children:
86
+ children = second.children
87
+
88
+ elif '*' in second.children:
89
+ children = first.children
90
+
91
+ else:
92
+ children = {}
93
+
94
+ for name, child, other_children in itertools.chain(
95
+ ((name, child, second.children)
96
+ for name, child in first.children.items()),
97
+ ((name, child, first.children)
98
+ for name, child in second.children.items())):
99
+
100
+ if name == '?':
101
+ candidates = other_children
102
+
103
+ else:
104
+ candidates = {}
105
+ other_child = other_children.get(name)
106
+ if other_child:
107
+ candidates[name] = other_child
108
+ other_child = other_children.get('?')
109
+ if other_child:
110
+ candidates['?'] = other_child
111
+
112
+ for other_name, other_child in candidates.items():
113
+ result_name = name if name != '?' else other_name
114
+ result_children = intersection(child, other_child)
115
+ prev_result_children = children.get(result_name)
116
+ children[result_name] = (
117
+ union(prev_result_children, result_children)
118
+ if prev_result_children else result_children)
119
+
120
+ return Node(is_leaf, children)
121
+
122
+
123
+ def _add_query_type(node, query_type):
124
+ is_leaf, children = node
125
+
126
+ if '*' in children:
127
+ return node
128
+
129
+ if not query_type:
130
+ return Node(True, children)
131
+
132
+ head, rest = query_type[0], query_type[1:]
133
+
134
+ if head == '*':
135
+ if rest:
136
+ raise ValueError('invalid query event type')
137
+ children.clear()
138
+ children['*'] = Node(True, {})
139
+
140
+ else:
141
+ child = children.get(head) or Node(False, {})
142
+ child = _add_query_type(child, rest)
143
+ children[head] = child
144
+
145
+ return node
@@ -0,0 +1,47 @@
1
+ from hat.event.common.subscription import common
2
+
3
+ from hat.event.common.subscription import _csubscription
4
+
5
+
6
+ class CSubscription(common.Subscription):
7
+ """C implementation of Subscription"""
8
+
9
+ def __init__(self, query_types):
10
+ self._subscription = _csubscription.Subscription(query_types)
11
+
12
+ def get_query_types(self):
13
+ return self._subscription.get_query_types()
14
+
15
+ def matches(self, event_type):
16
+ return self._subscription.matches(event_type)
17
+
18
+ def union(self, *others):
19
+ node = common.node_from_query_types(
20
+ self._subscription.get_query_types())
21
+
22
+ for other in others:
23
+ other_node = common.node_from_query_types(other.get_query_types())
24
+ node = common.union(node, other_node)
25
+
26
+ return CSubscription(common.node_to_query_types(node))
27
+
28
+ def intersection(self, *others):
29
+ node = common.node_from_query_types(
30
+ self._subscription.get_query_types())
31
+
32
+ for other in others:
33
+ other_node = common.node_from_query_types(other.get_query_types())
34
+ node = common.intersection(node, other_node)
35
+
36
+ return CSubscription(common.node_to_query_types(node))
37
+
38
+ def isdisjoint(self, other):
39
+ other_subscription = _get_other_subscription(other)
40
+ return self._subscription.isdisjoint(other_subscription)
41
+
42
+
43
+ def _get_other_subscription(other):
44
+ if isinstance(other, CSubscription):
45
+ return other._subscription
46
+
47
+ return CSubscription(other.get_query_types())._subscription
@@ -0,0 +1,97 @@
1
+ from hat.event.common.subscription import common
2
+
3
+
4
+ class PySubscription(common.Subscription):
5
+ """Python implementation of Subscription"""
6
+
7
+ def __init__(self, query_types):
8
+ self._root = common.node_from_query_types(query_types)
9
+
10
+ def get_query_types(self):
11
+ return common.node_to_query_types(self._root)
12
+
13
+ def matches(self, event_type):
14
+ return _matches(self._root, event_type, 0)
15
+
16
+ def union(self, *others):
17
+ node = self._root
18
+
19
+ for other in others:
20
+ other_node = _get_other_node(other)
21
+ node = common.union(node, other_node)
22
+
23
+ result = PySubscription([])
24
+ result._root = node
25
+ return result
26
+
27
+ def intersection(self, *others):
28
+ node = self._root
29
+
30
+ for other in others:
31
+ other_node = _get_other_node(other)
32
+ node = common.intersection(node, other_node)
33
+
34
+ result = PySubscription([])
35
+ result._root = node
36
+ return result
37
+
38
+ def isdisjoint(self, other):
39
+ other_node = _get_other_node(other)
40
+ return _isdisjoint(self._root, other_node)
41
+
42
+
43
+ def _get_other_node(other):
44
+ if isinstance(other, PySubscription):
45
+ return other._root
46
+
47
+ return common.node_from_query_types(other.get_query_types())
48
+
49
+
50
+ def _matches(node, event_type, event_type_index):
51
+ is_leaf, children = node
52
+
53
+ if '*' in children:
54
+ return True
55
+
56
+ if event_type_index >= len(event_type):
57
+ return is_leaf
58
+
59
+ child = children.get(event_type[event_type_index])
60
+ if child and _matches(child, event_type, event_type_index + 1):
61
+ return True
62
+
63
+ child = children.get('?')
64
+ if child and _matches(child, event_type, event_type_index + 1):
65
+ return True
66
+
67
+ return False
68
+
69
+
70
+ def _isdisjoint(first, second):
71
+ if first.is_leaf and second.is_leaf:
72
+ return False
73
+
74
+ if (('*' in first.children and (second.children or second.is_leaf)) or
75
+ ('*' in second.children and (first.children or first.is_leaf))):
76
+ return False
77
+
78
+ if '?' in first.children:
79
+ for child in second.children.values():
80
+ if not _isdisjoint(first.children['?'], child):
81
+ return False
82
+
83
+ if '?' in second.children:
84
+ for name, child in first.children.items():
85
+ if name == '?':
86
+ continue
87
+ if not _isdisjoint(second.children['?'], child):
88
+ return False
89
+
90
+ names = set(first.children.keys()).intersection(second.children.keys())
91
+ for name in names:
92
+ if name == '?':
93
+ continue
94
+ if not _isdisjoint(first.children[name], second.children[name]):
95
+ return False
96
+
97
+ return True
hat/event/component.py ADDED
@@ -0,0 +1,284 @@
1
+ """Eventer Component"""
2
+
3
+ from collections.abc import Collection
4
+ import asyncio
5
+ import contextlib
6
+ import logging
7
+ import types
8
+ import typing
9
+
10
+ from hat import aio
11
+ from hat import json
12
+ from hat import util
13
+ from hat.drivers import tcp
14
+ import hat.monitor.component
15
+
16
+ from hat.event import common
17
+ import hat.event.eventer.client
18
+
19
+
20
+ mlog: logging.Logger = logging.getLogger(__name__)
21
+ """Module logger"""
22
+
23
+ State: typing.TypeAlias = hat.monitor.component.State
24
+ """Component state"""
25
+
26
+ Runner: typing.TypeAlias = aio.Resource
27
+ """Component runner"""
28
+
29
+ RunnerCb: typing.TypeAlias = aio.AsyncCallable[
30
+ ['Component', 'ServerData', hat.event.eventer.client.Client],
31
+ Runner]
32
+ """Runner callback"""
33
+
34
+ StateCb: typing.TypeAlias = aio.AsyncCallable[['Component', State], None]
35
+ """State callback"""
36
+
37
+ CloseReqCb: typing.TypeAlias = aio.AsyncCallable[['Component'], None]
38
+ """Close request callback"""
39
+
40
+ StatusCb: typing.TypeAlias = aio.AsyncCallable[
41
+ ['Component', hat.event.eventer.client.Client, common.Status],
42
+ None]
43
+ """Status callback"""
44
+
45
+ EventsCb: typing.TypeAlias = aio.AsyncCallable[
46
+ ['Component', hat.event.eventer.client.Client, Collection[common.Event]],
47
+ None]
48
+ """Events callback"""
49
+
50
+
51
+ class ServerData(typing.NamedTuple):
52
+ """Server data"""
53
+ server_id: common.ServerId
54
+ addr: tcp.Address
55
+ server_token: str | None
56
+
57
+
58
+ async def connect(addr: tcp.Address,
59
+ name: str,
60
+ group: str,
61
+ server_group: str,
62
+ client_name: str,
63
+ runner_cb: RunnerCb,
64
+ *,
65
+ state_cb: StateCb | None = None,
66
+ close_req_cb: CloseReqCb | None = None,
67
+ status_cb: StatusCb | None = None,
68
+ events_cb: EventsCb | None = None,
69
+ server_data_queue_size: int = 1024,
70
+ reconnect_delay: float = 0.5,
71
+ observer_kwargs: dict[str, typing.Any] = {},
72
+ eventer_kwargs: dict[str, typing.Any] = {}
73
+ ) -> 'Component':
74
+ """Connect to local monitor server and create component
75
+
76
+ High-level interface for communication with Event Server, based on
77
+ information obtained from Monitor Server.
78
+
79
+ Component instance tries to establish active connection with
80
+ Event Server within monitor component group `server_group`. Once this
81
+ connection is established, `runner_cb` is called with currently active
82
+ eventer client instance. Result of calling `runner_cb` should be runner
83
+ representing user defined components activity associated with connection
84
+ to active Event Server. Once connection to Event Server is closed or new
85
+ active Event Server is detected, associated runner is closed. If new
86
+ connection to Event Server is successfully established,
87
+ `component_cb` will be called again to create new runner associated with
88
+ new instance of eventer client.
89
+
90
+ If runner is closed while connection to Event Server is open, component
91
+ is closed.
92
+
93
+ """
94
+ component = Component()
95
+ component._server_group = server_group
96
+ component._client_name = client_name
97
+ component._runner_cb = runner_cb
98
+ component._state_cb = state_cb
99
+ component._close_req_cb = close_req_cb
100
+ component._status_cb = status_cb
101
+ component._events_cb = events_cb
102
+ component._server_data_queue_size = server_data_queue_size
103
+ component._reconnect_delay = reconnect_delay
104
+ component._eventer_kwargs = eventer_kwargs
105
+ component._server_data = None
106
+ component._server_data_queue = None
107
+
108
+ component._monitor_component = await hat.monitor.component.connect(
109
+ addr=addr,
110
+ name=name,
111
+ group=group,
112
+ runner_cb=component._create_monitor_runner,
113
+ **{**observer_kwargs,
114
+ 'state_cb': component._on_state,
115
+ 'close_req_cb': component._on_close_req})
116
+
117
+ return component
118
+
119
+
120
+ class Component(aio.Resource):
121
+ """Eventer Component"""
122
+
123
+ @property
124
+ def async_group(self):
125
+ return self._monitor_component.async_group
126
+
127
+ @property
128
+ def state(self) -> State:
129
+ """State"""
130
+ return self._monitor_component.state
131
+
132
+ @property
133
+ def ready(self) -> bool:
134
+ """Ready"""
135
+ return self._monitor_component.ready
136
+
137
+ async def set_ready(self, ready: bool):
138
+ """Set ready"""
139
+ await self._monitor_component.set_ready(ready)
140
+
141
+ async def _on_state(self, monitor_component, state):
142
+ if self._server_data_queue is not None:
143
+ data = self._get_active_server_data(state)
144
+
145
+ if data != self._server_data:
146
+ self._server_data = data
147
+
148
+ with contextlib.suppress(aio.QueueClosedError):
149
+ await self._server_data_queue.put(data)
150
+
151
+ if self._state_cb:
152
+ await aio.call(self._state_cb, self, state)
153
+
154
+ async def _on_close_req(self, monitor_component):
155
+ if self._close_req_cb:
156
+ await aio.call(self._close_req_cb, self)
157
+
158
+ async def _on_status(self, eventer_client, status):
159
+ if self._status_cb:
160
+ await aio.call(self._status_cb, self, eventer_client, status)
161
+
162
+ async def _on_events(self, eventer_client, events):
163
+ if self._events_cb:
164
+ await aio.call(self._events_cb, self, eventer_client, events)
165
+
166
+ def _create_monitor_runner(self, monitor_component):
167
+ self._server_data = self._get_active_server_data(
168
+ monitor_component.state)
169
+ self._server_data_queue = aio.Queue(self._server_data_queue_size)
170
+
171
+ if self._server_data:
172
+ self._server_data_queue.put_nowait(self._server_data)
173
+
174
+ runner = aio.Group()
175
+ runner.spawn(self._server_data_loop, runner, self._server_data_queue)
176
+
177
+ return runner
178
+
179
+ def _get_active_server_data(self, state):
180
+ info = util.first(state.components, self._active_server_filter)
181
+ if not info:
182
+ return
183
+
184
+ server_id = json.get(info.data, 'server_id')
185
+ host = json.get(info.data, ['eventer_server', 'host'])
186
+ port = json.get(info.data, ['eventer_server', 'port'])
187
+ server_token = json.get(info.data, 'server_token')
188
+ if (not isinstance(server_id, int) or
189
+ not isinstance(host, str) or
190
+ not isinstance(port, int) or
191
+ not isinstance(server_token, (str, types.NoneType))):
192
+ return
193
+
194
+ client_token = self._eventer_kwargs.get('client_token')
195
+ if client_token is not None and client_token != server_token:
196
+ return
197
+
198
+ return ServerData(server_id=server_id,
199
+ addr=tcp.Address(host, port),
200
+ server_token=server_token)
201
+
202
+ def _active_server_filter(self, info):
203
+ return (info.group == self._server_group and
204
+ info.blessing_req.token is not None and
205
+ info.blessing_req.token == info.blessing_res.token)
206
+
207
+ async def _server_data_loop(self, async_group, server_data_queue):
208
+ try:
209
+ server_data = None
210
+ while True:
211
+ while not server_data or not server_data_queue.empty():
212
+ server_data = await server_data_queue.get_until_empty()
213
+
214
+ async with async_group.create_subgroup() as subgroup:
215
+ subgroup.spawn(self._client_loop, subgroup, server_data)
216
+ server_data = await server_data_queue.get_until_empty()
217
+
218
+ except Exception as e:
219
+ mlog.error("address loop error: %s", e, exc_info=e)
220
+ self.close()
221
+
222
+ finally:
223
+ async_group.close()
224
+ server_data_queue.close()
225
+
226
+ async def _client_loop(self, async_group, server_data):
227
+ try:
228
+ while True:
229
+ try:
230
+ kwargs = {**self._eventer_kwargs,
231
+ 'status_cb': self._on_status,
232
+ 'events_cb': self._on_events}
233
+
234
+ if 'server_id' not in kwargs:
235
+ kwargs['server_id'] = server_data.server_id
236
+
237
+ mlog.debug("connecting to server %s", server_data.addr)
238
+ client = await hat.event.eventer.client.connect(
239
+ addr=server_data.addr,
240
+ client_name=self._client_name,
241
+ **kwargs)
242
+
243
+ except Exception as e:
244
+ mlog.warning("error connecting to server: %s", e,
245
+ exc_info=e)
246
+ await asyncio.sleep(self._reconnect_delay)
247
+ continue
248
+
249
+ try:
250
+ mlog.debug("connected to server")
251
+ runner = await client.async_group.spawn(
252
+ aio.call, self._runner_cb, self, server_data, client)
253
+
254
+ try:
255
+ async with async_group.create_subgroup() as subgroup:
256
+ client_closing_task = subgroup.spawn(
257
+ client.wait_closing)
258
+ runner_closing_task = subgroup.spawn(
259
+ runner.wait_closing)
260
+
261
+ await asyncio.wait(
262
+ [client_closing_task, runner_closing_task],
263
+ return_when=asyncio.FIRST_COMPLETED)
264
+
265
+ if (runner_closing_task.done() and
266
+ not client_closing_task.done()):
267
+ self.close()
268
+ return
269
+
270
+ finally:
271
+ await aio.uncancellable(runner.async_close())
272
+
273
+ finally:
274
+ await aio.uncancellable(client.async_close())
275
+
276
+ mlog.debug("connection to server closed")
277
+ await asyncio.sleep(self._reconnect_delay)
278
+
279
+ except Exception as e:
280
+ mlog.error("client loop error: %s", e, exc_info=e)
281
+ self.close()
282
+
283
+ finally:
284
+ async_group.close()
@@ -0,0 +1,28 @@
1
+ """Eventer communication protocol"""
2
+
3
+ from hat.event.eventer.client import (StatusCb,
4
+ EventsCb,
5
+ EventerInitError,
6
+ connect,
7
+ Client)
8
+ from hat.event.eventer.server import (ConnectionId,
9
+ ConnectionInfo,
10
+ ConnectionCb,
11
+ RegisterCb,
12
+ QueryCb,
13
+ listen,
14
+ Server)
15
+
16
+
17
+ __all__ = ['StatusCb',
18
+ 'EventsCb',
19
+ 'EventerInitError',
20
+ 'connect',
21
+ 'Client',
22
+ 'ConnectionId',
23
+ 'ConnectionInfo',
24
+ 'ConnectionCb',
25
+ 'RegisterCb',
26
+ 'QueryCb',
27
+ 'listen',
28
+ 'Server']