opensignalbox-common 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.
@@ -0,0 +1 @@
1
+ __doc__ = "openSignalBox signalling simulator."
@@ -0,0 +1,72 @@
1
+ """
2
+ Here the configuration management is defined.
3
+ """
4
+
5
+ import os
6
+ import tomllib
7
+ from pathlib import Path
8
+
9
+ from appdirs import user_config_dir, user_data_dir
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+ from .version import __version__
13
+
14
+ __APPNAME__ = "openSignalBox"
15
+ __AUTHOR__ = "openSignalBox"
16
+ __USER_CONFIG_DIR__ = user_config_dir(__APPNAME__, __AUTHOR__, __version__)
17
+ __USER_DATA_DIR__ = user_data_dir(__APPNAME__, __AUTHOR__, __version__)
18
+ __DOTENV_NAME__ = "osb.env"
19
+ __DOTENV_PATH__ = os.path.join(__USER_CONFIG_DIR__, __DOTENV_NAME__)
20
+ __SETTINGS_NAME__ = "settings.toml"
21
+ __BOXES_DIRNAME__ = "boxes"
22
+ __MODULES_DIRNAME__ = "modules"
23
+ __CONFIG_DIRNAME__ = "config"
24
+ __BOX_TOMLNAME__ = "box.toml"
25
+ __MODULE_TOMLNAME__ = "module.toml"
26
+ __ROUTER_NAME__ = "router"
27
+ __STATES_DIRNAME__ = "states"
28
+
29
+
30
+ class EnvironmentVariables(BaseSettings):
31
+ workspace_path: Path = Path(__USER_DATA_DIR__) / "workspace"
32
+ model_config = SettingsConfigDict(env_file=__DOTENV_PATH__)
33
+
34
+
35
+ class OsbSettings(BaseSettings):
36
+ default_box: str = ""
37
+ default_state: str = ""
38
+ autostart: bool = False
39
+
40
+ def load(self) -> None:
41
+ try:
42
+ with open(
43
+ os.path.join(__WORKSPACE_PATH__, __SETTINGS_NAME__), "rb"
44
+ ) as settings_file:
45
+ settings_dict = tomllib.load(settings_file)
46
+ for setting in settings_dict:
47
+ setattr(self, setting.lower(), settings_dict[setting])
48
+ except FileNotFoundError:
49
+ raise
50
+
51
+ def save(self) -> None:
52
+ with open(
53
+ os.path.join(__WORKSPACE_PATH__, __SETTINGS_NAME__), "w"
54
+ ) as settings_file:
55
+ settings_file.write('DEFAULT_BOX="' + self.default_box + '"\n')
56
+ settings_file.write('DEFAULT_STATE="' + self.default_state + '"\n')
57
+ settings_file.write("AUTOSTART=" + str(self.autostart).lower() + "\n")
58
+
59
+
60
+ class SessionID:
61
+ value: str = ""
62
+
63
+
64
+ def write_dotenv():
65
+ # TODO Implement writing here for setting workspace in UI. Use python-dotenv to only change the workspace variable in case users use more variables here.
66
+ raise NotImplementedError
67
+
68
+
69
+ __ENV_VARS__ = EnvironmentVariables()
70
+ __WORKSPACE_PATH__ = __ENV_VARS__.workspace_path.absolute()
71
+ osb_settings = OsbSettings()
72
+ session_id_store = SessionID()
@@ -0,0 +1,6 @@
1
+ __all__ = ["pyre", "zbeacon", "zhelper"]
2
+ __version__ = "0.3.4"
3
+ __version_info__ = tuple(int(v) for v in __version__.split("."))
4
+
5
+ from .pyre import Pyre
6
+ from .pyre_event import PyreEvent
@@ -0,0 +1,338 @@
1
+ import logging
2
+ import uuid
3
+ from typing import Any
4
+ from uuid import UUID
5
+
6
+ from . import zhelper
7
+ import zmq
8
+
9
+ # local modules
10
+ from . import __version_info__
11
+ from .pyre_event import PyreEvent
12
+ from .pyre_node import PyreNode
13
+ from .zactor import ZActor
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ try:
18
+ raw_input # Python 2
19
+ except NameError:
20
+ raw_input = input # Python 3
21
+
22
+
23
+ class Pyre(object):
24
+ def __init__(
25
+ self,
26
+ name: str | None = None,
27
+ ctx: zmq.Context[zmq.Socket[bytes]] | None = None,
28
+ *args: Any,
29
+ **kwargs: Any,
30
+ ):
31
+ """Constructor, creates a new Zyre node. Note that until you start the
32
+ node it is silent and invisible to other nodes on the network.
33
+ The node name is provided to other nodes during discovery. If you
34
+ specify NULL, Zyre generates a randomized node name from the UUID.
35
+
36
+ Args:
37
+ name (str): The name of the node
38
+
39
+ Kwargs:
40
+ ctx: PyZMQ Context, if not specified a new context will be created
41
+ """
42
+ super(Pyre, self).__init__(*args, **kwargs)
43
+ self._ctx: zmq.Context[zmq.socket.Socket[bytes]] = ctx or zmq.Context()
44
+ self._uuid = None
45
+ self._name = name
46
+ self.verbose = False
47
+ self.inbox, self._outbox = zhelper.zcreate_pipe(self._ctx)
48
+
49
+ # Start node engine and wait for it to be ready
50
+ self.actor = ZActor(self._ctx, PyreNode, self._outbox)
51
+ # Send name, if any, to node backend
52
+ if self._name:
53
+ self.actor.send_unicode("SET NAME", zmq.SNDMORE)
54
+ self.actor.send_unicode(self._name)
55
+
56
+ # def __del__(self):
57
+ # We need to explicitly destroy the actor
58
+ # to make sure our node thread is stopped
59
+ # self.actor.destroy()
60
+
61
+ def __bool__(self):
62
+ "Determine whether the object is valid by converting to boolean" # Python 3
63
+ return True # TODO
64
+
65
+ def __nonzero__(self):
66
+ "Determine whether the object is valid by converting to boolean" # Python 2
67
+ return True # TODO
68
+
69
+ def uuid(self):
70
+ """Return our node UUID string, after successful initialization"""
71
+ if not self._uuid:
72
+ self.actor.send_unicode("UUID")
73
+ self._uuid = uuid.UUID(bytes=self.actor.recv())
74
+ return self._uuid
75
+
76
+ # Return our node name, after successful initialization
77
+ def name(self):
78
+ """Return our node name, after successful initialization"""
79
+ if not self._name:
80
+ self.actor.send_unicode("NAME")
81
+ self._name = self.actor.recv().decode("utf-8")
82
+ return self._name
83
+
84
+ # Not in Zyre api
85
+ def set_name(self, name):
86
+ logger.warning(
87
+ "DEPRECATED: set name in constructor, this method will be removed!"
88
+ )
89
+ self.actor.send_unicode("SET NAME", zmq.SNDMORE)
90
+ self.actor.send_unicode(name)
91
+
92
+ def set_header(self, key, value):
93
+ """Set node header; these are provided to other nodes during discovery
94
+ and come in each ENTER message."""
95
+ self.actor.send_unicode("SET HEADER", flags=zmq.SNDMORE)
96
+ self.actor.send_unicode(key, flags=zmq.SNDMORE)
97
+ self.actor.send_unicode(value)
98
+
99
+ def set_verbose(self):
100
+ """Set verbose mode; this tells the node to log all traffic as well as
101
+ all major events."""
102
+ self.actor.send_unicode("SET VERBOSE")
103
+
104
+ def set_port(self, port_nbr):
105
+ """Set UDP beacon discovery port; defaults to 5670, this call overrides
106
+ that so you can create independent clusters on the same network, for
107
+ e.g. development vs. production. Has no effect after zyre_start()."""
108
+ self.actor.send_unicode("SET PORT", zmq.SNDMORE)
109
+ self.actor.send(port_nbr)
110
+
111
+ def set_interval(self, interval):
112
+ """Set UDP beacon discovery interval, in milliseconds. Default is instant
113
+ beacon exploration followed by pinging every 1,000 msecs."""
114
+ self.actor.send_unicode("SET INTERVAL", zmq.SNDMORE)
115
+ self.actor.send_unicode(interval)
116
+
117
+ def set_interface(self, value):
118
+ """Set network interface for UDP beacons. If you do not set this, CZMQ will
119
+ choose an interface for you. On boxes with several interfaces you should
120
+ specify which one you want to use, or strange things can happen."""
121
+ logging.debug("set_interface not implemented") # TODO
122
+
123
+ # TODO: check args from zyre
124
+ def set_endpoint(self, format, *args):
125
+ """By default, Zyre binds to an ephemeral TCP port and broadcasts the local
126
+ host name using UDP beaconing. When you call this method, Zyre will use
127
+ gossip discovery instead of UDP beaconing. You MUST set-up the gossip
128
+ service separately using zyre_gossip_bind() and _connect(). Note that the
129
+ endpoint MUST be valid for both bind and connect operations. You can use
130
+ inproc://, ipc://, or tcp:// transports (for tcp://, use an IP address
131
+ that is meaningful to remote as well as local nodes). Returns 0 if
132
+ the bind was successful, else -1."""
133
+ self.actor.send_unicode("SET ENDPOINT", zmq.SNDMORE)
134
+ self.actor.send_unicode(format)
135
+
136
+ # TODO: We haven't implemented gossiping yet
137
+ # def gossip_bind(self, format, *args):
138
+ # def gossip_connect(self, format, *args):
139
+
140
+ def start(self):
141
+ """Start node, after setting header values. When you start a node it
142
+ begins discovery and connection. Returns 0 if OK, -1 if it wasn't
143
+ possible to start the node."""
144
+ self.actor.send_unicode("START")
145
+ # the backend will signal back
146
+ self.actor.resolve().wait()
147
+
148
+ def stop(self):
149
+ """Stop node; this signals to other peers that this node will go away.
150
+ This is polite; however you can also just destroy the node without
151
+ stopping it."""
152
+ self.actor.send_unicode("STOP", flags=zmq.DONTWAIT)
153
+ # the backend will signal back
154
+ self.actor.resolve().wait()
155
+ self.actor.destroy()
156
+
157
+ # Receive next message from node
158
+ def recv(self):
159
+ """Receive next message from network; the message may be a control
160
+ message (ENTER, EXIT, JOIN, LEAVE) or data (WHISPER, SHOUT).
161
+ """
162
+ return self.inbox.recv_multipart()
163
+
164
+ def join(self, group):
165
+ """Join a named group; after joining a group you can send messages to
166
+ the group and all Zyre nodes in that group will receive them."""
167
+ self.actor.send_unicode("JOIN", flags=zmq.SNDMORE)
168
+ self.actor.send_unicode(group)
169
+
170
+ def leave(self, group):
171
+ """Leave a group"""
172
+ self.actor.send_unicode("LEAVE", flags=zmq.SNDMORE)
173
+ self.actor.send_unicode(group)
174
+
175
+ # Send message to single peer; peer ID is first frame in message
176
+ def whisper(self, peer, msg_p):
177
+ """Send message to single peer, specified as a UUID string
178
+ Destroys message after sending"""
179
+ self.actor.send_unicode("WHISPER", flags=zmq.SNDMORE)
180
+ self.actor.send(peer.bytes, flags=zmq.SNDMORE)
181
+ if isinstance(msg_p, list):
182
+ self.actor.send_multipart(msg_p)
183
+ else:
184
+ self.actor.send(msg_p)
185
+
186
+ def shout(self, group, msg_p):
187
+ """Send message to a named group
188
+ Destroys message after sending"""
189
+ self.actor.send_unicode("SHOUT", flags=zmq.SNDMORE)
190
+ self.actor.send_unicode(group, flags=zmq.SNDMORE)
191
+ if isinstance(msg_p, list):
192
+ self.actor.send_multipart(msg_p)
193
+ else:
194
+ self.actor.send(msg_p)
195
+
196
+ # TODO: checks args from zyre
197
+ def whispers(self, peer: UUID, format: str, *args: Any):
198
+ """Send formatted string to a single peer specified as UUID string"""
199
+ self.actor.send_unicode("WHISPER", flags=zmq.SNDMORE)
200
+ self.actor.send(peer.bytes, flags=zmq.SNDMORE)
201
+ self.actor.send_unicode(format)
202
+
203
+ def shouts(self, group, format, *args):
204
+ """Send formatted string to a named group"""
205
+ self.actor.send_unicode("SHOUT", flags=zmq.SNDMORE)
206
+ self.actor.send_unicode(group, flags=zmq.SNDMORE)
207
+ self.actor.send_unicode(format)
208
+
209
+ def peers(self):
210
+ """Return list of current peer ids."""
211
+ self.actor.send_unicode("PEERS")
212
+ peers = self.actor.recv_pyobj()
213
+ return peers
214
+
215
+ def peers_by_group(self, group):
216
+ """Return list of current peer ids."""
217
+ self.actor.send_unicode("PEERS BY GROUP", flags=zmq.SNDMORE)
218
+ self.actor.send_unicode(group)
219
+ peers_by_group = self.actor.recv_pyobj()
220
+ return peers_by_group
221
+
222
+ def endpoint(self):
223
+ """Return own endpoint"""
224
+ self.actor.send_unicode("ENDPOINT")
225
+ endpoint = self.actor.recv_unicode()
226
+ return endpoint
227
+
228
+ def recent_events(self):
229
+ """Iterator that yields recent `PyreEvent`s"""
230
+ while self.socket().get(zmq.EVENTS) & zmq.POLLIN:
231
+ yield PyreEvent(self)
232
+
233
+ def events(self):
234
+ """Iterator that yields `PyreEvent`s indefinitely"""
235
+ while True:
236
+ yield PyreEvent(self)
237
+
238
+ # --------------------------------------------------------------------------
239
+ # Return the name of a connected peer. Caller owns the
240
+ # string.
241
+ # DEPRECATED: This is dropped in Zyre api. You receive names through events
242
+ def get_peer_name(self, peer):
243
+ logger.warning("get_peer_name() is deprecated, will be removed")
244
+ self.actor.send_unicode("PEER NAME", zmq.SNDMORE)
245
+ self.actor.send(peer.bytes)
246
+ name = self.actor.recv_unicode()
247
+ return name
248
+
249
+ def peer_address(self, peer):
250
+ """Return the endpoint of a connected peer."""
251
+ self.actor.send_unicode("PEER ENDPOINT", zmq.SNDMORE)
252
+ self.actor.send(peer.bytes)
253
+ adr = self.actor.recv_unicode()
254
+ return adr
255
+
256
+ def peer_header_value(self, peer, name):
257
+ """Return the value of a header of a conected peer.
258
+ Returns null if peer or key doesn't exist."""
259
+ self.actor.send_unicode("PEER HEADER", zmq.SNDMORE)
260
+ self.actor.send(peer.bytes, zmq.SNDMORE)
261
+ self.actor.send_unicode(name)
262
+ value = self.actor.recv_unicode()
263
+ return value
264
+
265
+ def peer_headers(self, peer):
266
+ """Return the value of a header of a conected peer.
267
+ Returns null if peer or key doesn't exist."""
268
+ self.actor.send_unicode("PEER HEADERS", zmq.SNDMORE)
269
+ self.actor.send(peer.bytes)
270
+ headers = self.actor.recv_pyobj()
271
+ return headers
272
+
273
+ def own_groups(self):
274
+ """Return list of currently joined groups."""
275
+ self.actor.send_unicode("OWN GROUPS")
276
+ groups = self.actor.recv_pyobj()
277
+ return groups
278
+
279
+ def peer_groups(self):
280
+ """Return list of groups known through connected peers."""
281
+ self.actor.send_unicode("PEER GROUPS")
282
+ groups = self.actor.recv_pyobj()
283
+ return groups
284
+
285
+ # Return node socket, for direct polling of socket
286
+ def socket(self):
287
+ """Return socket for talking to the Zyre node, for polling"""
288
+ return self.inbox
289
+
290
+ @staticmethod
291
+ def version():
292
+ return __version_info__
293
+
294
+
295
+ def chat_task(ctx, pipe):
296
+ n = Pyre(ctx=ctx)
297
+ n.join("CHAT")
298
+ n.start()
299
+
300
+ poller = zmq.Poller()
301
+ poller.register(pipe, zmq.POLLIN)
302
+ poller.register(n.socket(), zmq.POLLIN)
303
+ while True:
304
+ items = dict(poller.poll())
305
+ if pipe in items:
306
+ message = pipe.recv()
307
+ if message == "$TERM":
308
+ break
309
+ logger.debug("CHAT_TASK: {0}".format(message))
310
+ n.shout("CHAT", message)
311
+
312
+ if n.socket() in items:
313
+ event = PyreEvent(n)
314
+
315
+ logger.debug("NODE_MSG TYPE: {0}".format(event.type))
316
+ logger.debug("NODE_MSG PEER: {0}".format(event.peer_uuid))
317
+
318
+ if event.type == "SHOUT":
319
+ logger.debug("NODE_MSG GROUP: {0}".format(event.group))
320
+
321
+ logger.debug("NODE_MSG CONT: {0}".format(event.msg))
322
+ n.stop()
323
+
324
+
325
+ if __name__ == "__main__":
326
+ logging.basicConfig()
327
+ logging.getLogger("__main__").setLevel(logging.DEBUG)
328
+
329
+ ctx = zmq.Context()
330
+ chat_pipe = zhelper.zthread_fork(ctx, chat_task)
331
+ while True:
332
+ try:
333
+ msg = raw_input()
334
+ chat_pipe.send_string(msg)
335
+ except (KeyboardInterrupt, SystemExit):
336
+ chat_pipe.send_string("$TERM")
337
+ break
338
+ logger.debug("Exiting")
@@ -0,0 +1,62 @@
1
+ import json
2
+ import uuid
3
+
4
+
5
+ class PyreEvent(object):
6
+ """Parsing Pyre messages
7
+
8
+ This class provides a higher-level API to the Pyre.recv() call, by doing
9
+ work that you will want to do in many cases, such as unpacking the peer
10
+ headers for each ENTER event received.
11
+ """
12
+
13
+ def __init__(self, incoming: list[bytes]):
14
+ """Constructor, creates a new Pyre event. Receive an event from the Pyre node, wraps Pyre.recv.
15
+
16
+ Args:
17
+ node (Pyre): Pyre node
18
+ """
19
+ super(PyreEvent, self).__init__()
20
+
21
+ self.type: str = incoming.pop(0).decode("utf-8")
22
+ self.peer_uuid_bytes = incoming.pop(0)
23
+ self.peer_name = incoming.pop(0).decode("utf-8")
24
+ self.headers = ""
25
+ self.peer_addr = ""
26
+ self.group = ""
27
+ self.msg: list[bytes] = []
28
+ if self.type == "ENTER":
29
+ self.headers = json.loads(incoming.pop(0).decode("utf-8"))
30
+ self.peer_addr = incoming.pop(0).decode("utf-8")
31
+ elif self.type == "JOIN" or self.type == "LEAVE":
32
+ self.group = incoming.pop(0).decode("utf-8")
33
+ elif self.type == "WHISPER":
34
+ self.msg = incoming
35
+ elif self.type == "SHOUT":
36
+ self.group = incoming.pop(0).decode("utf-8")
37
+ self.msg = incoming
38
+
39
+ def header(self, name: str):
40
+ """Getter for single header values
41
+
42
+ Args:
43
+ name (str): Header name
44
+
45
+ Returns:
46
+ str: Header value
47
+ """
48
+ if self.headers and name in self.headers:
49
+ return self.headers[name]
50
+ return None
51
+
52
+ @property
53
+ def peer_uuid(self):
54
+ """Creates uuid.UUID object
55
+
56
+ Returns:
57
+ TYPE: uuid.UUID
58
+ """
59
+ return uuid.UUID(bytes=self.peer_uuid_bytes)
60
+
61
+ def __str__(self):
62
+ return "<%s %s from %s>" % (__name__, self.type, self.peer_uuid.hex)
@@ -0,0 +1,39 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+
6
+ class PyreGroup(object):
7
+ def __init__(self, name, peers={}):
8
+ self.name = name
9
+ # TODO perhaps warn if peers is not a set type
10
+ self.peers = peers
11
+
12
+ # def __del__(self):
13
+
14
+ def __repr__(self):
15
+ ret = "GROUPNAME={0}:\n".format(self.name)
16
+ for key, val in self.peers.items():
17
+ ret += "\t{0} {1}\n".format(key, val.name)
18
+ return ret
19
+
20
+ # Add peer to group
21
+ def join(self, peer):
22
+ self.peers[peer.get_identity()] = peer
23
+ peer.set_status(peer.get_status() + 1)
24
+
25
+ # Remove peer from group
26
+ def leave(self, peer):
27
+ peer_identity = peer.get_identity()
28
+ if peer_identity in self.peers:
29
+ self.peers.pop(peer.get_identity())
30
+
31
+ else:
32
+ logger.debug("Peer {0} is not in group {1}.".format(peer, self.name))
33
+
34
+ peer.set_status(peer.get_status() + 1)
35
+
36
+ # Send message to all peers in group
37
+ def send(self, msg):
38
+ for p in self.peers.values():
39
+ p.send(msg)